diff --git a/Cargo.lock b/Cargo.lock index 0b55b38..545982a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,7 +52,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", - "axum-macros", "bytes", "form_urlencoded", "futures-util", @@ -120,17 +119,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "base64" version = "0.22.1" diff --git a/Cargo.toml b/Cargo.toml index 2a473bd..903b294 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] argh = "0.1.19" -axum = { version = "0.8.9", optional = true, features = ["macros"] } +axum = { version = "0.8.9", optional = true } axum-extra = { version = "0.12.6", features = ["cookie"], optional = true } camino = "1.2.5" dn_escape = { path = "vendor/dn_escape" } diff --git a/src/db/common.rs b/src/db/common.rs index f7408c6..4f278b8 100644 --- a/src/db/common.rs +++ b/src/db/common.rs @@ -3,7 +3,7 @@ use tokio::sync::RwLock; use std::sync::Arc; use crate::db::error::BoxedError; -use crate::db::{DatabaseInterface, Domain, User, UserRef}; +use crate::db::{DatabaseInterface, UserRef}; #[derive(Clone, Debug)] pub struct Database { @@ -36,13 +36,4 @@ impl Database { tracing::debug!("Comparing {} and {}", user.password, password); Ok(user.password == password) } - - pub async fn domains_user_can_see(&self, user: &User) -> Result, BoxedError> { - Ok(self - .list_all_domains() - .await? - .into_iter() - .filter(|d| user.can_see_domain(&d.name)) - .collect()) - } } diff --git a/src/db/domain.rs b/src/db/domain.rs deleted file mode 100644 index 59a1a94..0000000 --- a/src/db/domain.rs +++ /dev/null @@ -1,6 +0,0 @@ -use serde::Serialize; - -#[derive(Clone, Debug, Default, Serialize)] -pub struct Domain { - pub name: String, -} diff --git a/src/db/error.rs b/src/db/error.rs index 9fdce05..ccdcd47 100644 --- a/src/db/error.rs +++ b/src/db/error.rs @@ -6,7 +6,6 @@ pub type BoxedError = Box; #[derive(Debug)] pub enum UserCreationError { - DomainNotFound(String), UserAlreadyExists(UserRef), Permissions, } @@ -14,7 +13,6 @@ pub enum UserCreationError { impl fmt::Display for UserCreationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::DomainNotFound(domain) => write!(f, "No domain {domain} to create user in"), Self::UserAlreadyExists(user) => write!(f, "User already exists: {user}"), Self::Permissions => write!(f, "You do not have permissions to create this user"), } @@ -22,25 +20,3 @@ impl fmt::Display for UserCreationError { } impl std::error::Error for UserCreationError {} - -#[derive(Debug)] -pub enum DomainCreationError { - DomainAlreadyExists(String), - InvalidDomain(String), -} - -impl fmt::Display for DomainCreationError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::DomainAlreadyExists(domain) => { - write!(f, "Cannot create domain {domain} because it already exists") - } - Self::InvalidDomain(domain) => write!( - f, - "Cannot create domain `{domain}` because it's not considered a valid domain" - ), - } - } -} - -impl std::error::Error for DomainCreationError {} diff --git a/src/db/interface.rs b/src/db/interface.rs index ca84fd9..668e03b 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -1,18 +1,7 @@ -use crate::db::error::{BoxedError, DomainCreationError, UserCreationError}; -use crate::db::{Database, Domain, User, UserRef}; +use crate::db::error::{BoxedError, UserCreationError}; +use crate::db::{Database, User, UserRef}; impl DatabaseInterface for Database { - async fn create_domain( - &mut self, - domain: &str, - ) -> Result, BoxedError> { - self.inner.write().await.create_domain(domain).await - } - - async fn get_domain(&self, domain: &str) -> Result, BoxedError> { - self.inner.read().await.get_domain(domain).await - } - async fn get_user(&self, user: &UserRef) -> Result, BoxedError> { self.inner.read().await.get_user(user).await } @@ -36,29 +25,12 @@ impl DatabaseInterface for Database { .await } - async fn list_all_domains(&self) -> Result, BoxedError> { - self.inner.read().await.list_all_domains().await - } - - async fn list_all_users(&self) -> Result, BoxedError> { - self.inner.read().await.list_all_users().await - } - - async fn list_domain_users(&self, domain: Option) -> Result, BoxedError> { - self.inner.read().await.list_domain_users(domain).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 { - fn create_domain( - &mut self, - domain: &str, - ) -> impl Future, BoxedError>> + Send; - fn get_domain( - &self, - domain: &str, - ) -> impl Future, BoxedError>> + Send; - fn get_user( &self, user: &UserRef, @@ -67,20 +39,13 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static { &mut self, user: User, ) -> Result, BoxedError>; - fn try_create_user( + #[expect(unused)] + async fn try_create_user( &mut self, new_user: User, current_user: &User, - ) -> impl Future, BoxedError>> + Send; - - fn list_all_domains(&self) -> impl Future, BoxedError>> + Send; - #[expect(unused)] - fn list_all_users(&self) -> impl Future, BoxedError>> + Send; - - /// List users on a specific domain. - /// - /// A `None` domain requested lists global service users. - fn list_domain_users( + ) -> 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 e170db0..62b98a3 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -1,15 +1,13 @@ use std::future::{Future, ready}; -use crate::db::error::{BoxedError, DomainCreationError, UserCreationError}; -use crate::db::{Database, DatabaseInterface, Domain, Group, Operation, User, UserRef}; +use crate::db::error::{BoxedError, UserCreationError}; +use crate::db::{Database, DatabaseInterface, Group, Operation, User, UserRef}; #[derive(Clone, Debug, Default)] pub struct MemoryDatabase { - // We store data in tables like in SQL - pub domains: Vec, + pub users: Vec, #[expect(unused)] pub groups: Vec, - pub users: Vec, } impl MemoryDatabase { @@ -19,46 +17,17 @@ impl MemoryDatabase { } impl DatabaseInterface for MemoryDatabase { - fn create_domain( - &mut self, - domain: &str, - ) -> impl Future, BoxedError>> { - if domain.is_empty() { - return ready(Ok(Err(DomainCreationError::InvalidDomain( - domain.to_string(), - )))); - } - - if self.domains.iter().find(|d| d.name == domain).is_some() { - return ready(Ok(Err(DomainCreationError::DomainAlreadyExists( - domain.to_string(), - )))); - } - - self.domains.push(Domain { - name: domain.to_string(), - }); - - ready(Ok(Ok(()))) - } - - fn get_domain(&self, domain: &str) -> impl Future, BoxedError>> { - let Some(domain) = self.domains.iter().find(|d| d.name == domain) else { - return ready(Ok(None)); - }; - - ready(Ok(Some(domain.clone()))) - } - fn get_user( &self, req_user: &UserRef, ) -> impl Future, BoxedError>> { - ready(Ok(self - .users - .iter() - .find(|u| u.username == req_user.username && u.domain == req_user.domain) - .cloned())) + for user in &self.users { + if user.username == req_user.username && user.domain == req_user.domain { + return ready(Ok(Some(user.clone()))); + } + } + + ready(Ok(None)) } async fn create_user( @@ -66,18 +35,10 @@ impl DatabaseInterface for MemoryDatabase { user: User, ) -> Result, BoxedError> { let user_ref = user.user_ref(); - if self.get_user(&user_ref).await?.is_some() { return Ok(Err(UserCreationError::UserAlreadyExists(user_ref))); } - // If a domain is requested (i.e. not a global user), make sure the domain exists - if let Some(req_domain) = &user.domain - && self.get_domain(req_domain).await?.is_none() - { - return Ok(Err(UserCreationError::DomainNotFound(req_domain.clone()))); - } - self.users.push(user); Ok(Ok(())) } @@ -91,6 +52,7 @@ impl DatabaseInterface for MemoryDatabase { // // TODO: for now we don't allow creating service users manually // so we assume there's a domain provided + // TODO: restrict user creation on non-declared domains let Some(new_user_domain) = &new_user.domain else { return Ok(Err(UserCreationError::Permissions)); }; @@ -103,23 +65,19 @@ impl DatabaseInterface for MemoryDatabase { self.create_user(new_user).await } - fn list_all_domains(&self) -> impl Future, BoxedError>> { - ready(Ok(self.domains.clone())) - } - - fn list_all_users(&self) -> impl Future, BoxedError>> { - ready(Ok(self.users.clone())) - } - - fn list_domain_users( + fn list_users( &self, domain: Option, ) -> impl Future, BoxedError>> { - ready(Ok(self - .users - .iter() - .filter(|u| u.domain == domain) - .cloned() - .collect())) + 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/mod.rs b/src/db/mod.rs index c2544e0..d77daca 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,7 +1,5 @@ mod common; pub use common::Database; -mod domain; -pub use domain::Domain; pub mod error; mod group; pub use group::Group; diff --git a/src/db/role.rs b/src/db/role.rs index 102933b..f717813 100644 --- a/src/db/role.rs +++ b/src/db/role.rs @@ -2,12 +2,11 @@ use serde::Serialize; #[derive(Clone, Debug)] pub enum Operation { - CreateDomain, CreateUser(String), ListUsers(Option), } -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, Serialize)] pub enum Role { /// Can do anything Admin, @@ -29,7 +28,6 @@ pub enum Role { impl Role { pub fn can_perform(&self, operation: &Operation) -> bool { match operation { - Operation::CreateDomain => self == &Self::Admin, Operation::CreateUser(op_domain) => match self { Self::Admin => true, Self::DomainAdmin(usr_domain) | Self::DomainModerator(usr_domain) => { @@ -46,12 +44,4 @@ impl Role { }, } } - - pub fn can_see_domain(&self, domain: &str) -> bool { - match self { - Self::Admin | Self::ReadonlyAdmin => true, - Self::DomainAdmin(d) | Self::DomainModerator(d) => domain == d, - Self::User => false, - } - } } diff --git a/src/db/user.rs b/src/db/user.rs index b7c8bcd..5a325b1 100644 --- a/src/db/user.rs +++ b/src/db/user.rs @@ -80,16 +80,6 @@ impl User { pub fn can_perform(&self, operation: &Operation) -> bool { self.role.can_perform(operation) } - - pub fn can_create_domain(&self) -> bool { - self.role.can_perform(&Operation::CreateDomain) - } - - pub fn can_see_domain(&self, domain: &str) -> bool { - let allowed = self.role.can_see_domain(domain); - tracing::debug!("{} can see domain {}: {}", self.mail, domain, allowed); - allowed - } } impl fmt::Display for User { diff --git a/src/http/domain.rs b/src/http/domain.rs deleted file mode 100644 index 8bccc5b..0000000 --- a/src/http/domain.rs +++ /dev/null @@ -1,81 +0,0 @@ -use axum::extract::{Form, Path, State}; -use axum::response::{Html, IntoResponse, Redirect, Response}; -use axum_extra::extract::cookie::CookieJar; -use http::StatusCode; -use minijinja::context; -use serde::Deserialize; - -use crate::db::{DatabaseInterface, Operation}; -use crate::http::HttpState; -use crate::http::login::login_page; - -pub async fn get_domain( - State(state): State>, - cookies: CookieJar, - Path(domain): Path, -) -> Response { - let Some(session) = state.sessions.get_session(&cookies) else { - return login_page(State(state), None).await.into_response(); - }; - - let domain = match state.db.get_domain(&domain).await { - Ok(Some(domain)) => domain, - Ok(None) => return format!("Domain not found: {domain}").into_response(), - Err(e) => { - return format!("Database error: {e}").into_response(); - } - }; - - let domain_users = match state.db.list_domain_users(Some(domain.name.clone())).await { - Ok(users) => users, - Err(e) => { - return format!("Database error: {e}").into_response(); - } - }; - - // Redundant because someone who can see the domain admin page for the moment - // always can create accounts. - let op = Operation::CreateUser(domain.name.clone()); - let can_create_user = session.user.can_perform(&op); - - let ctx = context! { - can_create_user, - domain, - user => session.user, - users => domain_users, - }; - - let page = state - .templates - .get_template("domain.html") - .unwrap() - .render(ctx) - .unwrap(); - (StatusCode::OK, Html(page)).into_response() -} - -#[derive(Clone, Debug, Deserialize)] -pub struct DomainCreationForm { - domainname: String, -} - -pub async fn create_domain( - State(mut state): State>, - cookies: CookieJar, - Form(form): Form, -) -> Response { - let Some(session) = state.sessions.get_session(&cookies) else { - return login_page(State(state), None).await.into_response(); - }; - - let op = Operation::CreateDomain; - if !session.user.can_perform(&op) { - return "Not authorized to create a new domain".into_response(); - } - - match state.db.create_domain(&form.domainname).await { - Ok(Ok(())) => Redirect::to(&format!("/domain/{}", form.domainname)).into_response(), - Ok(Err(e)) => format!("Failed to create domain {}: {}", form.domainname, e).into_response(), - Err(e) => format!("Database error: {e}").into_response(), - } -} diff --git a/src/http/home.rs b/src/http/home.rs deleted file mode 100644 index 595ac17..0000000 --- a/src/http/home.rs +++ /dev/null @@ -1,63 +0,0 @@ -use axum::extract::State; -use axum::response::{Html, IntoResponse, Response}; -use axum_extra::extract::cookie::CookieJar; -use http::StatusCode; -use minijinja::context; - -use crate::db::{DatabaseInterface, Operation}; -use crate::http::HttpState; -use crate::http::login::login_page; - -pub async fn home( - State(state): State>, - cookies: CookieJar, -) -> 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_domain_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 domains = match state.db.domains_user_can_see(&session.user).await { - Ok(domains) => domains, - Err(e) => { - return format!("Database error: {e}").into_response(); - } - }; - - let ctx = context! { - domains, - user => session.user, - can_create_domain => session.user.can_create_domain(), - other_users, - }; - - let page = state - .templates - .get_template("home.html") - .unwrap() - .render(ctx) - .unwrap(); - (StatusCode::OK, Html(page)).into_response() - } else { - login_page(State(state), None).await - } -} diff --git a/src/http/mod.rs b/src/http/mod.rs index def0f22..98a20ed 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,25 +1,26 @@ use axum::Router; +use axum::extract::State; +use axum::response::{Html, IntoResponse, Response}; use axum::routing::{get, post}; use axum::serve::Listener as AxumListener; -use minijinja::Environment; +use axum_extra::extract::cookie::CookieJar; +use http::StatusCode; #[cfg(not(feature = "embed"))] use minijinja::path_loader; +use minijinja::{Environment, context}; #[cfg(feature = "embed")] use static_serve::embed_assets; #[cfg(not(feature = "embed"))] use tower_http::services::ServeDir; -use crate::db::{Database, DatabaseInterface}; +use crate::db::{Database, DatabaseInterface, Operation}; use crate::listener::{Listener, ListenerKind}; use crate::stream::{AbstractSocketAddr, AbstractStreamKind}; -mod domain; -mod home; mod login; mod logout; mod session; use session::HttpSessionManager; -mod user; impl AxumListener for Listener { type Io = AbstractStreamKind; @@ -97,14 +98,52 @@ pub async fn http_listen(listener: Listener, db: Database< #[cfg(not(feature = "embed"))] let app = { Router::new().nest_service("/assets", ServeDir::new("assets")) }; let app = app - .route("/", get(home::home)) - .route("/login", get(home::home)) + .route("/", get(home)) + .route("/login", get(home)) .route("/login", post(login::post_login)) .route("/logout", get(logout::logout)) - .route("/domain/{domain}", get(domain::get_domain)) - .route("/domain", post(domain::create_domain)) - .route("/user", post(user::create_user)) .with_state(HttpState::new(db)); axum::serve(listener, app).await.unwrap(); } + +pub async fn home( + State(state): State>, + cookies: CookieJar, +) -> 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(ctx) + .unwrap(); + (StatusCode::OK, Html(page)).into_response() + } else { + login::login_page(State(state), None).await + } +} diff --git a/src/http/user.rs b/src/http/user.rs deleted file mode 100644 index d6eb054..0000000 --- a/src/http/user.rs +++ /dev/null @@ -1,49 +0,0 @@ -use axum::extract::{Form, State}; -use axum::response::{IntoResponse, Redirect, Response}; -use axum_extra::extract::cookie::CookieJar; -use serde::Deserialize; - -use crate::db::{DatabaseInterface, Role, User}; -use crate::http::HttpState; -use crate::http::login::login_page; - -#[derive(Clone, Debug, Deserialize)] -pub struct UserCreationForm { - pub username: String, - pub domain: String, - pub password: String, -} - -pub async fn create_user( - State(mut state): State>, - cookies: CookieJar, - Form(form): Form, -) -> Response { - let Some(session) = state.sessions.get_session(&cookies) else { - return login_page(State(state), None).await.into_response(); - }; - - // let domain = form.domain; - // let op = Operation::CreateUser(domain.clone()); - // if !session.user.can_perform(&op) { - // return format!("Not authorized to create a new user on domain {domain}").into_response() - // } - - let new_user = User { - mail: format!("{}@{}", form.username, form.domain), - username: form.username.clone(), - domain: Some(form.domain.clone()), - password: form.password, - role: Role::User, - }; - - match state.db.try_create_user(new_user, &session.user).await { - Ok(Ok(())) => Redirect::to(&format!("/domain/{}", form.domain)).into_response(), - Ok(Err(e)) => format!( - "Failed to create user {} on domain {}: {}", - form.username, form.domain, e - ) - .into_response(), - Err(e) => format!("Database error: {e}").into_response(), - } -} diff --git a/src/main.rs b/src/main.rs index cb6ee76..ace3805 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,24 +29,22 @@ async fn create_dummy_users(db: &mut Database) { .await .unwrap() .unwrap(); - for letter in &["a", "b", "c"] { - let domain = format!("{letter}.localhost"); - db.create_domain(&domain).await.unwrap().unwrap(); + for domain in &["a", "b", "c"] { db.create_user(User { - username: letter.to_string(), - domain: Some(domain.clone()), + username: domain.to_string(), + domain: Some(format!("{domain}.localhost")), password: "adminadmin".to_string(), - mail: format!("{letter}@{domain}"), - role: Role::DomainAdmin(domain.clone()), + mail: format!("{domain}@{domain}.localhost"), + role: Role::DomainAdmin(format!("{domain}.localhost")), }) .await .unwrap() .unwrap(); db.create_user(User { - username: format!("user{letter}"), - domain: Some(domain.clone()), + username: format!("user{domain}"), + domain: Some(format!("{domain}.localhost")), password: "adminadmin".to_string(), - mail: format!("user{letter}@{domain}"), + mail: format!("user{domain}@{domain}.localhost"), role: Role::User, }) .await diff --git a/templates/domain.html b/templates/domain.html deleted file mode 100644 index 758ad65..0000000 --- a/templates/domain.html +++ /dev/null @@ -1,30 +0,0 @@ -{% extends 'base.html' %} -{% block main %} -
- -

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

-
- Logout -
- {% if can_create_user %} -
-

Create user

-
- - - - -
-
- {% endif %} -
-

Users on {{ domain.name }}

-
    - {% for user in users %} -
  • {{ user.mail }}
  • - {% endfor %} -
-
-
-{% endblock %} - diff --git a/templates/home.html b/templates/home.html index d1f71b2..584fad4 100644 --- a/templates/home.html +++ b/templates/home.html @@ -6,26 +6,9 @@ - {% if can_create_domain %} -
-

Create domain

-
- - -
-
- {% endif %} -
-

Active domains you can see

- -
{% if other_users %}
-

Other users you have permission to see on your own domain

+

Other users you have permission to see

    {% for user in other_users %}
  • {{ user.mail }}