From b698b98feef5c154211100509ca4e673796f9886 Mon Sep 17 00:00:00 2001 From: wiryls Date: Tue, 21 Sep 2021 19:48:44 +0800 Subject: [PATCH 1/6] feat: try to add acl --- josh-proxy/src/acl.rs | 47 ++++++++++++++++++++++++++++++++ josh-proxy/src/bin/josh-proxy.rs | 23 ++++++++++++++++ josh-proxy/src/lib.rs | 1 + 3 files changed, 71 insertions(+) create mode 100644 josh-proxy/src/acl.rs diff --git a/josh-proxy/src/acl.rs b/josh-proxy/src/acl.rs new file mode 100644 index 00000000..3099fb8c --- /dev/null +++ b/josh-proxy/src/acl.rs @@ -0,0 +1,47 @@ +use toml; +use regex::Regex; +use serde::Deserialize; +use std::{collections::HashMap, result::Result}; + +#[derive(Clone)] +pub struct Validator { + rules: HashMap>, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Error { + Toml(toml::de::Error), + Regex(regex::Error), +} + +impl Validator { + + pub fn from_toml(src: &str) -> std::result::Result { + // parse Vec from text + let parse: Rule = toml::from_str(&src).map_err(Error::Toml)?; + // map Vec into HashMap> + let rules = parse.rule + .unwrap_or(Vec::new()) + .into_iter() + .map(|u| (u.user, u.repo.into_iter().map(|v| Regex::new(v.as_str()).map_err(Error::Regex)).collect::, Error>>())) + .map(|(w, x)| x.map(|y| (w, y))) + .collect::>, Error>>() + ?; + Ok(Validator{rules}) + } + + pub fn is_accessible(self: &Validator, user: &str, path: &str) -> bool { + self.rules.get(user).map(|x| x.iter().any(|r| r.is_match(path))).unwrap_or(true) + } +} + +#[derive(Debug, Deserialize)] +struct Rule { + pub rule: Option>, +} + +#[derive(Debug, Deserialize)] +struct Entry { + pub user: String, + pub repo: Vec, +} diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs index bf8174f0..5f21615f 100644 --- a/josh-proxy/src/bin/josh-proxy.rs +++ b/josh-proxy/src/bin/josh-proxy.rs @@ -42,6 +42,7 @@ struct JoshProxyService { fetch_timers: Arc>, fetch_permits: Arc, filter_permits: Arc, + validator: josh_proxy::acl::Validator, poll: Polls, } @@ -398,6 +399,14 @@ async fn call_service( return Ok(builder.body(hyper::Body::empty())?); } + if !serv.validator.is_accessible(&username, &remote_url) { + tracing::trace!("acl-validator"); + let builder = Response::builder() + .header("WWW-Authenticate", "Basic realm=User Visible Realm") + .status(hyper::StatusCode::UNAUTHORIZED); + return Ok(builder.body(hyper::Body::empty())?); + } + match fetch_upstream( serv.clone(), parsed_url.upstream_repo.to_owned(), @@ -605,6 +614,13 @@ async fn run_proxy() -> josh::JoshResult { josh_proxy::create_repo(&local)?; josh::cache::load(&local)?; + let validator = match ARGS.value_of("acl") { + Some(p) => std::fs::read_to_string(p).map_err(|_| josh::josh_error("failed to read acl file"))?, + None => "".to_string(), + }; + let validator = josh_proxy::acl::Validator::from_toml(validator.as_str()) + .map_err(|_| josh::josh_error("errors in acl file"))?; + let proxy_service = Arc::new(JoshProxyService { port, repo_path: local.to_owned(), @@ -615,6 +631,7 @@ async fn run_proxy() -> josh::JoshResult { ARGS.value_of("n").unwrap_or("1").parse()?, )), filter_permits: Arc::new(tokio::sync::Semaphore::new(10)), + validator: validator, }); let ps = proxy_service.clone(); @@ -769,6 +786,12 @@ fn parse_args() -> clap::ArgMatches<'static> { .help("Duration between forced cache refresh") .takes_value(true), ) + .arg( + clap::Arg::with_name("acl") + .long("acl") + .help("Specify a config file to control user access") + .takes_value(true) + ) .get_matches_from(args) } diff --git a/josh-proxy/src/lib.rs b/josh-proxy/src/lib.rs index 3f4c13ab..830ac8f1 100644 --- a/josh-proxy/src/lib.rs +++ b/josh-proxy/src/lib.rs @@ -1,3 +1,4 @@ +pub mod acl; pub mod auth; pub mod juniper_hyper; From 3a88744ed03e5a1a5506958e369ad60d940a8bdf Mon Sep 17 00:00:00 2001 From: Nex Zhu <4370605+NexZhu@users.noreply.github.com> Date: Wed, 22 Sep 2021 23:28:58 +0800 Subject: [PATCH 2/6] chore: add `JOSH_ACL` option to `run-josh.sh` --- run-josh.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/run-josh.sh b/run-josh.sh index cbe54f1d..b60f2043 100644 --- a/run-josh.sh +++ b/run-josh.sh @@ -1,3 +1,4 @@ #!/bin/bash +[ -n "${JOSH_ACL}" ] && acl_arg="--acl ${JOSH_ACL}" cd /josh/ -RUST_BACKTRACE=1 josh-proxy --gc --local=/data/git/ --remote="${JOSH_REMOTE}" ${JOSH_EXTRA_OPTS} +RUST_BACKTRACE=1 josh-proxy --gc --local=/data/git/ --remote="${JOSH_REMOTE}" ${acl_arg} ${JOSH_EXTRA_OPTS} From fc789a78fe37d8062a9b6d1a2559f560e89073b7 Mon Sep 17 00:00:00 2001 From: wiryls Date: Fri, 24 Sep 2021 00:23:10 +0800 Subject: [PATCH 3/6] feat: redesign toml schema --- josh-proxy/src/acl.rs | 59 ++++++++++++++++++++++++-------- josh-proxy/src/bin/josh-proxy.rs | 3 +- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/josh-proxy/src/acl.rs b/josh-proxy/src/acl.rs index 3099fb8c..cf6190e3 100644 --- a/josh-proxy/src/acl.rs +++ b/josh-proxy/src/acl.rs @@ -3,9 +3,10 @@ use regex::Regex; use serde::Deserialize; use std::{collections::HashMap, result::Result}; -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct Validator { - rules: HashMap>, + // note: repo user paths + rules: HashMap>>, } #[derive(Debug, Clone, PartialEq)] @@ -17,31 +18,59 @@ pub enum Error { impl Validator { pub fn from_toml(src: &str) -> std::result::Result { - // parse Vec from text - let parse: Rule = toml::from_str(&src).map_err(Error::Toml)?; - // map Vec into HashMap> - let rules = parse.rule + // parse Rule from text + let raw: Doc = toml::from_str(&src).map_err(Error::Toml)?; + + // map Rule into HashMap>> + let dst = |x: Vec| x + .into_iter() + .map(|v| Regex::new(v.as_str()).map_err(Error::Regex)) + .collect::, Error>>() + ; + let dst = |x: Option>| x .unwrap_or(Vec::new()) .into_iter() - .map(|u| (u.user, u.repo.into_iter().map(|v| Regex::new(v.as_str()).map_err(Error::Regex)).collect::, Error>>())) - .map(|(w, x)| x.map(|y| (w, y))) + .map(|m| (m.user, dst(m.path))) + .map(|(s, r)| r.map(|v| (s, v))) .collect::>, Error>>() + ; + let dst = raw.repo + .unwrap_or(Vec::new()) + .into_iter() + .map(|r| (r.name, dst(r.rule))) + .map(|(s, r)| r.map(|v| (s, v))) + .collect::, Error>>() ?; - Ok(Validator{rules}) + + // return value + Ok(Validator{rules: dst}) } - pub fn is_accessible(self: &Validator, user: &str, path: &str) -> bool { - self.rules.get(user).map(|x| x.iter().any(|r| r.is_match(path))).unwrap_or(true) + pub fn is_accessible(self: &Validator, user: &str, repo: &str, path: &str) -> bool { + // e.g. if "we" want to access "http://localhost:8080/a/b.git:/c/d.git" + // then user = we, repo = a/b, path = c/d + let repo = repo.trim_end_matches(".git"); + let path = path.trim_start_matches(":/"); + self.rules + .get(repo) + .and_then(|r| r.get(user).map(|x| x.iter().any(|r| r.is_match(path)))) + .unwrap_or(false) } } #[derive(Debug, Deserialize)] -struct Rule { - pub rule: Option>, +struct Doc { + pub repo: Option>, +} + +#[derive(Debug, Deserialize)] +struct Repo { + pub name: String, + pub rule: Option>, } #[derive(Debug, Deserialize)] -struct Entry { +struct Match { pub user: String, - pub repo: Vec, + pub path: Vec, } diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs index 5f21615f..287e4de0 100644 --- a/josh-proxy/src/bin/josh-proxy.rs +++ b/josh-proxy/src/bin/josh-proxy.rs @@ -399,7 +399,8 @@ async fn call_service( return Ok(builder.body(hyper::Body::empty())?); } - if !serv.validator.is_accessible(&username, &remote_url) { + // e.g. "http://localhost:port/a/b.git:/c/d.git" will become "a/b.git" ":/c/d" + if !serv.validator.is_accessible(&username, &parsed_url.upstream_repo, &parsed_url.filter) { tracing::trace!("acl-validator"); let builder = Response::builder() .header("WWW-Authenticate", "Basic realm=User Visible Realm") From 52059de3157dd4af4ec4aa358e10b265612afc8f Mon Sep 17 00:00:00 2001 From: wiryls Date: Fri, 24 Sep 2021 10:37:36 +0800 Subject: [PATCH 4/6] fix: allow to not use acl file --- josh-proxy/src/acl.rs | 30 +++++++++++++++++++----------- josh-proxy/src/bin/josh-proxy.rs | 11 +++++++---- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/josh-proxy/src/acl.rs b/josh-proxy/src/acl.rs index cf6190e3..912a9bc9 100644 --- a/josh-proxy/src/acl.rs +++ b/josh-proxy/src/acl.rs @@ -5,8 +5,8 @@ use std::{collections::HashMap, result::Result}; #[derive(Debug, Clone)] pub struct Validator { - // note: repo user paths - rules: HashMap>>, + // note: repo user paths + rules: Option>>>, } #[derive(Debug, Clone, PartialEq)] @@ -17,6 +17,10 @@ pub enum Error { impl Validator { + pub fn new() -> Validator { + Validator{rules: None} + } + pub fn from_toml(src: &str) -> std::result::Result { // parse Rule from text let raw: Doc = toml::from_str(&src).map_err(Error::Toml)?; @@ -43,18 +47,22 @@ impl Validator { ?; // return value - Ok(Validator{rules: dst}) + Ok(Validator{rules: Some(dst)}) } pub fn is_accessible(self: &Validator, user: &str, repo: &str, path: &str) -> bool { - // e.g. if "we" want to access "http://localhost:8080/a/b.git:/c/d.git" - // then user = we, repo = a/b, path = c/d - let repo = repo.trim_end_matches(".git"); - let path = path.trim_start_matches(":/"); - self.rules - .get(repo) - .and_then(|r| r.get(user).map(|x| x.iter().any(|r| r.is_match(path)))) - .unwrap_or(false) + match &self.rules { + None => true, + Some(rule) => { + // e.g. if "we" want to access "http://localhost:8080/a/b.git:/c/d.git" + // then user = we, repo = a/b, path = c/d + let repo = repo.trim_end_matches(".git"); + let path = path.trim_start_matches(":/"); + rule.get(repo) + .and_then(|r| r.get(user).map(|x| x.iter().any(|r| r.is_match(path)))) + .unwrap_or(false) + } + } } } diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs index 287e4de0..4ea747b3 100644 --- a/josh-proxy/src/bin/josh-proxy.rs +++ b/josh-proxy/src/bin/josh-proxy.rs @@ -616,11 +616,14 @@ async fn run_proxy() -> josh::JoshResult { josh::cache::load(&local)?; let validator = match ARGS.value_of("acl") { - Some(p) => std::fs::read_to_string(p).map_err(|_| josh::josh_error("failed to read acl file"))?, - None => "".to_string(), + None => josh_proxy::acl::Validator::new(), + Some(path) => { + let text = std::fs::read_to_string(path) + .map_err(|_| josh::josh_error("failed to read acl file"))?; + josh_proxy::acl::Validator::from_toml(text.as_str()) + .map_err(|_| josh::josh_error("errors in acl file"))? + } }; - let validator = josh_proxy::acl::Validator::from_toml(validator.as_str()) - .map_err(|_| josh::josh_error("errors in acl file"))?; let proxy_service = Arc::new(JoshProxyService { port, From 6b7c4a4dbdd9ef92d8d95ac7ef64b304b37af3e4 Mon Sep 17 00:00:00 2001 From: Nex Zhu <4370605+NexZhu@users.noreply.github.com> Date: Thu, 30 Sep 2021 17:52:22 +0800 Subject: [PATCH 5/6] fix: acl: trim starting "/" in repo name --- josh-proxy/src/acl.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/josh-proxy/src/acl.rs b/josh-proxy/src/acl.rs index 912a9bc9..ddc7bbc7 100644 --- a/josh-proxy/src/acl.rs +++ b/josh-proxy/src/acl.rs @@ -56,6 +56,7 @@ impl Validator { Some(rule) => { // e.g. if "we" want to access "http://localhost:8080/a/b.git:/c/d.git" // then user = we, repo = a/b, path = c/d + let repo = repo.trim_start_matches("/"); let repo = repo.trim_end_matches(".git"); let path = path.trim_start_matches(":/"); rule.get(repo) From 42ce8e733db00f5762a278eca72f12d968ecf37a Mon Sep 17 00:00:00 2001 From: wiryls Date: Fri, 1 Oct 2021 00:38:25 +0800 Subject: [PATCH 6/6] fix: run cargo fmt --- josh-proxy/src/acl.rs | 39 ++++++++++++++++---------------- josh-proxy/src/bin/josh-proxy.rs | 9 +++++--- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/josh-proxy/src/acl.rs b/josh-proxy/src/acl.rs index ddc7bbc7..81d6c242 100644 --- a/josh-proxy/src/acl.rs +++ b/josh-proxy/src/acl.rs @@ -1,7 +1,7 @@ -use toml; use regex::Regex; use serde::Deserialize; use std::{collections::HashMap, result::Result}; +use toml; #[derive(Debug, Clone)] pub struct Validator { @@ -16,9 +16,8 @@ pub enum Error { } impl Validator { - pub fn new() -> Validator { - Validator{rules: None} + Validator { rules: None } } pub fn from_toml(src: &str) -> std::result::Result { @@ -26,33 +25,33 @@ impl Validator { let raw: Doc = toml::from_str(&src).map_err(Error::Toml)?; // map Rule into HashMap>> - let dst = |x: Vec| x - .into_iter() - .map(|v| Regex::new(v.as_str()).map_err(Error::Regex)) - .collect::, Error>>() - ; - let dst = |x: Option>| x - .unwrap_or(Vec::new()) - .into_iter() - .map(|m| (m.user, dst(m.path))) - .map(|(s, r)| r.map(|v| (s, v))) - .collect::>, Error>>() - ; - let dst = raw.repo + let dst = |x: Vec| { + x.into_iter() + .map(|v| Regex::new(v.as_str()).map_err(Error::Regex)) + .collect::, Error>>() + }; + let dst = |x: Option>| { + x.unwrap_or(Vec::new()) + .into_iter() + .map(|m| (m.user, dst(m.path))) + .map(|(s, r)| r.map(|v| (s, v))) + .collect::>, Error>>() + }; + let dst = raw + .repo .unwrap_or(Vec::new()) .into_iter() .map(|r| (r.name, dst(r.rule))) .map(|(s, r)| r.map(|v| (s, v))) - .collect::, Error>>() - ?; + .collect::, Error>>()?; // return value - Ok(Validator{rules: Some(dst)}) + Ok(Validator { rules: Some(dst) }) } pub fn is_accessible(self: &Validator, user: &str, repo: &str, path: &str) -> bool { match &self.rules { - None => true, + None => true, Some(rule) => { // e.g. if "we" want to access "http://localhost:8080/a/b.git:/c/d.git" // then user = we, repo = a/b, path = c/d diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs index 4ea747b3..0ca1fe97 100644 --- a/josh-proxy/src/bin/josh-proxy.rs +++ b/josh-proxy/src/bin/josh-proxy.rs @@ -400,7 +400,10 @@ async fn call_service( } // e.g. "http://localhost:port/a/b.git:/c/d.git" will become "a/b.git" ":/c/d" - if !serv.validator.is_accessible(&username, &parsed_url.upstream_repo, &parsed_url.filter) { + if !serv + .validator + .is_accessible(&username, &parsed_url.upstream_repo, &parsed_url.filter) + { tracing::trace!("acl-validator"); let builder = Response::builder() .header("WWW-Authenticate", "Basic realm=User Visible Realm") @@ -616,7 +619,7 @@ async fn run_proxy() -> josh::JoshResult { josh::cache::load(&local)?; let validator = match ARGS.value_of("acl") { - None => josh_proxy::acl::Validator::new(), + None => josh_proxy::acl::Validator::new(), Some(path) => { let text = std::fs::read_to_string(path) .map_err(|_| josh::josh_error("failed to read acl file"))?; @@ -794,7 +797,7 @@ fn parse_args() -> clap::ArgMatches<'static> { clap::Arg::with_name("acl") .long("acl") .help("Specify a config file to control user access") - .takes_value(true) + .takes_value(true), ) .get_matches_from(args) }