feat: Implement JSON-backed database
This commit is contained in:
parent
44c083a8ef
commit
9933990fd3
12 changed files with 179 additions and 18 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -169,6 +169,9 @@ name = "camino"
|
||||||
version = "1.2.5"
|
version = "1.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d"
|
checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
|
|
@ -530,6 +533,7 @@ dependencies = [
|
||||||
"minijinja",
|
"minijinja",
|
||||||
"minijinja-embed",
|
"minijinja-embed",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"static-serve",
|
"static-serve",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ edition = "2024"
|
||||||
argh = "0.1.19"
|
argh = "0.1.19"
|
||||||
axum = { version = "0.8.9", optional = true, features = ["macros"] }
|
axum = { version = "0.8.9", optional = true, features = ["macros"] }
|
||||||
axum-extra = { version = "0.12.6", features = ["cookie"], optional = true }
|
axum-extra = { version = "0.12.6", features = ["cookie"], optional = true }
|
||||||
camino = "1.2.5"
|
camino = { version = "1.2.5", features = ["serde1"] }
|
||||||
dn_escape = { path = "vendor/dn_escape" }
|
dn_escape = { path = "vendor/dn_escape" }
|
||||||
futures-util = { version = "0.3.34", features = ["sink"] }
|
futures-util = { version = "0.3.34", features = ["sink"] }
|
||||||
http = { version = "1.5.0", optional = true }
|
http = { version = "1.5.0", optional = true }
|
||||||
|
|
@ -15,6 +15,7 @@ ldap3_proto = "0.8.1"
|
||||||
minijinja = { version = "2.24.0", optional = true }
|
minijinja = { version = "2.24.0", optional = true }
|
||||||
minijinja-embed = { version = "2.24.0", optional = true }
|
minijinja-embed = { version = "2.24.0", optional = true }
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
|
serde_json = "1.0.151"
|
||||||
static-serve = { version = "0.6.3", optional = true }
|
static-serve = { version = "0.6.3", optional = true }
|
||||||
tokio = { version = "1.53.1", features = ["macros", "net", "rt", "time", "sync"] }
|
tokio = { version = "1.53.1", features = ["macros", "net", "rt", "time", "sync"] }
|
||||||
tokio-util = { version = "0.7.19", features = ["codec"] }
|
tokio-util = { version = "0.7.19", features = ["codec"] }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
use argh::FromArgs;
|
use argh::FromArgs;
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
|
||||||
/// Run the llldap server
|
/// Run the llldap server
|
||||||
#[derive(FromArgs)]
|
#[derive(FromArgs)]
|
||||||
pub struct CliArgs {
|
pub struct CliArgs {
|
||||||
|
/// path to the JSON file for the database
|
||||||
|
#[argh(option)]
|
||||||
|
pub db: Option<Utf8PathBuf>,
|
||||||
|
|
||||||
/// address or socket to listen on for LDAP connections
|
/// address or socket to listen on for LDAP connections
|
||||||
#[argh(option, default = "String::from(\"127.0.0.1:3389\")")]
|
#[argh(option, default = "String::from(\"127.0.0.1:3389\")")]
|
||||||
pub listen_ldap: String,
|
pub listen_ldap: String,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
pub struct Domain {
|
pub struct Domain {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
145
src/db/filesystem.rs
Normal file
145
src/db/filesystem.rs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Error as JsonError;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
use std::io::Error as IOError;
|
||||||
|
|
||||||
|
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
|
||||||
|
use crate::db::{Database, DatabaseInterface, Domain, MemoryDatabase, User, UserRef};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FilesystemDatabaseError {
|
||||||
|
ReadFileIO(Utf8PathBuf, IOError),
|
||||||
|
ReadFileJson(Utf8PathBuf, JsonError),
|
||||||
|
WriteFileIO(Utf8PathBuf, IOError),
|
||||||
|
WriteFileJson(Utf8PathBuf, JsonError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FilesystemDatabaseError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}",
|
||||||
|
match self {
|
||||||
|
Self::ReadFileIO(path, e) => format!("Failed to read database file {path}: {e}"),
|
||||||
|
Self::ReadFileJson(path, e) =>
|
||||||
|
format!("Failed to parse database JSON file {path}: {e}"),
|
||||||
|
Self::WriteFileIO(path, e) => format!("Failed to write database file {path}: {e}"),
|
||||||
|
Self::WriteFileJson(path, e) =>
|
||||||
|
format!("Failed to convert database to JSON file {path}: {e}"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for FilesystemDatabaseError {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
|
pub struct FilesystemDatabase {
|
||||||
|
// TODO: Once we have common validation steps in place across DB backends, we can reduce cloning.
|
||||||
|
// For now, we clone the DB on every write operation, and update it when saving to disk
|
||||||
|
// succeeds.
|
||||||
|
pub inner: MemoryDatabase,
|
||||||
|
pub path: Utf8PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilesystemDatabase {
|
||||||
|
pub async fn from_path(path: impl AsRef<Utf8Path>) -> Result<Database<Self>, BoxedError> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let s = tokio::fs::read(path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Box::new(FilesystemDatabaseError::ReadFileIO(path.to_path_buf(), e)))?;
|
||||||
|
let inner: MemoryDatabase = serde_json::from_slice(&s)
|
||||||
|
.map_err(|e| Box::new(FilesystemDatabaseError::ReadFileJson(path.to_path_buf(), e)))?;
|
||||||
|
Ok(Database::new(Self {
|
||||||
|
inner,
|
||||||
|
path: path.to_path_buf(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save(&mut self) -> Result<(), BoxedError> {
|
||||||
|
let s = serde_json::to_string(&self)
|
||||||
|
.map_err(|e| Box::new(FilesystemDatabaseError::WriteFileJson(self.path.clone(), e)))?;
|
||||||
|
Ok(tokio::fs::write(&self.path, &s)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Box::new(FilesystemDatabaseError::WriteFileIO(self.path.clone(), e)))?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabaseInterface for FilesystemDatabase {
|
||||||
|
async fn create_domain(
|
||||||
|
&mut self,
|
||||||
|
domain: &str,
|
||||||
|
) -> Result<Result<(), DomainCreationError>, BoxedError> {
|
||||||
|
let mut new_db = self.inner.clone();
|
||||||
|
|
||||||
|
if let Err(e) = new_db.create_domain(domain).await? {
|
||||||
|
return Ok(Err(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save().await?;
|
||||||
|
self.inner = new_db;
|
||||||
|
|
||||||
|
Ok(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_domain(&self, domain: &str) -> impl Future<Output = Result<Option<Domain>, BoxedError>> {
|
||||||
|
self.inner.get_domain(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_user(
|
||||||
|
&self,
|
||||||
|
req_user: &UserRef,
|
||||||
|
) -> impl Future<Output = Result<Option<User>, BoxedError>> {
|
||||||
|
self.inner.get_user(req_user)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_user(
|
||||||
|
&mut self,
|
||||||
|
user: User,
|
||||||
|
) -> Result<Result<(), UserCreationError>, BoxedError> {
|
||||||
|
let mut new_db = self.inner.clone();
|
||||||
|
|
||||||
|
if let Err(e) = new_db.create_user(user).await? {
|
||||||
|
return Ok(Err(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save().await?;
|
||||||
|
self.inner = new_db;
|
||||||
|
|
||||||
|
Ok(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn try_create_user(
|
||||||
|
&mut self,
|
||||||
|
new_user: User,
|
||||||
|
current_user: &User,
|
||||||
|
) -> Result<Result<(), UserCreationError>, BoxedError> {
|
||||||
|
let mut new_db = self.inner.clone();
|
||||||
|
|
||||||
|
if let Err(e) = new_db.try_create_user(new_user, current_user).await? {
|
||||||
|
return Ok(Err(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.save().await?;
|
||||||
|
self.inner = new_db;
|
||||||
|
|
||||||
|
Ok(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_all_domains(&self) -> impl Future<Output = Result<Vec<Domain>, BoxedError>> {
|
||||||
|
self.inner.list_all_domains()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_all_users(&self) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
|
||||||
|
self.inner.list_all_users()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_domain_users(
|
||||||
|
&self,
|
||||||
|
domain: Option<String>,
|
||||||
|
) -> impl Future<Output = Result<Vec<User>, BoxedError>> {
|
||||||
|
self.inner.list_domain_users(domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#[derive(Clone, Debug)]
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct Group {
|
pub struct Group {
|
||||||
#[expect(unused)]
|
|
||||||
name: String,
|
name: String,
|
||||||
#[expect(unused)]
|
|
||||||
domain: String,
|
domain: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static {
|
||||||
fn get_user(
|
fn get_user(
|
||||||
&self,
|
&self,
|
||||||
user: &UserRef,
|
user: &UserRef,
|
||||||
) -> impl std::future::Future<Output = Result<Option<User>, BoxedError>> + Send;
|
) -> impl Future<Output = Result<Option<User>, BoxedError>> + Send;
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
&mut self,
|
&mut self,
|
||||||
user: User,
|
user: User,
|
||||||
|
|
@ -83,5 +83,5 @@ pub trait DatabaseInterface: Clone + Send + Sync + 'static {
|
||||||
fn list_domain_users(
|
fn list_domain_users(
|
||||||
&self,
|
&self,
|
||||||
domain: Option<String>,
|
domain: Option<String>,
|
||||||
) -> impl std::future::Future<Output = Result<Vec<User>, BoxedError>> + Send;
|
) -> impl Future<Output = Result<Vec<User>, BoxedError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
use std::future::{Future, ready};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use std::future::ready;
|
||||||
|
|
||||||
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
|
use crate::db::error::{BoxedError, DomainCreationError, UserCreationError};
|
||||||
use crate::db::{Database, DatabaseInterface, Domain, Group, Operation, User, UserRef};
|
use crate::db::{Database, DatabaseInterface, Domain, Group, Operation, User, UserRef};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||||
pub struct MemoryDatabase {
|
pub struct MemoryDatabase {
|
||||||
// We store data in tables like in SQL
|
// We store data in tables like in SQL
|
||||||
pub domains: Vec<Domain>,
|
pub domains: Vec<Domain>,
|
||||||
#[expect(unused)]
|
|
||||||
pub groups: Vec<Group>,
|
pub groups: Vec<Group>,
|
||||||
pub users: Vec<User>,
|
pub users: Vec<User>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ pub use common::Database;
|
||||||
mod domain;
|
mod domain;
|
||||||
pub use domain::Domain;
|
pub use domain::Domain;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
mod filesystem;
|
||||||
|
pub use filesystem::FilesystemDatabase;
|
||||||
mod group;
|
mod group;
|
||||||
pub use group::Group;
|
pub use group::Group;
|
||||||
mod interface;
|
mod interface;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum Operation {
|
pub enum Operation {
|
||||||
|
|
@ -7,12 +7,11 @@ pub enum Operation {
|
||||||
ListUsers(Option<String>),
|
ListUsers(Option<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
/// Can do anything
|
/// Can do anything
|
||||||
Admin,
|
Admin,
|
||||||
/// Can only read data across all vhosts
|
/// Can only read data across all vhosts
|
||||||
#[expect(unused)]
|
|
||||||
ReadonlyAdmin,
|
ReadonlyAdmin,
|
||||||
/// Can do anything on a domain, except removing
|
/// Can do anything on a domain, except removing
|
||||||
/// oneself as a domain admin.
|
/// oneself as a domain admin.
|
||||||
|
|
@ -20,7 +19,6 @@ pub enum Role {
|
||||||
/// Can not give away roles other than DomainModerator/User
|
/// Can not give away roles other than DomainModerator/User
|
||||||
DomainAdmin(String),
|
DomainAdmin(String),
|
||||||
/// Can create users and reset passwords on a domain
|
/// Can create users and reset passwords on a domain
|
||||||
#[expect(unused)]
|
|
||||||
DomainModerator(String),
|
DomainModerator(String),
|
||||||
/// Can only edit own profile
|
/// Can only edit own profile
|
||||||
User,
|
User,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
|
@ -54,7 +54,7 @@ impl fmt::Display for UserRef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
/// Username, without the domain part. Once set, cannot be edited.
|
/// Username, without the domain part. Once set, cannot be edited.
|
||||||
pub username: String,
|
pub username: String,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ mod stream;
|
||||||
#[cfg(feature = "http")]
|
#[cfg(feature = "http")]
|
||||||
use crate::http::http_listen;
|
use crate::http::http_listen;
|
||||||
use cli::CliArgs;
|
use cli::CliArgs;
|
||||||
use db::{Database, DatabaseInterface, MemoryDatabase, Role, User};
|
use db::{Database, DatabaseInterface, FilesystemDatabase, MemoryDatabase, Role, User};
|
||||||
use error::GlobalError;
|
use error::GlobalError;
|
||||||
use ldap::ldap_handler;
|
use ldap::ldap_handler;
|
||||||
use listener::ListenerPath;
|
use listener::ListenerPath;
|
||||||
|
|
@ -69,6 +69,11 @@ async fn main() -> Result<(), GlobalError> {
|
||||||
|
|
||||||
let ldap_listener = ListenerPath::new(&cli.listen_ldap)?.listener().await?;
|
let ldap_listener = ListenerPath::new(&cli.listen_ldap)?.listener().await?;
|
||||||
|
|
||||||
|
// let mut db = if let Some(db_path) = &cli.db {
|
||||||
|
// FilesystemDatabase::from_path(db_path).await.unwrap()
|
||||||
|
// } else {
|
||||||
|
// MemoryDatabase::new()
|
||||||
|
// };
|
||||||
let mut db = MemoryDatabase::new();
|
let mut db = MemoryDatabase::new();
|
||||||
create_dummy_users(&mut db).await;
|
create_dummy_users(&mut db).await;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue