From f1342ffb5a37b4c0996282307f59ed7eb150f106 Mon Sep 17 00:00:00 2001 From: Louis-Marie Givel Date: Mon, 13 Dec 2021 16:44:49 +0100 Subject: [PATCH] Add permissions to proxy --- josh-proxy/src/bin/josh-proxy.rs | 58 +++++++- josh-proxy/src/lib.rs | 1 + src/lib.rs | 66 +++++---- tests/proxy/permissions.t | 222 +++++++++++++++++++++++++++++++ tests/proxy/setup_test_env.sh | 8 +- 5 files changed, 324 insertions(+), 31 deletions(-) create mode 100644 tests/proxy/permissions.t diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs index 803c7ac4..01c1c697 100644 --- a/josh-proxy/src/bin/josh-proxy.rs +++ b/josh-proxy/src/bin/josh-proxy.rs @@ -46,6 +46,7 @@ struct JoshProxyService { fetch_permits: Arc, filter_permits: Arc, poll: Polls, + acl: Option<(String, String)>, } impl std::fmt::Debug for JoshProxyService { @@ -244,9 +245,11 @@ async fn do_filter( temp_ns: Arc, filter_spec: String, headref: String, + user: String, ) -> josh::JoshResult<()> { let permit = service.filter_permits.acquire().await; let heads_map = service.heads_map.clone(); + let acl = service.acl.clone(); let s = tracing::span!(tracing::Level::TRACE, "do_filter worker"); let r = tokio::task::spawn_blocking(move || { @@ -295,7 +298,28 @@ async fn do_filter( let mut headref = headref; - josh::filter_refs(&transaction, filter, &from_to, josh::filter::empty())?; + let permissions_filter = if let Some(acl) = acl { + let users = &acl.0; + let groups = &acl.1; + tracing::info!("User: {:?}, Repo: {:?}", user, upstream_repo); + let acl = match josh::get_acl(&users, &groups, &user, &upstream_repo) { + Ok(acl) => acl, + Err(e) => { + tracing::error!("Failed to read ACL file: {:?}", e); + (josh::filter::empty(), josh::filter::nop()) + } + }; + let whitelist = acl.0; + let blacklist = acl.1; + tracing::info!("Whitelist: {:?}, Blacklist: {:?}", whitelist, blacklist); + josh::filter::make_permissions_filter(filter, whitelist, blacklist) + } else { + josh::filter::empty() + }; + + tracing::info!("Permissions: {:?}", permissions_filter); + + josh::filter_refs(&transaction, filter, &from_to, permissions_filter)?; if headref == "HEAD" { headref = heads_map .read()? @@ -517,6 +541,7 @@ async fn call_service( &parsed_url.upstream_repo, &parsed_url.filter, &headref, + &username, ) .in_current_span() .await?; @@ -598,6 +623,7 @@ async fn prepare_namespace( upstream_repo: &str, filter_spec: &str, headref: &str, + user: &str, ) -> josh::JoshResult> { let temp_ns = Arc::new(josh_proxy::TmpGitNamespace::new( &serv.repo_path, @@ -606,6 +632,8 @@ async fn prepare_namespace( let serv = serv.clone(); + let user = if user == "" { "anonymous" } else { user }; + do_filter( serv.repo_path.clone(), serv.clone(), @@ -613,6 +641,7 @@ async fn prepare_namespace( temp_ns.to_owned(), filter_spec.to_owned(), headref.to_string(), + user.to_string(), ) .await?; @@ -635,6 +664,20 @@ async fn run_proxy() -> josh::JoshResult { josh_proxy::create_repo(&local)?; josh::cache::load(&local)?; + let acl = if ARGS.is_present("users") && ARGS.is_present("groups") { + println!( + "{}, {}", + ARGS.value_of("users").unwrap().to_string(), + ARGS.value_of("groups").unwrap().to_string() + ); + Some(( + ARGS.value_of("users").unwrap().to_string(), + ARGS.value_of("groups").unwrap().to_string(), + )) + } else { + None + }; + let proxy_service = Arc::new(JoshProxyService { port, repo_path: local.to_owned(), @@ -646,6 +689,7 @@ async fn run_proxy() -> josh::JoshResult { ARGS.value_of("n").unwrap_or("1").parse()?, )), filter_permits: Arc::new(tokio::sync::Semaphore::new(10)), + acl, }); let ps = proxy_service.clone(); @@ -800,6 +844,18 @@ fn parse_args() -> clap::ArgMatches<'static> { .help("Duration between forced cache refresh") .takes_value(true), ) + .arg( + clap::Arg::with_name("users") + .long("users") + .takes_value(true) + .help("YAML file listing the groups of the users"), + ) + .arg( + clap::Arg::with_name("groups") + .long("groups") + .takes_value(true) + .help("YAML file listing the access rights of the groups"), + ) .get_matches_from(args) } diff --git a/josh-proxy/src/lib.rs b/josh-proxy/src/lib.rs index 34d0151c..dcf69de2 100644 --- a/josh-proxy/src/lib.rs +++ b/josh-proxy/src/lib.rs @@ -422,6 +422,7 @@ impl TmpGitNamespace { pub fn new(repo_path: &std::path::Path, span: tracing::Span) -> TmpGitNamespace { let n = format!("request_{}", uuid::Uuid::new_v4()); let n2 = n.clone(); + TmpGitNamespace { name: n, repo_path: repo_path.to_owned(), diff --git a/src/lib.rs b/src/lib.rs index b981f18f..35643f93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -331,34 +331,42 @@ pub fn get_acl( let groups: Groups = serde_yaml::from_str(&groups) .map_err(|err| josh_error(format!("failed to parse groups file: {}", err).as_str()))?; - return users - .get(user) - .and_then(|u| { - let mut whitelist = filter::empty(); - let mut blacklist = filter::empty(); - for g in &u.groups { - let lists = groups.get(repo).and_then(|repo| { - repo.get(g.as_str()?).and_then(|group| { - let w = filter::parse(&group.whitelist); - let b = filter::parse(&group.blacklist); - Some((w, b)) - }) - })?; - if let Err(e) = lists.0 { - return Some(Err(JoshError(format!("Error parsing whitelist: {}", e)))); - } - if let Err(e) = lists.1 { - return Some(Err(JoshError(format!("Error parsing blacklist: {}", e)))); - } - if let Ok(w) = lists.0 { - whitelist = filter::compose(whitelist, w); - } - if let Ok(b) = lists.1 { - blacklist = filter::compose(blacklist, b); - } + let res = users.get(user).and_then(|u| { + let mut whitelist = filter::empty(); + let mut blacklist = filter::empty(); + for g in &u.groups { + let lists = groups.get(repo).and_then(|repo| { + repo.get(g.as_str()?).and_then(|group| { + let w = filter::parse(&group.whitelist); + let b = filter::parse(&group.blacklist); + Some((w, b)) + }) + })?; + if let Err(e) = lists.0 { + return Some(Err(JoshError(format!("Error parsing whitelist: {}", e)))); + } + if let Err(e) = lists.1 { + return Some(Err(JoshError(format!("Error parsing blacklist: {}", e)))); + } + if let Ok(w) = lists.0 { + whitelist = filter::compose(whitelist, w); } - println!("w: {:?}, b: {:?}", whitelist, blacklist); - Some(Ok((whitelist, blacklist))) - }) - .unwrap_or(Ok((filter::empty(), filter::nop()))); + if let Ok(b) = lists.1 { + blacklist = filter::compose(blacklist, b); + } + } + println!("w: {:?}, b: {:?}", whitelist, blacklist); + Some(Ok((whitelist, blacklist))) + }); + return match res { + Some(Ok(res)) => Ok(res), + Some(Err(e)) => { + tracing::warn!("ACL error: {:?}", e); + Ok((filter::empty(), filter::nop())) + } + None => { + tracing::warn!("ACL: none"); + Ok((filter::empty(), filter::nop())) + } + }; } diff --git a/tests/proxy/permissions.t b/tests/proxy/permissions.t new file mode 100644 index 00000000..7b5b034f --- /dev/null +++ b/tests/proxy/permissions.t @@ -0,0 +1,222 @@ + $ cat > users.yaml < anonymous: + > groups: ['devel'] + > EOF + $ cat > groups.yaml < /real_repo.git: + > devel: + > whitelist: | + > :[ + > ::sub1/ + > ::sub2/ + > ::whitelisted/ + > ::blacklisted/ + > ::whiteandblack/ + > ] + > blacklist: "::sub1/file1" + > EOF + + $ JOSH_USERS=$(pwd)/users.yaml JOSH_GROUPS=$(pwd)/groups.yaml . ${TESTDIR}/setup_test_env.sh + + $ cd ${TESTTMP} + + + $ git clone -q http://localhost:8001/real_repo.git + warning: You appear to have cloned an empty repository. + + $ curl -s http://localhost:8002/version + Version: 0.3.0 + + $ cd real_repo + + $ git status + On branch master + + No commits yet + + nothing to commit (create/copy files and use "git add" to track) + + $ git checkout -b master + Switched to a new branch 'master' + +# only whitelisted files which are not blacklisted + $ mkdir whitelisted + $ cat > whitelisted/workspace.josh < a/b = :/sub2 + > c = :/sub1:exclude[::file1] + > EOF + + $ git add whitelisted + $ git commit -m "add whitelisted" 1> /dev/null + +# whitelisted and blacklisted files + $ mkdir whiteandblack + $ cat > whiteandblack/workspace.josh < a/b = :/sub2 + > c = :/sub1 + > EOF + + $ git add whiteandblack + $ git commit -m "add whiteandblack" 1> /dev/null + +# only blacklisted files + $ mkdir blacklisted + $ cat > blacklisted/workspace.josh < ::sub1/file1 + > EOF + + $ git add blacklisted + $ git commit -m "add blacklisted" 1> /dev/null + + + $ echo content1 > file1 1> /dev/null + $ git add . + $ git commit -m "initial" 1> /dev/null + + $ git checkout -b new1 + Switched to a new branch 'new1' + $ echo content > newfile1 1> /dev/null + $ git add . + $ git commit -m "add newfile1" 1> /dev/null + + $ git checkout master 1> /dev/null + Switched to branch 'master' + $ echo content > newfile_master 1> /dev/null + $ git add . + $ git commit -m "newfile master" 1> /dev/null + + $ git merge -q new1 --no-ff + + $ mkdir sub3 + $ echo contents3 > sub3/file3 + $ git add sub3 + $ git commit -m "add file3" 1> /dev/null + + $ mkdir -p sub1/subsub + $ echo contents1 > sub1/subsub/file1 + $ git add . + $ git commit -m "sub1 subsub file1" 1> /dev/null + + $ echo content2 > sub1/file1 1> /dev/null + $ git add . + $ git commit -m "sub1 file1" 1> /dev/null + + $ mkdir sub2 + $ echo contents1 > sub2/file2 + $ git add sub2 + $ git commit -m "add file2" 1> /dev/null + + + $ git log --graph --pretty=%s + * add file2 + * sub1 file1 + * sub1 subsub file1 + * add file3 + * Merge branch 'new1' + |\ + | * add newfile1 + * | newfile master + |/ + * initial + * add blacklisted + * add whiteandblack + * add whitelisted + + $ tree + . + |-- blacklisted + | `-- workspace.josh + |-- file1 + |-- newfile1 + |-- newfile_master + |-- sub1 + | |-- file1 + | `-- subsub + | `-- file1 + |-- sub2 + | `-- file2 + |-- sub3 + | `-- file3 + |-- whiteandblack + | `-- workspace.josh + `-- whitelisted + `-- workspace.josh + + 7 directories, 10 files + + + $ git push + To http://localhost:8001/real_repo.git + * [new branch] master -> master + + $ cd ${TESTTMP} + +# whitelisted works + $ git clone -q http://localhost:8002/real_repo.git:workspace=whitelisted.git whitelisted + $ tree whitelisted + whitelisted + |-- a + | `-- b + | `-- file2 + |-- c + | `-- subsub + | `-- file1 + `-- workspace.josh + + 4 directories, 3 files + +# the others do not work + $ git clone -q http://localhost:8002/real_repo.git:workspace=whiteandblack.git whiteandblack + warning: You appear to have cloned an empty repository. + $ git clone -q http://localhost:8002/real_repo.git:workspace=blacklisted.git blacklisted + warning: You appear to have cloned an empty repository. + + $ bash ${TESTDIR}/destroy_test_env.sh + "real_repo.git" = [ + ':/blacklisted', + ':/sub1', + ':/sub1/subsub', + ':/sub2', + ':/sub3', + ':/whiteandblack', + ':/whitelisted', + ':workspace=blacklisted', + ':workspace=whiteandblack', + ':workspace=whitelisted', + ] + refs + |-- heads + |-- josh + | |-- filtered + | | `-- real_repo.git + | | |-- %3A%2Fblacklisted + | | | `-- HEAD + | | |-- %3A%2Fsub1 + | | | `-- HEAD + | | |-- %3A%2Fsub1%2Fsubsub + | | | `-- HEAD + | | |-- %3A%2Fsub2 + | | | `-- HEAD + | | |-- %3A%2Fsub3 + | | | `-- HEAD + | | |-- %3A%2Fwhiteandblack + | | | `-- HEAD + | | |-- %3A%2Fwhitelisted + | | | `-- HEAD + | | |-- %3Aworkspace=blacklisted + | | | `-- HEAD + | | |-- %3Aworkspace=whiteandblack + | | | `-- HEAD + | | `-- %3Aworkspace=whitelisted + | | `-- HEAD + | `-- upstream + | `-- real_repo.git + | |-- HEAD + | `-- refs + | `-- heads + | `-- master + |-- namespaces + `-- tags + + 20 directories, 12 files + diff --git a/tests/proxy/setup_test_env.sh b/tests/proxy/setup_test_env.sh index 7dc4f2ae..110cc4b2 100644 --- a/tests/proxy/setup_test_env.sh +++ b/tests/proxy/setup_test_env.sh @@ -22,11 +22,17 @@ echo $! > ${TESTTMP}/server_pid cp -R ${TESTDIR}/../../static/ static +ACL_OPT="" +if [ "$JOSH_USERS" -a "$JOSH_GROUPS" ]; then + ACL_OPT="--users=${JOSH_USERS} --groups=${JOSH_GROUPS}" +fi + ${TESTDIR}/../../target/debug/josh-proxy\ --port=8002\ --graphql-root\ --local=${TESTTMP}/remote/scratch/\ --remote=http://localhost:8001\ + ${ACL_OPT}\ > ${TESTTMP}/josh-proxy.out 2>&1 & echo $! > ${TESTTMP}/proxy_pid @@ -37,7 +43,7 @@ do COUNTER=$((COUNTER + 1)) if [ $COUNTER -ge 10 ]; then - >& echo "Starting josh proxy timed out" + echo "Starting josh proxy timed out" exit 1 fi done