diff --git a/src/db/domain.rs b/src/db/domain.rs new file mode 100644 index 0000000..5ea6d43 --- /dev/null +++ b/src/db/domain.rs @@ -0,0 +1,35 @@ +use crate::db::error::UserCreationError; +use crate::db::{Group, User, UserRef}; + +#[derive(Clone, Debug, Default)] +pub struct Domain { + pub name: String, + pub users: Vec, + #[expect(unused)] + pub groups: Vec, +} + +impl Domain { + pub fn get_user(&self, req_user: &UserRef) -> Option { + assert!( + req_user.domain.as_deref().unwrap_or("") == self.name, + "Should only call Domain::get_user on the matching domain. Asked for {:?} on domain {}", + req_user, + self.name + ); + + self.users + .iter() + .find(|u| u.username == req_user.username) + .cloned() + } + + pub fn create_user(&mut self, user: User, user_ref: UserRef) -> Result<(), UserCreationError> { + if self.get_user(&user_ref).is_some() { + return Err(UserCreationError::UserAlreadyExists(user_ref)); + } + + self.users.push(user); + Ok(()) + } +} diff --git a/src/db/error.rs b/src/db/error.rs index ccdcd47..9fdce05 100644 --- a/src/db/error.rs +++ b/src/db/error.rs @@ -6,6 +6,7 @@ pub type BoxedError = Box; #[derive(Debug)] pub enum UserCreationError { + DomainNotFound(String), UserAlreadyExists(UserRef), Permissions, } @@ -13,6 +14,7 @@ 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"), } @@ -20,3 +22,25 @@ 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 668e03b..ed6d86c 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -1,7 +1,18 @@ -use crate::db::error::{BoxedError, UserCreationError}; -use crate::db::{Database, User, UserRef}; +use crate::db::error::{BoxedError, DomainCreationError, UserCreationError}; +use crate::db::{Database, Domain, 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 } @@ -31,6 +42,12 @@ impl DatabaseInterface for Database { } pub trait DatabaseInterface: Clone + Send + Sync + 'static { + fn create_domain( + &mut self, + domain: &str, + ) -> impl Future, BoxedError>>; + fn get_domain(&self, domain: &str) -> impl Future, BoxedError>>; + fn get_user( &self, user: &UserRef, @@ -45,6 +62,10 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static { new_user: User, current_user: &User, ) -> Result, BoxedError>; + + /// Use empty string domain to request global users (TODO: this is not very DX) + /// + /// When domain does not exist, the returned list is empty. fn list_users( &self, domain: Option, diff --git a/src/db/memory.rs b/src/db/memory.rs index 62b98a3..b9b8545 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -1,46 +1,90 @@ use std::future::{Future, ready}; -use crate::db::error::{BoxedError, UserCreationError}; -use crate::db::{Database, DatabaseInterface, Group, Operation, User, UserRef}; +use crate::db::error::{BoxedError, DomainCreationError, UserCreationError}; +use crate::db::{Database, DatabaseInterface, Domain, Operation, User, UserRef}; #[derive(Clone, Debug, Default)] pub struct MemoryDatabase { - pub users: Vec, - #[expect(unused)] - pub groups: Vec, + pub domains: Vec, + /// Where global users/groups are registered + pub global_domain: Domain, } impl MemoryDatabase { pub fn new() -> Database { Database::new(Self::default()) } + + pub fn get_domain_mut(&mut self, domain: &str) -> Option<&mut Domain> { + self.domains.iter_mut().find(|d| d.name == domain) + } } impl DatabaseInterface for MemoryDatabase { - fn get_user( - &self, - req_user: &UserRef, - ) -> impl Future, BoxedError>> { - for user in &self.users { - if user.username == req_user.username && user.domain == req_user.domain { - return ready(Ok(Some(user.clone()))); - } + fn create_domain( + &mut self, + domain: &str, + ) -> impl Future, BoxedError>> { + if domain.is_empty() { + return ready(Ok(Err(DomainCreationError::InvalidDomain( + domain.to_string(), + )))); } - ready(Ok(None)) + 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(), + users: vec![], + groups: vec![], + }); + + ready(Ok(Ok(()))) } - async fn create_user( + 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()))) + } + + async fn get_user(&self, req_user: &UserRef) -> Result, BoxedError> { + let Some(req_domain) = &req_user.domain else { + // If no domain is provided for the user query, look up the global users + return Ok(self.global_domain.get_user(req_user)); + }; + + let Some(domain) = self.get_domain(req_domain).await? else { + return Ok(None); + }; + + Ok(domain.get_user(req_user)) + } + + fn create_user( &mut self, user: User, - ) -> Result, BoxedError> { + ) -> impl Future, BoxedError>> { let user_ref = user.user_ref(); - if self.get_user(&user_ref).await?.is_some() { - return Ok(Err(UserCreationError::UserAlreadyExists(user_ref))); - } - self.users.push(user); - Ok(Ok(())) + let Some(req_domain) = &user.domain else { + // No domain requested, this is a global user creation + return ready(Ok(self.global_domain.create_user(user, user_ref))); + }; + + let Some(domain) = self.get_domain_mut(req_domain) else { + return ready(Ok(Err(UserCreationError::DomainNotFound( + req_domain.clone(), + )))); + }; + + ready(Ok(domain.create_user(user, user_ref))) } async fn try_create_user( @@ -65,19 +109,23 @@ 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() + async fn list_users(&self, domain: Option) -> Result, BoxedError> { + if let Some(domain) = domain { + if domain.is_empty() { + Ok(self.global_domain.users.clone()) + } else { + let Some(domain) = self.get_domain(&domain).await? else { + return Ok(vec![]); + }; + Ok(domain.users.clone()) + } } else { - self.users.clone() - }; - ready(Ok(users)) + // Aggregate all users + let mut users = self.global_domain.users.clone(); + for domain in &self.domains { + users.extend(domain.users.clone()); + } + Ok(users) + } } } diff --git a/src/db/mod.rs b/src/db/mod.rs index d77daca..c2544e0 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,5 +1,7 @@ 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/main.rs b/src/main.rs index ace3805..cb6ee76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,22 +29,24 @@ async fn create_dummy_users(db: &mut Database) { .await .unwrap() .unwrap(); - for domain in &["a", "b", "c"] { + for letter in &["a", "b", "c"] { + let domain = format!("{letter}.localhost"); + db.create_domain(&domain).await.unwrap().unwrap(); db.create_user(User { - username: domain.to_string(), - domain: Some(format!("{domain}.localhost")), + username: letter.to_string(), + domain: Some(domain.clone()), password: "adminadmin".to_string(), - mail: format!("{domain}@{domain}.localhost"), - role: Role::DomainAdmin(format!("{domain}.localhost")), + mail: format!("{letter}@{domain}"), + role: Role::DomainAdmin(domain.clone()), }) .await .unwrap() .unwrap(); db.create_user(User { - username: format!("user{domain}"), - domain: Some(format!("{domain}.localhost")), + username: format!("user{letter}"), + domain: Some(domain.clone()), password: "adminadmin".to_string(), - mail: format!("user{domain}@{domain}.localhost"), + mail: format!("user{letter}@{domain}"), role: Role::User, }) .await