Initial Commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
1222
Cargo.lock
generated
Normal file
1222
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "gamma_basic_auth_proxy"
|
||||||
|
version = "1.0.0"
|
||||||
|
authors = ["Joakim Hulthe <joakim@hulthe.net>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
hyper = { version = "0.14", features = ["full"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
ron = "0.6.4"
|
||||||
|
compound-error = "0.1.2"
|
||||||
|
log = "0.4"
|
||||||
|
femme = "2"
|
||||||
|
kv-log-macro = "1"
|
||||||
|
base64 = "0.13"
|
||||||
|
reqwest = { version = "0.11.4", default-features = false, features = ["rustls-tls", "cookies", "multipart", "json"] }
|
||||||
27
Dockerfile
Normal file
27
Dockerfile
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
###################
|
||||||
|
### BUILD STAGE ###
|
||||||
|
###################
|
||||||
|
FROM rust:1.52 as build_stage
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN rustup target add x86_64-unknown-linux-musl
|
||||||
|
|
||||||
|
# Build project
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN cargo build --release --target x86_64-unknown-linux-musl
|
||||||
|
RUN strip target/x86_64-unknown-linux-musl/release/auth_proxy
|
||||||
|
|
||||||
|
########################
|
||||||
|
### PRODUCTION STAGE ###
|
||||||
|
########################
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
# Copy application binary
|
||||||
|
COPY --from=build_stage /app/target/x86_64-unknown-linux-musl/release/auth_proxy auth_proxy
|
||||||
|
|
||||||
|
CMD ["/auth_proxy", "--help"]
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
auth\_proxy
|
||||||
|
========
|
||||||
|
_An HTTP based reverse proxy for adding basic auth to your stack_
|
||||||
91
src/cache.rs
Normal file
91
src/cache.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use reqwest::Client;
|
||||||
|
use tokio::{sync::{RwLock, RwLockReadGuard, RwLockWriteGuard, Mutex}, time::{Duration, Instant}};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{Opt, gamma::{self, Credentials, User}};
|
||||||
|
|
||||||
|
struct CacheEntry {
|
||||||
|
last_checked: Instant,
|
||||||
|
login_result: Result<User, String>,
|
||||||
|
http_client: Client,
|
||||||
|
}
|
||||||
|
pub struct UserCache {
|
||||||
|
map: Mutex<HashMap<Credentials, Arc<RwLock<Option<CacheEntry>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arbitraty cache time, should be configurable
|
||||||
|
const CACHE_TIME: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
impl UserCache {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
UserCache {
|
||||||
|
map: Mutex::new(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn login(&self, opt: &Opt, credentials: &Credentials) -> Result<User, String> {
|
||||||
|
let mut map = self.map.lock().await;
|
||||||
|
|
||||||
|
// if an entry already exists
|
||||||
|
if let Some(user_state) = map.get(credentials) {
|
||||||
|
// clone it
|
||||||
|
let user_state = Arc::clone(user_state);
|
||||||
|
drop(map); // release the map lock
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let entry = RwLockReadGuard::map(user_state.read().await, |option| option.as_ref().expect("already initialized"));
|
||||||
|
let last_checked = entry.last_checked;
|
||||||
|
// if the entry has not expired
|
||||||
|
if last_checked.elapsed() < CACHE_TIME {
|
||||||
|
// take the fast path and just assume we are still logged in
|
||||||
|
break entry.login_result.clone();
|
||||||
|
} else {
|
||||||
|
// otherwise try to upgrade to a write lock so that we can refresh the session
|
||||||
|
drop(entry);
|
||||||
|
let mut entry = RwLockWriteGuard::map(user_state.write().await, |option| option.as_mut().expect("already initialized"));
|
||||||
|
|
||||||
|
// if the entry was updated when we didn't have the lock, retry
|
||||||
|
if entry.last_checked != last_checked {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let login_result = gamma::login(&mut entry.http_client, opt, credentials).await;
|
||||||
|
entry.last_checked = Instant::now();
|
||||||
|
entry.login_result = login_result.clone();
|
||||||
|
break login_result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if the entry did not exist, then we must log in
|
||||||
|
|
||||||
|
// create an empty lock and take the write handle
|
||||||
|
let lock = Arc::new(RwLock::new(None));
|
||||||
|
let mut entry = lock.write().await;
|
||||||
|
|
||||||
|
// then put it in the map so that we can release the map as fast as possible
|
||||||
|
map.insert(credentials.clone(), Arc::clone(&lock));
|
||||||
|
drop(map); // release the map lock
|
||||||
|
|
||||||
|
let mut client = Client::builder()
|
||||||
|
.cookie_store(true)
|
||||||
|
.timeout(Duration::from_secs(10 /* TODO: configure timeout */))
|
||||||
|
.build()
|
||||||
|
.expect("http client");
|
||||||
|
|
||||||
|
let login_result = gamma::login(&mut client, opt, credentials).await;
|
||||||
|
let last_checked = Instant::now();
|
||||||
|
|
||||||
|
// try to log in and cache the result
|
||||||
|
let new_entry = CacheEntry {
|
||||||
|
login_result: login_result.clone(),
|
||||||
|
last_checked,
|
||||||
|
http_client: client,
|
||||||
|
};
|
||||||
|
|
||||||
|
*entry = Some(new_entry);
|
||||||
|
|
||||||
|
login_result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/gamma.rs
Normal file
86
src/gamma.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use kv_log_macro::debug;
|
||||||
|
use reqwest::{Client, Response};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::Opt;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Hash, Serialize)]
|
||||||
|
pub struct Credentials {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct User {
|
||||||
|
pub cid: String,
|
||||||
|
pub username: String,
|
||||||
|
pub groups: Vec<Group>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct Group {
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(rename = "superGroup")]
|
||||||
|
pub super_group: SuperGroup,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize)]
|
||||||
|
pub struct SuperGroup {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_status(resp: &Response) -> Result<(), String> {
|
||||||
|
if resp.status().is_server_error() {
|
||||||
|
Err(format!("Gamma Error: {}", resp.status()))
|
||||||
|
} else if resp.status().is_client_error() {
|
||||||
|
Err("Invalid credentials".to_string())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn login(client: &mut Client, opt: &Opt, credentials: &Credentials) -> Result<User, String> {
|
||||||
|
|
||||||
|
let login_uri = format!("{}{}", opt.gamma, "/api/login");
|
||||||
|
let login_resp = client
|
||||||
|
.post(&login_uri)
|
||||||
|
.form(credentials)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("gamma: login failed: {}", e))?;
|
||||||
|
|
||||||
|
debug!("gamma: tried logging in", {
|
||||||
|
username: credentials.username.as_str(),
|
||||||
|
uri: login_uri.as_str(),
|
||||||
|
response: format!("{:?}", login_resp).as_str(),
|
||||||
|
});
|
||||||
|
|
||||||
|
check_status(&login_resp)?;
|
||||||
|
|
||||||
|
get_me(client, opt, &credentials.username).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub (crate) async fn get_me(client: &mut Client, opt: &Opt, username: &str) -> Result<User, String> {
|
||||||
|
let me_uri = format!("{}{}", opt.gamma, "/api/users/me");
|
||||||
|
let me_resp = client
|
||||||
|
.get(&me_uri)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("gamma: get user info failed: {}", e))?;
|
||||||
|
|
||||||
|
debug!("gamma: tried getting user info", {
|
||||||
|
username: username,
|
||||||
|
uri: me_uri.as_str(),
|
||||||
|
response: format!("{:?}", me_resp).as_str(),
|
||||||
|
});
|
||||||
|
|
||||||
|
check_status(&me_resp)?;
|
||||||
|
|
||||||
|
let user: User = me_resp
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("gamma: failed to deserialize json: {}", e))?;
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
317
src/main.rs
Normal file
317
src/main.rs
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
mod cache;
|
||||||
|
mod gamma;
|
||||||
|
mod rules;
|
||||||
|
|
||||||
|
use compound_error::CompoundError;
|
||||||
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
|
use hyper::{Body, Client, Request, Response, Server, Uri};
|
||||||
|
use kv_log_macro::{debug, error, info, warn};
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use cache::UserCache;
|
||||||
|
use gamma::{Credentials, User};
|
||||||
|
use rules::{Method, Permission, Rule};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Opt {
|
||||||
|
/// Example: http://my-server:80/
|
||||||
|
proxy: String,
|
||||||
|
|
||||||
|
/// Example: https://gamma.chalmers.it
|
||||||
|
gamma: String,
|
||||||
|
|
||||||
|
/// Example: "My special place"
|
||||||
|
realm: String,
|
||||||
|
|
||||||
|
rules: HashMap<String, Rule>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, CompoundError)]
|
||||||
|
enum Error {
|
||||||
|
Hyper(hyper::Error),
|
||||||
|
IO(std::io::Error),
|
||||||
|
}
|
||||||
|
struct State {
|
||||||
|
opt: Opt,
|
||||||
|
cache: UserCache,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn proxy_pass(mut req: Request<Body>, state: &State) -> Result<Response<Body>, Error> {
|
||||||
|
let req_uri = req.uri().clone();
|
||||||
|
|
||||||
|
info!("{:#?}", req);
|
||||||
|
|
||||||
|
let unauthorized = |msg| {
|
||||||
|
info!("respoinding with 401 Unauthorized due to {}", msg);
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(401)
|
||||||
|
.header(
|
||||||
|
"WWW-Authenticate",
|
||||||
|
format!(r#"Basic realm="{}", charset="UTF-8""#, state.opt.realm),
|
||||||
|
)
|
||||||
|
.header("Docker-Distribution-Api-Version", "registry/2.0")
|
||||||
|
.body(Body::from("Unauthorized"))
|
||||||
|
.expect("infallible response"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let forbidden = |msg| {
|
||||||
|
info!("respoinding with 403 Forbidden due to {}", msg);
|
||||||
|
Ok(Response::builder()
|
||||||
|
.status(403)
|
||||||
|
.body(Body::from("Forbidden"))
|
||||||
|
.expect("infallible response"))
|
||||||
|
};
|
||||||
|
|
||||||
|
let login = match req.headers_mut().remove("Authorization") {
|
||||||
|
None => {
|
||||||
|
info!("request received", {
|
||||||
|
uri: format!("{}", req_uri).as_str(),
|
||||||
|
method: req.method().as_str(),
|
||||||
|
provided_auth: false,
|
||||||
|
});
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Some(authorization) => {
|
||||||
|
info!("request received", {
|
||||||
|
uri: format!("{}", req_uri).as_str(),
|
||||||
|
method: req.method().as_str(),
|
||||||
|
provided_auth: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let credentials = authorization
|
||||||
|
.to_str()
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.strip_prefix("Basic "))
|
||||||
|
.and_then(|s| base64::decode(s).ok())
|
||||||
|
.and_then(|b| String::from_utf8(b).ok());
|
||||||
|
|
||||||
|
match credentials.as_ref().and_then(|s| s.split_once(":")) {
|
||||||
|
Some((user, pass)) => {
|
||||||
|
let credentials = Credentials {
|
||||||
|
username: user.to_string(),
|
||||||
|
password: pass.to_string(),
|
||||||
|
};
|
||||||
|
match state.cache.login(&state.opt, &credentials).await {
|
||||||
|
Ok(user) => Some(user),
|
||||||
|
Err(e) => {
|
||||||
|
warn!("{}", e);
|
||||||
|
return unauthorized("invalid login");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("client did not provide valid a \"Authorization\" header");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match validate(&state.opt, &req, &login) {
|
||||||
|
Validation::Allowed => {
|
||||||
|
info!("success"); /* success! continue to proxy */
|
||||||
|
}
|
||||||
|
Validation::NotAllowed => return forbidden("failed validation"),
|
||||||
|
Validation::RequiresLogin => return unauthorized("not logged in"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let proxy_uri: Uri = state.opt.proxy.parse().expect("proxy uri");
|
||||||
|
let mut new_uri = Uri::builder().authority(proxy_uri.authority().unwrap().clone());
|
||||||
|
new_uri = new_uri.scheme(proxy_uri.scheme_str().unwrap_or("http"));
|
||||||
|
|
||||||
|
if let Some(paq) = req_uri.path_and_query().cloned() {
|
||||||
|
new_uri = new_uri.path_and_query(paq);
|
||||||
|
}
|
||||||
|
|
||||||
|
*req.uri_mut() = new_uri.build().expect("uri");
|
||||||
|
|
||||||
|
let client = Client::new();
|
||||||
|
let mut error = None;
|
||||||
|
let response = match client.request(req).await {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
error = Some(format!("{:?}", e));
|
||||||
|
Response::builder()
|
||||||
|
.status(503)
|
||||||
|
.body("503 Service Unavailable".into())
|
||||||
|
.expect("infallible response")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(e) = error {
|
||||||
|
warn!("{}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_opt() -> Result<Opt, String> {
|
||||||
|
let env = |name| std::env::var(name).map_err(|e| format!("{}: {}", name, e));
|
||||||
|
|
||||||
|
Ok(Opt {
|
||||||
|
proxy: env("PROXY_HOST")?,
|
||||||
|
realm: env("AUTH_REALM")?,
|
||||||
|
gamma: env("GAMMA_HOST")?,
|
||||||
|
rules: std::env::vars()
|
||||||
|
.filter(|(name, _)| name.starts_with("AUTH_RULE_"))
|
||||||
|
.map(|(name, rule)| Rule::parse(&rule).map(|rule| (name, rule)))
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Validation {
|
||||||
|
/// Validataion succeeded, client pay proceed
|
||||||
|
Allowed,
|
||||||
|
|
||||||
|
/// The client is not allowed to proceed, even if it provided credentials
|
||||||
|
NotAllowed,
|
||||||
|
|
||||||
|
/// The client should provide credentials and try again
|
||||||
|
RequiresLogin,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(opt: &Opt, req: &Request<Body>, login: &Option<User>) -> Validation {
|
||||||
|
// filter irrelevant rules
|
||||||
|
let mut applicable_rules: Vec<_> = opt
|
||||||
|
.rules
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, rule)| {
|
||||||
|
let req_path: PathBuf = req.uri().path().into();
|
||||||
|
req_path.starts_with(&rule.path)
|
||||||
|
})
|
||||||
|
.filter(|(_, rule)| match rule.method {
|
||||||
|
Method::Any => true,
|
||||||
|
Method::GET => req.method() == hyper::Method::GET,
|
||||||
|
Method::POST => req.method() == hyper::Method::POST,
|
||||||
|
Method::PUT => req.method() == hyper::Method::PUT,
|
||||||
|
Method::DELETE => req.method() == hyper::Method::DELETE,
|
||||||
|
Method::HEAD => req.method() == hyper::Method::HEAD,
|
||||||
|
Method::OPTIONS => req.method() == hyper::Method::OPTIONS,
|
||||||
|
Method::CONNECT => req.method() == hyper::Method::CONNECT,
|
||||||
|
Method::PATCH => req.method() == hyper::Method::PATCH,
|
||||||
|
Method::TRACE => req.method() == hyper::Method::TRACE,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// find the most relevant rule
|
||||||
|
applicable_rules.sort_by(|&(a_name, a), &(b_name, b)| {
|
||||||
|
// example priorities:
|
||||||
|
// 1. /foo/bar/baz/ POST
|
||||||
|
// 2. /foo/bar/baz/ Any
|
||||||
|
// 3. /foo/bar Any
|
||||||
|
|
||||||
|
let a_len = a.path.components().count();
|
||||||
|
let b_len = b.path.components().count();
|
||||||
|
|
||||||
|
a_len.cmp(&b_len).then_with(|| match (a.method, b.method) {
|
||||||
|
(ma, mb) if ma == mb => panic!("conflicting rules, {} and {}", a_name, b_name),
|
||||||
|
(Method::Any, _) => Ordering::Less,
|
||||||
|
(_, Method::Any) => Ordering::Greater,
|
||||||
|
_ => unreachable!("Rules for different methods can't conflict"),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
debug!("list of applicable rules: {:?}", applicable_rules);
|
||||||
|
|
||||||
|
let (name, rule) = match applicable_rules.last() {
|
||||||
|
Some(&last) => last,
|
||||||
|
|
||||||
|
// No rules exist, so we default to not allowed
|
||||||
|
None => return Validation::NotAllowed,
|
||||||
|
};
|
||||||
|
|
||||||
|
info!("checking connecting against rule {}", name);
|
||||||
|
|
||||||
|
fn check_logged_in(login: &Option<User>) -> Validation {
|
||||||
|
if login.is_some() {
|
||||||
|
Validation::Allowed
|
||||||
|
} else {
|
||||||
|
Validation::RequiresLogin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_is_group(group: &str, login: &Option<User>) -> Validation {
|
||||||
|
match login.as_ref() {
|
||||||
|
Some(user) => {
|
||||||
|
info!("check_is_group", { user: user.username.as_str(), group: group });
|
||||||
|
let mut groups = user
|
||||||
|
.groups
|
||||||
|
.iter()
|
||||||
|
.flat_map(|group| [&group.name, &group.super_group.name]);
|
||||||
|
|
||||||
|
if groups.find(|&user_group| user_group == group).is_some() {
|
||||||
|
Validation::Allowed
|
||||||
|
} else {
|
||||||
|
Validation::NotAllowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Validation::RequiresLogin,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_permission(permission: &Permission, login: &Option<User>) -> Validation {
|
||||||
|
let recurse = |perm| check_permission(perm, login);
|
||||||
|
|
||||||
|
match permission {
|
||||||
|
Permission::AllowAll => Validation::Allowed,
|
||||||
|
Permission::AnyUser => check_logged_in(login),
|
||||||
|
Permission::Group(group) => check_is_group(&group, login),
|
||||||
|
Permission::And(a, b) => match (recurse(a), recurse(b)) {
|
||||||
|
(Validation::Allowed, Validation::Allowed) => Validation::Allowed,
|
||||||
|
(Validation::NotAllowed, _) | (_, Validation::NotAllowed) => Validation::NotAllowed,
|
||||||
|
(Validation::RequiresLogin, _) | (_, Validation::RequiresLogin) => {
|
||||||
|
Validation::RequiresLogin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Permission::Or(a, b) => match (recurse(a), recurse(b)) {
|
||||||
|
(Validation::NotAllowed, Validation::NotAllowed) => Validation::NotAllowed,
|
||||||
|
(Validation::Allowed, _) | (_, Validation::Allowed) => Validation::Allowed,
|
||||||
|
(Validation::RequiresLogin, _) | (_, Validation::RequiresLogin) => {
|
||||||
|
Validation::RequiresLogin
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Permission::Not(rule) => match recurse(rule) {
|
||||||
|
Validation::Allowed => Validation::NotAllowed,
|
||||||
|
Validation::NotAllowed | Validation::RequiresLogin => Validation::Allowed,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate request against the rule
|
||||||
|
check_permission(&rule.permission, login)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
//femme::start();
|
||||||
|
femme::with_level(femme::LevelFilter::Debug);
|
||||||
|
|
||||||
|
let opt: Opt = match get_opt() {
|
||||||
|
Ok(opt) => opt,
|
||||||
|
Err(e) => {
|
||||||
|
error!("{}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("{:#?}", opt);
|
||||||
|
|
||||||
|
let cache = UserCache::new();
|
||||||
|
|
||||||
|
let state: &'static State = Box::leak(Box::new(State { opt, cache }));
|
||||||
|
|
||||||
|
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
|
||||||
|
info!("Listening on 0.0.0.0:3000");
|
||||||
|
|
||||||
|
let make_svc = make_service_fn(|_conn| async move {
|
||||||
|
Ok::<_, Error>(service_fn(move |r| proxy_pass(r, state)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let server = Server::bind(&addr).serve(make_svc);
|
||||||
|
|
||||||
|
if let Err(e) = server.await {
|
||||||
|
error!("server error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
225
src/me.json
Normal file
225
src/me.json
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
{
|
||||||
|
"id": "e514bbda-97c8-4879-b62a-3e2c350f8032",
|
||||||
|
"cid": "hulthe",
|
||||||
|
"nick": "Tux🐧",
|
||||||
|
"firstName": "Joakim",
|
||||||
|
"lastName": "Hulthe",
|
||||||
|
"email": "joakim@hulthe.net",
|
||||||
|
"phone": "703644214",
|
||||||
|
"language": "sv",
|
||||||
|
"avatarUrl": "https://gamma.chalmers.it/api/uploads/8314961ee67fbfa57f603fff17ffbecc58bbf490.jpg",
|
||||||
|
"gdpr": true,
|
||||||
|
"userAgreement": true,
|
||||||
|
"accountLocked": false,
|
||||||
|
"acceptanceYear": 2016,
|
||||||
|
"authorities": [],
|
||||||
|
"activated": true,
|
||||||
|
"enabled": true,
|
||||||
|
"username": "hulthe",
|
||||||
|
"accountNonLocked": true,
|
||||||
|
"accountNonExpired": true,
|
||||||
|
"credentialsNonExpired": true,
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": "b6be176b-5705-49f5-8ac2-8646aec748ba",
|
||||||
|
"becomesActive": 1483142400000,
|
||||||
|
"becomesInactive": 1514678400000,
|
||||||
|
"description": {
|
||||||
|
"sv": "digIT 17/18",
|
||||||
|
"en": "digIT 17/18"
|
||||||
|
},
|
||||||
|
"email": "digit17@chalmers.it",
|
||||||
|
"function": {
|
||||||
|
"sv": "digIT 17/18",
|
||||||
|
"en": "digIT 17/18"
|
||||||
|
},
|
||||||
|
"name": "digit17",
|
||||||
|
"prettyName": "digIT 17/18",
|
||||||
|
"avatarURL": null,
|
||||||
|
"superGroup": {
|
||||||
|
"id": "e2eb1ee6-d1c9-4ec6-bbd1-fdd5910060c7",
|
||||||
|
"name": "didit",
|
||||||
|
"prettyName": "didIT",
|
||||||
|
"type": "ALUMNI",
|
||||||
|
"email": "didit@chalmers.it"
|
||||||
|
},
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "6e0f5714-65b9-46e1-8d6d-0273487bea48",
|
||||||
|
"becomesActive": 1568764800000,
|
||||||
|
"becomesInactive": 1603065600000,
|
||||||
|
"description": {
|
||||||
|
"sv": "Dataskyddsombud 2019",
|
||||||
|
"en": "Dataskyddsombud 2019"
|
||||||
|
},
|
||||||
|
"email": "dpo19@chalmers.it",
|
||||||
|
"function": {
|
||||||
|
"sv": "Dataskyddsombud 2019",
|
||||||
|
"en": "Dataskyddsombud 2019"
|
||||||
|
},
|
||||||
|
"name": "dpo19",
|
||||||
|
"prettyName": "Dataskyddsombud 2019",
|
||||||
|
"avatarURL": null,
|
||||||
|
"superGroup": {
|
||||||
|
"id": "6586de15-94a3-4c7a-bd31-e50854d0b5eb",
|
||||||
|
"name": "dpo",
|
||||||
|
"prettyName": "dpo",
|
||||||
|
"type": "COMMITTEE",
|
||||||
|
"email": "dpo@chalmers.it"
|
||||||
|
},
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d45861bb-938a-47bd-a4ce-d5152c697278",
|
||||||
|
"becomesActive": 1582329600000,
|
||||||
|
"becomesInactive": 1617321600000,
|
||||||
|
"description": {
|
||||||
|
"sv": "DrawIT 20",
|
||||||
|
"en": "DrawIT 20"
|
||||||
|
},
|
||||||
|
"email": "drawit20@chalmers.it",
|
||||||
|
"function": {
|
||||||
|
"sv": "DrawIT 20",
|
||||||
|
"en": "DrawIT 20"
|
||||||
|
},
|
||||||
|
"name": "drawit20",
|
||||||
|
"prettyName": "DrawIT 20",
|
||||||
|
"avatarURL": null,
|
||||||
|
"superGroup": {
|
||||||
|
"id": "25502aca-9f41-4110-a29d-04aeeed93411",
|
||||||
|
"name": "dragit",
|
||||||
|
"prettyName": "DragIT",
|
||||||
|
"type": "ALUMNI",
|
||||||
|
"email": "dragit@chalmers.it"
|
||||||
|
},
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4da2800d-72e9-4052-a857-f95cca89fca5",
|
||||||
|
"becomesActive": 1451692800000,
|
||||||
|
"becomesInactive": 1483315200000,
|
||||||
|
"description": {
|
||||||
|
"sv": "LaggIT 2016",
|
||||||
|
"en": "LaggIT 2016"
|
||||||
|
},
|
||||||
|
"email": "laggit16@chalmers.it",
|
||||||
|
"function": {
|
||||||
|
"sv": "LaggIT 2016",
|
||||||
|
"en": "LaggIT 2016"
|
||||||
|
},
|
||||||
|
"name": "laggit16",
|
||||||
|
"prettyName": "LaggIT 16",
|
||||||
|
"avatarURL": null,
|
||||||
|
"superGroup": {
|
||||||
|
"id": "d7bb514c-31e9-4af3-9119-3fa03f643ea5",
|
||||||
|
"name": "ragequit",
|
||||||
|
"prettyName": "ragequIT",
|
||||||
|
"type": "ALUMNI",
|
||||||
|
"email": "ragequit@chalmers.it"
|
||||||
|
},
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "8015807b-8680-4f0f-8bd1-d2bf1ffbc130",
|
||||||
|
"becomesActive": 1514851200000,
|
||||||
|
"becomesInactive": 1546387200000,
|
||||||
|
"description": {
|
||||||
|
"sv": "laggIT 2018",
|
||||||
|
"en": "laggIT 2018"
|
||||||
|
},
|
||||||
|
"email": "laggit18@chalmers.it",
|
||||||
|
"function": {
|
||||||
|
"sv": "laggIT 2018",
|
||||||
|
"en": "laggIT 2018"
|
||||||
|
},
|
||||||
|
"name": "laggit18",
|
||||||
|
"prettyName": "laggIT 18",
|
||||||
|
"avatarURL": null,
|
||||||
|
"superGroup": {
|
||||||
|
"id": "d7bb514c-31e9-4af3-9119-3fa03f643ea5",
|
||||||
|
"name": "ragequit",
|
||||||
|
"prettyName": "ragequIT",
|
||||||
|
"type": "ALUMNI",
|
||||||
|
"email": "ragequit@chalmers.it"
|
||||||
|
},
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ef3054b0-78cb-494a-8352-2c2ee62b54b0",
|
||||||
|
"becomesActive": 1483228800000,
|
||||||
|
"becomesInactive": 1514764800000,
|
||||||
|
"description": {
|
||||||
|
"sv": "LaggIT 17",
|
||||||
|
"en": "LaggIT 17"
|
||||||
|
},
|
||||||
|
"email": "laggit17@chalmers.it",
|
||||||
|
"function": {
|
||||||
|
"sv": "LaggIT 17",
|
||||||
|
"en": "LaggIT 17"
|
||||||
|
},
|
||||||
|
"name": "laggit17",
|
||||||
|
"prettyName": "LaggIT 17",
|
||||||
|
"avatarURL": null,
|
||||||
|
"superGroup": {
|
||||||
|
"id": "d7bb514c-31e9-4af3-9119-3fa03f643ea5",
|
||||||
|
"name": "ragequit",
|
||||||
|
"prettyName": "ragequIT",
|
||||||
|
"type": "ALUMNI",
|
||||||
|
"email": "ragequit@chalmers.it"
|
||||||
|
},
|
||||||
|
"active": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c246af65-531d-49e5-9b3a-9e97cb5b32b0",
|
||||||
|
"becomesActive": 1616803200000,
|
||||||
|
"becomesInactive": 1651363200000,
|
||||||
|
"description": {
|
||||||
|
"sv": "",
|
||||||
|
"en": ""
|
||||||
|
},
|
||||||
|
"email": "drawit21@chalmers.it",
|
||||||
|
"function": {
|
||||||
|
"sv": "",
|
||||||
|
"en": ""
|
||||||
|
},
|
||||||
|
"name": "drawit 21",
|
||||||
|
"prettyName": "DrawIT 21",
|
||||||
|
"avatarURL": null,
|
||||||
|
"superGroup": {
|
||||||
|
"id": "09728fbb-fead-4f12-aec9-7bfe39c8cc6f",
|
||||||
|
"name": "drawit",
|
||||||
|
"prettyName": "DrawIT",
|
||||||
|
"type": "SOCIETY",
|
||||||
|
"email": "drawit@chalmers.it"
|
||||||
|
},
|
||||||
|
"active": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b6756bd1-8fab-48e6-9204-1bf0b097c4e1",
|
||||||
|
"becomesActive": 1546214400000,
|
||||||
|
"becomesInactive": 1577750400000,
|
||||||
|
"description": {
|
||||||
|
"sv": "DrawIT 19",
|
||||||
|
"en": "DrawIT 19"
|
||||||
|
},
|
||||||
|
"email": "drawit19@chalmers.it",
|
||||||
|
"function": {
|
||||||
|
"sv": "DrawIT 19",
|
||||||
|
"en": "DrawIT 19"
|
||||||
|
},
|
||||||
|
"name": "drawit19",
|
||||||
|
"prettyName": "DrawIT 19",
|
||||||
|
"avatarURL": null,
|
||||||
|
"superGroup": {
|
||||||
|
"id": "25502aca-9f41-4110-a29d-04aeeed93411",
|
||||||
|
"name": "dragit",
|
||||||
|
"prettyName": "DragIT",
|
||||||
|
"type": "ALUMNI",
|
||||||
|
"email": "dragit@chalmers.it"
|
||||||
|
},
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"websiteURLs": null
|
||||||
|
}
|
||||||
90
src/rules.rs
Normal file
90
src/rules.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
//! Rules for configuring who has access to what endpoints
|
||||||
|
//!
|
||||||
|
//! ## Examples:
|
||||||
|
//! ```
|
||||||
|
//! Rule { path: "/v2/", method: GET, permission: And(AnyUser, Not(Group("styrit"))) }
|
||||||
|
//! Rule { path: "/v2/_catalog", permission: AllowAll }
|
||||||
|
//! Rule { path: "/v2/image/*/push", permission: Or(Group("didit"), Group("digit")) }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Rule {
|
||||||
|
pub path: PathBuf,
|
||||||
|
|
||||||
|
#[serde(default)]
|
||||||
|
pub method: Method,
|
||||||
|
|
||||||
|
pub permission: Permission,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub enum Method {
|
||||||
|
GET,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
DELETE,
|
||||||
|
HEAD,
|
||||||
|
OPTIONS,
|
||||||
|
CONNECT,
|
||||||
|
PATCH,
|
||||||
|
TRACE,
|
||||||
|
Any,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Method {
|
||||||
|
fn default() -> Self {
|
||||||
|
Method::Any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Permission {
|
||||||
|
/// No authorization required
|
||||||
|
AllowAll,
|
||||||
|
|
||||||
|
/// Required the user to be logged in
|
||||||
|
AnyUser,
|
||||||
|
|
||||||
|
/// Requires the user to be logged in and part of the specific group
|
||||||
|
Group(String),
|
||||||
|
|
||||||
|
Or(Box<Permission>, Box<Permission>),
|
||||||
|
And(Box<Permission>, Box<Permission>),
|
||||||
|
Not(Box<Permission>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rule {
|
||||||
|
pub fn parse(s: &str) -> Result<Rule, String> {
|
||||||
|
ron::from_str(s).map_err(|e| format!("Failed to parse Rule: {}", e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Rule {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.method {
|
||||||
|
Method::Any => {}
|
||||||
|
_ => write!(f, "{:?} ", self.method)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "{:?} ", self.path)?;
|
||||||
|
write!(f, "permission: {}", self.permission)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Permission {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Permission::AllowAll => write!(f, "allow all"),
|
||||||
|
Permission::Group(group) => write!(f, "is group '{}'", group),
|
||||||
|
Permission::AnyUser => write!(f, "is logged in"),
|
||||||
|
Permission::Or(a, b) => write!(f, "({}) or ({})", a, b),
|
||||||
|
Permission::And(a, b) => write!(f, "({}) and ({})", a, b),
|
||||||
|
Permission::Not(perm) => write!(f, "not {}", perm),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user