Add more debugging information in authorize route, and cargo fmt

This commit is contained in:
selfhoster selfhoster 2023-08-27 19:40:47 +00:00
parent 9481c0ff7e
commit 907f22bfd4
17 changed files with 307 additions and 164 deletions

View File

@ -8,4 +8,4 @@ use std::path::PathBuf;
pub struct Cli { pub struct Cli {
/// Where to place the UNIX socket for SSOWat /// Where to place the UNIX socket for SSOWat
pub path: PathBuf, pub path: PathBuf,
} }

View File

@ -7,7 +7,10 @@ use std::path::PathBuf;
#[snafu(visibility(pub))] #[snafu(visibility(pub))]
pub enum Error { pub enum Error {
#[snafu(display("Failed to spawn unix socket at {}", path.display()))] #[snafu(display("Failed to spawn unix socket at {}", path.display()))]
SocketCreate { path: PathBuf, source: std::io::Error }, SocketCreate {
path: PathBuf,
source: std::io::Error,
},
#[snafu(display("Failed to spawn a web server"))] #[snafu(display("Failed to spawn a web server"))]
Server { source: hyper::Error }, Server { source: hyper::Error },
@ -16,19 +19,30 @@ pub enum Error {
Yunohost { source: yunohost_api::Error }, Yunohost { source: yunohost_api::Error },
#[snafu(display("{}", source))] #[snafu(display("{}", source))]
Session { source: crate::state::sessions::SessionError }, Session {
source: crate::state::sessions::SessionError,
},
#[snafu(display("Failed to executed tokio task"))] #[snafu(display("Failed to executed tokio task"))]
TokioTask { source: tokio::task::JoinError }, TokioTask { source: tokio::task::JoinError },
#[snafu(display("Failed to set permissions on file {}", path.display()))] #[snafu(display("Failed to set permissions on file {}", path.display()))]
Permissions { path: PathBuf, source: std::io::Error }, Permissions {
path: PathBuf,
source: std::io::Error,
},
#[snafu(display("Failed to set owner on file {}", path.display()))] #[snafu(display("Failed to set owner on file {}", path.display()))]
PermissionsChown { path: PathBuf, source: file_owner::FileOwnerError }, PermissionsChown {
path: PathBuf,
source: file_owner::FileOwnerError,
},
#[snafu(display("Failed to set group on file {}", path.display()))] #[snafu(display("Failed to set group on file {}", path.display()))]
PermissionsChgrp { path: PathBuf, source: file_owner::FileOwnerError }, PermissionsChgrp {
path: PathBuf,
source: file_owner::FileOwnerError,
},
#[snafu(display("No cookie jar"))] #[snafu(display("No cookie jar"))]
Cookie, Cookie,

View File

@ -1,7 +1,11 @@
#[macro_use] extern crate async_trait; #[macro_use]
#[macro_use] extern crate axum; extern crate async_trait;
#[macro_use] extern crate log; #[macro_use]
#[macro_use] extern crate serde; extern crate axum;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde;
use clap::Parser; use clap::Parser;
@ -25,9 +29,7 @@ async fn main() -> Result<(), error::Error> {
.await .await
.unwrap(); .unwrap();
let state = Arc::new( let state = Arc::new(state::AppState::new().await?);
state::AppState::new().await?
);
let app = routes::router(Some("/ssowat/".to_string()), state); let app = routes::router(Some("/ssowat/".to_string()), state);
utils::socket::serve(&path, app).await?; utils::socket::serve(&path, app).await?;

View File

@ -1,36 +1,40 @@
use axum::{ use axum::{
RequestPartsExt, extract::{FromRequestParts, State},
extract::{FromRequestParts, State}, http::{header::HeaderMap, request::Parts, StatusCode},
http::{StatusCode, header::HeaderMap, request::Parts}, response::IntoResponse,
RequestPartsExt,
}; };
use snafu::prelude::*; use snafu::prelude::*;
use url::Url; use url::Url;
use crate::{ use crate::{
error::*, error::*,
state::{RoutableAppState, LoggedInUser},
state::sessions::*, state::sessions::*,
state::{LoggedInUser, RoutableAppState},
}; };
// TODO: Implement as a typed header // TODO: Implement as a typed header
#[derive(Debug)]
pub struct OriginalURI(Url); pub struct OriginalURI(Url);
#[async_trait] #[async_trait]
impl FromRequestParts<RoutableAppState> for OriginalURI { impl FromRequestParts<RoutableAppState> for OriginalURI {
type Rejection = Error; type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result<Self, Self::Rejection> { async fn from_request_parts(
let headers: HeaderMap = parts.extract().await.unwrap(); parts: &mut Parts,
if let Some(uri) = headers.get("X-Original-URI") { state: &RoutableAppState,
// TODO: error ) -> Result<Self, Self::Rejection> {
Ok( let headers: HeaderMap = parts.extract().await.unwrap();
OriginalURI(Url::parse(uri.to_str().unwrap()).unwrap()) if let Some(uri) = headers.get("X-Original-URI") {
) trace!("Received original URI: {}", uri.to_str().unwrap());
} else { // TODO: error
// TODO: error Ok(OriginalURI(Url::parse(uri.to_str().unwrap()).unwrap()))
panic!() } else {
} // TODO: error
} panic!()
}
}
} }
impl AsRef<Url> for OriginalURI { impl AsRef<Url> for OriginalURI {
@ -39,24 +43,43 @@ impl AsRef<Url> for OriginalURI {
} }
} }
#[debug_handler] #[debug_handler]
pub async fn route(user: Option<LoggedInUser>, uri: Option<OriginalURI>, state: State<RoutableAppState>) -> StatusCode { pub async fn route(
user: Option<LoggedInUser>,
uri: Option<OriginalURI>,
state: State<RoutableAppState>,
) -> impl IntoResponse {
debug!("Starting authorization request");
if uri.is_none() { if uri.is_none() {
panic!(); panic!();
} }
let uri = uri.unwrap(); let uri = uri.unwrap();
let username = user.map(|u| u.username().clone()); let username = user.map(|u| u.username().clone());
debug!(
"Requesting authorization for user {:?} URI {}",
username,
uri.0.as_str()
);
match state.permissions.ssowat_config() { match state.permissions.ssowat_config() {
Ok(conf) => { Ok(conf) => {
let mut headers = HeaderMap::new();
if conf.user_has_permission_for_uri(username.as_ref(), uri.as_ref()) { if conf.user_has_permission_for_uri(username.as_ref(), uri.as_ref()) {
StatusCode::OK debug!("User {:?} is authorized.", username);
if let Some(username) = username {
headers.insert("Remote-User", username.as_str().parse().unwrap());
headers.insert("user", username.as_str().parse().unwrap());
}
let res = (StatusCode::OK, headers).into_response();
debug!("{:?}", res);
res
} else { } else {
StatusCode::FORBIDDEN debug!("User {:?} is not authorized.", username);
(StatusCode::FORBIDDEN, headers).into_response()
} }
}, Err(e) => { }
Err(e) => {
panic!() panic!()
} }
} }

View File

@ -1,16 +1,16 @@
use axum::{ use axum::{
extract::{FromRequest, Form, Json, State}, extract::{Form, FromRequest, Json, State},
http::{self, Request, StatusCode}, http::{self, Request, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
RequestExt, RequestExt,
}; };
use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; use axum_typed_multipart::{TryFromMultipart, TypedMultipart};
use tower_cookies::Cookies; use tower_cookies::Cookies;
use yunohost_api::{Username, Password}; use yunohost_api::{Password, Username};
use crate::{ use crate::{
error::*, error::*,
state::{COOKIE_NAME, RoutableAppState, LoggedInUser}, state::{LoggedInUser, RoutableAppState, COOKIE_NAME},
}; };
#[derive(Debug, TryFromMultipart, Deserialize)] #[derive(Debug, TryFromMultipart, Deserialize)]
@ -29,16 +29,20 @@ where
B::Data: Into<axum::body::Bytes>, B::Data: Into<axum::body::Bytes>,
B::Error: Into<axum::BoxError> + Send + std::error::Error, B::Error: Into<axum::BoxError> + Send + std::error::Error,
B: Send + 'static + axum::body::HttpBody, B: Send + 'static + axum::body::HttpBody,
S: Send S: Send,
{ {
type Rejection = Response; type Rejection = Response;
async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejection> { async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejection> {
let headers = req.headers(); let headers = req.headers();
if let Some(mime) = headers.get(http::header::CONTENT_TYPE).and_then(|v| v.to_str().ok()) { if let Some(mime) = headers
.get(http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
{
if mime.starts_with("application/json") { if mime.starts_with("application/json") {
let Json(login_form): Json<LoginForm> = req.extract().await.map_err(IntoResponse::into_response)?; let Json(login_form): Json<LoginForm> =
req.extract().await.map_err(IntoResponse::into_response)?;
return Ok(login_form); return Ok(login_form);
} }
@ -48,7 +52,8 @@ where
} }
if mime.starts_with("multipart/form-data") { if mime.starts_with("multipart/form-data") {
let TypedMultipart(login_form): TypedMultipart<LoginForm> = req.extract().await.map_err(IntoResponse::into_response)?; let TypedMultipart(login_form): TypedMultipart<LoginForm> =
req.extract().await.map_err(IntoResponse::into_response)?;
return Ok(login_form); return Ok(login_form);
} }
Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()) Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response())
@ -59,19 +64,37 @@ where
} }
#[debug_handler] #[debug_handler]
pub async fn route(user: Option<LoggedInUser>, cookies: Cookies, state: State<RoutableAppState>, form: LoginForm) -> Result<String, Error> { pub async fn route(
user: Option<LoggedInUser>,
cookies: Cookies,
state: State<RoutableAppState>,
form: LoginForm,
) -> Result<String, Error> {
trace!("ROUTE: /login/"); trace!("ROUTE: /login/");
if let Some(username) = user { if let Some(username) = user {
return Ok(format!("Welcome back, {}! You were already logged in.", username)); return Ok(format!(
"Welcome back, {}! You were already logged in.",
username
));
} }
debug!("Performing login attempt for user {}", &form.username); debug!("Performing login attempt for user {}", &form.username);
// No cookie, or cookie is invalid. Perform login. // No cookie, or cookie is invalid. Perform login.
if state.check_login(&form.username, &form.password).await.unwrap() { if state
debug!("Login was successful for user {}. Saving cookie now.", &form.username); .check_login(&form.username, &form.password)
let session = state.sessions.make_session(COOKIE_NAME, &form.username).await; .await
.unwrap()
{
debug!(
"Login was successful for user {}. Saving cookie now.",
&form.username
);
let session = state
.sessions
.make_session(COOKIE_NAME, &form.username)
.await;
cookies.add(session.cookie()); cookies.add(session.cookie());
Ok(format!("Welcome {}", session.username())) Ok(format!("Welcome {}", session.username()))
} else { } else {

View File

@ -1,4 +1,4 @@
use tower_cookies::{Cookies, Cookie}; use tower_cookies::{Cookie, Cookies};
use std::borrow::Cow; use std::borrow::Cow;
@ -7,7 +7,10 @@ use crate::state::LoggedOutUser;
pub async fn route(user: Option<LoggedOutUser>, cookies: Cookies) -> String { pub async fn route(user: Option<LoggedOutUser>, cookies: Cookies) -> String {
if let Some(user) = user { if let Some(user) = user {
let cookie = user.cookie(); let cookie = user.cookie();
cookies.remove(Cookie::new(Cow::Owned(cookie.name().to_string()), Cow::Owned(cookie.value().to_string()))); cookies.remove(Cookie::new(
Cow::Owned(cookie.name().to_string()),
Cow::Owned(cookie.value().to_string()),
));
return format!("Goodbye, {}. You are now logged out.", user.username()); return format!("Goodbye, {}. You are now logged out.", user.username());
} }

View File

@ -21,8 +21,7 @@ pub fn router(subpath: Option<String>, state: RoutableAppState) -> Router {
.layer(CookieManagerLayer::new()) .layer(CookieManagerLayer::new())
.with_state(state); .with_state(state);
if let Some(p) = subpath { if let Some(p) = subpath {
Router::new() Router::new().nest(&p, app)
.nest(&p, app)
} else { } else {
app app
} }

View File

@ -1,15 +1,11 @@
use axum::{ use axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt};
extract::FromRequestParts,
http::request::Parts,
RequestPartsExt,
};
use snafu::prelude::*; use snafu::prelude::*;
use tower_cookies::Cookies; use tower_cookies::Cookies;
use yunohost_api::Username; use yunohost_api::Username;
use crate::{ use crate::{
error::*, error::*,
state::{COOKIE_NAME, RoutableAppState, sessions::*}, state::{sessions::*, RoutableAppState, COOKIE_NAME},
}; };
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -33,8 +29,6 @@ impl LoggedInUser {
pub fn timestamp(&self) -> i64 { pub fn timestamp(&self) -> i64 {
self.timestamp self.timestamp
} }
} }
impl From<Session> for LoggedInUser { impl From<Session> for LoggedInUser {
@ -59,30 +53,51 @@ impl AsRef<Username> for LoggedInUser {
impl FromRequestParts<RoutableAppState> for LoggedInUser { impl FromRequestParts<RoutableAppState> for LoggedInUser {
type Rejection = Error; type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result<Self, Self::Rejection> { async fn from_request_parts(
parts: &mut Parts,
state: &RoutableAppState,
) -> Result<Self, Self::Rejection> {
let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?; let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?;
trace!("[SESSION:{}/login] Checking if the user has a valid session", COOKIE_NAME); trace!(
"[SESSION:{}/login] Checking if the user has a valid session",
COOKIE_NAME
);
let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) { let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) {
trace!("[SESSION:{}/login] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie); trace!(
"[SESSION:{}/login] User claims to have a session with cookie: {}",
COOKIE_NAME,
&session_cookie
);
session_cookie session_cookie
} else { } else {
trace!("[SESSION:{}/login] No current session cookie", COOKIE_NAME); trace!("[SESSION:{}/login] No current session cookie", COOKIE_NAME);
return Err(Error::Session { return Err(Error::Session {
source: SessionError::NoSession { source: SessionError::NoSession {
cookie_name: COOKIE_NAME.to_string(), cookie_name: COOKIE_NAME.to_string(),
} },
}); });
}; };
if let Some(session) = state.sessions.verify_cookie_session(session_cookie.value()).await.context(SessionSnafu)? { if let Some(session) = state
debug!("[SESSION:{}/login] User {} resumed session.", COOKIE_NAME, &session.username); .sessions
.verify_cookie_session(session_cookie.value())
.await
.context(SessionSnafu)?
{
debug!(
"[SESSION:{}/login] User {} resumed session.",
COOKIE_NAME, &session.username
);
return Ok(session.into()); return Ok(session.into());
} else { } else {
trace!("[SESSION:{}/login] User session is invalid or has expired.", COOKIE_NAME); trace!(
"[SESSION:{}/login] User session is invalid or has expired.",
COOKIE_NAME
);
return Err(Error::Session { return Err(Error::Session {
source: SessionError::InvalidOrExpired { source: SessionError::InvalidOrExpired {
cookie_name: COOKIE_NAME.to_string(), cookie_name: COOKIE_NAME.to_string(),
} },
}); });
} }
} }

View File

@ -1,16 +1,12 @@
use axum::{ use axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt};
extract::FromRequestParts,
http::request::Parts,
RequestPartsExt,
};
use snafu::prelude::*; use snafu::prelude::*;
use std::borrow::Cow; use std::borrow::Cow;
use tower_cookies::{Cookies, Cookie}; use tower_cookies::{Cookie, Cookies};
use yunohost_api::Username; use yunohost_api::Username;
use crate::{ use crate::{
error::*, error::*,
state::{COOKIE_NAME, RoutableAppState, sessions::*}, state::{sessions::*, RoutableAppState, COOKIE_NAME},
}; };
pub struct LoggedOutUser(Session); pub struct LoggedOutUser(Session);
@ -28,9 +24,9 @@ impl LoggedOutUser {
Cookie::build( Cookie::build(
Cow::Owned(self.0.cookie_name.to_string()), Cow::Owned(self.0.cookie_name.to_string()),
Cow::Owned(self.0.content.to_string()), Cow::Owned(self.0.content.to_string()),
).path( )
Cow::Owned("/".to_string()) .path(Cow::Owned("/".to_string()))
).finish() .finish()
} }
} }
@ -44,25 +40,47 @@ impl From<Session> for LoggedOutUser {
impl FromRequestParts<RoutableAppState> for LoggedOutUser { impl FromRequestParts<RoutableAppState> for LoggedOutUser {
type Rejection = Error; type Rejection = Error;
async fn from_request_parts(parts: &mut Parts, state: &RoutableAppState) -> Result<Self, Self::Rejection> { async fn from_request_parts(
parts: &mut Parts,
state: &RoutableAppState,
) -> Result<Self, Self::Rejection> {
let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?; let cookies: Cookies = parts.extract().await.map_err(|_| Error::Cookie)?;
trace!("[SESSION:{}/logout] Checking if the user has a valid session", COOKIE_NAME); trace!(
"[SESSION:{}/logout] Checking if the user has a valid session",
COOKIE_NAME
);
trace!("{:#?}", parts.headers); trace!("{:#?}", parts.headers);
let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) { let session_cookie = if let Some(session_cookie) = cookies.get(COOKIE_NAME) {
trace!("[SESSION:{}/logout] User claims to have a session with cookie: {}", COOKIE_NAME, &session_cookie); trace!(
"[SESSION:{}/logout] User claims to have a session with cookie: {}",
COOKIE_NAME,
&session_cookie
);
session_cookie session_cookie
} else { } else {
trace!("[SESSION:{}/logout] No current session cookie", COOKIE_NAME); trace!("[SESSION:{}/logout] No current session cookie", COOKIE_NAME);
return Err(Error::Session { return Err(Error::Session {
source: SessionError::NoSession { source: SessionError::NoSession {
cookie_name: COOKIE_NAME.to_string(), cookie_name: COOKIE_NAME.to_string(),
} },
}); });
}; };
if let Some(session) = state.sessions.verify_cookie_session(session_cookie.value()).await.context(SessionSnafu)? { if let Some(session) = state
debug!("[SESSION:{}/logout] User {} resumed session, requesting to log out.", COOKIE_NAME, &session.username); .sessions
if state.sessions.invalidate_session(session.timestamp, &session.content).await { .verify_cookie_session(session_cookie.value())
.await
.context(SessionSnafu)?
{
debug!(
"[SESSION:{}/logout] User {} resumed session, requesting to log out.",
COOKIE_NAME, &session.username
);
if state
.sessions
.invalidate_session(session.timestamp, &session.content)
.await
{
return Ok(session.into()); return Ok(session.into());
} else { } else {
warn!("[SESSION:{}/logout] User {} requested logout but was already logged out. Here's the session:\n{:#?}", COOKIE_NAME, &session.username, &session); warn!("[SESSION:{}/logout] User {} requested logout but was already logged out. Here's the session:\n{:#?}", COOKIE_NAME, &session.username, &session);
@ -70,15 +88,18 @@ impl FromRequestParts<RoutableAppState> for LoggedOutUser {
return Err(Error::Session { return Err(Error::Session {
source: SessionError::InvalidOrExpired { source: SessionError::InvalidOrExpired {
cookie_name: COOKIE_NAME.to_string(), cookie_name: COOKIE_NAME.to_string(),
} },
}); });
} }
} else { } else {
trace!("[SESSION:{}/logout] User session is invalid or has expired.", COOKIE_NAME); trace!(
"[SESSION:{}/logout] User session is invalid or has expired.",
COOKIE_NAME
);
return Err(Error::Session { return Err(Error::Session {
source: SessionError::InvalidOrExpired { source: SessionError::InvalidOrExpired {
cookie_name: COOKIE_NAME.to_string(), cookie_name: COOKIE_NAME.to_string(),
} },
}); });
} }
} }

View File

@ -1,5 +1,5 @@
use snafu::prelude::*; use snafu::prelude::*;
use yunohost_api::{YunohostUsers, Username, Password, YunohostPermissions}; use yunohost_api::{Password, Username, YunohostPermissions, YunohostUsers};
use std::sync::Arc; use std::sync::Arc;
@ -33,7 +33,14 @@ impl AppState {
}) })
} }
pub async fn check_login(&self, username: &Username, password: &Password) -> Result<bool, Error> { pub async fn check_login(
self.users.check_credentials(username, password).await.context(YunohostSnafu) &self,
username: &Username,
password: &Password,
) -> Result<bool, Error> {
self.users
.check_credentials(username, password)
.await
.context(YunohostSnafu)
} }
} }

View File

@ -1,4 +1,4 @@
use ring::{hmac,rand}; use ring::{hmac, rand};
use snafu::prelude::*; use snafu::prelude::*;
use std::borrow::Cow; use std::borrow::Cow;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -16,9 +16,15 @@ pub enum SessionError {
#[snafu(display("Malformed cookie: {}", content))] #[snafu(display("Malformed cookie: {}", content))]
MalformedCookie { content: String }, MalformedCookie { content: String },
#[snafu(display("Malformed hex cookie signature: {}", sig))] #[snafu(display("Malformed hex cookie signature: {}", sig))]
MalformedCookieSig { sig: String, source: hex::FromHexError }, MalformedCookieSig {
sig: String,
source: hex::FromHexError,
},
#[snafu(display("Malformed cookie UNIX timestamp: {}", timestamp))] #[snafu(display("Malformed cookie UNIX timestamp: {}", timestamp))]
MalformedCookieTimestamp { timestamp: String, source: std::num::ParseIntError }, MalformedCookieTimestamp {
timestamp: String,
source: std::num::ParseIntError,
},
#[snafu(display("Malformed cookie username (empty)"))] #[snafu(display("Malformed cookie username (empty)"))]
MalformedCookieUsername { source: yunohost_api::Error }, MalformedCookieUsername { source: yunohost_api::Error },
#[snafu(display("Invalid or expired session {}", cookie_name))] #[snafu(display("Invalid or expired session {}", cookie_name))]
@ -44,7 +50,6 @@ pub struct SessionManager {
pub invalidated_cookies: RwLock<Vec<Session>>, pub invalidated_cookies: RwLock<Vec<Session>>,
/// Expiration duration for set cookies, in seconds /// Expiration duration for set cookies, in seconds
pub expiration_secs: u64, pub expiration_secs: u64,
} }
impl SessionManager { impl SessionManager {
@ -59,7 +64,7 @@ impl SessionManager {
cookies: RwLock::new(Vec::new()), cookies: RwLock::new(Vec::new()),
invalidated_cookies: RwLock::new(Vec::new()), invalidated_cookies: RwLock::new(Vec::new()),
// TODO: make expiration configurable // TODO: make expiration configurable
expiration_secs: 3600 * 24 * 7 expiration_secs: 3600 * 24 * 7,
}) })
} }
@ -67,15 +72,24 @@ impl SessionManager {
/// - Err(_) if the cookie format is really wrong /// - Err(_) if the cookie format is really wrong
/// - Ok(Some(Session)) if the user is still logged in /// - Ok(Some(Session)) if the user is still logged in
/// - Ok(None) if the user is no longer lgoged in (invalid/expired cookie) /// - Ok(None) if the user is no longer lgoged in (invalid/expired cookie)
pub async fn verify_cookie_session(&self, cookie_claim: &str) -> Result<Option<Session>, SessionError> { pub async fn verify_cookie_session(
&self,
cookie_claim: &str,
) -> Result<Option<Session>, SessionError> {
// First check the expiration of the claimed cookie. // First check the expiration of the claimed cookie.
// If the timestamp was messed with, it will fail further verification. // If the timestamp was messed with, it will fail further verification.
if let Some(claimed_timestamp) = Session::has_expired(cookie_claim, self.expiration_secs)? { if let Some(claimed_timestamp) = Session::has_expired(cookie_claim, self.expiration_secs)? {
// The claimed timestamp is still valid. Check if we ever had that cookie in memory. // The claimed timestamp is still valid. Check if we ever had that cookie in memory.
// If the server was restarted, user will have to login again. // If the server was restarted, user will have to login again.
if let Some(valid_cookie) = self.find_cookie(&self.cookies, claimed_timestamp, &cookie_claim).await { if let Some(valid_cookie) = self
.find_cookie(&self.cookies, claimed_timestamp, &cookie_claim)
.await
{
// Make sure the session hasn't been invalidated // Make sure the session hasn't been invalidated
if let Some(_invalidated_cookie) = self.find_cookie(&self.invalidated_cookies, claimed_timestamp, &cookie_claim).await { if let Some(_invalidated_cookie) = self
.find_cookie(&self.invalidated_cookies, claimed_timestamp, &cookie_claim)
.await
{
// User has logged out or been removed from the system // User has logged out or been removed from the system
Ok(None) Ok(None)
} else { } else {
@ -98,12 +112,10 @@ impl SessionManager {
pub async fn make_session(&self, cookie_name: &str, username: &Username) -> Session { pub async fn make_session(&self, cookie_name: &str, username: &Username) -> Session {
let now = now(); let now = now();
let signable_payload = format!("{now}:{cookie_name}:{username}"); let signable_payload = format!("{now}:{cookie_name}:{username}");
let signed_payload = hmac::sign(&self.secret, signable_payload.as_bytes()).as_ref().to_vec(); let signed_payload = hmac::sign(&self.secret, signable_payload.as_bytes())
let cookie_payload = format!( .as_ref()
"{}:{}", .to_vec();
signable_payload, let cookie_payload = format!("{}:{}", signable_payload, hex::encode(&signed_payload),);
hex::encode(&signed_payload),
);
let cookie = Session { let cookie = Session {
cookie_name: cookie_name.to_string(), cookie_name: cookie_name.to_string(),
@ -126,9 +138,16 @@ impl SessionManager {
/// Invalidated a previously-set session cookie /// Invalidated a previously-set session cookie
pub async fn invalidate_session(&self, timestamp: i64, content: &str) -> bool { pub async fn invalidate_session(&self, timestamp: i64, content: &str) -> bool {
if let Some(cookie) = self.find_cookie(&self.cookies, timestamp, content).await { if let Some(cookie) = self.find_cookie(&self.cookies, timestamp, content).await {
debug!("[SESSION:{}] User {} is logging out", &cookie.cookie_name, &cookie.username); debug!(
"[SESSION:{}] User {} is logging out",
&cookie.cookie_name, &cookie.username
);
// Make sure the cookie was not already invalidated to avoid DOSing our invalidated cookie storage // Make sure the cookie was not already invalidated to avoid DOSing our invalidated cookie storage
if self.find_cookie(&self.invalidated_cookies, timestamp, content).await.is_none() { if self
.find_cookie(&self.invalidated_cookies, timestamp, content)
.await
.is_none()
{
let mut jar = self.invalidated_cookies.write().await; let mut jar = self.invalidated_cookies.write().await;
jar.push(cookie); jar.push(cookie);
return true; return true;
@ -140,12 +159,20 @@ impl SessionManager {
} }
/// Helper method to find a cookie with a specific timestamp, name and username in a cookie jar /// Helper method to find a cookie with a specific timestamp, name and username in a cookie jar
async fn find_cookie(&self, jar: &RwLock<Vec<Session>>, timestamp: i64, content: &str) -> Option<Session> { async fn find_cookie(
jar.read().await.iter().find(|cookie| { &self,
// First compare the timestamp (cheapest operation to invalidate the match) jar: &RwLock<Vec<Session>>,
cookie.timestamp == timestamp timestamp: i64,
&& cookie.content == content content: &str,
}).cloned() ) -> Option<Session> {
jar.read()
.await
.iter()
.find(|cookie| {
// First compare the timestamp (cheapest operation to invalidate the match)
cookie.timestamp == timestamp && cookie.content == content
})
.cloned()
} }
} }
@ -167,13 +194,16 @@ pub struct Session {
impl Session { impl Session {
/// Extrats the timestamp from a stringy cookie /// Extrats the timestamp from a stringy cookie
pub fn timestamp(cookie: &str) -> Result<i64, SessionError> { pub fn timestamp(cookie: &str) -> Result<i64, SessionError> {
let (timestamp, _rest) = cookie.split_once(':') let (timestamp, _rest) = cookie.split_once(':').context(MalformedCookieSnafu {
.context(MalformedCookieSnafu { content: cookie.to_string() })?; content: cookie.to_string(),
let timestamp: i64 = timestamp.parse().context(MalformedCookieTimestampSnafu { timestamp })?; })?;
let timestamp: i64 = timestamp
.parse()
.context(MalformedCookieTimestampSnafu { timestamp })?;
Ok(timestamp) Ok(timestamp)
} }
/// Verifies whether a given cookie string has expired, before parsing it entirely. Returns: /// Verifies whether a given cookie string has expired, before parsing it entirely. Returns:
/// - Err(SessionError) when the cookie is malformed /// - Err(SessionError) when the cookie is malformed
/// - Ok(Some(timestamp)) when the cookie is still valid /// - Ok(Some(timestamp)) when the cookie is still valid
@ -193,7 +223,7 @@ impl Session {
Ok(None) Ok(None)
} }
} }
/// The typed [`yunohost_api::Username`] for which this cookie is deemed valid. /// The typed [`yunohost_api::Username`] for which this cookie is deemed valid.
pub fn username(&self) -> &Username { pub fn username(&self) -> &Username {
&self.username &self.username
@ -203,8 +233,8 @@ impl Session {
Cookie::build( Cookie::build(
Cow::Owned(self.cookie_name.to_string()), Cow::Owned(self.cookie_name.to_string()),
Cow::Owned(self.content.to_string()), Cow::Owned(self.content.to_string()),
).path( )
Cow::Owned("/".to_string()) .path(Cow::Owned("/".to_string()))
).finish() .finish()
} }
} }

View File

@ -1,15 +1,8 @@
use file_owner::PathExt; use file_owner::PathExt;
use snafu::prelude::*; use snafu::prelude::*;
use tokio::{ use tokio::{fs::set_permissions, task::spawn_blocking};
fs::set_permissions,
task::spawn_blocking,
};
use std::{ use std::{fs::Permissions, os::unix::fs::PermissionsExt, path::Path};
fs::Permissions,
os::unix::fs::PermissionsExt,
path::Path,
};
use crate::error::*; use crate::error::*;
@ -45,34 +38,41 @@ impl FSPermissions {
pub async fn apply_to(&self, path: &Path) -> Result<(), Error> { pub async fn apply_to(&self, path: &Path) -> Result<(), Error> {
if let Some(mode) = self.mode { if let Some(mode) = self.mode {
set_permissions( set_permissions(path, Permissions::from_mode(mode))
path, .await
Permissions::from_mode(mode) .context(PermissionsSnafu {
).await.context(PermissionsSnafu { path: path.to_path_buf()})?; path: path.to_path_buf(),
})?;
} }
if let Some(owner) = &self.owner { if let Some(owner) = &self.owner {
let owner = owner.to_string(); let owner = owner.to_string();
let path = path.to_path_buf(); let path = path.to_path_buf();
let _ = spawn_blocking(move || -> Result<(), Error> { let _ = spawn_blocking(move || -> Result<(), Error> {
Ok( Ok(path
path.set_owner(owner.as_str()) .set_owner(owner.as_str())
.context(PermissionsChownSnafu { path: path.to_path_buf() })? .context(PermissionsChownSnafu {
) path: path.to_path_buf(),
}).await.context(TokioTaskSnafu)?; })?)
})
.await
.context(TokioTaskSnafu)?;
} }
if let Some(group) = &self.group { if let Some(group) = &self.group {
let group = group.to_string(); let group = group.to_string();
let path = path.to_path_buf(); let path = path.to_path_buf();
let _ = spawn_blocking(move || -> Result<(), Error> { let _ = spawn_blocking(move || -> Result<(), Error> {
Ok( Ok(path
path.set_group(group.as_str()) .set_group(group.as_str())
.context(PermissionsChgrpSnafu { path: path.to_path_buf() })? .context(PermissionsChgrpSnafu {
) path: path.to_path_buf(),
}).await.context(TokioTaskSnafu)?; })?)
})
.await
.context(TokioTaskSnafu)?;
} }
Ok(()) Ok(())
} }
} }

View File

@ -1,3 +1,3 @@
pub mod fs; pub mod fs;
pub mod socket;
pub mod time; pub mod time;
pub mod socket;

View File

@ -1,7 +1,4 @@
use axum::{ use axum::{extract::connect_info, Router};
Router,
extract::connect_info,
};
use futures::ready; use futures::ready;
use hyper::{ use hyper::{
client::connect::{Connected, Connection}, client::connect::{Connected, Connection},
@ -21,10 +18,7 @@ use tokio::{
}; };
use tower::BoxError; use tower::BoxError;
use crate::{ use crate::{error::*, utils::fs::FSPermissions};
error::*,
utils::fs::FSPermissions,
};
pub struct ServerAccept { pub struct ServerAccept {
uds: UnixListener, uds: UnixListener,
@ -32,9 +26,7 @@ pub struct ServerAccept {
impl ServerAccept { impl ServerAccept {
pub fn new(uds: UnixListener) -> ServerAccept { pub fn new(uds: UnixListener) -> ServerAccept {
ServerAccept { ServerAccept { uds }
uds,
}
} }
} }
@ -64,10 +56,7 @@ impl AsyncWrite for ClientConnection {
Pin::new(&mut self.stream).poll_write(cx, buf) Pin::new(&mut self.stream).poll_write(cx, buf)
} }
fn poll_flush( fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
Pin::new(&mut self.stream).poll_flush(cx) Pin::new(&mut self.stream).poll_flush(cx)
} }
@ -121,16 +110,16 @@ pub async fn serve(path: &Path, app: Router) -> Result<(), Error> {
.await .await
.unwrap(); .unwrap();
let uds = UnixListener::bind(path.clone()) let uds = UnixListener::bind(path.clone()).context(SocketCreateSnafu { path: path.clone() })?;
.context(SocketCreateSnafu { path: path.clone() })?;
// TODO: make proper permissions // TODO: make proper permissions
// Apply 777 permissions // Apply 777 permissions
FSPermissions::new().chmod(0o777).apply_to(&path).await?; FSPermissions::new().chmod(0o777).apply_to(&path).await?;
hyper::Server::builder(ServerAccept::new(uds)) hyper::Server::builder(ServerAccept::new(uds))
.serve(app.into_make_service_with_connect_info::<UdsConnectInfo>()) .serve(app.into_make_service_with_connect_info::<UdsConnectInfo>())
.await.context(ServerSnafu)?; .await
.context(ServerSnafu)?;
Ok(()) Ok(())
} }

View File

@ -1,5 +1,5 @@
use chrono::Utc; use chrono::Utc;
pub fn now() -> i64 { pub fn now() -> i64 {
Utc::now().timestamp() Utc::now().timestamp()
} }

View File

@ -1,3 +1,5 @@
#[macro_use] extern crate log;
mod cache; mod cache;
pub use cache::JsonCache; pub use cache::JsonCache;
mod credentials; mod credentials;

View File

@ -22,6 +22,7 @@ impl SSOWatConfig {
if let Some(domain) = uri.domain() { if let Some(domain) = uri.domain() {
if ! self.domains.contains(&domain.to_string()) { if ! self.domains.contains(&domain.to_string()) {
// Domain not managed // Domain not managed
trace!("Domain {} not managed by Yunohost", &domain);
return None; return None;
} }
@ -31,19 +32,25 @@ impl SSOWatConfig {
.trim_start_matches("s") .trim_start_matches("s")
.trim_start_matches("://"); .trim_start_matches("://");
trace!("Checking permissions for {}", stripped_uri);
// Check which app matches this URI, to find corresponding permission // Check which app matches this URI, to find corresponding permission
for (key, val) in &self.permissions { for (key, val) in &self.permissions {
for uri_format in &val.uris { for uri_format in &val.uris {
if uri_format.starts_with("re:") { if uri_format.starts_with("re:") {
trace!("Checking if URI matches regex: {}", uri_format);
let uri_format = uri_format.trim_start_matches("re:"); let uri_format = uri_format.trim_start_matches("re:");
// TODO: generate regex in advance // TODO: generate regex in advance
// TODO: error // TODO: error
let re = Regex::new(uri_format).unwrap(); let re = Regex::new(uri_format).unwrap();
if re.is_match(stripped_uri) { if re.is_match(stripped_uri) {
trace!("Found URI matches regex app: {}", key);
return Some(key.clone()); return Some(key.clone());
} }
} else { } else {
trace!("Checking if URI starts with: {}", uri_format);
if stripped_uri.starts_with(uri_format) { if stripped_uri.starts_with(uri_format) {
trace!("Found URI matches app: {}", key);
return Some(key.clone()); return Some(key.clone());
} }
} }
@ -51,9 +58,11 @@ impl SSOWatConfig {
} }
// No app URI matched // No app URI matched
trace!("No application matched for URI {}", stripped_uri);
return None; return None;
} else { } else {
// No domain (eg. http://8.8.8.8/) // No domain (eg. http://8.8.8.8/)
trace!("No domain requested for permission request");
return None; return None;
} }
@ -96,3 +105,9 @@ pub struct Permission {
pub struct PermissionName { pub struct PermissionName {
name: String, name: String,
} }
impl std::fmt::Display for PermissionName {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.name.fmt(fmt)
}
}