From bd4144a5ba7cd7c5c184d547b800a765c84c41ef Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Sat, 26 Aug 2023 12:39:11 +0000 Subject: [PATCH] Start authorization route --- Cargo.toml | 3 +- src/routes/authorize.rs | 63 ++++++++++++++++++++++++++ src/routes/mod.rs | 2 + src/state/login.rs | 6 +++ src/state/mod.rs | 5 +- ssowat.toml | 3 ++ yunohost-api/src/permissions/ssowat.rs | 49 +++++++++++++++++++- 7 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/routes/authorize.rs create mode 100644 ssowat.toml diff --git a/Cargo.toml b/Cargo.toml index dadc47a..f1db002 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,4 +25,5 @@ axum_typed_multipart = "0.8" async-trait = "0.1" serde = { version = "1", features = [ "derive" ] } file-owner = { version = "0.1" } -tower-cookies = "0.9" \ No newline at end of file +tower-cookies = "0.9" +url = "2.4" diff --git a/src/routes/authorize.rs b/src/routes/authorize.rs new file mode 100644 index 0000000..717aca8 --- /dev/null +++ b/src/routes/authorize.rs @@ -0,0 +1,63 @@ +use axum::{ + RequestPartsExt, + extract::{FromRequestParts, State}, + http::{StatusCode, header::HeaderMap, request::Parts}, +}; +use snafu::prelude::*; +use url::Url; + +use crate::{ + error::*, + state::{RoutableAppState, LoggedInUser}, + state::sessions::*, +}; + +// TODO: Implement as a typed header +pub struct OriginalURI(Url); + +#[async_trait] +impl FromRequestParts for OriginalURI { + type Rejection = Error; + + async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result { + let headers: HeaderMap = parts.extract().await.unwrap(); + if let Some(uri) = headers.get("X-Original-URI") { + // TODO: error + Ok( + OriginalURI(Url::parse(uri.to_str().unwrap()).unwrap()) + ) + } else { + // TODO: error + panic!() + } + } +} + +impl AsRef for OriginalURI { + fn as_ref(&self) -> &Url { + &self.0 + } +} + + +#[debug_handler] +pub async fn route(user: Option, uri: Option, state: State) -> StatusCode { + if uri.is_none() { + panic!(); + } + + let uri = uri.unwrap(); + let username = user.map(|u| u.username().clone()); + + match state.permissions.ssowat_config() { + Ok(conf) => { + if conf.user_has_permission_for_uri(username.as_ref(), uri.as_ref()) { + StatusCode::OK + } else { + StatusCode::FORBIDDEN + } + }, Err(e) => { + panic!() + } + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 7075864..384ddd7 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -6,6 +6,7 @@ use tower_cookies::CookieManagerLayer; use crate::state::RoutableAppState; +mod authorize; mod index; mod login; mod logout; @@ -16,6 +17,7 @@ pub fn router(subpath: Option, state: RoutableAppState) -> Router { .route("/", get(index::route)) .route("/login/", post(login::route)) .route("/logout/", get(logout::route)) + .route("/authorize/", get(authorize::route)) .layer(CookieManagerLayer::new()) .with_state(state); if let Some(p) = subpath { diff --git a/src/state/login.rs b/src/state/login.rs index a443b83..8384974 100644 --- a/src/state/login.rs +++ b/src/state/login.rs @@ -49,6 +49,12 @@ impl std::fmt::Display for LoggedInUser { } } +impl AsRef for LoggedInUser { + fn as_ref(&self) -> &Username { + &self.username() + } +} + #[async_trait] impl FromRequestParts for LoggedInUser { type Rejection = Error; diff --git a/src/state/mod.rs b/src/state/mod.rs index 623213b..206582c 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -1,5 +1,5 @@ use snafu::prelude::*; -use yunohost_api::{YunohostUsers, Username, Password}; +use yunohost_api::{YunohostUsers, Username, Password, YunohostPermissions}; use std::sync::Arc; @@ -19,6 +19,7 @@ pub const COOKIE_NAME: &'static str = "yunohost.ssowat"; pub struct AppState { pub sessions: SessionManager, pub users: YunohostUsers, + pub permissions: YunohostPermissions, } impl AppState { @@ -27,6 +28,8 @@ impl AppState { sessions: SessionManager::new().context(SessionSnafu)?, // Timeout in ms users: YunohostUsers::new(500).await.context(YunohostSnafu)?, + // TODO: make async + permissions: YunohostPermissions::new().context(YunohostSnafu)?, }) } diff --git a/ssowat.toml b/ssowat.toml new file mode 100644 index 0000000..8c32524 --- /dev/null +++ b/ssowat.toml @@ -0,0 +1,3 @@ +socket = /tmp/ssowat.socket +socket_group = www-data +socket_mode = "0o770" diff --git a/yunohost-api/src/permissions/ssowat.rs b/yunohost-api/src/permissions/ssowat.rs index e295838..5cb6f90 100644 --- a/yunohost-api/src/permissions/ssowat.rs +++ b/yunohost-api/src/permissions/ssowat.rs @@ -1,3 +1,4 @@ +use regex::Regex; use serde::{Serialize, Deserialize}; use url::Url; @@ -23,12 +24,58 @@ impl SSOWatConfig { // Domain not managed return None; } + + // Strip protocol but keep full URL + let stripped_uri = AsRef::::as_ref(uri) + .trim_start_matches("http") + .trim_start_matches("s") + .trim_start_matches("://"); + + // Check which app matches this URI, to find corresponding permission + for (key, val) in &self.permissions { + for uri_format in &val.uris { + if uri_format.starts_with("re:") { + let uri_format = uri_format.trim_start_matches("re:"); + // TODO: generate regex in advance + // TODO: error + let re = Regex::new(uri_format).unwrap(); + if re.is_match(stripped_uri) { + return Some(key.clone()); + } + } else { + if stripped_uri.starts_with(uri_format) { + return Some(key.clone()); + } + } + } + } + + // No app URI matched + return None; } else { // No domain (eg. http://8.8.8.8/) return None; } - todo!(); + } + + pub fn user_has_permission_for_uri(&self, username: Option<&Username>, uri: &Url) -> bool { + if let Some(permission_name) = self.permission_for_uri(uri) { + let permission = self.permissions.get(&permission_name).unwrap(); + if permission.public { + return true; + } + + if let Some(username) = username { + permission.users.contains(username) + } else { + // User is not logged-in. Non-public URIs are not authorized + false + } + } else { + // No permission matching this URI + false + } } }