Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions josh-proxy/src/acl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use regex::Regex;
use serde::Deserialize;
use std::{collections::HashMap, result::Result};
use toml;

#[derive(Debug, Clone)]
pub struct Validator {
// note: repo user paths
rules: Option<HashMap<String, HashMap<String, Vec<Regex>>>>,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Error {
Toml(toml::de::Error),
Regex(regex::Error),
}

impl Validator {
pub fn new() -> Validator {
Validator { rules: None }
}

pub fn from_toml(src: &str) -> std::result::Result<Validator, Error> {
// parse Rule from text
let raw: Doc = toml::from_str(&src).map_err(Error::Toml)?;

// map Rule into HashMap<String, HashMap<String, Vec<Regex>>>
let dst = |x: Vec<String>| {
x.into_iter()
.map(|v| Regex::new(v.as_str()).map_err(Error::Regex))
.collect::<Result<Vec<Regex>, Error>>()
};
let dst = |x: Option<Vec<Match>>| {
x.unwrap_or(Vec::new())
.into_iter()
.map(|m| (m.user, dst(m.path)))
.map(|(s, r)| r.map(|v| (s, v)))
.collect::<Result<HashMap<String, Vec<Regex>>, 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::<Result<HashMap<String, _>, Error>>()?;

// return value
Ok(Validator { rules: Some(dst) })
}

pub fn is_accessible(self: &Validator, user: &str, repo: &str, path: &str) -> bool {
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_start_matches("/");
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)
}
}
}
}

#[derive(Debug, Deserialize)]
struct Doc {
pub repo: Option<Vec<Repo>>,
}

#[derive(Debug, Deserialize)]
struct Repo {
pub name: String,
pub rule: Option<Vec<Match>>,
}

#[derive(Debug, Deserialize)]
struct Match {
pub user: String,
pub path: Vec<String>,
}
30 changes: 30 additions & 0 deletions josh-proxy/src/bin/josh-proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ struct JoshProxyService {
fetch_timers: Arc<RwLock<FetchTimers>>,
fetch_permits: Arc<tokio::sync::Semaphore>,
filter_permits: Arc<tokio::sync::Semaphore>,
validator: josh_proxy::acl::Validator,
poll: Polls,
}

Expand Down Expand Up @@ -398,6 +399,18 @@ async fn call_service(
return Ok(builder.body(hyper::Body::empty())?);
}

// 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")
.status(hyper::StatusCode::UNAUTHORIZED);
return Ok(builder.body(hyper::Body::empty())?);
}

match fetch_upstream(
serv.clone(),
parsed_url.upstream_repo.to_owned(),
Expand Down Expand Up @@ -605,6 +618,16 @@ async fn run_proxy() -> josh::JoshResult<i32> {
josh_proxy::create_repo(&local)?;
josh::cache::load(&local)?;

let validator = match ARGS.value_of("acl") {
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 proxy_service = Arc::new(JoshProxyService {
port,
repo_path: local.to_owned(),
Expand All @@ -615,6 +638,7 @@ async fn run_proxy() -> josh::JoshResult<i32> {
ARGS.value_of("n").unwrap_or("1").parse()?,
)),
filter_permits: Arc::new(tokio::sync::Semaphore::new(10)),
validator: validator,
});

let ps = proxy_service.clone();
Expand Down Expand Up @@ -769,6 +793,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)
}

Expand Down
1 change: 1 addition & 0 deletions josh-proxy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod acl;
pub mod auth;
pub mod juniper_hyper;

Expand Down
3 changes: 2 additions & 1 deletion run-josh.sh
Original file line number Diff line number Diff line change
@@ -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}