mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 18:12:27 +00:00
Migrate principal store to sqlite
This commit is contained in:
12
.sqlx/query-0195268daddd2d171577c93d1bae1b8937405bcefffa8f1f9b9c9f7f2084088f.json
generated
Normal file
12
.sqlx/query-0195268daddd2d171577c93d1bae1b8937405bcefffa8f1f9b9c9f7f2084088f.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM app_tokens WHERE (principal, id) = (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "0195268daddd2d171577c93d1bae1b8937405bcefffa8f1f9b9c9f7f2084088f"
|
||||||
|
}
|
||||||
12
.sqlx/query-0fd3167a58cbfb4ee44249dbc346d2d9077adfa04c35c8c6f2a1e24720baf753.json
generated
Normal file
12
.sqlx/query-0fd3167a58cbfb4ee44249dbc346d2d9077adfa04c35c8c6f2a1e24720baf753.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO app_tokens\n (id, principal, token, displayname)\n VALUES (?, ?, ?, ?)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "0fd3167a58cbfb4ee44249dbc346d2d9077adfa04c35c8c6f2a1e24720baf753"
|
||||||
|
}
|
||||||
38
.sqlx/query-1ebaf3fd99bee2382abc931a1eeb29badc3aabcf6b8fd58e4cf92721588a9966.json
generated
Normal file
38
.sqlx/query-1ebaf3fd99bee2382abc931a1eeb29badc3aabcf6b8fd58e4cf92721588a9966.json
generated
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT id, displayname AS name, token, created_at AS \"created_at: _\" FROM app_tokens WHERE principal = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "token",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at: _",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Datetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "1ebaf3fd99bee2382abc931a1eeb29badc3aabcf6b8fd58e4cf92721588a9966"
|
||||||
|
}
|
||||||
44
.sqlx/query-23a07f4a732f95ff7483cd1cfe3b74af4fe6b97546a631bc96d03bdc3d764ed0.json
generated
Normal file
44
.sqlx/query-23a07f4a732f95ff7483cd1cfe3b74af4fe6b97546a631bc96d03bdc3d764ed0.json
generated
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS \"memberships: Json<Vec<Option<String>>>\"\n FROM principals\n LEFT JOIN memberships ON principals.id == memberships.principal\n GROUP BY principals.id\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "displayname",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "principal_type",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password_hash",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memberships: Json<Vec<Option<String>>>",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "23a07f4a732f95ff7483cd1cfe3b74af4fe6b97546a631bc96d03bdc3d764ed0"
|
||||||
|
}
|
||||||
12
.sqlx/query-2f043f62a7c0eae1023e319f0bc8f35dfdcf6a8247e03b1de3e2cabb2d3ab8ae.json
generated
Normal file
12
.sqlx/query-2f043f62a7c0eae1023e319f0bc8f35dfdcf6a8247e03b1de3e2cabb2d3ab8ae.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n REPLACE INTO principals\n (id, displayname, principal_type, password_hash)\n VALUES (?, ?, ?, ?)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "2f043f62a7c0eae1023e319f0bc8f35dfdcf6a8247e03b1de3e2cabb2d3ab8ae"
|
||||||
|
}
|
||||||
12
.sqlx/query-3a1dbfbe9d22a62f1830d004548b7e805bcb9fdd24b49c8c9efa93df149b1002.json
generated
Normal file
12
.sqlx/query-3a1dbfbe9d22a62f1830d004548b7e805bcb9fdd24b49c8c9efa93df149b1002.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM principals WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "3a1dbfbe9d22a62f1830d004548b7e805bcb9fdd24b49c8c9efa93df149b1002"
|
||||||
|
}
|
||||||
12
.sqlx/query-879e2717335db3b04884fc91173c8507272f1804b27b6a7f61cbe1fbb01265cd.json
generated
Normal file
12
.sqlx/query-879e2717335db3b04884fc91173c8507272f1804b27b6a7f61cbe1fbb01265cd.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM memberships WHERE (principal, member_of) = (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "879e2717335db3b04884fc91173c8507272f1804b27b6a7f61cbe1fbb01265cd"
|
||||||
|
}
|
||||||
44
.sqlx/query-95dce97b2e3224c327690c36777e3ece84a9529551696198b745dd8c743c8a38.json
generated
Normal file
44
.sqlx/query-95dce97b2e3224c327690c36777e3ece84a9529551696198b745dd8c743c8a38.json
generated
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS \"memberships: Json<Vec<Option<String>>>\"\n FROM (SELECT * FROM principals WHERE id = ?) AS principals\n LEFT JOIN memberships ON principals.id == memberships.principal\n GROUP BY principals.id\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "displayname",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "principal_type",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password_hash",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "memberships: Json<Vec<Option<String>>>",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "95dce97b2e3224c327690c36777e3ece84a9529551696198b745dd8c743c8a38"
|
||||||
|
}
|
||||||
12
.sqlx/query-e947709ba03b108765082d1c4cff3dd8cb485fba5819ac914e20cb8e97037da9.json
generated
Normal file
12
.sqlx/query-e947709ba03b108765082d1c4cff3dd8cb485fba5819ac914e20cb8e97037da9.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "REPLACE INTO memberships (principal, member_of) VALUES (?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "e947709ba03b108765082d1c4cff3dd8cb485fba5819ac914e20cb8e97037da9"
|
||||||
|
}
|
||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -3197,8 +3197,6 @@ dependencies = [
|
|||||||
"derive_more 2.0.1",
|
"derive_more 2.0.1",
|
||||||
"ical",
|
"ical",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"password-auth",
|
|
||||||
"pbkdf2",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
"rstest",
|
"rstest",
|
||||||
@@ -3210,7 +3208,6 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
|
||||||
"tracing",
|
"tracing",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@@ -3222,12 +3219,17 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_more 2.0.1",
|
"derive_more 2.0.1",
|
||||||
|
"password-auth",
|
||||||
|
"password-hash",
|
||||||
|
"pbkdf2",
|
||||||
|
"rand 0.8.5",
|
||||||
"rustical_store",
|
"rustical_store",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -83,7 +83,9 @@ sqlx = { version = "0.8", default-features = false, features = [
|
|||||||
"runtime-tokio",
|
"runtime-tokio",
|
||||||
"macros",
|
"macros",
|
||||||
"migrate",
|
"migrate",
|
||||||
|
"json",
|
||||||
] }
|
] }
|
||||||
|
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||||
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
||||||
ical = { version = "0.11", features = ["generator", "serde"] }
|
ical = { version = "0.11", features = ["generator", "serde"] }
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<h3>Groups</h3>
|
<h3>Groups</h3>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for group in user.memberships %}
|
{% for group in user.memberships() %}
|
||||||
<li>{{ group }}</li>
|
<li>{{ group }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<th>Created at</th>
|
<th>Created at</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
{% for app_token in user.app_tokens %}
|
{% for app_token in app_tokens %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ app_token.name }}</td>
|
<td>{{ app_token.name }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ use routes::{
|
|||||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc};
|
use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc};
|
||||||
use rustical_store::{
|
use rustical_store::{
|
||||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||||
auth::{AuthenticationMiddleware, AuthenticationProvider, User},
|
auth::{AuthenticationMiddleware, AuthenticationProvider, User, user::AppToken},
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -51,16 +51,18 @@ pub fn generate_app_token() -> String {
|
|||||||
#[template(path = "pages/user.html")]
|
#[template(path = "pages/user.html")]
|
||||||
struct UserPage {
|
struct UserPage {
|
||||||
pub user: User,
|
pub user: User,
|
||||||
|
pub app_tokens: Vec<AppToken>,
|
||||||
pub calendars: Vec<Calendar>,
|
pub calendars: Vec<Calendar>,
|
||||||
pub deleted_calendars: Vec<Calendar>,
|
pub deleted_calendars: Vec<Calendar>,
|
||||||
pub addressbooks: Vec<Addressbook>,
|
pub addressbooks: Vec<Addressbook>,
|
||||||
pub deleted_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>,
|
path: Path<String>,
|
||||||
cal_store: Data<CS>,
|
cal_store: Data<CS>,
|
||||||
addr_store: Data<AS>,
|
addr_store: Data<AS>,
|
||||||
|
auth_provider: Data<AP>,
|
||||||
user: User,
|
user: User,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
@@ -91,6 +93,7 @@ async fn route_user_named<CS: CalendarStore, AS: AddressbookStore>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
UserPage {
|
UserPage {
|
||||||
|
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||||
calendars,
|
calendars,
|
||||||
deleted_calendars,
|
deleted_calendars,
|
||||||
addressbooks,
|
addressbooks,
|
||||||
@@ -216,7 +219,7 @@ pub fn configure_frontend<AP: AuthenticationProvider, CS: CalendarStore, AS: Add
|
|||||||
)
|
)
|
||||||
.service(
|
.service(
|
||||||
web::resource("/user/{user}")
|
web::resource("/user/{user}")
|
||||||
.get(route_user_named::<CS, AS>)
|
.get(route_user_named::<CS, AS, AP>)
|
||||||
.name(ROUTE_USER_NAMED),
|
.name(ROUTE_USER_NAMED),
|
||||||
)
|
)
|
||||||
// App token management
|
// App token management
|
||||||
@@ -287,7 +290,6 @@ impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
|||||||
displayname: None,
|
displayname: None,
|
||||||
principal_type: Default::default(),
|
principal_type: Default::default(),
|
||||||
password: None,
|
password: None,
|
||||||
app_tokens: vec![],
|
|
||||||
memberships: vec![],
|
memberships: vec![],
|
||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -16,16 +16,13 @@ chrono = { workspace = true }
|
|||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
lazy_static = { workspace = true }
|
lazy_static = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
password-auth = { workspace = true }
|
|
||||||
actix-web = { workspace = true }
|
actix-web = { workspace = true }
|
||||||
actix-session = { workspace = true }
|
actix-session = { workspace = true }
|
||||||
actix-web-httpauth = { workspace = true }
|
actix-web-httpauth = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
pbkdf2 = { workspace = true }
|
|
||||||
chrono-tz = { workspace = true }
|
chrono-tz = { workspace = true }
|
||||||
derive_more = { workspace = true }
|
derive_more = { workspace = true }
|
||||||
rustical_xml.workspace = true
|
rustical_xml.workspace = true
|
||||||
toml.workspace = true
|
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
pub mod toml_user_store;
|
|
||||||
pub mod user;
|
pub mod user;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
@@ -21,8 +20,12 @@ pub trait AuthenticationProvider: 'static {
|
|||||||
token: String,
|
token: String,
|
||||||
) -> Result<String, Error>;
|
) -> Result<String, Error>;
|
||||||
async fn remove_app_token(&self, user_id: &str, token_id: &str) -> Result<(), 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 middleware::AuthenticationMiddleware;
|
||||||
pub use toml_user_store::{TomlPrincipalStore, TomlUserStoreConfig};
|
use user::AppToken;
|
||||||
pub use user::User;
|
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 derive_more::Display;
|
||||||
use rustical_xml::ValueSerialize;
|
use rustical_xml::ValueSerialize;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::future::{Ready, ready};
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
future::{Ready, ready},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::Secret;
|
use crate::Secret;
|
||||||
|
|
||||||
/// https://datatracker.ietf.org/doc/html/rfc5545#section-3.2.3
|
/// 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")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum PrincipalType {
|
pub enum PrincipalType {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -24,8 +27,27 @@ pub enum PrincipalType {
|
|||||||
// TODO: X-Name, IANA-token
|
// TODO: X-Name, IANA-token
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValueSerialize for PrincipalType {
|
impl TryFrom<&str> for PrincipalType {
|
||||||
fn serialize(&self) -> String {
|
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 {
|
match self {
|
||||||
PrincipalType::Individual => "INDIVIDUAL",
|
PrincipalType::Individual => "INDIVIDUAL",
|
||||||
PrincipalType::Group => "GROUP",
|
PrincipalType::Group => "GROUP",
|
||||||
@@ -33,7 +55,18 @@ impl ValueSerialize for PrincipalType {
|
|||||||
PrincipalType::Room => "ROOM",
|
PrincipalType::Room => "ROOM",
|
||||||
PrincipalType::Unknown => "UNKNOWN",
|
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 principal_type: PrincipalType,
|
||||||
pub password: Option<Secret<String>>,
|
pub password: Option<Secret<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub app_tokens: Vec<AppToken>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub memberships: Vec<String>,
|
pub memberships: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ use serde::{Deserialize, Serialize};
|
|||||||
#[derive(From, Clone, Deserialize, Serialize, AsRef)]
|
#[derive(From, Clone, Deserialize, Serialize, AsRef)]
|
||||||
pub struct Secret<T>(pub T);
|
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> {
|
impl<T> std::fmt::Debug for Secret<T> {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str("Secret")
|
f.write_str("Secret")
|
||||||
|
|||||||
@@ -16,3 +16,8 @@ thiserror = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
chrono.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 serde::Serialize;
|
||||||
use sqlx::{sqlite::SqliteConnectOptions, Pool, Sqlite, SqlitePool};
|
use sqlx::{Pool, Sqlite, SqlitePool, sqlite::SqliteConnectOptions};
|
||||||
|
|
||||||
pub mod addressbook_store;
|
pub mod addressbook_store;
|
||||||
pub mod calendar_store;
|
pub mod calendar_store;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod principal_store;
|
||||||
pub mod subscription_store;
|
pub mod subscription_store;
|
||||||
|
|
||||||
pub use error::Error;
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@ cargo install --locked --git https://github.com/lennart-k/rustical
|
|||||||
docker run \
|
docker run \
|
||||||
-p 4000:4000 \
|
-p 4000:4000 \
|
||||||
-v YOUR_DATA_DIR:/var/lib/rustical/ \
|
-v YOUR_DATA_DIR:/var/lib/rustical/ \
|
||||||
-v YOUR_PRINCIPALS_TOML:/etc/rustical/principals.toml \
|
|
||||||
-v YOUR_CONFIG_TOML:/etc/rustical/config.toml \ # (1)!
|
-v YOUR_CONFIG_TOML:/etc/rustical/config.toml \ # (1)!
|
||||||
-e RUSTICAL__CONFIG_OPTION="asd" \ # (2)!
|
-e RUSTICAL__CONFIG_OPTION="asd" \ # (2)!
|
||||||
ghcr.io/lennart-k/rustical
|
ghcr.io/lennart-k/rustical
|
||||||
@@ -55,41 +54,16 @@ Every variable is
|
|||||||
- Dots become `__`
|
- Dots become `__`
|
||||||
- Arrays are JSON-encoded
|
- Arrays are JSON-encoded
|
||||||
|
|
||||||
|
|
||||||
## Users and groups
|
## Users and groups
|
||||||
|
|
||||||
Next, configure the principals by creating a file specified in `auth.toml.path` (by default `/etc/rustical/principals.toml`) and inserting your principals:
|
Next, you will want to set up your principals.
|
||||||
|
Using the `rustical principals` command you can manage principals and passwords.
|
||||||
|
|
||||||
```toml
|
Groups and rooms are also just principals and you can specify them as such using the `--principal-type` parameter.
|
||||||
[[principals]]
|
To assign a user to a group you can use the `rustical membership` command. Being a member to a principal means that you can completely act on their behalf and see their collections.
|
||||||
id = "user"
|
|
||||||
displayname = "User"
|
|
||||||
password = "$argon2id$......."
|
|
||||||
app_tokens = [
|
|
||||||
{id = "1", name = "Token", token = "$pbkdf2-sha256$........"},
|
|
||||||
]
|
|
||||||
memberships = ["group:amazing_group"]
|
|
||||||
|
|
||||||
[[principals]]
|
You can also completely skip this and instead configure OpenID Connect.
|
||||||
id = "group:amazing_group"
|
In that case your user will automatically be created when logging in through the frontend.
|
||||||
user_type = "group"
|
|
||||||
displayname = "Amazing group"
|
|
||||||
```
|
|
||||||
|
|
||||||
Password hashes can be generated with
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rustical pwhash
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker
|
|
||||||
|
|
||||||
You can also run the upper commands in Docker with
|
|
||||||
|
|
||||||
```sh
|
|
||||||
docker run --rm ghcr.io/lennart-k/rustical rustical gen-config
|
|
||||||
docker run -it --rm ghcr.io/lennart-k/rustical rustical pwhash
|
|
||||||
```
|
|
||||||
|
|
||||||
## Password vs app tokens
|
## Password vs app tokens
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub struct MembershipArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_membership_command(
|
pub async fn handle_membership_command(
|
||||||
user_store: impl AuthenticationProvider,
|
user_store: &impl AuthenticationProvider,
|
||||||
MembershipArgs { command }: MembershipArgs,
|
MembershipArgs { command }: MembershipArgs,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let id = match &command {
|
let id = match &command {
|
||||||
@@ -42,28 +42,22 @@ pub async fn handle_membership_command(
|
|||||||
MembershipCommand::Remove(RemoveArgs { id, .. }) => id,
|
MembershipCommand::Remove(RemoveArgs { id, .. }) => id,
|
||||||
MembershipCommand::List(ListArgs { id }) => id,
|
MembershipCommand::List(ListArgs { id }) => id,
|
||||||
};
|
};
|
||||||
let mut principal = user_store
|
|
||||||
.get_principal(id)
|
|
||||||
.await?
|
|
||||||
.unwrap_or_else(|| panic!("Principal {id} does not exist"));
|
|
||||||
|
|
||||||
match command {
|
match &command {
|
||||||
MembershipCommand::Assign(AssignArgs { to, .. }) => {
|
MembershipCommand::Assign(AssignArgs { to, .. }) => {
|
||||||
if principal.memberships.contains(&to) {
|
user_store.add_membership(id, to).await?;
|
||||||
println!("Principal is already member of {to}");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
principal.memberships.push(to);
|
|
||||||
user_store.insert_principal(principal, true).await?;
|
|
||||||
println!("Membership assigned");
|
println!("Membership assigned");
|
||||||
}
|
}
|
||||||
MembershipCommand::Remove(RemoveArgs { to, .. }) => {
|
MembershipCommand::Remove(RemoveArgs { to, .. }) => {
|
||||||
principal.memberships.retain(|principal| principal != &to);
|
user_store.remove_membership(id, to).await?;
|
||||||
user_store.insert_principal(principal, true).await?;
|
|
||||||
println!("Membership removed");
|
println!("Membership removed");
|
||||||
}
|
}
|
||||||
MembershipCommand::List(ListArgs { .. }) => {
|
MembershipCommand::List(ListArgs { .. }) => {
|
||||||
for membership in principal.memberships {
|
let principal = user_store
|
||||||
|
.get_principal(id)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_else(|| panic!("Principal {id} does not exist"));
|
||||||
|
for membership in principal.memberships() {
|
||||||
println!("{membership}");
|
println!("{membership}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ use password_hash::PasswordHasher;
|
|||||||
use pbkdf2::Params;
|
use pbkdf2::Params;
|
||||||
use rand::{RngCore, rngs::OsRng};
|
use rand::{RngCore, rngs::OsRng};
|
||||||
use rustical_frontend::FrontendConfig;
|
use rustical_frontend::FrontendConfig;
|
||||||
use rustical_store::auth::TomlUserStoreConfig;
|
|
||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
AuthConfig, Config, DataStoreConfig, DavPushConfig, HttpConfig, SqliteDataStoreConfig,
|
Config, DataStoreConfig, DavPushConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig,
|
||||||
TracingConfig,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mod membership;
|
mod membership;
|
||||||
@@ -28,9 +26,6 @@ pub fn generate_frontend_secret() -> [u8; 64] {
|
|||||||
pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
|
pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
|
||||||
let config = Config {
|
let config = Config {
|
||||||
http: HttpConfig::default(),
|
http: HttpConfig::default(),
|
||||||
auth: AuthConfig::Toml(TomlUserStoreConfig {
|
|
||||||
path: "/etc/rustical/principals.toml".to_owned(),
|
|
||||||
}),
|
|
||||||
data_store: DataStoreConfig::Sqlite(SqliteDataStoreConfig {
|
data_store: DataStoreConfig::Sqlite(SqliteDataStoreConfig {
|
||||||
db_url: "/var/lib/rustical/db.sqlite3".to_owned(),
|
db_url: "/var/lib/rustical/db.sqlite3".to_owned(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::membership::{MembershipArgs, handle_membership_command};
|
use super::membership::{MembershipArgs, handle_membership_command};
|
||||||
use crate::config::{self, Config};
|
use crate::{config::Config, get_data_stores};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use figment::{
|
use figment::{
|
||||||
Figment,
|
Figment,
|
||||||
@@ -8,7 +8,7 @@ use figment::{
|
|||||||
use password_hash::PasswordHasher;
|
use password_hash::PasswordHasher;
|
||||||
use password_hash::SaltString;
|
use password_hash::SaltString;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rustical_store::auth::{AuthenticationProvider, TomlPrincipalStore, User, user::PrincipalType};
|
use rustical_store::auth::{AuthenticationProvider, User, user::PrincipalType};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
pub struct PrincipalsArgs {
|
pub struct PrincipalsArgs {
|
||||||
@@ -66,13 +66,11 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> {
|
|||||||
.merge(Env::prefixed("RUSTICAL_").split("__"))
|
.merge(Env::prefixed("RUSTICAL_").split("__"))
|
||||||
.extract()?;
|
.extract()?;
|
||||||
|
|
||||||
let user_store = match config.auth {
|
let (_, _, _, principal_store, _) = get_data_stores(true, &config.data_store).await?;
|
||||||
config::AuthConfig::Toml(config) => TomlPrincipalStore::new(config)?,
|
|
||||||
};
|
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
Command::List => {
|
Command::List => {
|
||||||
for principal in user_store.get_principals().await? {
|
for principal in principal_store.get_principals().await? {
|
||||||
println!(
|
println!(
|
||||||
"{} (displayname={}) [{}]",
|
"{} (displayname={}) [{}]",
|
||||||
principal.id,
|
principal.id,
|
||||||
@@ -101,13 +99,12 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
user_store
|
principal_store
|
||||||
.insert_principal(
|
.insert_principal(
|
||||||
User {
|
User {
|
||||||
id,
|
id,
|
||||||
displayname: name,
|
displayname: name,
|
||||||
principal_type: principal_type.unwrap_or_default(),
|
principal_type: principal_type.unwrap_or_default(),
|
||||||
app_tokens: vec![],
|
|
||||||
password,
|
password,
|
||||||
memberships: vec![],
|
memberships: vec![],
|
||||||
},
|
},
|
||||||
@@ -117,7 +114,7 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> {
|
|||||||
println!("Principal created");
|
println!("Principal created");
|
||||||
}
|
}
|
||||||
Command::Remove(RemoveArgs { id }) => {
|
Command::Remove(RemoveArgs { id }) => {
|
||||||
user_store.remove_principal(&id).await?;
|
principal_store.remove_principal(&id).await?;
|
||||||
println!("Principal {id} removed");
|
println!("Principal {id} removed");
|
||||||
}
|
}
|
||||||
Command::Edit(EditArgs {
|
Command::Edit(EditArgs {
|
||||||
@@ -127,7 +124,7 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> {
|
|||||||
name,
|
name,
|
||||||
principal_type,
|
principal_type,
|
||||||
}) => {
|
}) => {
|
||||||
let mut principal = user_store
|
let mut principal = principal_store
|
||||||
.get_principal(&id)
|
.get_principal(&id)
|
||||||
.await?
|
.await?
|
||||||
.unwrap_or_else(|| panic!("Principal {id} does not exist"));
|
.unwrap_or_else(|| panic!("Principal {id} does not exist"));
|
||||||
@@ -153,10 +150,12 @@ pub async fn cmd_principals(args: PrincipalsArgs) -> anyhow::Result<()> {
|
|||||||
if let Some(principal_type) = principal_type {
|
if let Some(principal_type) = principal_type {
|
||||||
principal.principal_type = principal_type;
|
principal.principal_type = principal_type;
|
||||||
}
|
}
|
||||||
user_store.insert_principal(principal, true).await?;
|
principal_store.insert_principal(principal, true).await?;
|
||||||
println!("Principal {id} updated");
|
println!("Principal {id} updated");
|
||||||
}
|
}
|
||||||
Command::Membership(args) => handle_membership_command(user_store, args).await?,
|
Command::Membership(args) => {
|
||||||
|
handle_membership_command(principal_store.as_ref(), args).await?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use rustical_frontend::FrontendConfig;
|
use rustical_frontend::FrontendConfig;
|
||||||
use rustical_oidc::OidcConfig;
|
use rustical_oidc::OidcConfig;
|
||||||
use rustical_store::auth::TomlUserStoreConfig;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
@@ -32,13 +31,6 @@ pub enum DataStoreConfig {
|
|||||||
Sqlite(SqliteDataStoreConfig),
|
Sqlite(SqliteDataStoreConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub enum AuthConfig {
|
|
||||||
Toml(TomlUserStoreConfig),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Default)]
|
#[derive(Debug, Deserialize, Serialize, Default)]
|
||||||
#[serde(deny_unknown_fields, default)]
|
#[serde(deny_unknown_fields, default)]
|
||||||
pub struct TracingConfig {
|
pub struct TracingConfig {
|
||||||
@@ -80,7 +72,6 @@ impl Default for NextcloudLoginConfig {
|
|||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub data_store: DataStoreConfig,
|
pub data_store: DataStoreConfig,
|
||||||
pub auth: AuthConfig,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub http: HttpConfig,
|
pub http: HttpConfig,
|
||||||
pub frontend: FrontendConfig,
|
pub frontend: FrontendConfig,
|
||||||
|
|||||||
108
src/main.rs
108
src/main.rs
@@ -11,10 +11,11 @@ use figment::Figment;
|
|||||||
use figment::providers::{Env, Format, Toml};
|
use figment::providers::{Env, Format, Toml};
|
||||||
use rustical_dav_push::notifier::push_notifier;
|
use rustical_dav_push::notifier::push_notifier;
|
||||||
use rustical_frontend::nextcloud_login::NextcloudFlows;
|
use rustical_frontend::nextcloud_login::NextcloudFlows;
|
||||||
use rustical_store::auth::TomlPrincipalStore;
|
use rustical_store::auth::AuthenticationProvider;
|
||||||
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
|
use rustical_store::{AddressbookStore, CalendarStore, CollectionOperation, SubscriptionStore};
|
||||||
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
|
use rustical_store_sqlite::addressbook_store::SqliteAddressbookStore;
|
||||||
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
|
use rustical_store_sqlite::calendar_store::SqliteCalendarStore;
|
||||||
|
use rustical_store_sqlite::principal_store::SqlitePrincipalStore;
|
||||||
use rustical_store_sqlite::{SqliteStore, create_db_pool};
|
use rustical_store_sqlite::{SqliteStore, create_db_pool};
|
||||||
use setup_tracing::setup_tracing;
|
use setup_tracing::setup_tracing;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -51,6 +52,7 @@ async fn get_data_stores(
|
|||||||
Arc<impl AddressbookStore>,
|
Arc<impl AddressbookStore>,
|
||||||
Arc<impl CalendarStore>,
|
Arc<impl CalendarStore>,
|
||||||
Arc<impl SubscriptionStore>,
|
Arc<impl SubscriptionStore>,
|
||||||
|
Arc<impl AuthenticationProvider>,
|
||||||
Receiver<CollectionOperation>,
|
Receiver<CollectionOperation>,
|
||||||
)> {
|
)> {
|
||||||
Ok(match &config {
|
Ok(match &config {
|
||||||
@@ -62,7 +64,14 @@ async fn get_data_stores(
|
|||||||
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
let addressbook_store = Arc::new(SqliteAddressbookStore::new(db.clone(), send.clone()));
|
||||||
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
let cal_store = Arc::new(SqliteCalendarStore::new(db.clone(), send));
|
||||||
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
let subscription_store = Arc::new(SqliteStore::new(db.clone()));
|
||||||
(addressbook_store, cal_store, subscription_store, recv)
|
let principal_store = Arc::new(SqlitePrincipalStore::new(db.clone()));
|
||||||
|
(
|
||||||
|
addressbook_store,
|
||||||
|
cal_store,
|
||||||
|
subscription_store,
|
||||||
|
principal_store,
|
||||||
|
recv,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -83,7 +92,7 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
setup_tracing(&config.tracing);
|
setup_tracing(&config.tracing);
|
||||||
|
|
||||||
let (addr_store, cal_store, subscription_store, update_recv) =
|
let (addr_store, cal_store, subscription_store, principal_store, update_recv) =
|
||||||
get_data_stores(!args.no_migrations, &config.data_store).await?;
|
get_data_stores(!args.no_migrations, &config.data_store).await?;
|
||||||
|
|
||||||
if config.dav_push.enabled {
|
if config.dav_push.enabled {
|
||||||
@@ -94,10 +103,6 @@ async fn main() -> Result<()> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_store = match config.auth {
|
|
||||||
config::AuthConfig::Toml(config) => Arc::new(TomlPrincipalStore::new(config)?),
|
|
||||||
};
|
|
||||||
|
|
||||||
let nextcloud_flows = Arc::new(NextcloudFlows::default());
|
let nextcloud_flows = Arc::new(NextcloudFlows::default());
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
@@ -105,7 +110,7 @@ async fn main() -> Result<()> {
|
|||||||
addr_store.clone(),
|
addr_store.clone(),
|
||||||
cal_store.clone(),
|
cal_store.clone(),
|
||||||
subscription_store.clone(),
|
subscription_store.clone(),
|
||||||
user_store.clone(),
|
principal_store.clone(),
|
||||||
config.frontend.clone(),
|
config.frontend.clone(),
|
||||||
config.oidc.clone(),
|
config.oidc.clone(),
|
||||||
config.nextcloud_login.clone(),
|
config.nextcloud_login.clone(),
|
||||||
@@ -131,94 +136,27 @@ mod tests {
|
|||||||
get_data_stores,
|
get_data_stores,
|
||||||
};
|
};
|
||||||
use actix_web::{http::StatusCode, test::TestRequest};
|
use actix_web::{http::StatusCode, test::TestRequest};
|
||||||
use anyhow::anyhow;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use rustical_frontend::FrontendConfig;
|
use rustical_frontend::FrontendConfig;
|
||||||
use rustical_frontend::nextcloud_login::NextcloudFlows;
|
use rustical_frontend::nextcloud_login::NextcloudFlows;
|
||||||
use rustical_store::auth::AuthenticationProvider;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct MockUserStore;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl AuthenticationProvider for MockUserStore {
|
|
||||||
async fn get_principals(
|
|
||||||
&self,
|
|
||||||
) -> Result<Vec<rustical_store::auth::User>, rustical_store::Error> {
|
|
||||||
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
|
||||||
}
|
|
||||||
async fn get_principal(
|
|
||||||
&self,
|
|
||||||
_id: &str,
|
|
||||||
) -> Result<Option<rustical_store::auth::User>, rustical_store::Error> {
|
|
||||||
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_principal(&self, _id: &str) -> Result<(), rustical_store::Error> {
|
|
||||||
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn validate_password(
|
|
||||||
&self,
|
|
||||||
_user_id: &str,
|
|
||||||
_password: &str,
|
|
||||||
) -> Result<Option<rustical_store::auth::User>, rustical_store::Error> {
|
|
||||||
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn validate_app_token(
|
|
||||||
&self,
|
|
||||||
_user_id: &str,
|
|
||||||
_token: &str,
|
|
||||||
) -> Result<Option<rustical_store::auth::User>, rustical_store::Error> {
|
|
||||||
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_app_token(
|
|
||||||
&self,
|
|
||||||
_user_id: &str,
|
|
||||||
_name: String,
|
|
||||||
_token: String,
|
|
||||||
) -> Result<String, rustical_store::Error> {
|
|
||||||
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn remove_app_token(
|
|
||||||
&self,
|
|
||||||
_user_id: &str,
|
|
||||||
_token_id: &str,
|
|
||||||
) -> Result<(), rustical_store::Error> {
|
|
||||||
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn insert_principal(
|
|
||||||
&self,
|
|
||||||
_user: rustical_store::auth::User,
|
|
||||||
_overwrite: bool,
|
|
||||||
) -> Result<(), rustical_store::Error> {
|
|
||||||
Err(rustical_store::Error::Other(anyhow!("Not implemented")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_main() {
|
async fn test_main() {
|
||||||
let (addr_store, cal_store, subscription_store, _update_recv) = get_data_stores(
|
let (addr_store, cal_store, subscription_store, principal_store, _update_recv) =
|
||||||
true,
|
get_data_stores(
|
||||||
&crate::config::DataStoreConfig::Sqlite(crate::config::SqliteDataStoreConfig {
|
true,
|
||||||
db_url: "".to_owned(),
|
&crate::config::DataStoreConfig::Sqlite(crate::config::SqliteDataStoreConfig {
|
||||||
}),
|
db_url: "".to_owned(),
|
||||||
)
|
}),
|
||||||
.await
|
)
|
||||||
.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
let user_store = Arc::new(MockUserStore);
|
|
||||||
|
|
||||||
let app = make_app(
|
let app = make_app(
|
||||||
addr_store,
|
addr_store,
|
||||||
cal_store,
|
cal_store,
|
||||||
subscription_store,
|
subscription_store,
|
||||||
user_store,
|
principal_store,
|
||||||
FrontendConfig {
|
FrontendConfig {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
secret_key: generate_frontend_secret(),
|
secret_key: generate_frontend_secret(),
|
||||||
|
|||||||
Reference in New Issue
Block a user