refactor: Store domains separately from users

This commit is contained in:
selfhoster selfhoster 2026-09-19 09:25:25 +02:00
commit 58a8c375f0
5 changed files with 59 additions and 88 deletions

View file

@ -1,35 +1,4 @@
use crate::db::error::UserCreationError;
use crate::db::{Group, User, UserRef};
#[derive(Clone, Debug, Default)]
pub struct Domain {
pub name: String,
pub users: Vec<User>,
#[expect(unused)]
pub groups: Vec<Group>,
}
impl Domain {
pub fn get_user(&self, req_user: &UserRef) -> Option<User> {
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(())
}
}

View file

@ -36,8 +36,12 @@ impl<D: DatabaseInterface> DatabaseInterface for Database<D> {
.await
}
async fn list_users(&self, domain: Option<String>) -> Result<Vec<User>, BoxedError> {
self.inner.read().await.list_users(domain).await
async fn list_all_users(&self) -> Result<Vec<User>, BoxedError> {
self.inner.read().await.list_all_users().await
}
async fn list_domain_users(&self, domain: Option<String>) -> Result<Vec<User>, BoxedError> {
self.inner.read().await.list_domain_users(domain).await
}
}
@ -63,10 +67,13 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static {
current_user: &User,
) -> Result<Result<(), UserCreationError>, BoxedError>;
/// Use empty string domain to request global users (TODO: this is not very DX)
#[expect(unused)]
fn list_all_users(&self) -> impl Future<Output = Result<Vec<User>, BoxedError>>;
/// List users on a specific domain.
///
/// When domain does not exist, the returned list is empty.
fn list_users(
/// A `None` domain requested lists global service users.
fn list_domain_users(
&self,
domain: Option<String>,
) -> impl std::future::Future<Output = Result<Vec<User>, BoxedError>> + Send;

View file

@ -1,23 +1,21 @@
use std::future::{Future, ready};
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
use crate::db::{Database, DatabaseInterface, Domain, Operation, User, UserRef};
use crate::db::{Database, DatabaseInterface, Domain, Group, Operation, User, UserRef};
#[derive(Clone, Debug, Default)]
pub struct MemoryDatabase {
// We store data in tables like in SQL
pub domains: Vec<Domain>,
/// Where global users/groups are registered
pub global_domain: Domain,
#[expect(unused)]
pub groups: Vec<Group>,
pub users: Vec<User>,
}
impl MemoryDatabase {
pub fn new() -> Database<Self> {
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 {
@ -39,8 +37,6 @@ impl DatabaseInterface for MemoryDatabase {
self.domains.push(Domain {
name: domain.to_string(),
users: vec![],
groups: vec![],
});
ready(Ok(Ok(())))
@ -54,37 +50,36 @@ impl DatabaseInterface for MemoryDatabase {
ready(Ok(Some(domain.clone())))
}
async fn get_user(&self, req_user: &UserRef) -> Result<Option<User>, 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 get_user(
&self,
req_user: &UserRef,
) -> impl Future<Output = Result<Option<User>, BoxedError>> {
ready(Ok(self
.users
.iter()
.find(|u| u.username == req_user.username && u.domain == req_user.domain)
.cloned()))
}
fn create_user(
async fn create_user(
&mut self,
user: User,
) -> impl Future<Output = Result<Result<(), UserCreationError>, BoxedError>> {
) -> Result<Result<(), UserCreationError>, BoxedError> {
let user_ref = user.user_ref();
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)));
};
if self.get_user(&user_ref).await?.is_some() {
return Ok(Err(UserCreationError::UserAlreadyExists(user_ref)));
}
let Some(domain) = self.get_domain_mut(req_domain) else {
return ready(Ok(Err(UserCreationError::DomainNotFound(
req_domain.clone(),
))));
};
// 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())));
}
ready(Ok(domain.create_user(user, user_ref)))
self.users.push(user);
Ok(Ok(()))
}
async fn try_create_user(
@ -109,23 +104,19 @@ impl DatabaseInterface for MemoryDatabase {
self.create_user(new_user).await
}
async fn list_users(&self, domain: Option<String>) -> Result<Vec<User>, 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 {
// Aggregate all users
let mut users = self.global_domain.users.clone();
for domain in &self.domains {
users.extend(domain.users.clone());
}
Ok(users)
}
fn list_all_users(&self) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
ready(Ok(self.users.clone()))
}
fn list_domain_users(
&self,
domain: Option<String>,
) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
ready(Ok(self
.users
.iter()
.filter(|u| u.domain == domain)
.cloned()
.collect()))
}
}

View file

@ -115,7 +115,11 @@ pub async fn home<D: DatabaseInterface>(
// 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 {
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();

View file

@ -8,7 +8,7 @@
</div>
{% if other_users %}
<div>
<h2>Other users you have permission to see</h2>
<h2>Other users you have permission to see on your own domain</h2>
<ul>
{% for user in other_users %}
<li>{{ user.mail }}</li>