aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKunoiSayami <[email protected]>2021-05-15 02:38:31 +0800
committerKunoiSayami <[email protected]>2021-05-15 02:38:31 +0800
commit8d68ac0b56fa6be0eb5215f346d513b9a2ef37b8 (patch)
treefc0f0881a842759cc4fe8ac42abf3917bc85a72f
parent722d7da76380d8f3814f5f2a59e3ef14928b6203 (diff)
feat(core): Set protect policy default enabledv0.4.1
* fix(auth): Fix authenticate cookie failure in root page
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml5
-rw-r--r--src/datastructures.rs196
-rw-r--r--src/main.rs148
4 files changed, 188 insertions, 168 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cb44441..964e52e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -377,12 +377,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cgit-simple-authentication"
-version = "0.4.0"
+version = "0.4.1"
dependencies = [
"anyhow",
"argon2",
"base64",
"clap",
+ "cpufeatures",
"env_logger",
"handlebars",
"log",
@@ -473,9 +474,9 @@ checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "cpufeatures"
-version = "0.1.1"
+version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dec1028182c380cc45a2e2c5ec841134f2dfd0f8f5f0a5bcd68004f81b5efdf4"
+checksum = "281f563b2c3a0e535ab12d81d3c5859045795256ad269afa7c19542585b68f93"
dependencies = [
"libc",
]
diff --git a/Cargo.toml b/Cargo.toml
index 6f038c4..b0a36b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "cgit-simple-authentication"
-version = "0.4.0"
+version = "0.4.1"
authors = ["KunoiSayami <[email protected]>"]
edition = "2018"
@@ -29,4 +29,5 @@ tempdir = "0.3"
regex = "1"
[target.aarch64-unknown-linux-musl.dependencies]
-openssl = { version = "0.10", features = ["vendored"] } \ No newline at end of file
+openssl = { version = "0.10", features = ["vendored"] }
+cpufeatures = "0.1.3" \ No newline at end of file
diff --git a/src/datastructures.rs b/src/datastructures.rs
index 81e04aa..2f5230c 100644
--- a/src/datastructures.rs
+++ b/src/datastructures.rs
@@ -78,11 +78,28 @@ pub(crate) trait TestSuite {
pub struct Config {
pub cookie_ttl: u64,
database: String,
- //access_node: hashmap,
pub bypass_root: bool,
- //secret: String,
pub(crate) test: bool,
- protected_repos: ProtectedRepo,
+
+ /// To set specify repository protect, You should setup repo's protect attribute
+ /// First, set cgit-simple-auth-protect to none in /etc/cgitrc file
+ ///
+ /// # Examples
+ ///
+ /// In /etc/cgitrc:
+ /// ```conf
+ /// cgit-simple-auth-protect=none
+ /// ```
+ ///
+ /// In repo.conf
+ /// ```conf
+ /// repo.url=test
+ /// repo.protected=true
+ /// ```
+ ///
+ /// Default behavior is protect all repository
+ protect_all: bool,
+ protected_repos: Vec<String>,
}
impl Default for Config {
@@ -91,8 +108,8 @@ impl Default for Config {
cookie_ttl: DEFAULT_COOKIE_TTL,
database: DEFAULT_DATABASE_LOCATION.to_string(),
bypass_root: false,
- //secret: Default::default(),
test: false,
+ protect_all: true,
protected_repos: Default::default(),
}
}
@@ -105,10 +122,12 @@ impl Config {
pub fn load_from_path<P: AsRef<Path>>(path: P) -> Self {
let file = read_to_string(&path).unwrap_or_default();
+
let mut cookie_ttl: u64 = DEFAULT_COOKIE_TTL;
let mut database: &str = "/etc/cgit/auth.db";
let mut bypass_root: bool = false;
- //let mut secret: &str = "";
+ let mut protect_all: bool = true;
+
for line in file.lines() {
let line = line.trim();
if !line.contains('=') || !line.starts_with("cgit-simple-auth-") {
@@ -120,23 +139,74 @@ impl Config {
} else {
line.split_once('=').unwrap()
};
+ let value = value.trim();
let key_name = key.split_once("auth-").unwrap().1.trim();
match key_name {
"cookie-ttl" => cookie_ttl = value.parse().unwrap_or(DEFAULT_COOKIE_TTL),
"database" => database = value,
"bypass-root" => bypass_root = value.to_lowercase().eq("true"),
- //"secret" => secret = value,
+ "protect" => protect_all = !value.to_lowercase().eq("none"),
_ => {}
}
}
+
+ let protected_repos = if !protect_all {
+ Self::load_protect_repos_from_file(path)
+ } else {
+ Default::default()
+ };
+
Self {
cookie_ttl,
database: database.to_string(),
bypass_root,
test: false,
- //secret: secret.to_string(),
- protected_repos: ProtectedRepo::load_from_file(path),
+ protect_all,
+ protected_repos,
+ }
+ }
+
+ pub fn load_protect_repos_from_file<P: AsRef<Path>>(path: P) -> Vec<String> {
+ let context = read_to_string(path).unwrap();
+
+ let mut protect_repos = Vec::new();
+
+ let mut current_repo: &str = "";
+
+ for line in context.lines() {
+ let line = line.trim();
+ if line.starts_with('#') || !line.contains('=') {
+ continue;
+ }
+
+ let (key, value) = if line.contains('#') {
+ line.split_once('#')
+ .unwrap()
+ .0
+ .trim()
+ .split_once('=')
+ .unwrap()
+ } else {
+ line.split_once('=').unwrap()
+ };
+
+ if key.eq("repo.url") {
+ current_repo = value.trim();
+ continue;
+ }
+
+ if key.eq("repo.protect") {
+ if value.trim().to_lowercase().eq("true") && !current_repo.is_empty() {
+ protect_repos.push(current_repo.to_string());
+ }
+ }
+
+ if key.eq("include") {
+ let r = Self::load_protect_repos_from_file(value);
+ protect_repos.extend(r)
+ }
}
+ protect_repos
}
pub fn get_database_location(&self) -> &str {
@@ -206,7 +276,15 @@ impl Config {
}
pub fn check_repo_protect(&self, repo: &str) -> bool {
- self.protected_repos.query_is_protected(repo)
+ if self.protect_all {
+ return true;
+ }
+ self.protected_repos.iter().any(|x| x.eq(repo))
+ }
+
+ #[cfg(test)]
+ pub(crate) fn query_is_all_protected(&self) -> bool {
+ self.protect_all
}
}
@@ -217,7 +295,8 @@ impl TestSuite for Config {
bypass_root: false,
cookie_ttl: DEFAULT_COOKIE_TTL,
test: true,
- protected_repos: Default::default(),
+ protect_all: false,
+ protected_repos: vec!["test".to_string()],
}
}
}
@@ -402,100 +481,3 @@ impl std::fmt::Display for Cookie {
write!(f, "{}", base64::encode(s))
}
}
-
-
-/// To set specify repository protect, You should setup repo's protect attribute
-///
-/// # Examples
-///
-/// ```conf
-/// repo.url=test
-/// repo.protected=true
-/// ```
-///
-/// OR:
-///
-/// Set all repository under protected, by set cgit-simple-auth-protect=full
-
-#[derive(Debug, Clone)]
-pub struct ProtectedRepo {
- protect_all: bool,
- protect_repos: Vec<String>,
-}
-
-impl ProtectedRepo {
-
- pub fn load_from_file<P: AsRef<Path>>(path: P) -> Self {
- let context = read_to_string(path).unwrap();
-
- let mut protect_repos = Vec::new();
-
- let mut protect_all = false;
- let mut current_repo: &str = "";
-
- for line in context.lines() {
- let line = line.trim();
- if line.starts_with('#') || !line.contains('=') {
- continue
- }
-
- let (key, value) = if line.contains('#') {
- line.split_once('#').unwrap().0.trim().split_once('=').unwrap()
- } else {
- line.split_once('=').unwrap()
- };
-
- if key.eq("repo.url") {
- current_repo = value.trim();
- continue
- }
-
- if key.eq("repo.protect") {
- if value.trim().to_lowercase().eq("true") && !current_repo.is_empty() {
- protect_repos.push(current_repo.to_string());
- }
- }
-
- if key.eq("include") {
- let r = Self::load_from_file(value);
- if r.protect_all {
- protect_all = true;
- break
- }
- protect_repos.extend(r.protect_repos)
- }
-
- if key.eq("cgit-simple-auth-protect") && value.trim().to_lowercase().eq("full") {
- protect_all = true;
- break
- }
- }
- if protect_all {
- protect_repos.clear();
- }
- Self {
- protect_all,
- protect_repos,
- }
- }
-
- pub fn query_is_protected(&self, repo: &str) -> bool {
- if self.protect_all {
- return true
- }
- self.protect_repos.iter().any(|x| x.eq(repo))
- }
-
- pub fn query_is_all_protected(&self) -> bool {
- self.protect_all
- }
-}
-
-impl Default for ProtectedRepo {
- fn default() -> Self {
- Self {
- protect_all: true,
- protect_repos: Default::default()
- }
- }
-}
diff --git a/src/main.rs b/src/main.rs
index 7edd019..b422db1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -55,7 +55,6 @@ impl<R: BufRead, W: Write> IOModule<R, W> {
//log::debug!("{}", buffer);
let data = datastructures::FormData::from(buffer);
-
let ret = verify_login(&cfg, &data).await;
if let Err(ref e) = ret {
@@ -112,7 +111,7 @@ async fn cmd_authenticate_cookie(matches: &ArgMatches<'_>, cfg: Config) -> Resul
bypass = true;
}
- if bypass || !cfg.check_repo_protect(repo){
+ if bypass || (!repo.is_empty() && !cfg.check_repo_protect(repo)) {
return Ok(true);
}
@@ -123,24 +122,23 @@ async fn cmd_authenticate_cookie(matches: &ArgMatches<'_>, cfg: Config) -> Resul
let redis_conn = redis::Client::open("redis://127.0.0.1/")?;
let mut conn = redis_conn.get_async_connection().await?;
+ let redis_key = format!("cgit_repo_{}", repo);
if !repo.is_empty() {
- let key = format!("cgit_repo_{}", repo);
- if !conn.exists(&key).await? {
+ if !conn.exists(&redis_key).await? {
let mut sql_conn = SqliteConnectOptions::from_str(cfg.get_database_location())?
.read_only(true)
.connect()
.await?;
- if let Some((users, )) =
- sqlx::query_as::<_, (String, )>(r#"SELECT "users" FROM "repos" WHERE "repo" = ? "#)
- .bind(repo)
- .fetch_optional(&mut sql_conn)
- .await?
+ if let Some((users,)) =
+ sqlx::query_as::<_, (String,)>(r#"SELECT "users" FROM "repos" WHERE "repo" = ? "#)
+ .bind(repo)
+ .fetch_optional(&mut sql_conn)
+ .await?
{
- let iter = users.split_whitespace().collect::<Vec<&str>>();
- conn.sadd(&key, iter).await?;
+ let users = users.split_whitespace().collect::<Vec<&str>>();
+ conn.sadd(&redis_key, users).await?;
}
}
- // TODO: redis repository ACL check should goes here
}
if let Ok(Some(cookie)) = Cookie::load_from_request(cookies) {
@@ -149,7 +147,16 @@ async fn cmd_authenticate_cookie(matches: &ArgMatches<'_>, cfg: Config) -> Resul
.await
{
if cookie.eq_body(r.as_str()) {
- return Ok(true);
+ if repo.is_empty() {
+ return Ok(true);
+ }
+ if conn
+ .sismember::<_, _, i32>(&redis_key, cookie.get_user())
+ .await?
+ == 1
+ {
+ return Ok(true);
+ }
}
}
log::debug!("{:?}", cookie);
@@ -209,12 +216,11 @@ async fn verify_login(cfg: &Config, data: &FormData) -> Result<bool> {
.connect()
.await?;
- let (passwd_hash,) = sqlx::query_as::<_, (String,)>(
- r#"SELECT "password" FROM "accounts" WHERE "user" = ?"#,
- )
- .bind(data.get_user())
- .fetch_one(&mut conn)
- .await?;
+ let (passwd_hash,) =
+ sqlx::query_as::<_, (String,)>(r#"SELECT "password" FROM "accounts" WHERE "user" = ?"#)
+ .bind(data.get_user())
+ .fetch_one(&mut conn)
+ .await?;
let parsed_hash = PasswordHash::new(passwd_hash.as_str()).unwrap();
Ok(data.verify_password(&parsed_hash))
@@ -426,13 +432,20 @@ async fn cmd_upgrade_database(cfg: Config) -> Result<()> {
Ok(())
}
-async fn cmd_repo_user_control(matches: &ArgMatches<'_>, cfg: Config, is_delete: bool) -> Result<()> {
+async fn cmd_repo_user_control(
+ matches: &ArgMatches<'_>,
+ cfg: Config,
+ is_delete: bool,
+) -> Result<()> {
let repo = matches.value_of("repo").unwrap_or("");
let user = matches.value_of("user").unwrap_or("");
let clear_all = matches.is_present("clear-all");
- if repo.is_empty() || (is_delete && !clear_all && user.is_empty()) || (!is_delete && user.is_empty()) {
+ 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"));
}
@@ -445,10 +458,11 @@ async fn cmd_repo_user_control(matches: &ArgMatches<'_>, cfg: Config, is_delete:
.bind(repo)
.fetch_optional(&mut conn)
.await?
- .is_none() {
+ .is_none()
+ {
if is_delete {
println!("Row is empty.");
- return Ok(())
+ return Ok(());
}
sqlx::query(r#"INSERT INTO "repos" VALUES (?, ?)"#)
.bind(repo)
@@ -488,7 +502,7 @@ async fn cmd_repo_user_control(matches: &ArgMatches<'_>, cfg: Config, is_delete:
.await?;
let redis_key = format!("cgit_repo_{}", repo);
- if redis_conn.exists::<_, i32>(&redis_key).await? == 0{
+ if redis_conn.exists::<_, i32>(&redis_key).await? == 0 {
redis_conn.sadd::<_, _, i32>(&redis_key, users).await?;
} else {
if is_delete {
@@ -503,11 +517,12 @@ async fn cmd_repo_user_control(matches: &ArgMatches<'_>, cfg: Config, is_delete:
}
if !clear_all {
- println!("{} user {} {} repository {} ACL successful",
- if is_delete { "Delete" } else { "Add" },
- user,
- if is_delete { "from" } else { "to" },
- repo,
+ println!(
+ "{} user {} {} repository {} ACL successful",
+ if is_delete { "Delete" } else { "Add" },
+ user,
+ if is_delete { "from" } else { "to" },
+ repo,
);
} else {
println!("Clear all users from repository {} ACL", repo);
@@ -562,9 +577,7 @@ async fn async_main(arg_matches: ArgMatches<'_>) -> Result<i32> {
("upgrade", Some(_matches)) => {
cmd_upgrade_database(cfg).await?;
}
- ("repoadd", Some(matches)) => {
- cmd_repo_user_control(matches, cfg, false).await?
- }
+ ("repoadd", Some(matches)) => cmd_repo_user_control(matches, cfg, false).await?,
("repodel", Some(matches)) => {
cmd_repo_user_control(matches, cfg, true).await?;
}
@@ -713,8 +726,8 @@ fn main() -> Result<()> {
#[cfg(test)]
mod test {
- use crate::datastructures::{rand_str, Config, TestSuite, ProtectedRepo};
- use crate::{cmd_add_user, cmd_authenticate_cookie, cmd_init};
+ use crate::datastructures::{rand_str, Config, TestSuite};
+ use crate::{cmd_add_user, cmd_authenticate_cookie, cmd_init, cmd_repo_user_control};
use crate::{get_arg_matches, IOModule};
use argon2::{
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
@@ -870,12 +883,24 @@ mod test {
#[test]
fn test_03_insert_repo() {
lock(&PathBuf::from("test/USER_WRITTEN"), 5);
- let matches = crate::get_arg_matches(Some(vec!["a", "repoadd", "hunter2", "hunter2"]));
+ let matches = crate::get_arg_matches(Some(vec!["a", "repoadd", "test", "hunter2"]));
+ match matches.subcommand() {
+ ("repoadd", Some(matches)) => {
+ tokio::runtime::Builder::new_current_thread()
+ .enable_all()
+ .build()
+ .unwrap()
+ .block_on(cmd_repo_user_control(matches, Config::generate_test_config(), false))
+ .unwrap();
+ std::fs::File::create("test/REPO_USER_ADDED").unwrap();
+ }
+ _ => unreachable!()
+ }
}
#[test]
fn test_91_auth_pass() {
- lock(&PathBuf::from("test/USER_WRITTEN"), 7);
+ lock(&PathBuf::from("test/REPO_USER_ADDED"), 7);
let s = test_auth_post();
@@ -893,6 +918,16 @@ mod test {
#[test]
fn test_92_authenticate_cookie() {
+ test_authenticate_cookie("test");
+ }
+
+ #[test]
+ #[should_panic]
+ fn test_93_authenticate_cookie() {
+ test_authenticate_cookie("repo");
+ }
+
+ fn test_authenticate_cookie(repo: &str) {
lock(&PathBuf::from("test/RESPONSE"), 15);
let mut buffer = String::new();
@@ -923,7 +958,7 @@ mod test {
"/",
"git.example.com",
"on",
- "",
+ repo,
"",
"/",
"/?p=login",
@@ -958,31 +993,32 @@ mod test {
fn test_02_protected_repo_parser() {
let tmpdir = tempdir::TempDir::new("test").unwrap();
- let another_file_path = format!("include={}/REPO_SETTING # TEST", tmpdir.path().to_str().unwrap());
+ let another_file_path = format!(
+ "include={}/REPO_SETTING # TEST\ncgit-simple-auth-protect=none",
+ tmpdir.path().to_str().unwrap()
+ );
write_to_specify_file(&tmpdir.path().join("CFG"), another_file_path.as_bytes()).unwrap();
+ write_to_specify_file(
+ &tmpdir.path().join("REPO_SETTING"),
+ b"repo.url=test\nrepo.protect=true",
+ )
+ .unwrap();
+ let cfg = Config::load_from_path(tmpdir.path().join("CFG"));
- write_to_specify_file(&tmpdir.path().join("REPO_SETTING"), b"repo.url=test\nrepo.protect=true").unwrap();
-
-
- let result = ProtectedRepo::load_from_file(tmpdir.path().join("CFG"));
-
- assert!(result.query_is_protected("test"));
- assert!(!result.query_is_all_protected());
-
- write_to_specify_file(&tmpdir.path().join("REPO_SETTING"), b"repo.protect=true\nrepo.url=test").unwrap();
-
- let result = ProtectedRepo::load_from_file(tmpdir.path().join("CFG"));
-
- assert!(!result.query_is_protected("test"));
- assert!(!result.query_is_all_protected());
+ assert!(cfg.check_repo_protect("test"));
+ assert!(!cfg.query_is_all_protected());
- write_to_specify_file(&tmpdir.path().join("REPO_SETTING"), b"repo.protect=true\nrepo.url=test\n\ncgit-simple-auth-protect=full").unwrap();
+ write_to_specify_file(
+ &tmpdir.path().join("REPO_SETTING"),
+ b"repo.protect=true\nrepo.url=test",
+ )
+ .unwrap();
- let result = ProtectedRepo::load_from_file(tmpdir.path().join("CFG"));
+ let cfg = Config::load_from_path(tmpdir.path().join("CFG"));
- assert!(result.query_is_all_protected());
- assert!(result.query_is_protected("test"));
+ assert!(!cfg.check_repo_protect("test"));
+ assert!(!cfg.query_is_all_protected());
tmpdir.close().unwrap();
}