diff --git a/src/db/error.rs b/src/db/error.rs index 573bbe1..ccdcd47 100644 --- a/src/db/error.rs +++ b/src/db/error.rs @@ -5,12 +5,18 @@ use crate::db::UserRef; pub type BoxedError = Box; #[derive(Debug)] -pub struct UserAlreadyExists(pub UserRef); +pub enum UserCreationError { + UserAlreadyExists(UserRef), + Permissions, +} -impl fmt::Display for UserAlreadyExists { +impl fmt::Display for UserCreationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "User already exists: {}", self.0) + match self { + Self::UserAlreadyExists(user) => write!(f, "User already exists: {user}"), + Self::Permissions => write!(f, "You do not have permissions to create this user"), + } } } -impl std::error::Error for UserAlreadyExists {} +impl std::error::Error for UserCreationError {} diff --git a/src/db/interface.rs b/src/db/interface.rs index 7a6d8ad..53a9ce2 100644 --- a/src/db/interface.rs +++ b/src/db/interface.rs @@ -1,4 +1,4 @@ -use crate::db::error::{BoxedError, UserAlreadyExists}; +use crate::db::error::{BoxedError, UserCreationError}; use crate::db::{Database, User, UserRef}; impl DatabaseInterface for Database { @@ -9,9 +9,21 @@ impl DatabaseInterface for Database { async fn create_user( &mut self, user: User, - ) -> Result, BoxedError> { + ) -> Result, BoxedError> { self.inner.write().await.create_user(user).await } + + async fn try_create_user( + &mut self, + new_user: User, + current_user: &User, + ) -> Result, BoxedError> { + self.inner + .write() + .await + .try_create_user(new_user, current_user) + .await + } } pub trait DatabaseInterface: Clone + Send + Sync + 'static { @@ -19,5 +31,11 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static { async fn create_user( &mut self, user: User, - ) -> Result, BoxedError>; + ) -> Result, BoxedError>; + #[expect(unused)] + async fn try_create_user( + &mut self, + new_user: User, + current_user: &User, + ) -> Result, BoxedError>; } diff --git a/src/db/memory.rs b/src/db/memory.rs index a3cfad5..75aea66 100644 --- a/src/db/memory.rs +++ b/src/db/memory.rs @@ -1,7 +1,7 @@ use std::future::{Future, ready}; -use crate::db::error::{BoxedError, UserAlreadyExists}; -use crate::db::{Database, DatabaseInterface, Group, User, UserRef}; +use crate::db::error::{BoxedError, UserCreationError}; +use crate::db::{Database, DatabaseInterface, Group, Operation, User, UserRef}; #[derive(Clone, Debug, Default)] pub struct MemoryDatabase { @@ -33,13 +33,35 @@ impl DatabaseInterface for MemoryDatabase { async fn create_user( &mut self, user: User, - ) -> Result, BoxedError> { + ) -> Result, BoxedError> { let user_ref = user.user_ref(); if self.get_user(&user_ref).await?.is_some() { - return Ok(Err(UserAlreadyExists(user_ref))); + return Ok(Err(UserCreationError::UserAlreadyExists(user_ref))); } self.users.push(user); Ok(Ok(())) } + + async fn try_create_user( + &mut self, + new_user: User, + current_user: &User, + ) -> Result, BoxedError> { + // First check permissions, then apply the operation + // + // 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)); + }; + + let op = Operation::CreateUser(new_user_domain.clone()); + if !current_user.can_perform(&op) { + return Ok(Err(UserCreationError::Permissions)); + } + + self.create_user(new_user).await + } } diff --git a/src/db/mod.rs b/src/db/mod.rs index b54747a..d77daca 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -7,5 +7,7 @@ mod interface; pub use interface::DatabaseInterface; mod memory; pub use memory::MemoryDatabase; +mod role; +pub use role::{Operation, Role}; mod user; pub use user::{User, UserRef}; diff --git a/src/db/role.rs b/src/db/role.rs new file mode 100644 index 0000000..aa42854 --- /dev/null +++ b/src/db/role.rs @@ -0,0 +1,37 @@ +#[derive(Clone, Debug)] +pub enum Operation { + CreateUser(String), +} + +#[derive(Clone, Debug)] +pub enum Role { + /// Can do anything + Admin, + /// Can only read data across all vhosts + #[expect(unused)] + ReadonlyAdmin, + /// Can do anything on a domain, except removing + /// oneself as a domain admin. + /// + /// Can not give away roles other than DomainModerator/User + DomainAdmin(String), + /// Can create users and reset passwords on a domain + #[expect(unused)] + DomainModerator(String), + /// Can only edit own profile + User, +} + +impl Role { + pub fn can_perform(&self, operation: &Operation) -> bool { + match operation { + Operation::CreateUser(op_domain) => match self { + Self::Admin => true, + Self::DomainAdmin(usr_domain) | Self::DomainModerator(usr_domain) => { + op_domain == usr_domain + } + _ => false, + }, + } + } +} diff --git a/src/db/user.rs b/src/db/user.rs index 4301d73..3986e44 100644 --- a/src/db/user.rs +++ b/src/db/user.rs @@ -1,23 +1,35 @@ use std::fmt; +use crate::db::{Operation, Role}; + #[derive(Clone, Debug)] pub struct UserRef { pub username: String, - pub domain: String, + pub domain: Option, } impl fmt::Display for UserRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}@{}", self.username, self.domain) + if let Some(domain) = &self.domain { + write!(f, "{}@{}", self.username, domain) + } else { + write!(f, "{}", self.username) + } } } #[derive(Clone, Debug)] pub struct User { + /// Username, without the domain part. Once set, cannot be edited. pub username: String, - pub domain: String, + /// Domain of the user. Once set, cannot be edited. + /// + /// Service accounts may exist with an empty domain. + pub domain: Option, pub password: String, + /// Mail for the user. Computed from username/domain, cannot be edited. pub mail: String, + pub role: Role, // recovery_mail: String, } @@ -28,10 +40,19 @@ impl User { domain: self.domain.clone(), } } + + #[expect(unused)] + pub fn can_perform(&self, operation: &Operation) -> bool { + self.role.can_perform(operation) + } } impl fmt::Display for User { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}@{}", self.username, self.domain) + if let Some(domain) = &self.domain { + write!(f, "{}@{}", self.username, domain) + } else { + write!(f, "{}", self.username) + } } } diff --git a/src/ldap/dn.rs b/src/ldap/dn.rs index 0f492f8..33371fe 100644 --- a/src/ldap/dn.rs +++ b/src/ldap/dn.rs @@ -149,8 +149,10 @@ impl Dn { let mut keys = VecMap::new(); keys.insert_or_append("uid", &user.username); keys.insert_or_append("ou", "people"); - for domain_component in user.domain.split('.') { - keys.insert_or_append("dc", domain_component); + if let Some(domain_components) = &user.domain { + for domain_component in domain_components.split('.') { + keys.insert_or_append("dc", domain_component); + } } Self { keys } diff --git a/src/ldap/filter/mail.rs b/src/ldap/filter/mail.rs index 0f17ac0..625ca8d 100644 --- a/src/ldap/filter/mail.rs +++ b/src/ldap/filter/mail.rs @@ -109,7 +109,7 @@ impl MailFilter { pub fn to_user_ref(&self) -> UserRef { UserRef { username: self.username.clone(), - domain: self.domain.clone(), + domain: Some(self.domain.clone()), } } } diff --git a/src/ldap/op/bind.rs b/src/ldap/op/bind.rs index 5b44c41..fabdfa6 100644 --- a/src/ldap/op/bind.rs +++ b/src/ldap/op/bind.rs @@ -76,9 +76,15 @@ impl BindDn { pub fn to_user_ref(&self) -> UserRef { let username = self.0.keys.get("uid").unwrap()[0].clone(); + // TODO: if we want service domains to connect over LDAP + // we need to remove this unwrap and allow a BindDn to + // not have a domain part. let domain = self.0.keys.get("dc").unwrap().join("."); - UserRef { username, domain } + UserRef { + username, + domain: Some(domain), + } } } diff --git a/src/main.rs b/src/main.rs index 0edea35..2b63b97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,18 +12,39 @@ mod stream; #[cfg(feature = "http")] use crate::http::http_listen; use cli::CliArgs; -use db::{Database, DatabaseInterface, MemoryDatabase, User}; +use db::{Database, DatabaseInterface, MemoryDatabase, Role, User}; use error::GlobalError; use ldap::ldap_handler; use listener::ListenerPath; async fn create_dummy_users(db: &mut Database) { + db.create_user(User { + username: "admin".to_string(), + domain: None, + password: "adminadmin".to_string(), + mail: "TODO".to_string(), + role: Role::Admin, + }) + .await + .unwrap() + .unwrap(); for domain in &["a", "b", "c"] { db.create_user(User { username: domain.to_string(), - domain: format!("{domain}.localhost"), + domain: Some(format!("{domain}.localhost")), password: "adminadmin".to_string(), mail: format!("{domain}@{domain}.localhost"), + role: Role::DomainAdmin(format!("{domain}.localhost")), + }) + .await + .unwrap() + .unwrap(); + db.create_user(User { + username: domain.to_string(), + domain: Some(format!("user{domain}.localhost")), + password: "adminadmin".to_string(), + mail: format!("user{domain}@{domain}.localhost"), + role: Role::User, }) .await .unwrap()