Migrate principal store to sqlite

This commit is contained in:
Lennart
2025-04-26 14:13:37 +02:00
parent 1f915b73de
commit 87112f3794
28 changed files with 597 additions and 351 deletions

View File

@@ -16,16 +16,13 @@ chrono = { workspace = true }
regex = { workspace = true }
lazy_static = { workspace = true }
thiserror = { workspace = true }
password-auth = { workspace = true }
actix-web = { workspace = true }
actix-session = { workspace = true }
actix-web-httpauth = { workspace = true }
tracing = { workspace = true }
pbkdf2 = { workspace = true }
chrono-tz = { workspace = true }
derive_more = { workspace = true }
rustical_xml.workspace = true
toml.workspace = true
tokio.workspace = true
rand.workspace = true
uuid.workspace = true

View File

@@ -1,5 +1,4 @@
pub mod middleware;
pub mod toml_user_store;
pub mod user;
use crate::error::Error;
use async_trait::async_trait;
@@ -21,8 +20,12 @@ pub trait AuthenticationProvider: 'static {
token: String,
) -> Result<String, Error>;
async fn remove_app_token(&self, user_id: &str, token_id: &str) -> Result<(), Error>;
async fn get_app_tokens(&self, principal: &str) -> Result<Vec<AppToken>, Error>;
async fn add_membership(&self, principal: &str, member_of: &str) -> Result<(), Error>;
async fn remove_membership(&self, principal: &str, member_of: &str) -> Result<(), Error>;
}
pub use middleware::AuthenticationMiddleware;
pub use toml_user_store::{TomlPrincipalStore, TomlUserStoreConfig};
use user::AppToken;
pub use user::User;

View File

@@ -1,170 +0,0 @@
use super::{AuthenticationProvider, user::AppToken};
use crate::{auth::User, error::Error};
use anyhow::anyhow;
use async_trait::async_trait;
use password_hash::PasswordHasher;
use pbkdf2::{
Params,
password_hash::{self, SaltString, rand_core::OsRng},
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, io, ops::Deref};
use tokio::sync::RwLock;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
struct TomlDataModel {
principals: Vec<User>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TomlUserStoreConfig {
pub path: String,
}
#[derive(Debug)]
pub struct TomlPrincipalStore {
pub principals: RwLock<HashMap<String, User>>,
config: TomlUserStoreConfig,
}
#[derive(thiserror::Error, Debug)]
pub enum TomlStoreError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Error parsing users toml: {0}")]
Toml(#[from] toml::de::Error),
}
impl TomlPrincipalStore {
pub fn new(config: TomlUserStoreConfig) -> Result<Self, TomlStoreError> {
let TomlDataModel { principals } = toml::from_str(&fs::read_to_string(&config.path)?)?;
Ok(Self {
principals: RwLock::new(HashMap::from_iter(
principals.into_iter().map(|user| (user.id.clone(), user)),
)),
config,
})
}
fn save(&self, principals: &HashMap<String, User>) -> Result<(), Error> {
let out = toml::to_string_pretty(&TomlDataModel {
principals: principals
.iter()
.map(|(_, value)| value.to_owned())
.collect(),
})
.map_err(|_| anyhow!("Error saving principal database"))?;
fs::write(&self.config.path, out)?;
Ok(())
}
}
#[async_trait]
impl AuthenticationProvider for TomlPrincipalStore {
async fn get_principals(&self) -> Result<Vec<User>, crate::Error> {
Ok(self.principals.read().await.values().cloned().collect())
}
async fn get_principal(&self, id: &str) -> Result<Option<User>, crate::Error> {
Ok(self.principals.read().await.get(id).cloned())
}
async fn insert_principal(&self, user: User, overwrite: bool) -> Result<(), crate::Error> {
let mut principals = self.principals.write().await;
if !overwrite && principals.contains_key(&user.id) {
return Err(Error::AlreadyExists);
}
principals.insert(user.id.clone(), user);
self.save(principals.deref())?;
Ok(())
}
async fn remove_principal(&self, id: &str) -> Result<(), crate::Error> {
let mut principals = self.principals.write().await;
principals.remove(id);
self.save(principals.deref())?;
Ok(())
}
async fn validate_password(
&self,
user_id: &str,
password_input: &str,
) -> Result<Option<User>, Error> {
let user: User = match self.get_principal(user_id).await? {
Some(user) => user,
None => return Ok(None),
};
let password = match &user.password {
Some(password) => password,
None => return Ok(None),
};
if password_auth::verify_password(password_input, password.as_ref()).is_ok() {
return Ok(Some(user));
}
Ok(None)
}
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error> {
let user: User = match self.get_principal(user_id).await? {
Some(user) => user,
None => return Ok(None),
};
for app_token in &user.app_tokens {
if password_auth::verify_password(token, app_token.token.as_ref()).is_ok() {
return Ok(Some(user));
}
}
Ok(None)
}
/// Returns an identifier for the new app token
async fn add_app_token(
&self,
user_id: &str,
name: String,
token: String,
) -> Result<String, Error> {
let mut principals = self.principals.write().await;
if let Some(principal) = principals.get_mut(user_id) {
let id = uuid::Uuid::new_v4().to_string();
let salt = SaltString::generate(OsRng);
let token_hash = pbkdf2::Pbkdf2
.hash_password_customized(
token.as_bytes(),
None,
None,
Params {
rounds: 1000,
..Default::default()
},
&salt,
)
.map_err(|_| Error::PasswordHash)?
.to_string();
principal.app_tokens.push(AppToken {
name,
token: token_hash.into(),
created_at: Some(chrono::Utc::now()),
id: id.clone(),
});
self.save(principals.deref())?;
Ok(id)
} else {
Err(Error::NotFound)
}
}
async fn remove_app_token(&self, user_id: &str, token_id: &str) -> Result<(), Error> {
let mut principals = self.principals.write().await;
if let Some(principal) = principals.get_mut(user_id) {
principal
.app_tokens
.retain(|AppToken { id, .. }| token_id != id);
self.save(principals.deref())?;
}
Ok(())
}
}

View File

@@ -7,12 +7,15 @@ use chrono::{DateTime, Utc};
use derive_more::Display;
use rustical_xml::ValueSerialize;
use serde::{Deserialize, Serialize};
use std::future::{Ready, ready};
use std::{
fmt::Display,
future::{Ready, ready},
};
use crate::Secret;
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.3
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Display, clap::ValueEnum)]
#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum PrincipalType {
#[default]
@@ -24,8 +27,27 @@ pub enum PrincipalType {
// TODO: X-Name, IANA-token
}
impl ValueSerialize for PrincipalType {
fn serialize(&self) -> String {
impl TryFrom<&str> for PrincipalType {
type Error = crate::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(match value {
"INDIVIDUAL" => Self::Individual,
"GROUP" => Self::Group,
"RESOURCE" => Self::Resource,
"ROOM" => Self::Room,
"UNKNOWN" => Self::Unknown,
_ => {
return Err(crate::Error::InvalidData(
"Invalid principal type".to_owned(),
));
}
})
}
}
impl PrincipalType {
pub fn as_str(&self) -> &'static str {
match self {
PrincipalType::Individual => "INDIVIDUAL",
PrincipalType::Group => "GROUP",
@@ -33,7 +55,18 @@ impl ValueSerialize for PrincipalType {
PrincipalType::Room => "ROOM",
PrincipalType::Unknown => "UNKNOWN",
}
.to_owned()
}
}
impl Display for PrincipalType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl ValueSerialize for PrincipalType {
fn serialize(&self) -> String {
self.to_string()
}
}
@@ -55,8 +88,6 @@ pub struct User {
pub principal_type: PrincipalType,
pub password: Option<Secret<String>>,
#[serde(default)]
pub app_tokens: Vec<AppToken>,
#[serde(default)]
pub memberships: Vec<String>,
}

View File

@@ -5,6 +5,12 @@ use serde::{Deserialize, Serialize};
#[derive(From, Clone, Deserialize, Serialize, AsRef)]
pub struct Secret<T>(pub T);
impl<T> Secret<T> {
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> std::fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Secret")