diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/authentication.rs | 630 | ||||
| -rw-r--r-- | src/datastructures.rs | 160 | ||||
| -rw-r--r-- | src/main.rs | 629 | ||||
| -rw-r--r-- | src/test.rs | 48 |
4 files changed, 708 insertions, 759 deletions
diff --git a/src/authentication.rs b/src/authentication.rs new file mode 100644 index 0000000..dd27c25 --- /dev/null +++ b/src/authentication.rs @@ -0,0 +1,630 @@ +use crate::datastructures::{Config, Cookie, FormData, WrapConfigure}; +use anyhow::Result; +use clap::ArgMatches; +use handlebars::Handlebars; +use itertools::Itertools as _; +use redis::AsyncCommands; +use serde::Serialize; +use sqlx::sqlite::SqliteConnectOptions; +use sqlx::{ConnectOptions, Connection, SqliteConnection}; +use std::env; +use std::io::{BufRead, Write}; +use std::str::FromStr; +use tempfile::TempDir; +use tokio_stream::StreamExt as _; + +pub(crate) struct IOModule<R, W> { + reader: R, + writer: W, +} + +impl<R, W> IOModule<R, W> { + pub fn new(reader: R, writer: W) -> Self { + Self { reader, writer } + } +} + +impl<R: BufRead, W: Write> IOModule<R, W> { + // Processing the `authenticate-post` called by cgit. + pub(crate) async fn cmd_authenticate_post( + &mut self, + matches: &ArgMatches, + cfg: Config, + ) -> Result<()> { + // Read stdin from upstream. + let mut buffer = String::new(); + self.reader.read_to_string(&mut buffer)?; + + //log::debug!("{}", buffer); + let data = FormData::from(buffer); + + let cfg = WrapConfigure::from(cfg); + log::trace!("Method is {}", cfg.get_authorizer().method()); + + // Establish Redis connection early for rate limiting + session storage + let mut conn = + crate::datastructures::get_redis_connection(cfg.get_config().redis_url.as_str()) + .await?; + + // Rate limit check + let max_attempts = cfg.get_config().max_login_attempts; + let login_timeout = cfg.get_config().login_timeout; + + if max_attempts > 0 { + let rate_limit_key = format!("cgit_auth_failed_{}", data.get_user()); + let attempt_count: u64 = conn.get(&rate_limit_key).await.unwrap_or(0); + if attempt_count >= max_attempts { + log::warn!( + "User {} is locked out due to too many failed login attempts", + data.get_user() + ); + writeln!(&mut self.writer, "Status: 403 Forbidden")?; + writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?; + writeln!(&mut self.writer)?; + return Ok(()); + } + } + + let ret = verify_login(&cfg, &data).await; + + if let Err(ref e) = ret { + eprintln!("{e:?}"); + #[cfg(test)] + eprintln!( + "If database locked error occurs frequently, \ + please use environment DISK_WAIT_TIME to specify longer time." + ); + log::error!("{e:?}") + } + + if ret.unwrap_or(false) { + // Clear failed attempt counter on successful login + if max_attempts > 0 { + let rate_limit_key = format!("cgit_auth_failed_{}", data.get_user()); + let _: () = conn.del(&rate_limit_key).await.unwrap_or(()); + } + + let cookie = Cookie::generate(data.get_user()); + + conn.set_ex::<_, _, String>( + format!("cgit_auth_{}", cookie.get_key()), + cookie.get_body(), + cfg.get_config().cookie_ttl, + ) + .await?; + + let cookie_value = cookie.to_string(); + + let is_secure = matches + .get_one::<String>("https") + .map(|s| s.as_str()) + .is_some_and(|x| matches!(x, "yes" | "on" | "1")); + let domain = matches + .get_one::<String>("http-host") + .map(|s| s.as_str()) + .unwrap_or("*"); + let location = matches + .get_one::<String>("http-referer") + .map(|s| s.as_str()) + .unwrap_or("/"); + let cookie_suffix = if is_secure { "; Secure" } else { "" }; + writeln!(&mut self.writer, "Status: 302 Found")?; + writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?; + writeln!(&mut self.writer, "Location: {location}")?; + writeln!( + &mut self.writer, + "Set-Cookie: cgit_auth={cookie_value}; Domain={domain}; Max-Age={}; HttpOnly; SameSite=Lax{cookie_suffix}", + cfg.get_config().cookie_ttl * 10, + )?; + } else { + // Increment rate limit counter on failed login + if max_attempts > 0 { + let rate_limit_key = format!("cgit_auth_failed_{}", data.get_user()); + let new_count: u64 = conn.incr(&rate_limit_key, 1u64).await.unwrap_or(1); + if new_count == 1 { + let _: () = conn + .expire(&rate_limit_key, login_timeout as i64) + .await + .unwrap_or(()); + } + log::info!( + "Failed login attempt {new_count}/{max_attempts} for user {}", + data.get_user() + ); + } + writeln!(&mut self.writer, "Status: 403 Forbidden")?; + writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?; + } + + writeln!(&mut self.writer)?; + Ok(()) + } +} + +// Processing the `authenticate-cookie` called by cgit. +pub(crate) async fn cmd_authenticate_cookie(matches: &ArgMatches, cfg: Config) -> Result<bool> { + let cookies = matches + .get_one::<String>("http-cookie") + .map(|s| s.as_str()) + .unwrap_or(""); + let repo = matches + .get_one::<String>("repo") + .map(|s| s.as_str()) + .unwrap_or(""); + /*let current_url = matches.value_of("current-url").unwrap_or("");*/ + + let mut bypass = false; + + if cfg.bypass_root /*&& current_url.eq("/")*/ && repo.is_empty() { + bypass = true; + } + + if bypass || (!repo.is_empty() && !cfg.check_repo_protect(repo)) { + return Ok(true); + } + + if cookies.is_empty() { + return Ok(false); + } + + let mut conn = crate::datastructures::get_redis_connection(cfg.redis_url.as_str()).await?; + + let redis_key = format!("cgit_repo_{repo}"); + if !repo.is_empty() && !conn.exists(&redis_key).await? { + let sql_conn = SqliteConnectOptions::from_str(cfg.get_database_location())? + .read_only(true) + .immutable(true) + .disable_statement_logging() + .connect() + .await; + if let Err(ref e) = sql_conn { + log::error!( + "Got error while open sqlite connection: {e:?}\nDatabase location: {}", + cfg.get_database_location() + ); + } + let mut sql_conn = sql_conn?; + if let Some((users,)) = + sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ? "#) + .bind(repo) + .fetch_optional(&mut sql_conn) + .await? + { + let users = users.split_whitespace().collect::<Vec<&str>>(); + let _: () = conn.sadd(&redis_key, users).await?; + } + } + + if let Ok(Some(cookie)) = Cookie::load_from_request(cookies) { + log::debug!("Cookie is {cookie:?}"); + if let Ok(r) = conn + .get::<_, String>(format!("cgit_auth_{}", cookie.get_key())) + .await + { + conn.expire::<_, bool>( + format!("cgit_auth_{}", cookie.get_key()), + cfg.cookie_ttl as i64, + ) + .await?; + if cookie.eq_body(r.as_str()) { + if repo.is_empty() { + return Ok(true); + } + if conn + .sismember::<_, _, i32>(&redis_key, cookie.get_user()) + .await? + == 1 + { + return Ok(true); + } + } + } + log::debug!("{cookie:?}"); + } + + Ok(false) +} + +pub(crate) async fn cmd_init(cfg: Config) -> Result<()> { + let loc = std::path::Path::new(cfg.get_database_location()); + let exists = loc.exists(); + if !exists { + std::fs::File::create(loc)?; + } + + let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; + + if exists { + let rows = sqlx::query(r#"SELECT name FROM sqlite_master WHERE type='table' AND name=?"#) + .bind("auth_meta") + .fetch_all(&mut conn) + .await?; + + if !rows.is_empty() { + return Ok(()); + } + } + + sqlx::query(crate::database::current::CREATE_TABLES) + .execute(&mut conn) + .await?; + println!("Initialize the database successfully"); + + drop(conn); + + cfg.write_database_commit_timestamp().await?; + Ok(()) +} + +pub(crate) async fn verify_login(cfg: &WrapConfigure, data: &FormData) -> Result<bool> { + cfg.hook().await?; + data.authorize(cfg.get_authorizer()).await +} + +#[derive(Serialize)] +pub struct Meta<'a> { + action: &'a str, + redirect: &'a str, + version: &'a str, +} + +// Processing the `body` called by cgit. +pub(crate) async fn cmd_body(matches: &ArgMatches, _cfg: Config) { + let source = include_str!("authentication_page.html"); + let handlebars = Handlebars::new(); + let meta = Meta { + action: matches + .get_one::<String>("login-url") + .map(|s| s.as_str()) + .unwrap_or_default(), + redirect: matches + .get_one::<String>("current-url") + .map(|s| s.as_str()) + .unwrap_or_default(), + version: env!("CARGO_PKG_VERSION"), + }; + handlebars + .render_template_to_write(source, &meta, std::io::stdout()) + .unwrap(); +} + +pub(crate) async fn cmd_add_user(matches: &ArgMatches, cfg: Config) -> Result<()> { + let re = regex::Regex::new(r"^[a-zA-Z0-9_][a-zA-Z0-9_.\-]*[a-zA-Z0-9_]$").unwrap(); + let user = matches + .get_one::<String>("user") + .map(|s| s.as_str()) + .unwrap_or_default(); + let passwd = matches + .get_one::<String>("password") + .map(|s| s.to_string()) + .unwrap_or_default(); + if user.is_empty() || passwd.is_empty() { + return Err(anyhow::Error::msg("Invalid user or password length")); + } + + if user.len() >= 20 { + return Err(anyhow::Error::msg("Username length should less than 21")); + } + + if !re.is_match(user) { + return Err(anyhow::Error::msg( + "Username must be at least 2 characters, contain only alphanumeric characters, underscores, dots, or hyphens, and must not start or end with a dot or hyphen", + )); + } + + let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; + + let items = sqlx::query(r#"SELECT 1 FROM "accounts" WHERE "user" = ? "#) + .bind(user) + .fetch_all(&mut conn) + .await?; + + if !items.is_empty() { + return Err(anyhow::Error::msg("User already exists!")); + } + + let uid = uuid::Uuid::new_v4().to_string(); + + sqlx::query(r#"INSERT INTO "accounts" VALUES (?, ?, ?) "#) + .bind(user) + .bind(FormData::gen_string_argon2_hash(&passwd)?) + .bind(&uid) + .execute(&mut conn) + .await?; + + println!("Insert {user} ({uid}) to database"); + + drop(conn); + + cfg.write_database_commit_timestamp().await?; + Ok(()) +} + +pub(crate) async fn cmd_list_user(cfg: Config) -> Result<()> { + let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; + + let (count,) = sqlx::query_as::<_, (i32,)>(r#"SELECT COUNT(*) FROM "accounts""#) + .fetch_one(&mut conn) + .await?; + + if count > 0 { + let mut iter = + sqlx::query_as::<_, (String,)>(r#"SELECT "user" FROM "accounts""#).fetch(&mut conn); + + println!( + "There is {count} user{} in database", + if count > 1 { "s" } else { "" } + ); + while let Some(Ok((row,))) = iter.next().await { + println!("{row}") + } + } else { + println!("There is not user exists.") + } + + Ok(()) +} + +pub(crate) async fn cmd_delete_user(matches: &ArgMatches, cfg: Config) -> Result<()> { + let user = matches + .get_one::<String>("user") + .map(|s| s.as_str()) + .unwrap_or(""); + if user.is_empty() { + return Err(anyhow::Error::msg("Please input a valid username")); + } + + let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; + + let items = sqlx::query_as::<_, (i32,)>(r#"SELECT 1 FROM "accounts" WHERE "user" = ?"#) + .bind(user) + .fetch_all(&mut conn) + .await?; + + if items.is_empty() { + return Err(anyhow::Error::msg(format!("User {user} not found"))); + } + + sqlx::query(r#"DELETE FROM "accounts" WHERE "user" = ?"#) + .bind(user) + .execute(&mut conn) + .await?; + + println!("Delete {user} from database"); + + cfg.write_database_commit_timestamp().await?; + Ok(()) +} + +pub(crate) async fn cmd_reset_database(matches: &ArgMatches, cfg: Config) -> Result<()> { + if !matches.contains_id("confirm") { + return Err(anyhow::anyhow!( + "Please add --confirm argument to process reset", + )); + } + + let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; + + sqlx::query(crate::database::current::DROP_TABLES) + .execute(&mut conn) + .await?; + + sqlx::query(crate::database::current::CREATE_TABLES) + .execute(&mut conn) + .await?; + + println!("Reset database successfully"); + + cfg.write_database_commit_timestamp().await?; + Ok(()) +} + +pub(crate) async fn cmd_upgrade_database(cfg: Config) -> Result<()> { + let tmp_dir = TempDir::new()?; + + let v2_path = tmp_dir.path().join("v2.db"); + let v3_path = tmp_dir.path().join("v3.db"); + + drop(std::fs::File::create(&v3_path).expect("Create v3 database failure")); + + std::fs::copy(cfg.get_database_location(), &v2_path) + .expect("Copy v2 database to tempdir failure"); + + let mut origin_conn = SqliteConnectOptions::from_str(v2_path.as_path().to_str().unwrap())? + .read_only(true) + .immutable(true) + .connect() + .await?; + + let (v,) = sqlx::query_as::<_, (String,)>( + r#"SELECT "value" FROM "auth_meta" WHERE "key" = 'version' "#, + ) + .fetch_optional(&mut origin_conn) + .await? + .unwrap(); + + #[allow(deprecated)] + if v.eq(crate::database::previous::VERSION) { + let mut conn = SqliteConnection::connect(v3_path.as_path().to_str().unwrap()).await?; + + sqlx::query(crate::database::current::CREATE_TABLES) + .execute(&mut conn) + .await?; + + let mut iter = sqlx::query_as::<_, (String, String, String)>(r#"SELECT * FROM "accounts""#) + .fetch(&mut origin_conn); + + while let Some(Ok((user, passwd, uid))) = iter.next().await { + sqlx::query(r#"INSERT INTO "accounts" VALUES (?, ?, ?)"#) + .bind(user.as_str()) + .bind(passwd) + .bind(uid.as_str()) + .execute(&mut conn) + .await?; + log::debug!("Process user: {user} ({uid})"); + } + drop(conn); + + std::fs::copy(&v3_path, cfg.get_database_location()) + .expect("Copy back to database location failure"); + println!("Upgrade database successful"); + } else { + eprintln!( + "Got database version {v} but {} required", + crate::database::previous::VERSION + ) + } + drop(origin_conn); + tmp_dir.close()?; + + cfg.write_database_commit_timestamp().await?; + Ok(()) +} + +pub(crate) async fn cmd_repo_user_control( + matches: &ArgMatches, + cfg: Config, + is_delete: bool, +) -> Result<()> { + let repo = matches + .get_one::<String>("repo") + .map(|s| s.as_str()) + .unwrap_or(""); + let user = matches + .get_one::<String>("user") + .map(|s| s.as_str()) + .unwrap_or_default(); + + let clear_all = is_delete && matches.contains_id("clear-all"); + + if repo.is_empty() + || (is_delete && !clear_all && user.is_empty()) + || (!is_delete && user.is_empty()) + { + return Err(anyhow::Error::msg("Invalid repository or username")); + } + + let mut redis_conn = + crate::datastructures::get_redis_connection(cfg.redis_url.as_str()).await?; + + let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; + + if sqlx::query(r#"SELECT "users" FROM "repos" WHERE "repo" = ?"#) + .bind(repo) + .fetch_optional(&mut conn) + .await? + .is_none() + { + if is_delete { + println!("Row is empty."); + return Ok(()); + } + sqlx::query(r#"INSERT INTO "repos" VALUES (?, '')"#) + .bind(repo) + .execute(&mut conn) + .await?; + } + + let (users,) = + sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ?"#) + .bind(repo) + .fetch_optional(&mut conn) + .await? + .unwrap(); + let mut users = users.split_whitespace().collect::<Vec<&str>>(); + + if let Some(index) = users.clone().into_iter().position(|x| x.eq(user)) { + if is_delete { + if clear_all { + users.clear(); + } else { + users.remove(index); + } + } else { + return Err(anyhow::Error::msg("User already in repository ACL")); + } + } + + if !is_delete { + users.push(user); + } + + sqlx::query(r#"UPDATE "repos" SET "users" = ? WHERE "repo" = ?"#) + .bind(users.join(" ")) + .bind(repo) + .execute(&mut conn) + .await?; + + let redis_key = format!("cgit_repo_{repo}"); + if redis_conn.exists::<_, i32>(&redis_key).await? == 0 { + redis_conn.sadd::<_, _, i32>(&redis_key, users).await?; + } else if is_delete { + if clear_all { + redis_conn.del::<_, i32>(&redis_key).await?; + } else { + redis_conn.srem::<_, _, i32>(&redis_key, user).await?; + } + } else { + redis_conn.sadd::<_, _, i32>(&redis_key, user).await?; + } + + if !clear_all { + println!( + "{} user {user} {} repository {repo} ACL successful", + if is_delete { "Delete" } else { "Add" }, + if is_delete { "from" } else { "to" }, + ); + } else { + println!("Clear all users from repository {repo} ACL"); + } + + Ok(()) +} + +pub(crate) async fn cmd_list_repos_acl(arg_matches: &ArgMatches, cfg: Config) -> Result<()> { + let repo = arg_matches + .get_one::<String>("repo") + .map(|s| s.as_str()) + .unwrap_or(""); + + let mut conn = SqliteConnectOptions::from_str(cfg.get_database_location())? + .read_only(true) + .immutable(true) + .connect() + .await?; + + if repo.is_empty() { + let (length,) = sqlx::query_as::<_, (i32,)>(r#"SELECT COUNT(*) FROM "repos""#) + .fetch_optional(&mut conn) + .await? + .unwrap_or((0,)); + + println!( + "There is total {length} {} in database", + if length == 1 { + "repository" + } else { + "repositories" + }, + ); + + let mut iter = + sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "repos""#).fetch(&mut conn); + while let Some(Ok((repo, users))) = iter.next().await { + println!("{repo}: {}", users.split_whitespace().join(",")) + } + } else { + let ret = + sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "repos" WHERE "repo" = ?"#) + .bind(repo) + .fetch_optional(&mut conn) + .await?; + if let Some((repo, users)) = ret { + println!("{repo}: {}", users.split_whitespace().join(",")) + } else { + println!("Repository {repo} not register in database") + } + } + + Ok(()) +} diff --git a/src/datastructures.rs b/src/datastructures.rs index 7f062b3..8b8637f 100644 --- a/src/datastructures.rs +++ b/src/datastructures.rs @@ -18,17 +18,12 @@ use anyhow::Result; use argon2::{ Argon2, - password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng}, + password_hash::{PasswordHasher, PasswordVerifier, phc::PasswordHash}, }; use base64::Engine; -#[cfg(feature = "pam")] -pub use ds_pam::*; use log::error; -use rand::Rng; -use serde::{Deserialize, Serialize}; +use rand::RngExt; use sqlx::ConnectOptions; -#[cfg(feature = "pam")] -use std::borrow::BorrowMut; use std::borrow::Cow; use std::fmt::{Debug, Formatter}; use std::fs::read_to_string; @@ -40,6 +35,9 @@ use url::form_urlencoded; const DEFAULT_CONFIG_LOCATION: &str = "/etc/cgitrc"; const DEFAULT_COOKIE_TTL: u64 = 1200; const DEFAULT_DATABASE_LOCATION: &str = "/etc/cgit/auth.db"; +pub(crate) const DEFAULT_REDIS_URL: &str = "redis://127.0.0.1/"; +const DEFAULT_MAX_LOGIN_ATTEMPTS: u64 = 5; +const DEFAULT_LOGIN_TIMEOUT: u64 = 900; pub const CACHE_DIR: &str = "/var/cache/cgit"; pub type RandIntType = u32; pub const COOKIE_LENGTH: usize = 32; @@ -73,75 +71,25 @@ pub fn rand_str(len: usize) -> String { password } -pub(crate) trait TestSuite { - fn generate_test_config() -> Self; +pub(crate) async fn get_redis_connection( + redis_url: &str, +) -> Result<redis::aio::MultiplexedConnection> { + let client = redis::Client::open(redis_url)?; + Ok(client.get_multiplexed_async_connection().await?) } -#[cfg(feature = "pam")] -pub mod ds_pam { - - #[derive(Debug, Clone)] - pub struct PAMConfig { - use_pam: bool, - provider: String, - } - - impl From<&str> for PAMConfig { - fn from(s: &str) -> Self { - let use_pam = !s.to_lowercase().eq("false"); - Self { - use_pam, - provider: s.to_string(), - } - } - } - - impl PAMConfig { - pub fn get_enabled(&self) -> bool { - self.use_pam - } - - pub fn get_provider(&self) -> &String { - &self.provider - } - } - - impl Default for PAMConfig { - fn default() -> Self { - Self { - use_pam: false, - provider: "system-auth".to_string(), - } - } - } - - #[derive(Debug, Clone)] - pub struct PAMAuthorizer { - provider: String, - } - - impl PAMAuthorizer { - pub fn provider(&self) -> &str { - &self.provider - } - } - - impl From<&PAMConfig> for PAMAuthorizer { - fn from(cfg: &PAMConfig) -> Self { - Self { - provider: cfg.get_provider().clone(), - } - } - } +pub(crate) trait TestSuite { + fn generate_test_config() -> Self; } #[derive(Debug, Clone)] pub struct Config { pub cookie_ttl: u64, database: String, + pub redis_url: String, pub bypass_root: bool, - #[cfg(feature = "pam")] - pam_config: PAMConfig, + pub max_login_attempts: u64, + pub login_timeout: u64, pub(crate) test: bool, protect_config: ProtectSettings, } @@ -151,9 +99,10 @@ impl Default for Config { Self { cookie_ttl: DEFAULT_COOKIE_TTL, database: DEFAULT_DATABASE_LOCATION.to_string(), + redis_url: DEFAULT_REDIS_URL.to_string(), bypass_root: false, - #[cfg(feature = "pam")] - pam_config: Default::default(), + max_login_attempts: DEFAULT_MAX_LOGIN_ATTEMPTS, + login_timeout: DEFAULT_LOGIN_TIMEOUT, test: false, protect_config: Default::default(), } @@ -170,11 +119,12 @@ impl Config { let mut cookie_ttl: u64 = DEFAULT_COOKIE_TTL; let mut database: &str = "/etc/cgit/auth.db"; + let mut redis_url: &str = DEFAULT_REDIS_URL; let mut bypass_root: bool = false; + let mut max_login_attempts: u64 = DEFAULT_MAX_LOGIN_ATTEMPTS; + let mut login_timeout: u64 = DEFAULT_LOGIN_TIMEOUT; let mut protect_enabled: bool = true; let mut protect_white_list_mode: bool = true; - #[cfg(feature = "pam")] - let mut use_pam: &str = "false"; //let mut skip_user_access_check: bool = false; for line in file.lines() { @@ -193,9 +143,12 @@ impl Config { match key_name { "cookie-ttl" => cookie_ttl = value.parse().unwrap_or(DEFAULT_COOKIE_TTL), "database" => database = value, + "redis-url" => redis_url = value, "bypass-root" => bypass_root = value.to_lowercase().eq("true"), - #[cfg(feature = "pam")] - "use-pam" => use_pam = value, + "max-login-attempts" => { + max_login_attempts = value.parse().unwrap_or(DEFAULT_MAX_LOGIN_ATTEMPTS) + } + "login-timeout" => login_timeout = value.parse().unwrap_or(DEFAULT_LOGIN_TIMEOUT), "protect" => match value.to_lowercase().as_str() { "full" => { protect_enabled = true; @@ -217,10 +170,11 @@ impl Config { Self { cookie_ttl, database: database.to_string(), + redis_url: redis_url.to_string(), bypass_root, + max_login_attempts, + login_timeout, - #[cfg(feature = "pam")] - pam_config: PAMConfig::from(use_pam), test: false, protect_config: ProtectSettings::from_path( protect_enabled, @@ -307,11 +261,6 @@ impl Config { self.protect_config.query_is_all_protected() } - #[cfg(feature = "pam")] - fn get_pam_config(&self) -> &PAMConfig { - &self.pam_config - } - pub fn get_test_status(&self) -> bool { self.test } @@ -321,10 +270,11 @@ impl TestSuite for Config { fn generate_test_config() -> Self { Self { database: "test/tmp.db".to_string(), + redis_url: DEFAULT_REDIS_URL.to_string(), bypass_root: false, cookie_ttl: DEFAULT_COOKIE_TTL, - #[cfg(feature = "pam")] - pam_config: Default::default(), + max_login_attempts: DEFAULT_MAX_LOGIN_ATTEMPTS, + login_timeout: DEFAULT_LOGIN_TIMEOUT, test: true, protect_config: ProtectSettings::generate_test_config(), } @@ -505,11 +455,10 @@ impl FormData { pub fn gen_string_argon2_hash(s: &str) -> Result<String> { let passwd = s.as_bytes(); - let salt = SaltString::generate(&mut OsRng); let argon2_alg = Argon2::default(); - Ok(argon2_alg.hash_password(passwd, &salt).unwrap().to_string()) + Ok(argon2_alg.hash_password(passwd).unwrap().to_string()) } pub fn set_password(&mut self, password: String) { @@ -562,12 +511,12 @@ impl From<String> for FormData { } } -#[derive(Serialize, Deserialize)] +/* #[derive(Serialize, Deserialize)] struct IvFile { iv: String, timestamp: u64, } - + */ #[derive(Debug)] pub struct Cookie { timestamp: u64, @@ -589,7 +538,9 @@ impl Cookie { pub fn load_from_request(cookies: &str) -> Result<Option<Self>> { let mut cookie_self = None; for cookie in cookies.split(';').map(|x| x.trim()) { - let (key, value) = cookie.split_once('=').unwrap(); + let Some((key, value)) = cookie.split_once('=') else { + continue; + }; if key.eq("cgit_auth") { let value = base64::engine::general_purpose::STANDARD .decode(value) @@ -651,8 +602,6 @@ impl std::fmt::Display for Cookie { #[derive(Debug, Clone)] pub enum AuthorizerType { - #[cfg(feature = "pam")] - PAM, Password, } @@ -662,8 +611,6 @@ impl std::fmt::Display for AuthorizerType { f, "{}", match self { - #[cfg(feature = "pam")] - AuthorizerType::PAM => "PAM", AuthorizerType::Password => "PASSWORD", } ) @@ -698,7 +645,6 @@ pub struct WrapConfigure { } impl From<Config> for WrapConfigure { - #[cfg(not(feature = "pam"))] fn from(cfg: Config) -> Self { let authorizer = Box::new(SQLAuthorizer::from(&cfg)); Self { @@ -706,36 +652,6 @@ impl From<Config> for WrapConfigure { authorizer, } } - #[cfg(feature = "pam")] - fn from(cfg: Config) -> Self { - let authorizer: Box<dyn Authorizer> = if cfg.get_pam_config().get_enabled() { - Box::new(PAMAuthorizer::from(cfg.get_pam_config())) - } else { - Box::new(SQLAuthorizer::from(&cfg)) - }; - Self { - config: cfg, - authorizer, - } - } -} - -#[cfg(feature = "pam")] -#[async_trait::async_trait] -impl Authorizer for PAMAuthorizer { - fn method(&self) -> AuthorizerType { - AuthorizerType::PAM - } - - async fn verify(&self, user: &str, password: &str) -> Result<bool> { - let service = self.provider(); - - let mut auth = pam::Client::with_password(service).unwrap(); - auth.conversation_mut() - .borrow_mut() - .set_credentials(user, password); - Ok(auth.authenticate().is_ok() && auth.open_session().is_ok()) - } } #[derive(Debug, Clone)] diff --git a/src/main.rs b/src/main.rs index f392792..3b9e04b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,599 +15,19 @@ ** along with this program. If not, see <https://www.gnu.org/licenses/>. */ +mod authentication; mod database; mod datastructures; #[cfg(test)] mod test; -#[cfg(feature = "pam")] -use crate::datastructures::AuthorizerType; -use crate::datastructures::{Config, Cookie, FormData, TestSuite, WrapConfigure}; +use crate::datastructures::{Config, TestSuite}; use anyhow::Result; use clap::{Arg, ArgMatches, Command}; -use handlebars::Handlebars; -use itertools::Itertools as _; use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; -use redis::AsyncCommands; -use serde::Serialize; -use sqlx::sqlite::SqliteConnectOptions; -use sqlx::{ConnectOptions, Connection, SqliteConnection}; use std::env; -use std::io::{BufRead, Write}; -use std::str::FromStr; -use tempfile::TempDir; -use tokio_stream::StreamExt as _; - -struct IOModule<R, W> { - reader: R, - writer: W, -} - -impl<R: BufRead, W: Write> IOModule<R, W> { - // Processing the `authenticate-post` called by cgit. - async fn cmd_authenticate_post(&mut self, matches: &ArgMatches, cfg: Config) -> Result<()> { - // Read stdin from upstream. - let mut buffer = String::new(); - self.reader.read_to_string(&mut buffer)?; - - //log::debug!("{}", buffer); - let data = datastructures::FormData::from(buffer); - - let cfg = WrapConfigure::from(cfg); - log::trace!("Method is {}", cfg.get_authorizer().method()); - let ret = verify_login(&cfg, &data).await; - - if let Err(ref e) = ret { - eprintln!("{e:?}"); - #[cfg(test)] - eprintln!( - "If database locked error occurs frequently, \ - please use environment DISK_WAIT_TIME to specify longer time." - ); - log::error!("{e:?}") - } - - if ret.unwrap_or(false) { - let redis_conn = redis::Client::open("redis://127.0.0.1/")?; - let cookie = Cookie::generate(data.get_user()); - let mut conn = redis_conn.get_multiplexed_async_connection().await?; - - conn.set_ex::<_, _, String>( - format!("cgit_auth_{}", cookie.get_key()), - cookie.get_body(), - cfg.get_config().cookie_ttl, - ) - .await?; - - let cookie_value = cookie.to_string(); - - let is_secure = matches - .get_one::<String>("https") - .map(|s| s.as_str()) - .is_some_and(|x| matches!(x, "yes" | "on" | "1")); - let domain = matches - .get_one::<String>("http-host") - .map(|s| s.as_str()) - .unwrap_or("*"); - let location = matches - .get_one::<String>("http-referer") - .map(|s| s.as_str()) - .unwrap_or("/"); - let cookie_suffix = if is_secure { "; secure" } else { "" }; - writeln!(&mut self.writer, "Status: 302 Found")?; - writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?; - writeln!(&mut self.writer, "Location: {}", location)?; - writeln!( - &mut self.writer, - "Set-Cookie: cgit_auth={cookie_value}; Domain={domain}; Max-Age={}; HttpOnly{cookie_suffix}", - cfg.get_config().cookie_ttl * 10, - )?; - } else { - writeln!(&mut self.writer, "Status: 403 Forbidden")?; - writeln!(&mut self.writer, "Cache-Control: no-cache, no-store")?; - } - - writeln!(&mut self.writer)?; - Ok(()) - } -} - -// Processing the `authenticate-cookie` called by cgit. -async fn cmd_authenticate_cookie(matches: &ArgMatches, cfg: Config) -> Result<bool> { - let cookies = matches - .get_one::<String>("http-cookie") - .map(|s| s.as_str()) - .unwrap_or(""); - let repo = matches - .get_one::<String>("repo") - .map(|s| s.as_str()) - .unwrap_or(""); - /*let current_url = matches.value_of("current-url").unwrap_or("");*/ - - let mut bypass = false; - - if cfg.bypass_root /*&& current_url.eq("/")*/ && repo.is_empty() { - bypass = true; - } - - if bypass || (!repo.is_empty() && !cfg.check_repo_protect(repo)) { - return Ok(true); - } - - if cookies.is_empty() { - return Ok(false); - } - - let redis_conn = redis::Client::open("redis://127.0.0.1/")?; - let mut conn = redis_conn.get_multiplexed_async_connection().await?; - - let redis_key = format!("cgit_repo_{repo}"); - if !repo.is_empty() && !conn.exists(&redis_key).await? { - let sql_conn = SqliteConnectOptions::from_str(cfg.get_database_location())? - .read_only(true) - .immutable(true) - .disable_statement_logging() - .connect() - .await; - if let Err(ref e) = sql_conn { - log::error!( - "Got error while open sqlite connection: {e:?}\nDatabase location: {}", - cfg.get_database_location() - ); - } - let mut sql_conn = sql_conn?; - if let Some((users,)) = - sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ? "#) - .bind(repo) - .fetch_optional(&mut sql_conn) - .await? - { - let users = users.split_whitespace().collect::<Vec<&str>>(); - let _: () = conn.sadd(&redis_key, users).await?; - } - } - - if let Ok(Some(cookie)) = Cookie::load_from_request(cookies) { - log::debug!("Cookie is {cookie:?}"); - if let Ok(r) = conn - .get::<_, String>(format!("cgit_auth_{}", cookie.get_key())) - .await - { - conn.expire::<_, bool>( - format!("cgit_auth_{}", cookie.get_key()), - cfg.cookie_ttl as i64, - ) - .await?; - if cookie.eq_body(r.as_str()) { - if repo.is_empty() { - return Ok(true); - } - if conn - .sismember::<_, _, i32>(&redis_key, cookie.get_user()) - .await? - == 1 - { - return Ok(true); - } - } - } - log::debug!("{cookie:?}"); - } - - Ok(false) -} - -async fn cmd_init(cfg: Config) -> Result<()> { - let loc = std::path::Path::new(cfg.get_database_location()); - let exists = loc.exists(); - if !exists { - std::fs::File::create(loc)?; - } - - let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; - - if exists { - let rows = sqlx::query(r#"SELECT name FROM sqlite_master WHERE type='table' AND name=?"#) - .bind("auth_meta") - .fetch_all(&mut conn) - .await?; - - if !rows.is_empty() { - return Ok(()); - } - } - - sqlx::query(database::current::CREATE_TABLES) - .execute(&mut conn) - .await?; - println!("Initialize the database successfully"); - - drop(conn); - - cfg.write_database_commit_timestamp().await?; - Ok(()) -} - -#[cfg(not(feature = "pam"))] -async fn verify_login(cfg: &WrapConfigure, data: &FormData) -> Result<bool> { - cfg.hook().await?; - data.authorize(cfg.get_authorizer()).await -} - -#[cfg(feature = "pam")] -async fn verify_login(cfg: &WrapConfigure, data: &FormData) -> Result<bool> { - if let AuthorizerType::Password = cfg.get_authorizer().method() { - cfg.hook().await?; - } - data.authorize(cfg.get_authorizer()).await -} - -#[derive(Serialize)] -pub struct Meta<'a> { - action: &'a str, - redirect: &'a str, - version: &'a str, -} - -// Processing the `body` called by cgit. -async fn cmd_body(matches: &ArgMatches, _cfg: Config) { - let source = include_str!("authentication_page.html"); - let handlebars = Handlebars::new(); - let meta = Meta { - action: matches - .get_one::<String>("login-url") - .map(|s| s.as_str()) - .unwrap_or_default(), - redirect: matches - .get_one::<String>("current-url") - .map(|s| s.as_str()) - .unwrap_or_default(), - version: env!("CARGO_PKG_VERSION"), - }; - handlebars - .render_template_to_write(source, &meta, std::io::stdout()) - .unwrap(); -} - -async fn cmd_add_user(matches: &ArgMatches, cfg: Config) -> Result<()> { - let re = regex::Regex::new(r"^\w+$").unwrap(); - let user = matches - .get_one::<String>("user") - .map(|s| s.as_str()) - .unwrap_or_default(); - let passwd = matches - .get_one::<String>("password") - .map(|s| s.to_string()) - .unwrap_or_default(); - if user.is_empty() || passwd.is_empty() { - return Err(anyhow::Error::msg("Invalid user or password length")); - } - - if user.len() >= 20 { - return Err(anyhow::Error::msg("Username length should less than 21")); - } - - if !re.is_match(user) { - return Err(anyhow::Error::msg( - "Username must pass regex check\"^\\w+$\"", - )); - } - - let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; - - let items = sqlx::query(r#"SELECT 1 FROM "accounts" WHERE "user" = ? "#) - .bind(user) - .fetch_all(&mut conn) - .await?; - - if !items.is_empty() { - return Err(anyhow::Error::msg("User already exists!")); - } - - let uid = uuid::Uuid::new_v4().to_string(); - - sqlx::query(r#"INSERT INTO "accounts" VALUES (?, ?, ?) "#) - .bind(user) - .bind(FormData::gen_string_argon2_hash(&passwd)?) - .bind(&uid) - .execute(&mut conn) - .await?; - - println!("Insert {user} ({uid}) to database"); - - drop(conn); - - cfg.write_database_commit_timestamp().await?; - Ok(()) -} - -async fn cmd_list_user(cfg: Config) -> Result<()> { - let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; - - let (count,) = sqlx::query_as::<_, (i32,)>(r#"SELECT COUNT(*) FROM "accounts""#) - .fetch_one(&mut conn) - .await?; - - if count > 0 { - let mut iter = - sqlx::query_as::<_, (String,)>(r#"SELECT "user" FROM "accounts""#).fetch(&mut conn); - - println!( - "There is {count} user{} in database", - if count > 1 { "s" } else { "" } - ); - while let Some(Ok((row,))) = iter.next().await { - println!("{row}") - } - } else { - println!("There is not user exists.") - } - - Ok(()) -} - -async fn cmd_delete_user(matches: &ArgMatches, cfg: Config) -> Result<()> { - let user = matches - .get_one::<String>("user") - .map(|s| s.as_str()) - .unwrap_or(""); - if user.is_empty() { - return Err(anyhow::Error::msg("Please input a valid username")); - } - - let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; - - let items = sqlx::query_as::<_, (i32,)>(r#"SELECT 1 FROM "accounts" WHERE "user" = ?"#) - .bind(user) - .fetch_all(&mut conn) - .await?; - - if items.is_empty() { - return Err(anyhow::Error::msg(format!("User {user} not found"))); - } - - sqlx::query(r#"DELETE FROM "accounts" WHERE "user" = ?"#) - .bind(user) - .execute(&mut conn) - .await?; - - println!("Delete {user} from database"); - - cfg.write_database_commit_timestamp().await?; - Ok(()) -} - -async fn cmd_reset_database(matches: &ArgMatches, cfg: Config) -> Result<()> { - if !matches.contains_id("confirm") { - return Err(anyhow::Error::msg( - "Please add --confirm argument to process reset", - )); - } - - let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; - - sqlx::query(database::current::DROP_TABLES) - .execute(&mut conn) - .await?; - - sqlx::query(database::current::CREATE_TABLES) - .execute(&mut conn) - .await?; - - println!("Reset database successfully"); - - cfg.write_database_commit_timestamp().await?; - Ok(()) -} - -async fn cmd_upgrade_database(cfg: Config) -> Result<()> { - let tmp_dir = TempDir::new()?; - - let v2_path = tmp_dir.path().join("v2.db"); - let v3_path = tmp_dir.path().join("v3.db"); - - drop(std::fs::File::create(&v3_path).expect("Create v3 database failure")); - - std::fs::copy(cfg.get_database_location(), &v2_path) - .expect("Copy v2 database to tempdir failure"); - - let mut origin_conn = SqliteConnectOptions::from_str(v2_path.as_path().to_str().unwrap())? - .read_only(true) - .immutable(true) - .connect() - .await?; - - let (v,) = sqlx::query_as::<_, (String,)>( - r#"SELECT "value" FROM "auth_meta" WHERE "key" = 'version' "#, - ) - .fetch_optional(&mut origin_conn) - .await? - .unwrap(); - - #[allow(deprecated)] - if v.eq(database::previous::VERSION) { - let mut conn = SqliteConnection::connect(v3_path.as_path().to_str().unwrap()).await?; - - sqlx::query(database::current::CREATE_TABLES) - .execute(&mut conn) - .await?; - - let mut iter = sqlx::query_as::<_, (String, String, String)>(r#"SELECT * FROM "accounts""#) - .fetch(&mut origin_conn); - - while let Some(Ok((user, passwd, uid))) = iter.next().await { - sqlx::query(r#"INSERT INTO "accounts" VALUES (?, ?, ?)"#) - .bind(user.as_str()) - .bind(passwd) - .bind(uid.as_str()) - .execute(&mut conn) - .await?; - log::debug!("Process user: {user} ({uid})"); - } - drop(conn); - - std::fs::copy(&v3_path, cfg.get_database_location()) - .expect("Copy back to database location failure"); - println!("Upgrade database successful"); - } else { - eprintln!( - "Got database version {v} but {} required", - database::previous::VERSION - ) - } - drop(origin_conn); - tmp_dir.close()?; - - cfg.write_database_commit_timestamp().await?; - Ok(()) -} - -async fn cmd_repo_user_control(matches: &ArgMatches, cfg: Config, is_delete: bool) -> Result<()> { - let repo = matches - .get_one::<String>("repo") - .map(|s| s.as_str()) - .unwrap_or(""); - let user = matches - .get_one::<String>("user") - .map(|s| s.as_str()) - .unwrap_or_default(); - - let clear_all = is_delete && matches.contains_id("clear-all"); - - if repo.is_empty() - || (is_delete && !clear_all && user.is_empty()) - || (!is_delete && user.is_empty()) - { - return Err(anyhow::Error::msg("Invalid repository or username")); - } - - let redis_client = redis::Client::open("redis://127.0.0.1/")?; - let mut redis_conn = redis_client.get_multiplexed_async_connection().await?; - - let mut conn = SqliteConnection::connect(cfg.get_database_location()).await?; - - if sqlx::query(r#"SELECT "users" FROM "repos" WHERE "repo" = ?"#) - .bind(repo) - .fetch_optional(&mut conn) - .await? - .is_none() - { - if is_delete { - println!("Row is empty."); - return Ok(()); - } - sqlx::query(r#"INSERT INTO "repos" VALUES (?, ?)"#) - .bind(repo) - .bind("") - .execute(&mut conn) - .await?; - } - - let (users,) = - sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ?"#) - .bind(repo) - .fetch_optional(&mut conn) - .await? - .unwrap(); - let mut users = users.split_whitespace().collect::<Vec<&str>>(); - - if let Some(index) = users.clone().into_iter().position(|x| x.eq(user)) { - if is_delete { - if clear_all { - users.clear(); - } else { - users.remove(index); - } - } else { - return Err(anyhow::Error::msg("User already in repository ACL")); - } - } - - if !is_delete { - users.push(user); - } - - sqlx::query(r#"UPDATE "repos" SET "users" = ? WHERE "repo" = ?"#) - .bind(users.join(" ")) - .bind(repo) - .execute(&mut conn) - .await?; - - let redis_key = format!("cgit_repo_{repo}"); - if redis_conn.exists::<_, i32>(&redis_key).await? == 0 { - redis_conn.sadd::<_, _, i32>(&redis_key, users).await?; - } else if is_delete { - if clear_all { - redis_conn.del::<_, i32>(&redis_key).await?; - } else { - redis_conn.srem::<_, _, i32>(&redis_key, user).await?; - } - } else { - redis_conn.sadd::<_, _, i32>(&redis_key, user).await?; - } - - if !clear_all { - println!( - "{} user {user} {} repository {repo} ACL successful", - if is_delete { "Delete" } else { "Add" }, - if is_delete { "from" } else { "to" }, - ); - } else { - println!("Clear all users from repository {repo} ACL"); - } - - Ok(()) -} - -async fn cmd_list_repos_acl(arg_matches: &ArgMatches, cfg: Config) -> Result<()> { - let repo = arg_matches - .get_one::<String>("repo") - .map(|s| s.as_str()) - .unwrap_or(""); - - let mut conn = SqliteConnectOptions::from_str(cfg.get_database_location())? - .read_only(true) - .immutable(true) - .connect() - .await?; - - if repo.is_empty() { - let (length,) = sqlx::query_as::<_, (i32,)>(r#"SELECT COUNT(*) FROM "repos""#) - .fetch_optional(&mut conn) - .await? - .unwrap_or((0,)); - - println!( - "There is total {length} {} in database", - if length == 1 { - "repository" - } else { - "repositories" - }, - ); - - let mut iter = - sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "repos""#).fetch(&mut conn); - while let Some(Ok((repo, users))) = iter.next().await { - println!("{repo}: {}", users.split_whitespace().join(",")) - } - } else { - let ret = - sqlx::query_as::<_, (String, String)>(r#"SELECT * FROM "repos" WHERE "repo" = ?"#) - .bind(repo) - .fetch_optional(&mut conn) - .await?; - if let Some((repo, users)) = ret { - println!("{repo}: {}", users.split_whitespace().join(",")) - } else { - println!("Repository {repo} not register in database") - } - } - - Ok(()) -} async fn async_main(arg_matches: ArgMatches) -> Result<i32> { let cfg = if env::args().any(|x| x.eq("--test")) { @@ -617,7 +37,7 @@ async fn async_main(arg_matches: ArgMatches) -> Result<i32> { }; match arg_matches.subcommand() { Some(("authenticate-cookie", matches)) => { - if let Ok(should_pass) = cmd_authenticate_cookie(matches, cfg).await { + if let Ok(should_pass) = authentication::cmd_authenticate_cookie(matches, cfg).await { if should_pass { return Ok(1); } @@ -628,46 +48,45 @@ async fn async_main(arg_matches: ArgMatches) -> Result<i32> { let input = stdin.lock(); let output = std::io::stdout(); - let mut module = IOModule { - reader: input, - writer: output, - }; + let mut module = authentication::IOModule::new(input, output); module.cmd_authenticate_post(matches, cfg).await?; } Some(("body", matches)) => { - cmd_body(matches, cfg).await; + authentication::cmd_body(matches, cfg).await; } Some(("user", matches)) => match matches.subcommand() { Some(("add", matches)) => { - cmd_add_user(matches, cfg).await?; + authentication::cmd_add_user(matches, cfg).await?; } Some(("del", matches)) => { - cmd_delete_user(matches, cfg).await?; + authentication::cmd_delete_user(matches, cfg).await?; } Some(("list", _matches)) => { - cmd_list_user(cfg).await?; + authentication::cmd_list_user(cfg).await?; } _ => {} }, Some(("database", matches)) => match matches.subcommand() { Some(("init", _matches)) => { - cmd_init(cfg).await?; + authentication::cmd_init(cfg).await?; } Some(("upgrade", _matches)) => { - cmd_upgrade_database(cfg).await?; + authentication::cmd_upgrade_database(cfg).await?; } Some(("reset", matches)) => { - cmd_reset_database(matches, cfg).await?; + authentication::cmd_reset_database(matches, cfg).await?; } _ => {} }, Some(("repo", matches)) => match matches.subcommand() { - Some(("add", matches)) => cmd_repo_user_control(matches, cfg, false).await?, + Some(("add", matches)) => { + authentication::cmd_repo_user_control(matches, cfg, false).await? + } Some(("del", matches)) => { - cmd_repo_user_control(matches, cfg, true).await?; + authentication::cmd_repo_user_control(matches, cfg, true).await?; } Some(("list", matches)) => { - cmd_list_repos_acl(matches, cfg).await?; + authentication::cmd_list_repos_acl(matches, cfg).await?; } _ => {} }, @@ -714,7 +133,7 @@ fn get_arg_matches(arguments: Option<Vec<&str>>) -> ArgMatches { ) .subcommand( Command::new("database") - .about("Database rated commands") + .about("Database related commands") .subcommand( Command::new("init") .about("Init sqlite database") @@ -735,7 +154,7 @@ fn get_arg_matches(arguments: Option<Vec<&str>>) -> ArgMatches { ) .subcommand( Command::new("user") - .about("Users rated commands") + .about("User related commands") .subcommand( Command::new("add") .about("Add user to database") @@ -758,7 +177,7 @@ fn get_arg_matches(arguments: Option<Vec<&str>>) -> ArgMatches { ) .subcommand( Command::new("repo") - .about("Repository ACL rated commands") + .about("Repository ACL related commands") .subcommand( Command::new("add") .about("Add user to repository") @@ -814,20 +233,18 @@ fn main() -> Result<()> { let logfile = match logfile { Ok(f) => f, Err(e) => { - return Err(anyhow::Error::msg(format!( + return Err(anyhow::anyhow!( "Got error while append to {logfile_path}: {e:?}", - ))); + )); } }; let config = log4rs::Config::builder() .appender(Appender::builder().build("logfile", Box::new(logfile))) - .logger( + .loggers([ log4rs::config::Logger::builder().build("handlebars::render", log::LevelFilter::Warn), - ) - .logger( log4rs::config::Logger::builder().build("handlebars::context", log::LevelFilter::Warn), - ) + ]) .build( Root::builder() .appender("logfile") diff --git a/src/test.rs b/src/test.rs index 3d54d71..f9c6981 100644 --- a/src/test.rs +++ b/src/test.rs @@ -18,17 +18,17 @@ #[cfg(test)] mod core { use crate::{ - IOModule, cmd_add_user, cmd_authenticate_cookie, cmd_init, cmd_repo_user_control, + authentication::{ + IOModule, cmd_add_user, cmd_authenticate_cookie, cmd_init, cmd_repo_user_control, + }, datastructures::{Config, TestSuite, rand_str}, get_arg_matches, }; use argon2::{ Argon2, - password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + password_hash::{PasswordHasher, PasswordVerifier, phc::PasswordHash}, }; use redis::AsyncCommands; - #[cfg(feature = "pam")] - use std::borrow::BorrowMut; use std::io::{Read, Write}; use std::path::Path; use std::path::PathBuf; @@ -37,13 +37,11 @@ mod core { #[test] fn test_argon2() { - use argon2::password_hash::rand_core::OsRng; let passwd = b"hunter2"; - let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); - argon2.hash_password(passwd, &salt).unwrap(); + argon2.hash_password(passwd).unwrap(); } #[test] @@ -55,8 +53,9 @@ mod core { } async fn async_test_redis() -> anyhow::Result<()> { - let redis_conn = redis::Client::open("redis://127.0.0.1/")?; - let mut conn = redis_conn.get_multiplexed_async_connection().await?; + let mut conn = + crate::datastructures::get_redis_connection(crate::datastructures::DEFAULT_REDIS_URL) + .await?; let s = rand_str(crate::datastructures::COOKIE_LENGTH); conn.set_ex::<_, _, String>("auth_test", &s, 60).await?; @@ -99,10 +98,7 @@ mod core { "/?p=login", ])); let mut output = Vec::new(); - let mut module = IOModule { - reader: &correct_input[..], - writer: &mut output, - }; + let mut module = IOModule::new(&correct_input[..], &mut output); let cfg = Config::generate_test_config(); @@ -347,10 +343,15 @@ mod core { } async fn clear_redis_setting() -> anyhow::Result<()> { - let client = redis::Client::open("redis://127.0.0.1")?; - let mut conn = client.get_multiplexed_async_connection().await?; + let mut conn = + crate::datastructures::get_redis_connection(crate::datastructures::DEFAULT_REDIS_URL) + .await?; - for key in &["cgit_repo_test", "cgit_repo_repo"] { + for key in &[ + "cgit_repo_test", + "cgit_repo_repo", + "cgit_auth_failed_hunter2", + ] { conn.del::<_, i32>(*key).await?; } Ok(()) @@ -367,19 +368,4 @@ mod core { .block_on(clear_redis_setting()) .unwrap(); } - - #[cfg(feature = "pam")] - #[ignore] - #[test] - fn test_pam() { - let service = option_env!("pam_service").unwrap_or("system-auth"); - let user = option_env!("pam_user").unwrap_or("user"); - let password = option_env!("pam_password").unwrap_or("password"); - - let mut auth = pam::Client::with_password(service).unwrap(); - auth.conversation_mut() - .borrow_mut() - .set_credentials(user, password); - assert!(auth.authenticate().is_ok() && auth.open_session().is_ok()) - } } |
