mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 22:52:22 +00:00
Migrate principal store to sqlite
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
<h3>Groups</h3>
|
||||
|
||||
<ul>
|
||||
{% for group in user.memberships %}
|
||||
{% for group in user.memberships() %}
|
||||
<li>{{ group }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -28,7 +28,7 @@
|
||||
<th>Created at</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
{% for app_token in user.app_tokens %}
|
||||
{% for app_token in app_tokens %}
|
||||
<tr>
|
||||
<td>{{ app_token.name }}</td>
|
||||
<td>
|
||||
|
||||
@@ -24,7 +24,7 @@ use routes::{
|
||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc};
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User},
|
||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User, user::AppToken},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
@@ -51,16 +51,18 @@ pub fn generate_app_token() -> String {
|
||||
#[template(path = "pages/user.html")]
|
||||
struct UserPage {
|
||||
pub user: User,
|
||||
pub app_tokens: Vec<AppToken>,
|
||||
pub calendars: Vec<Calendar>,
|
||||
pub deleted_calendars: Vec<Calendar>,
|
||||
pub addressbooks: Vec<Addressbook>,
|
||||
pub deleted_addressbooks: Vec<Addressbook>,
|
||||
}
|
||||
|
||||
async fn route_user_named<CS: CalendarStore, AS: AddressbookStore>(
|
||||
async fn route_user_named<CS: CalendarStore, AS: AddressbookStore, AP: AuthenticationProvider>(
|
||||
path: Path<String>,
|
||||
cal_store: Data<CS>,
|
||||
addr_store: Data<AS>,
|
||||
auth_provider: Data<AP>,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
) -> impl Responder {
|
||||
@@ -91,6 +93,7 @@ async fn route_user_named<CS: CalendarStore, AS: AddressbookStore>(
|
||||
}
|
||||
|
||||
UserPage {
|
||||
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||
calendars,
|
||||
deleted_calendars,
|
||||
addressbooks,
|
||||
@@ -216,7 +219,7 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
|
||||
)
|
||||
.service(
|
||||
web::resource("/user/{user}")
|
||||
.get(route_user_named::<CS, AS>)
|
||||
.get(route_user_named::<CS, AS, AP>)
|
||||
.name(ROUTE_USER_NAMED),
|
||||
)
|
||||
// App token management
|
||||
@@ -287,7 +290,6 @@ impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
||||
displayname: None,
|
||||
principal_type: Default::default(),
|
||||
password: None,
|
||||
app_tokens: vec![],
|
||||
memberships: vec![],
|
||||
},
|
||||
false,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -16,3 +16,8 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
derive_more.workspace = true
|
||||
chrono.workspace = true
|
||||
password-auth.workspace = true
|
||||
password-hash.workspace = true
|
||||
uuid.workspace = true
|
||||
rand.workspace = true
|
||||
pbkdf2.workspace = true
|
||||
|
||||
28
crates/store_sqlite/migrations/4_principals.sql
Normal file
28
crates/store_sqlite/migrations/4_principals.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
CREATE TABLE principals (
|
||||
id TEXT NOT NULL,
|
||||
displayname TEXT,
|
||||
principal_type TEXT NOT NULL,
|
||||
password_hash TEXT,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE app_tokens (
|
||||
id TEXT NOT NULL,
|
||||
principal TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
displayname TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (principal)
|
||||
REFERENCES principals (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE memberships (
|
||||
principal TEXT NOT NULL,
|
||||
member_of TEXT NOT NULL,
|
||||
PRIMARY KEY (principal, member_of),
|
||||
CONSTRAINT fk_membership_principal
|
||||
FOREIGN KEY (principal) REFERENCES principals (id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_membership_member_of
|
||||
FOREIGN KEY (member_of) REFERENCES principals (id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -1,9 +1,10 @@
|
||||
use serde::Serialize;
|
||||
use sqlx::{sqlite::SqliteConnectOptions, Pool, Sqlite, SqlitePool};
|
||||
use sqlx::{Pool, Sqlite, SqlitePool, sqlite::SqliteConnectOptions};
|
||||
|
||||
pub mod addressbook_store;
|
||||
pub mod calendar_store;
|
||||
pub mod error;
|
||||
pub mod principal_store;
|
||||
pub mod subscription_store;
|
||||
|
||||
pub use error::Error;
|
||||
|
||||
250
crates/store_sqlite/src/principal_store.rs
Normal file
250
crates/store_sqlite/src/principal_store.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
use async_trait::async_trait;
|
||||
use derive_more::Constructor;
|
||||
use password_hash::PasswordHasher;
|
||||
use pbkdf2::{Params, password_hash::SaltString};
|
||||
use rand::rngs::OsRng;
|
||||
use rustical_store::{
|
||||
Error, Secret,
|
||||
auth::{AuthenticationProvider, User, user::AppToken},
|
||||
};
|
||||
use sqlx::{SqlitePool, types::Json};
|
||||
use tracing::instrument;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct PrincipalRow {
|
||||
id: String,
|
||||
displayname: Option<String>,
|
||||
principal_type: String,
|
||||
password_hash: Option<String>,
|
||||
memberships: Option<Json<Vec<Option<String>>>>,
|
||||
}
|
||||
|
||||
impl TryFrom<PrincipalRow> for User {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: PrincipalRow) -> Result<Self, Self::Error> {
|
||||
Ok(User {
|
||||
id: value.id,
|
||||
displayname: value.displayname,
|
||||
password: value.password_hash.map(Secret::from),
|
||||
principal_type: value.principal_type.as_str().try_into()?,
|
||||
memberships: value
|
||||
.memberships
|
||||
.map(|val| val.0)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Constructor)]
|
||||
pub struct SqlitePrincipalStore {
|
||||
db: SqlitePool,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AuthenticationProvider for SqlitePrincipalStore {
|
||||
#[instrument]
|
||||
async fn get_principals(&self) -> Result<Vec<User>, Error> {
|
||||
let result: Result<Vec<User>, Error> = sqlx::query_as!(
|
||||
PrincipalRow,
|
||||
r#"
|
||||
SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS "memberships: Json<Vec<Option<String>>>"
|
||||
FROM principals
|
||||
LEFT JOIN memberships ON principals.id == memberships.principal
|
||||
GROUP BY principals.id
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?
|
||||
.into_iter()
|
||||
.map(User::try_from)
|
||||
.collect();
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_principal(&self, id: &str) -> Result<Option<User>, Error> {
|
||||
let row= sqlx::query_as!(
|
||||
PrincipalRow,
|
||||
r#"
|
||||
SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS "memberships: Json<Vec<Option<String>>>"
|
||||
FROM (SELECT * FROM principals WHERE id = ?) AS principals
|
||||
LEFT JOIN memberships ON principals.id == memberships.principal
|
||||
GROUP BY principals.id
|
||||
"#,
|
||||
id
|
||||
)
|
||||
.fetch_optional(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?
|
||||
.map(User::try_from);
|
||||
if let Some(row) = row {
|
||||
Ok(Some(row?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn remove_principal(&self, id: &str) -> Result<(), Error> {
|
||||
sqlx::query!(r#"DELETE FROM principals WHERE id = ?"#, id)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn insert_principal(
|
||||
&self,
|
||||
user: User,
|
||||
overwrite: bool,
|
||||
) -> Result<(), rustical_store::Error> {
|
||||
// Would be cleaner to put this into a transaction but for now it will be fine
|
||||
if !overwrite && self.get_principal(&user.id).await?.is_some() {
|
||||
return Err(Error::AlreadyExists);
|
||||
}
|
||||
let principal_type = user.principal_type.as_str();
|
||||
let password = user.password.map(Secret::into_inner);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
REPLACE INTO principals
|
||||
(id, displayname, principal_type, password_hash)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"#,
|
||||
user.id,
|
||||
user.displayname,
|
||||
principal_type,
|
||||
password
|
||||
)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn get_app_tokens(&self, principal: &str) -> Result<Vec<AppToken>, Error> {
|
||||
Ok(sqlx::query_as!(
|
||||
AppToken,
|
||||
r#"SELECT id, displayname AS name, token, created_at AS "created_at: _" FROM app_tokens WHERE principal = ?"#,
|
||||
principal
|
||||
)
|
||||
.fetch_all(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?)
|
||||
}
|
||||
|
||||
#[instrument(skip(token))]
|
||||
async fn validate_app_token(&self, user_id: &str, token: &str) -> Result<Option<User>, Error> {
|
||||
for app_token in &self.get_app_tokens(user_id).await? {
|
||||
if password_auth::verify_password(token, app_token.token.as_ref()).is_ok() {
|
||||
return self.get_principal(user_id).await;
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn remove_app_token(&self, user_id: &str, token_id: &str) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
r#"DELETE FROM app_tokens WHERE (principal, id) = (?, ?)"#,
|
||||
user_id,
|
||||
token_id
|
||||
)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(password_input))]
|
||||
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)
|
||||
}
|
||||
|
||||
#[instrument(skip(token))]
|
||||
async fn add_app_token(
|
||||
&self,
|
||||
user_id: &str,
|
||||
name: String,
|
||||
token: String,
|
||||
) -> Result<String, Error> {
|
||||
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();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO app_tokens
|
||||
(id, principal, token, displayname)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"#,
|
||||
id,
|
||||
user_id,
|
||||
token_hash,
|
||||
name
|
||||
)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn add_membership(&self, principal: &str, member_of: &str) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
r#"REPLACE INTO memberships (principal, member_of) VALUES (?, ?)"#,
|
||||
principal,
|
||||
member_of
|
||||
)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
async fn remove_membership(&self, principal: &str, member_of: &str) -> Result<(), Error> {
|
||||
sqlx::query!(
|
||||
r#"DELETE FROM memberships WHERE (principal, member_of) = (?, ?)"#,
|
||||
principal,
|
||||
member_of
|
||||
)
|
||||
.execute(&self.db)
|
||||
.await
|
||||
.map_err(crate::Error::from)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user