diff --git a/src/db/interface.rs b/src/db/interface.rs index 53a9ce2..668e03b 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -24,10 +24,17 @@ impl DatabaseInterface for Database { .try_create_user(new_user, current_user) .await } + + async fn list_users(&self, domain: Option) -> Result, BoxedError> { + self.inner.read().await.list_users(domain).await + } } pub trait DatabaseInterface: Clone + Send + Sync + 'static { - async fn get_user(&self, user: &UserRef) -> Result, BoxedError>; + fn get_user( + &self, + user: &UserRef, + ) -> impl std::future::Future, BoxedError>> + Send; async fn create_user( &mut self, user: User, @@ -38,4 +45,8 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static { new_user: User, current_user: &User, ) -> Result, BoxedError>; + fn list_users( + &self, + domain: Option, + ) -> impl std::future::Future, BoxedError>> + Send; } diff --git a/src/db/memory.rs b/src/db/memory.rs index 75aea66..56c5934 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -64,4 +64,18 @@ impl DatabaseInterface for MemoryDatabase { self.create_user(new_user).await } + + fn list_users(&self, domain: Option) -> impl Future, BoxedError>> { + let users = if let Some(domain) = domain { + self + .users + .iter() + .filter(|user| user.domain.as_ref() == Some(&domain)) + .cloned() + .collect() + } else { + self.users.clone() + }; + ready(Ok(users)) + } } diff --git a/src/db/role.rs b/src/db/role.rs index aa42854..f717813 100644 --- a/src/db/role.rs +++ b/src/db/role.rs @@ -1,9 +1,12 @@ +use serde::Serialize; + #[derive(Clone, Debug)] pub enum Operation { CreateUser(String), + ListUsers(Option), } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub enum Role { /// Can do anything Admin, @@ -32,6 +35,13 @@ impl Role { } _ => false, }, + Operation::ListUsers(op_domain) => match self { + Self::Admin => true, + Self::DomainAdmin(usr_domain) | Self::DomainModerator(usr_domain) => { + op_domain.as_ref() == Some(usr_domain) + } + _ => false, + }, } } } diff --git a/src/db/user.rs b/src/db/user.rs index 3986e44..5a325b1 100644 --- a/src/db/user.rs +++ b/src/db/user.rs @@ -1,13 +1,49 @@ +use serde::Serialize; + use std::fmt; use crate::db::{Operation, Role}; +/// A requested user/domain combo for login, lowercased. +/// +/// Domain may be empty, but a value with more than one +/// `@` is considered invalid. #[derive(Clone, Debug)] pub struct UserRef { pub username: String, pub domain: Option, } +#[derive(Clone, Debug)] +pub struct InvalidUserRef(pub String); + +impl std::error::Error for InvalidUserRef {} + +impl fmt::Display for InvalidUserRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Invalid username: {}", self.0) + } +} + +impl UserRef { + pub fn from_user_maybe_domain(value: &str) -> Result { + let value = value.to_lowercase(); + + let mut parts = value.split('@'); + let username = parts.next().unwrap(); + let domain = parts.next(); + + if parts.next().is_some() { + return Err(InvalidUserRef(value.clone())); + } + + Ok(Self { + username: username.to_string(), + domain: domain.map(Into::into), + }) + } +} + impl fmt::Display for UserRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(domain) = &self.domain { @@ -18,7 +54,7 @@ impl fmt::Display for UserRef { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] pub struct User { /// Username, without the domain part. Once set, cannot be edited. pub username: String, @@ -41,7 +77,6 @@ impl User { } } - #[expect(unused)] pub fn can_perform(&self, operation: &Operation) -> bool { self.role.can_perform(operation) } diff --git a/src/http/login.rs b/src/http/login.rs index 2ffd75f..f62a7b0 100644 --- a/src/http/login.rs +++ b/src/http/login.rs @@ -5,7 +5,7 @@ use http::StatusCode; use minijinja::context; use serde::{Deserialize, Serialize}; -use crate::db::DatabaseInterface; +use crate::db::{DatabaseInterface, UserRef}; use crate::http::HttpState; #[derive(Debug, Deserialize)] @@ -23,14 +23,14 @@ pub enum LoginError { pub async fn login_page( State(state): State>, login_error: Option, -) -> (StatusCode, Html) { +) -> Response { let page = state .templates .get_template("login.html") .unwrap() .render(context! {login_error => login_error}) .unwrap(); - (StatusCode::OK, Html(page)) + (StatusCode::OK, Html(page)).into_response() } pub async fn post_login( @@ -40,9 +40,37 @@ pub async fn post_login( ) -> Response { if let Some(_session) = state.sessions.get_session(&cookies) { // Already logged in - (cookies, Redirect::to("/")).into_response() - } else if form.username == "admin" && form.password == "adminadmin" { - let cookies = state.sessions.add_session("admin", true, cookies); + return (cookies, Redirect::to("/")).into_response(); + } + + let req_user = match UserRef::from_user_maybe_domain(&form.username) { + Ok(req_user) => req_user, + Err(e) => { + return e.to_string().into_response(); + } + }; + + let success = match state.db.check_password(&req_user, &form.password).await { + Ok(success) => success, + Err(e) => { + return format!("Database error: {e}").into_response(); + } + }; + + if success { + let maybe_user = match state.db.get_user(&req_user).await { + Ok(user) => user, + Err(e) => { + return format!("Database error: {e}").into_response(); + } + }; + + let Some(user) = maybe_user else { + return "Woops, user has been deleted while you were logging in. What are the chances?!".to_string() + .into_response(); + }; + + let cookies = state.sessions.add_session(user, cookies); (cookies, Redirect::to("/")).into_response() } else { login_page(State(state), Some(LoginError::InvalidCredentials)) diff --git a/src/http/mod.rs b/src/http/mod.rs index 2f6070c..89187c2 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,6 +1,6 @@ use axum::Router; use axum::extract::State; -use axum::response::Html; +use axum::response::{Html, IntoResponse, Response}; use axum::routing::{get, post}; use axum::serve::Listener as AxumListener; use axum_extra::extract::cookie::CookieJar; @@ -8,7 +8,7 @@ use http::StatusCode; use minijinja::{Environment, context, path_loader}; use static_serve::embed_assets; -use crate::db::{Database, DatabaseInterface}; +use crate::db::{Database, DatabaseInterface, Operation}; use crate::listener::{Listener, ListenerKind}; use crate::stream::{AbstractSocketAddr, AbstractStreamKind}; @@ -92,15 +92,39 @@ pub async fn http_listen(listener: Listener, db: Database< pub async fn home( State(state): State>, cookies: CookieJar, -) -> (StatusCode, Html) { +) -> Response { if let Some(session) = state.sessions.get_session(&cookies) { + // When the user has no domain (service admin) list all domains + let op = Operation::ListUsers(session.user.domain.clone()); + let other_users = if session.user.can_perform(&op) { + match state.db.list_users(session.user.domain.clone()).await { + Ok(other_users) => other_users, + Err(e) => { + return format!("Database error: {e}").into_response(); + } + } + } else { + vec![] + }; + + tracing::info!( + "Found {} users on domain {:?}", + other_users.len(), + session.user.domain + ); + + let ctx = context! { + user => session.user, + other_users, + }; + let page = state .templates .get_template("home.html") .unwrap() - .render(context! {username => session.username}) + .render(ctx) .unwrap(); - (StatusCode::OK, Html(page)) + (StatusCode::OK, Html(page)).into_response() } else { login::login_page(State(state), None).await } diff --git a/src/http/session.rs b/src/http/session.rs index f1e951c..5a3a8db 100644 --- a/src/http/session.rs +++ b/src/http/session.rs @@ -3,6 +3,8 @@ use uuid::Uuid; use std::sync::{Arc, RwLock}; +use crate::db::User; + pub const COOKIE_NAME: &str = "lldap_session"; #[derive(Clone, Debug)] @@ -17,17 +19,13 @@ impl HttpSessionManager { } } - pub fn add_session(&self, username: &str, is_admin: bool, cookies: CookieJar) -> CookieJar { + pub fn add_session(&self, user: User, cookies: CookieJar) -> CookieJar { let uuid = Uuid::new_v4(); let mut cookie = Cookie::new(COOKIE_NAME, uuid.to_string()); cookie.set_path("/"); - let session = HttpSession { - username: username.to_string(), - is_admin, - uuid, - }; + let session = HttpSession { user, uuid }; { self.inner.write().unwrap().push(session); @@ -67,9 +65,16 @@ impl HttpSessionManager { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct HttpSession { - pub username: String, - pub is_admin: bool, + // TODO: by storing the session here, it means + // it needs to be updated every time we change the user. + pub user: User, pub uuid: Uuid, } + +impl PartialEq for HttpSession { + fn eq(&self, other: &Self) -> bool { + self.uuid == other.uuid + } +} diff --git a/src/main.rs b/src/main.rs index 2b63b97..ace3805 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,8 @@ async fn create_dummy_users(db: &mut Database) { username: "admin".to_string(), domain: None, password: "adminadmin".to_string(), - mail: "TODO".to_string(), + // TODO: what should we put here? + mail: "admin".to_string(), role: Role::Admin, }) .await @@ -40,8 +41,8 @@ async fn create_dummy_users(db: &mut Database) { .unwrap() .unwrap(); db.create_user(User { - username: domain.to_string(), - domain: Some(format!("user{domain}.localhost")), + username: format!("user{domain}"), + domain: Some(format!("{domain}.localhost")), password: "adminadmin".to_string(), mail: format!("user{domain}@{domain}.localhost"), role: Role::User, diff --git a/templates/home.html b/templates/home.html index a142aee..584fad4 100644 --- a/templates/home.html +++ b/templates/home.html @@ -2,10 +2,20 @@ {% block main %}
-

You are logged in as {{ username }}

+

You are logged in as {{ user.username }}

+ {% if other_users %} +
+

Other users you have permission to see

+
    + {% for user in other_users %} +
  • {{ user.mail }}
  • + {% endfor %} +
+
+ {% endif %}
{% endblock %}