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