mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 20:32:48 +00:00
Implement almost all previous features
This commit is contained in:
256
Cargo.lock
generated
256
Cargo.lock
generated
@@ -17,41 +17,6 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cipher",
|
||||
"cpufeatures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"ghash",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
@@ -337,37 +302,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum_session"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cbd59ac11f92412bd86308ce6c9d579e4a6ab627a2c7074afa5e4ef7144900d"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"cookie",
|
||||
"dashmap",
|
||||
"forwarded-header-value",
|
||||
"futures",
|
||||
"hmac",
|
||||
"http",
|
||||
"http-body",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.75"
|
||||
@@ -533,16 +467,6 @@ dependencies = [
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.39"
|
||||
@@ -610,11 +534,7 @@ version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"base64 0.22.1",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"subtle",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
@@ -683,19 +603,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
@@ -758,20 +668,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -1016,16 +912,6 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "forwarded-header-value"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9"
|
||||
dependencies = [
|
||||
"nonempty",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -1034,7 +920,6 @@ checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
@@ -1120,7 +1005,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
@@ -1170,16 +1054,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ghash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||
dependencies = [
|
||||
"opaque-debug",
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
@@ -1228,12 +1102,6 @@ version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
@@ -1616,15 +1484,6 @@ version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -1727,6 +1586,7 @@ checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1808,12 +1668,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonempty"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@@ -1918,12 +1772,6 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "openidconnect"
|
||||
version = "4.0.0"
|
||||
@@ -2285,18 +2133,6 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"opaque-debug",
|
||||
"universal-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.2"
|
||||
@@ -2832,6 +2668,9 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-sessions",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
@@ -2863,6 +2702,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -2890,6 +2730,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -2962,6 +2803,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-sessions",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
@@ -2991,11 +2833,13 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum_session",
|
||||
"axum-extra",
|
||||
"headers",
|
||||
"openidconnect",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"tower-sessions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3028,6 +2872,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-sessions",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
@@ -3877,6 +3722,22 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-cookies"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"cookie",
|
||||
"futures-util",
|
||||
"http",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.6"
|
||||
@@ -3893,6 +3754,7 @@ dependencies = [
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3907,6 +3769,57 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"http",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower-cookies",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tower-sessions-core",
|
||||
"tower-sessions-memory-store",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions-core"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"base64 0.22.1",
|
||||
"futures",
|
||||
"http",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions-memory-store"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower-sessions-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
@@ -4047,16 +3960,6 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "universal-hash"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
@@ -4096,7 +3999,6 @@ dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ base64 = "0.22"
|
||||
thiserror = "2.0"
|
||||
quick-xml = { version = "0.37" }
|
||||
rust-embed = "8.5"
|
||||
tower-sessions = "0.14"
|
||||
futures-core = "0.3.31"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
mime_guess = "2.0"
|
||||
@@ -92,6 +93,7 @@ sqlx-sqlite = { version = "0.8", features = ["bundled"] }
|
||||
ical = { version = "0.11", features = ["generator", "serde"] }
|
||||
toml = "0.8"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["trace", "normalize-path"] }
|
||||
rustical_dav = { path = "./crates/dav/" }
|
||||
rustical_dav_push = { path = "./crates/dav_push/" }
|
||||
rustical_store = { path = "./crates/store/" }
|
||||
@@ -155,9 +157,10 @@ tracing-subscriber = { version = "0.3", features = [
|
||||
"registry",
|
||||
] }
|
||||
figment = { version = "0.10", features = ["env", "toml"] }
|
||||
|
||||
tower-sessions.workspace = true
|
||||
rand.workspace = true
|
||||
rpassword.workspace = true
|
||||
tower.workspace = true
|
||||
argon2.workspace = true
|
||||
pbkdf2.workspace = true
|
||||
password-hash.workspace = true
|
||||
@@ -166,3 +169,4 @@ rustical_dav.workspace = true
|
||||
rustical_dav_push.workspace = true
|
||||
rustical_oidc.workspace = true
|
||||
quick-xml.workspace = true
|
||||
tower-http.workspace = true
|
||||
|
||||
@@ -31,3 +31,4 @@ rustical_dav_push.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
http.workspace = true
|
||||
headers.workspace = true
|
||||
tower-http.workspace = true
|
||||
|
||||
@@ -27,6 +27,7 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
|
||||
|
||||
for href in &cal_query.href {
|
||||
if let Some(filename) = href.strip_prefix(path) {
|
||||
let filename = filename.trim_start_matches("/");
|
||||
if let Some(object_id) = filename.strip_suffix(".ics") {
|
||||
match store.get_object(principal, cal_id, object_id).await {
|
||||
Ok(object) => result.push(object),
|
||||
|
||||
@@ -7,7 +7,7 @@ use axum::extract::{Path, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||
use http::StatusCode;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use rustical_ical::CalendarObject;
|
||||
use rustical_store::CalendarStore;
|
||||
use rustical_store::auth::User;
|
||||
@@ -53,13 +53,19 @@ pub async fn put_event<C: CalendarStore>(
|
||||
}): Path<CalendarObjectPathComponents>,
|
||||
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
|
||||
user: User,
|
||||
if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
header_map: HeaderMap,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(crate::Error::Unauthorized);
|
||||
}
|
||||
|
||||
// https://github.com/hyperium/headers/issues/204
|
||||
if !header_map.contains_key("If-None-Match") {
|
||||
if_none_match = None;
|
||||
}
|
||||
|
||||
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
||||
if_none_match == IfNoneMatch::any()
|
||||
} else {
|
||||
|
||||
@@ -95,7 +95,7 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarSetReso
|
||||
type Principal = User;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control, extended-mkcol";
|
||||
const DAV_HEADER: &str = "1, 3, access-control, extended-mkcol, calendar-access";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
|
||||
@@ -26,41 +26,10 @@ pub struct CalDavPrincipalUri(&'static str);
|
||||
|
||||
impl PrincipalUri for CalDavPrincipalUri {
|
||||
fn principal_uri(&self, principal: &str) -> String {
|
||||
format!("{}/{}", self.0, principal)
|
||||
format!("{}/principal/{}", self.0, principal)
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn caldav_service<
|
||||
// AP: AuthenticationProvider,
|
||||
// AS: AddressbookStore,
|
||||
// C: CalendarStore,
|
||||
// S: SubscriptionStore,
|
||||
// >(
|
||||
// prefix: &'static str,
|
||||
// auth_provider: Arc<AP>,
|
||||
// store: Arc<C>,
|
||||
// addr_store: Arc<AS>,
|
||||
// subscription_store: Arc<S>,
|
||||
// ) -> impl HttpServiceFactory {
|
||||
// let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store));
|
||||
//
|
||||
// RootResourceService::<_, User, CalDavPrincipalUri>::new(PrincipalResourceService {
|
||||
// auth_provider: auth_provider.clone(),
|
||||
// sub_store: subscription_store.clone(),
|
||||
// birthday_store: birthday_store.clone(),
|
||||
// cal_store: store.clone(),
|
||||
// })
|
||||
// .actix_scope()
|
||||
// .wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
// .wrap(options_handler())
|
||||
// .app_data(Data::from(store.clone()))
|
||||
// .app_data(Data::from(birthday_store.clone()))
|
||||
// .app_data(Data::new(CalDavPrincipalUri::new(
|
||||
// format!("{prefix}/principal").leak(),
|
||||
// )))
|
||||
// .service(subscription_resource(subscription_store))
|
||||
// }
|
||||
|
||||
pub fn caldav_router<
|
||||
AP: AuthenticationProvider,
|
||||
AS: AddressbookStore,
|
||||
|
||||
@@ -155,7 +155,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore, BS: Ca
|
||||
type Principal = User;
|
||||
type PrincipalUri = CalDavPrincipalUri;
|
||||
|
||||
const DAV_HEADER: &str = "1, 3, access-control";
|
||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
|
||||
|
||||
async fn get_resource(
|
||||
&self,
|
||||
|
||||
@@ -28,3 +28,4 @@ uuid.workspace = true
|
||||
rustical_dav_push.workspace = true
|
||||
rustical_ical.workspace = true
|
||||
http.workspace = true
|
||||
tower-http.workspace = true
|
||||
|
||||
@@ -9,7 +9,7 @@ use axum::extract::{Path, State};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum_extra::TypedHeader;
|
||||
use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
|
||||
use http::StatusCode;
|
||||
use http::{HeaderMap, StatusCode};
|
||||
use rustical_dav::privileges::UserPrivilege;
|
||||
use rustical_dav::resource::Resource;
|
||||
use rustical_ical::AddressObject;
|
||||
@@ -62,13 +62,19 @@ pub async fn put_object<AS: AddressbookStore>(
|
||||
}): Path<AddressObjectPathComponents>,
|
||||
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
|
||||
user: User,
|
||||
if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
header_map: HeaderMap,
|
||||
body: String,
|
||||
) -> Result<Response, Error> {
|
||||
if !user.is_principal(&principal) {
|
||||
return Err(Error::Unauthorized);
|
||||
}
|
||||
|
||||
// https://github.com/hyperium/headers/issues/204
|
||||
if !header_map.contains_key("If-None-Match") {
|
||||
if_none_match = None;
|
||||
}
|
||||
|
||||
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
|
||||
if_none_match == IfNoneMatch::any()
|
||||
} else {
|
||||
|
||||
@@ -12,7 +12,7 @@ use rustical_dav::{
|
||||
use rustical_ical::AddressObject;
|
||||
use rustical_store::{AddressbookStore, auth::User};
|
||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::{convert::Infallible, sync::Arc};
|
||||
use tower::Service;
|
||||
|
||||
@@ -108,10 +108,23 @@ impl Resource for AddressObjectResource {
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_vcf_name<'de, D>(deserializer: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let name: String = Deserialize::deserialize(deserializer)?;
|
||||
if let Some(object_id) = name.strip_suffix(".vcf") {
|
||||
Ok(object_id.to_owned())
|
||||
} else {
|
||||
Err(serde::de::Error::custom("Missing .vcf extension"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AddressObjectPathComponents {
|
||||
pub principal: String,
|
||||
pub addressbook_id: String,
|
||||
#[serde(deserialize_with = "deserialize_vcf_name")]
|
||||
pub object_id: String,
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
|
||||
|
||||
for href in &addressbook_multiget.href {
|
||||
if let Some(filename) = href.strip_prefix(path) {
|
||||
let filename = filename.trim_start_matches("/");
|
||||
if let Some(object_id) = filename.strip_suffix(".vcf") {
|
||||
match store
|
||||
.get_object(principal, addressbook_id, object_id, false)
|
||||
|
||||
@@ -23,7 +23,7 @@ pub struct CardDavPrincipalUri(&'static str);
|
||||
|
||||
impl PrincipalUri for CardDavPrincipalUri {
|
||||
fn principal_uri(&self, principal: &str) -> String {
|
||||
format!("{}/{}", self.0, principal)
|
||||
format!("{}/principal/{}", self.0, principal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,17 @@ pub(crate) async fn axum_route_delete<R: ResourceService>(
|
||||
Path(path): Path<R::PathComponents>,
|
||||
State(resource_service): State<R>,
|
||||
principal: R::Principal,
|
||||
if_match: Option<TypedHeader<IfMatch>>,
|
||||
if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
mut if_match: Option<TypedHeader<IfMatch>>,
|
||||
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
|
||||
header_map: HeaderMap,
|
||||
) -> Result<(), R::Error> {
|
||||
// https://github.com/hyperium/headers/issues/204
|
||||
if !header_map.contains_key("If-Match") {
|
||||
if_match = None;
|
||||
}
|
||||
if !header_map.contains_key("If-None-Match") {
|
||||
if_none_match = None;
|
||||
}
|
||||
let no_trash = header_map
|
||||
.get("X-No-Trashbin")
|
||||
.map(|val| matches!(val.to_str(), Ok("1")))
|
||||
@@ -46,6 +53,7 @@ pub async fn route_delete<R: ResourceService>(
|
||||
}
|
||||
|
||||
if let Some(if_match) = if_match {
|
||||
dbg!(&if_match);
|
||||
if !resource.satisfies_if_match(&if_match) {
|
||||
// Precondition failed
|
||||
return Err(crate::Error::PreconditionFailed.into());
|
||||
|
||||
@@ -10,7 +10,9 @@ use crate::xml::PropfindType;
|
||||
use axum::extract::{Extension, OriginalUri, Path, State};
|
||||
use rustical_xml::PropName;
|
||||
use rustical_xml::XmlDocument;
|
||||
use tracing::instrument;
|
||||
|
||||
#[instrument(skip(path, resource_service, puri))]
|
||||
pub(crate) async fn axum_route_propfind<R: ResourceService>(
|
||||
Path(path): Path<R::PathComponents>,
|
||||
State(resource_service): State<R>,
|
||||
|
||||
@@ -30,3 +30,4 @@ tracing.workspace = true
|
||||
rustical_oidc.workspace = true
|
||||
axum-extra.workspace= true
|
||||
headers.workspace = true
|
||||
tower-sessions = "0.14"
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
use rand::RngCore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn generate_frontend_secret() -> [u8; 64] {
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let mut secret = [0u8; 64];
|
||||
rng.fill_bytes(&mut secret);
|
||||
secret
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -16,10 +7,6 @@ fn default_true() -> bool {
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FrontendConfig {
|
||||
#[serde(serialize_with = "hex::serde::serialize")]
|
||||
#[serde(deserialize_with = "hex::serde::deserialize")]
|
||||
#[serde(default = "generate_frontend_secret")]
|
||||
pub secret_key: [u8; 64],
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_true")]
|
||||
|
||||
@@ -1,357 +1,158 @@
|
||||
use askama::Template;
|
||||
// use askama_web::WebTemplate;
|
||||
// use assets::{Assets, EmbedService};
|
||||
use async_trait::async_trait;
|
||||
use axum::{Extension, Router, response::IntoResponse, routing::get};
|
||||
use http::Uri;
|
||||
use rand::{Rng, distributions::Alphanumeric};
|
||||
use rustical_oidc::OidcConfig;
|
||||
// use routes::{
|
||||
// addressbook::{route_addressbook, route_addressbook_restore},
|
||||
// calendar::{route_calendar, route_calendar_restore},
|
||||
// login::{route_get_login, route_post_login, route_post_logout},
|
||||
// };
|
||||
// use rustical_oidc::{OidcConfig, OidcServiceConfig, UserStore, configure_oidc};
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||
auth::{
|
||||
AuthenticationMiddleware, AuthenticationProvider, User, middleware::AuthenticationLayer,
|
||||
user::AppToken,
|
||||
},
|
||||
use axum::{
|
||||
Extension, RequestExt, Router,
|
||||
body::Body,
|
||||
extract::{OriginalUri, Request},
|
||||
middleware::{self, Next},
|
||||
response::Response,
|
||||
routing::{get, post},
|
||||
};
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use http::{Method, StatusCode};
|
||||
use rustical_oidc::{OidcConfig, OidcServiceConfig, route_get_oidc_callback, route_post_oidc};
|
||||
use rustical_store::{
|
||||
AddressbookStore, CalendarStore,
|
||||
auth::{AuthenticationProvider, middleware::AuthenticationLayer},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use tower_sessions::{
|
||||
Expiry, SessionManagerLayer, SessionStore,
|
||||
cookie::{SameSite, time::Duration},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
mod assets;
|
||||
mod config;
|
||||
pub mod nextcloud_login;
|
||||
mod oidc_user_store;
|
||||
mod routes;
|
||||
|
||||
pub const ROUTE_NAME_HOME: &str = "frontend_home";
|
||||
pub const ROUTE_USER_NAMED: &str = "frontend_user_named";
|
||||
|
||||
pub use config::{FrontendConfig, generate_frontend_secret};
|
||||
pub use config::FrontendConfig;
|
||||
use oidc_user_store::OidcUserStore;
|
||||
|
||||
use crate::{
|
||||
assets::{Assets, EmbedService},
|
||||
routes::login::{route_get_login, route_post_login},
|
||||
routes::{
|
||||
addressbook::{route_addressbook, route_addressbook_restore},
|
||||
app_token::{route_delete_app_token, route_post_app_token},
|
||||
calendar::{route_calendar, route_calendar_restore},
|
||||
login::{route_get_login, route_post_login, route_post_logout},
|
||||
user::{route_get_home, route_root, route_user_named},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn generate_app_token() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.map(char::from)
|
||||
.take(64)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// #[derive(Template, WebTemplate)]
|
||||
// #[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>,
|
||||
// pub is_apple: bool,
|
||||
// }
|
||||
//
|
||||
// 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 {
|
||||
// let user_id = path.into_inner();
|
||||
// if user_id != user.id {
|
||||
// return actix_web::HttpResponse::Unauthorized().body("Unauthorized");
|
||||
// }
|
||||
//
|
||||
// let mut calendars = vec![];
|
||||
// for group in user.memberships() {
|
||||
// calendars.extend(cal_store.get_calendars(group).await.unwrap());
|
||||
// }
|
||||
//
|
||||
// let mut deleted_calendars = vec![];
|
||||
// for group in user.memberships() {
|
||||
// deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
|
||||
// }
|
||||
//
|
||||
// let mut addressbooks = vec![];
|
||||
// for group in user.memberships() {
|
||||
// addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
|
||||
// }
|
||||
//
|
||||
// let mut deleted_addressbooks = vec![];
|
||||
// for group in user.memberships() {
|
||||
// deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
|
||||
// }
|
||||
//
|
||||
// let is_apple = req
|
||||
// .headers()
|
||||
// .get(header::USER_AGENT)
|
||||
// .and_then(|user_agent| user_agent.to_str().ok())
|
||||
// .map(|ua| ua.contains("Apple") || ua.contains("Mac OS"))
|
||||
// .unwrap_or_default();
|
||||
//
|
||||
// UserPage {
|
||||
// app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||
// calendars,
|
||||
// deleted_calendars,
|
||||
// addressbooks,
|
||||
// deleted_addressbooks,
|
||||
// user,
|
||||
// is_apple,
|
||||
// }
|
||||
// .respond_to(&req)
|
||||
// }
|
||||
//
|
||||
// async fn route_get_home(user: User, req: HttpRequest) -> Redirect {
|
||||
// Redirect::to(
|
||||
// req.url_for(ROUTE_USER_NAMED, &[user.id])
|
||||
// .unwrap()
|
||||
// .to_string(),
|
||||
// )
|
||||
// .see_other()
|
||||
// }
|
||||
//
|
||||
// async fn route_root(user: Option<User>, req: HttpRequest) -> impl Responder {
|
||||
// let redirect_url = match user {
|
||||
// Some(_) => req.url_for_static(ROUTE_NAME_HOME).unwrap(),
|
||||
// None => req
|
||||
// .resource_map()
|
||||
// .url_for::<[_; 0], String>(&req, "frontend_login", [])
|
||||
// .unwrap(),
|
||||
// };
|
||||
// web::Redirect::to(redirect_url.to_string()).permanent()
|
||||
// }
|
||||
//
|
||||
// #[derive(Template)]
|
||||
// #[template(path = "apple_configuration/template.xml")]
|
||||
// pub struct AppleConfig {
|
||||
// token_name: String,
|
||||
// account_description: String,
|
||||
// hostname: String,
|
||||
// caldav_principal_url: String,
|
||||
// carddav_principal_url: String,
|
||||
// user: String,
|
||||
// token: String,
|
||||
// caldav_profile_uuid: Uuid,
|
||||
// carddav_profile_uuid: Uuid,
|
||||
// plist_uuid: Uuid,
|
||||
// }
|
||||
//
|
||||
// #[derive(Debug, Clone, Deserialize)]
|
||||
// pub(crate) struct PostAppTokenForm {
|
||||
// name: String,
|
||||
// #[serde(default)]
|
||||
// apple: bool,
|
||||
// }
|
||||
//
|
||||
// async fn route_post_app_token<AP: AuthenticationProvider>(
|
||||
// user: User,
|
||||
// auth_provider: Data<AP>,
|
||||
// path: Path<String>,
|
||||
// Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
||||
// req: HttpRequest,
|
||||
// ) -> Result<HttpResponse, rustical_store::Error> {
|
||||
// assert!(!name.is_empty());
|
||||
// assert_eq!(path.into_inner(), user.id);
|
||||
// let token = generate_app_token();
|
||||
// auth_provider
|
||||
// .add_app_token(&user.id, name.to_owned(), token.clone())
|
||||
// .await?;
|
||||
// if apple {
|
||||
// let hostname = req.full_url().host_str().unwrap().to_owned();
|
||||
// let profile = AppleConfig {
|
||||
// token_name: name,
|
||||
// account_description: format!("{}@{}", &user.id, &hostname),
|
||||
// hostname,
|
||||
// caldav_principal_url: req
|
||||
// .url_for("caldav_principal", [&user.id])
|
||||
// .unwrap()
|
||||
// .to_string(),
|
||||
// carddav_principal_url: req
|
||||
// .url_for("carddav_principal", [&user.id])
|
||||
// .unwrap()
|
||||
// .to_string(),
|
||||
// user: user.id.to_owned(),
|
||||
// token,
|
||||
// caldav_profile_uuid: Uuid::new_v4(),
|
||||
// carddav_profile_uuid: Uuid::new_v4(),
|
||||
// plist_uuid: Uuid::new_v4(),
|
||||
// }
|
||||
// .render()
|
||||
// .unwrap();
|
||||
// Ok(HttpResponse::Ok()
|
||||
// .insert_header(header::ContentDisposition::attachment(format!(
|
||||
// "rustical-{}.mobileconfig",
|
||||
// user.id
|
||||
// )))
|
||||
// .insert_header((
|
||||
// header::CONTENT_TYPE,
|
||||
// "application/x-apple-aspen-config; charset=utf-8",
|
||||
// ))
|
||||
// .body(profile))
|
||||
// } else {
|
||||
// Ok(HttpResponse::Ok().body(token))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// async fn route_delete_app_token<AP: AuthenticationProvider>(
|
||||
// user: User,
|
||||
// auth_provider: Data<AP>,
|
||||
// path: Path<(String, String)>,
|
||||
// ) -> Result<Redirect, rustical_store::Error> {
|
||||
// let (path_user, token_id) = path.into_inner();
|
||||
// assert_eq!(path_user, user.id);
|
||||
// auth_provider.remove_app_token(&user.id, &token_id).await?;
|
||||
// Ok(Redirect::to("/frontend/user").see_other())
|
||||
// }
|
||||
//
|
||||
// pub(crate) fn unauthorized_handler<B>(
|
||||
// res: ServiceResponse<B>,
|
||||
// ) -> actix_web::Result<ErrorHandlerResponse<B>> {
|
||||
// let (req, _) = res.into_parts();
|
||||
// let redirect_uri = req.uri().to_string();
|
||||
// let mut login_url = req.url_for_static("frontend_login").unwrap();
|
||||
// login_url
|
||||
// .query_pairs_mut()
|
||||
// .append_pair("redirect_uri", &redirect_uri);
|
||||
// let login_url = login_url.to_string();
|
||||
//
|
||||
// let response = HttpResponse::Unauthorized().body(format!(
|
||||
// r#"<!Doctype html>
|
||||
// <html>
|
||||
// <head>
|
||||
// <meta http-equiv="refresh" content="1; url={login_url}" />
|
||||
// </head>
|
||||
// <body>
|
||||
// Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
||||
// </body>
|
||||
// <html>
|
||||
// "#
|
||||
// ));
|
||||
//
|
||||
// let res = ServiceResponse::new(req, response)
|
||||
// .map_into_boxed_body()
|
||||
// .map_into_right_body();
|
||||
//
|
||||
// Ok(ErrorHandlerResponse::Response(res))
|
||||
// }
|
||||
//
|
||||
// pub fn session_middleware(frontend_secret: [u8; 64]) -> SessionMiddleware<impl SessionStore> {
|
||||
// SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&frontend_secret))
|
||||
// .cookie_secure(true)
|
||||
// .cookie_same_site(SameSite::Strict)
|
||||
// .cookie_content_security(CookieContentSecurity::Private)
|
||||
// .build()
|
||||
// }
|
||||
|
||||
pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: AddressbookStore>(
|
||||
pub fn frontend_router<
|
||||
AP: AuthenticationProvider,
|
||||
CS: CalendarStore,
|
||||
AS: AddressbookStore,
|
||||
S: SessionStore + Clone,
|
||||
>(
|
||||
auth_provider: Arc<AP>,
|
||||
cal_store: Arc<CS>,
|
||||
addr_store: Arc<AS>,
|
||||
frontend_config: FrontendConfig,
|
||||
oidc_config: Option<OidcConfig>,
|
||||
session_store: S,
|
||||
) -> Router {
|
||||
let mut router = Router::new().layer(AuthenticationLayer::new(auth_provider.clone()));
|
||||
let mut router = Router::new();
|
||||
router = router
|
||||
.route("/", get(route_root))
|
||||
.route("/user", get(route_get_home))
|
||||
.route("/user/{user}", get(route_user_named::<CS, AS, AP>))
|
||||
// App token management
|
||||
.route("/user/{user}/app_token", post(route_post_app_token::<AP>))
|
||||
.route(
|
||||
// POST because HTML5 forms don't support DELETE method
|
||||
"/user/{user}/app_token/{id}/delete",
|
||||
post(route_delete_app_token::<AP>),
|
||||
)
|
||||
// Calendar
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}",
|
||||
get(route_calendar::<CS>),
|
||||
)
|
||||
.route(
|
||||
"/user/{user}/calendar/{calendar}/restore",
|
||||
post(route_calendar_restore::<CS>),
|
||||
)
|
||||
// Addressbook
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}",
|
||||
get(route_addressbook::<AS>),
|
||||
)
|
||||
.route(
|
||||
"/user/{user}/addressbook/{addressbook}/restore",
|
||||
post(route_addressbook_restore::<AS>),
|
||||
)
|
||||
.route("/login", get(route_get_login).post(route_post_login::<AP>))
|
||||
.route_service("/assets/{*file}", EmbedService::<Assets>::new())
|
||||
.route("/logout", post(route_post_logout))
|
||||
.route_service("/assets/{*file}", EmbedService::<Assets>::new());
|
||||
|
||||
if let Some(oidc_config) = oidc_config.clone() {
|
||||
router = router
|
||||
.route("/login/oidc", post(route_post_oidc))
|
||||
.route(
|
||||
"/login/oidc/callback",
|
||||
get(route_get_oidc_callback::<OidcUserStore<AP>>),
|
||||
)
|
||||
.layer(Extension(OidcUserStore(auth_provider.clone())))
|
||||
.layer(Extension(OidcServiceConfig {
|
||||
default_redirect_path: "/frontend/user",
|
||||
session_key_user_id: "user",
|
||||
}))
|
||||
.layer(Extension(oidc_config));
|
||||
}
|
||||
|
||||
router
|
||||
.layer(AuthenticationLayer::new(auth_provider.clone()))
|
||||
.layer(
|
||||
SessionManagerLayer::new(session_store)
|
||||
.with_secure(true)
|
||||
.with_same_site(SameSite::Strict)
|
||||
.with_expiry(Expiry::OnInactivity(Duration::hours(2))),
|
||||
)
|
||||
.layer(Extension(auth_provider.clone()))
|
||||
.layer(Extension(cal_store.clone()))
|
||||
.layer(Extension(addr_store.clone()))
|
||||
.layer(Extension(frontend_config.clone()))
|
||||
.layer(Extension(oidc_config.clone()));
|
||||
// .wrap(session_middleware(frontend_config.secret_key))
|
||||
// .service(web::resource("").route(web::method(Method::GET).to(route_root)))
|
||||
// .service(
|
||||
// web::resource("/user")
|
||||
// .get(route_get_home)
|
||||
// .name(ROUTE_NAME_HOME),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/user/{user}")
|
||||
// .get(route_user_named::<CS, AS, AP>)
|
||||
// .name(ROUTE_USER_NAMED),
|
||||
// )
|
||||
// // App token management
|
||||
// .service(web::resource("/user/{user}/app_token").post(route_post_app_token::<AP>))
|
||||
// .service(
|
||||
// // POST because HTML5 forms don't support DELETE method
|
||||
// web::resource("/user/{user}/app_token/{id}/delete").post(route_delete_app_token::<AP>),
|
||||
// )
|
||||
// // Calendar
|
||||
// .service(web::resource("/user/{user}/calendar/{calendar}").get(route_calendar::<CS>))
|
||||
// .service(
|
||||
// web::resource("/user/{user}/calendar/{calendar}/restore")
|
||||
// .post(route_calendar_restore::<CS>),
|
||||
// )
|
||||
// // Addressbook
|
||||
// .service(
|
||||
// web::resource("/user/{user}/addressbook/{addressbook}").get(route_addressbook::<AS>),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/user/{user}/addressbook/{addressbook}/restore")
|
||||
// .post(route_addressbook_restore::<AS>),
|
||||
// )
|
||||
// // Login
|
||||
// .service(
|
||||
// web::resource("/login")
|
||||
// .name("frontend_login")
|
||||
// .get(route_get_login)
|
||||
// .post(route_post_login::<AP>),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/logout")
|
||||
// .name("frontend_logout")
|
||||
// .post(route_post_logout),
|
||||
// );
|
||||
|
||||
// if let Some(oidc_config) = oidc_config {
|
||||
// scope = scope.service(web::scope("/login/oidc").configure(|cfg| {
|
||||
// configure_oidc(
|
||||
// cfg,
|
||||
// oidc_config,
|
||||
// OidcServiceConfig {
|
||||
// default_redirect_route_name: ROUTE_NAME_HOME,
|
||||
// session_key_user_id: "user",
|
||||
// },
|
||||
// Arc::new(OidcUserStore(auth_provider.clone())),
|
||||
// )
|
||||
// }));
|
||||
// }
|
||||
|
||||
router
|
||||
.layer(Extension(oidc_config.clone()))
|
||||
.layer(middleware::from_fn(unauthorized_handler))
|
||||
}
|
||||
|
||||
async fn unauthorized_handler(mut request: Request, next: Next) -> Response {
|
||||
let meth = request.method().clone();
|
||||
let OriginalUri(uri) = request.extract_parts().await.unwrap();
|
||||
let resp = next.run(request).await;
|
||||
if resp.status() == StatusCode::UNAUTHORIZED {
|
||||
// This is a dumb hack since parsed Urls cannot be relative
|
||||
let mut login_url: Url = "http://github.com/frontend/login".parse().unwrap();
|
||||
if meth == Method::GET {
|
||||
login_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("redirect_uri", uri.path());
|
||||
}
|
||||
let path = login_url.path();
|
||||
let query = login_url
|
||||
.query()
|
||||
.map(|query| format!("?{query}"))
|
||||
.unwrap_or_default();
|
||||
let login_url = format!("{path}{query}");
|
||||
let mut resp = Response::builder().status(StatusCode::UNAUTHORIZED);
|
||||
let hdrs = resp.headers_mut().unwrap();
|
||||
hdrs.typed_insert(ContentType::html());
|
||||
return resp
|
||||
.body(Body::new(format!(
|
||||
r#"<!Doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="1; url={login_url}" />
|
||||
</head>
|
||||
<body>
|
||||
Unauthorized, redirecting to <a href="{login_url}">login page</a>
|
||||
</body>
|
||||
<html>
|
||||
"#,
|
||||
)))
|
||||
.unwrap();
|
||||
}
|
||||
resp
|
||||
}
|
||||
//
|
||||
// struct OidcUserStore<AP: AuthenticationProvider>(Arc<AP>);
|
||||
//
|
||||
// #[async_trait]
|
||||
// impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
||||
// type Error = rustical_store::Error;
|
||||
//
|
||||
// async fn user_exists(&self, id: &str) -> Result<bool, Self::Error> {
|
||||
// Ok(self.0.get_principal(id).await?.is_some())
|
||||
// }
|
||||
//
|
||||
// async fn insert_user(&self, id: &str) -> Result<(), Self::Error> {
|
||||
// self.0
|
||||
// .insert_principal(
|
||||
// User {
|
||||
// id: id.to_owned(),
|
||||
// displayname: None,
|
||||
// principal_type: Default::default(),
|
||||
// password: None,
|
||||
// memberships: vec![],
|
||||
// },
|
||||
// false,
|
||||
// )
|
||||
// .await
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Extension, Router, middleware};
|
||||
use chrono::{DateTime, Utc};
|
||||
// use routes::{get_nextcloud_flow, post_nextcloud_flow, post_nextcloud_login, post_nextcloud_poll};
|
||||
use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider};
|
||||
use routes::{get_nextcloud_flow, post_nextcloud_flow, post_nextcloud_login, post_nextcloud_poll};
|
||||
use rustical_store::auth::AuthenticationProvider;
|
||||
use rustical_store::auth::middleware::AuthenticationLayer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
// mod routes;
|
||||
use tower_sessions::cookie::SameSite;
|
||||
use tower_sessions::cookie::time::Duration;
|
||||
use tower_sessions::{Expiry, SessionManagerLayer, SessionStore};
|
||||
|
||||
use crate::unauthorized_handler;
|
||||
mod routes;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct NextcloudFlow {
|
||||
@@ -42,32 +50,26 @@ pub struct NextcloudFlows {
|
||||
flows: RwLock<HashMap<String, NextcloudFlow>>,
|
||||
}
|
||||
|
||||
// use crate::{session_middleware, unauthorized_handler};
|
||||
//
|
||||
// pub fn configure_nextcloud_login<AP: AuthenticationProvider>(
|
||||
// cfg: &mut ServiceConfig,
|
||||
// nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||
// auth_provider: Arc<AP>,
|
||||
// frontend_secret: [u8; 64],
|
||||
// ) {
|
||||
// cfg.service(
|
||||
// web::scope("/index.php/login/v2")
|
||||
// .wrap(ErrorHandlers::new().handler(StatusCode::UNAUTHORIZED, unauthorized_handler))
|
||||
// .wrap(AuthenticationMiddleware::new(auth_provider.clone()))
|
||||
// .wrap(session_middleware(frontend_secret))
|
||||
// .app_data(Data::from(nextcloud_flows_state))
|
||||
// .app_data(Data::from(auth_provider.clone()))
|
||||
// .service(web::resource("").post(post_nextcloud_login))
|
||||
// .service(
|
||||
// web::resource("/poll/{flow}")
|
||||
// .name("nc_login_poll")
|
||||
// .post(post_nextcloud_poll::<AP>),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/flow/{flow}")
|
||||
// .name("nc_login_flow")
|
||||
// .get(get_nextcloud_flow)
|
||||
// .post(post_nextcloud_flow),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
pub fn nextcloud_login_router<AP: AuthenticationProvider, S: SessionStore + Clone>(
|
||||
nextcloud_flows_state: Arc<NextcloudFlows>,
|
||||
auth_provider: Arc<AP>,
|
||||
session_store: S,
|
||||
) -> Router {
|
||||
Router::new()
|
||||
.route("/poll/{flow}", post(post_nextcloud_poll::<AP>))
|
||||
.route(
|
||||
"/flow/{flow}",
|
||||
get(get_nextcloud_flow).post(post_nextcloud_flow),
|
||||
)
|
||||
.route("/", post(post_nextcloud_login))
|
||||
.layer(Extension(nextcloud_flows_state))
|
||||
.layer(Extension(auth_provider.clone()))
|
||||
.layer(AuthenticationLayer::new(auth_provider.clone()))
|
||||
.layer(
|
||||
SessionManagerLayer::new(session_store)
|
||||
.with_secure(true)
|
||||
.with_same_site(SameSite::Strict)
|
||||
.with_expiry(Expiry::OnInactivity(Duration::hours(2))),
|
||||
)
|
||||
.layer(middleware::from_fn(unauthorized_handler))
|
||||
}
|
||||
|
||||
@@ -1,48 +1,39 @@
|
||||
use crate::generate_app_token;
|
||||
|
||||
use super::{
|
||||
NextcloudFlow, NextcloudFlows, NextcloudLoginPoll, NextcloudLoginResponse,
|
||||
NextcloudSuccessResponse,
|
||||
};
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
http::header::{self},
|
||||
web::{Data, Form, Html, Json, Path},
|
||||
};
|
||||
use crate::routes::app_token::generate_app_token;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Extension, Form, Json,
|
||||
extract::Path,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use axum_extra::{TypedHeader, extract::Host};
|
||||
use chrono::{Duration, Utc};
|
||||
use headers::UserAgent;
|
||||
use http::StatusCode;
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
|
||||
pub(crate) async fn post_nextcloud_login(
|
||||
req: HttpRequest,
|
||||
state: Data<NextcloudFlows>,
|
||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
||||
Host(host): Host,
|
||||
) -> Json<NextcloudLoginResponse> {
|
||||
let flow_id = uuid::Uuid::new_v4().to_string();
|
||||
let token = uuid::Uuid::new_v4().to_string();
|
||||
let poll_url = req
|
||||
.resource_map()
|
||||
.url_for(&req, "nc_login_poll", [&flow_id])
|
||||
.unwrap();
|
||||
let flow_url = req
|
||||
.resource_map()
|
||||
.url_for(&req, "nc_login_flow", [&flow_id])
|
||||
.unwrap();
|
||||
|
||||
let app_name = req
|
||||
.headers()
|
||||
.get(header::USER_AGENT)
|
||||
.map(|val| val.to_str().unwrap_or("Unknown client"))
|
||||
.unwrap_or("Unknown client");
|
||||
|
||||
let app_name = user_agent.to_string();
|
||||
let mut flows = state.flows.write().await;
|
||||
// Flows must not last longer than 10 minutes
|
||||
// We also enforce that condition here to prevent a memory leak where unpolled flows would
|
||||
// never be cleaned up
|
||||
flows.retain(|_, flow| Utc::now() - flow.created_at < Duration::minutes(10));
|
||||
flows.insert(
|
||||
flow_id,
|
||||
flow_id.clone(),
|
||||
NextcloudFlow {
|
||||
app_name: app_name.to_owned(),
|
||||
created_at: Utc::now(),
|
||||
@@ -51,10 +42,10 @@ pub(crate) async fn post_nextcloud_login(
|
||||
},
|
||||
);
|
||||
Json(NextcloudLoginResponse {
|
||||
login: flow_url.to_string(),
|
||||
login: format!("https://{host}/index.php/login/v2/flow/{flow_id}"),
|
||||
poll: NextcloudLoginPoll {
|
||||
token,
|
||||
endpoint: poll_url.to_string(),
|
||||
endpoint: format!("https://{host}/index.php/login/v2/poll/{flow_id}"),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -66,13 +57,11 @@ pub(crate) struct NextcloudPollForm {
|
||||
}
|
||||
|
||||
pub(crate) async fn post_nextcloud_poll<AP: AuthenticationProvider>(
|
||||
state: Data<NextcloudFlows>,
|
||||
form: Form<NextcloudPollForm>,
|
||||
path: Path<String>,
|
||||
auth_provider: Data<AP>,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, rustical_store::Error> {
|
||||
let flow_id = path.into_inner();
|
||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||
Path(flow_id): Path<String>,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Form(form): Form<NextcloudPollForm>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
let mut flows = state.flows.write().await;
|
||||
|
||||
// Flows must not last longer than 10 minutes
|
||||
@@ -80,7 +69,7 @@ pub(crate) async fn post_nextcloud_poll<AP: AuthenticationProvider>(
|
||||
|
||||
if let Some(flow) = flows.get(&flow_id).cloned() {
|
||||
if flow.token != form.token {
|
||||
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
|
||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
||||
}
|
||||
if let Some(response) = &flow.response {
|
||||
auth_provider
|
||||
@@ -91,13 +80,13 @@ pub(crate) async fn post_nextcloud_poll<AP: AuthenticationProvider>(
|
||||
)
|
||||
.await?;
|
||||
flows.remove(&flow_id);
|
||||
Ok(Json(response).respond_to(&req).map_into_boxed_body())
|
||||
Ok(Json(response).into_response())
|
||||
} else {
|
||||
// Not done yet, re-insert flow
|
||||
Ok(HttpResponse::NotFound().finish())
|
||||
Ok(StatusCode::NOT_FOUND.into_response())
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::Unauthorized().body("Unauthorized"))
|
||||
Ok(StatusCode::UNAUTHORIZED.into_response())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,16 +97,14 @@ struct NextcloudLoginPage {
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(state, req))]
|
||||
#[instrument(skip(state))]
|
||||
pub(crate) async fn get_nextcloud_flow(
|
||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||
Path(flow_id): Path<String>,
|
||||
user: User,
|
||||
state: Data<NextcloudFlows>,
|
||||
path: Path<String>,
|
||||
req: HttpRequest,
|
||||
) -> Result<impl Responder, rustical_store::Error> {
|
||||
let flow_id = path.into_inner();
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if let Some(flow) = state.flows.read().await.get(&flow_id) {
|
||||
Ok(Html::new(
|
||||
Ok(Html(
|
||||
NextcloudLoginPage {
|
||||
username: user.displayname.unwrap_or(user.id),
|
||||
app_name: flow.app_name.to_owned(),
|
||||
@@ -125,10 +112,9 @@ pub(crate) async fn get_nextcloud_flow(
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body())
|
||||
.into_response())
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body("Login flow not found"))
|
||||
Ok((StatusCode::NOT_FOUND, "Login flow not found").into_response())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,32 +129,30 @@ struct NextcloudLoginSuccessPage {
|
||||
app_name: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(state, req))]
|
||||
#[instrument(skip(state))]
|
||||
pub(crate) async fn post_nextcloud_flow(
|
||||
user: User,
|
||||
state: Data<NextcloudFlows>,
|
||||
path: Path<String>,
|
||||
req: HttpRequest,
|
||||
form: Form<NextcloudAuthorizeForm>,
|
||||
) -> Result<impl Responder, rustical_store::Error> {
|
||||
let flow_id = path.into_inner();
|
||||
Extension(state): Extension<Arc<NextcloudFlows>>,
|
||||
Path(flow_id): Path<String>,
|
||||
Host(host): Host,
|
||||
Form(form): Form<NextcloudAuthorizeForm>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if let Some(flow) = state.flows.write().await.get_mut(&flow_id) {
|
||||
flow.app_name = form.into_inner().app_name;
|
||||
flow.app_name = form.app_name;
|
||||
flow.response = Some(NextcloudSuccessResponse {
|
||||
server: req.full_url().origin().unicode_serialization(),
|
||||
server: format!("https://{host}"),
|
||||
login_name: user.id.to_owned(),
|
||||
app_password: generate_app_token(),
|
||||
});
|
||||
Ok(Html::new(
|
||||
Ok(Html(
|
||||
NextcloudLoginSuccessPage {
|
||||
app_name: flow.app_name.to_owned(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body())
|
||||
.into_response())
|
||||
} else {
|
||||
Ok(HttpResponse::NotFound().body("Login flow not found"))
|
||||
Ok((StatusCode::NOT_FOUND, "Login flow not found").into_response())
|
||||
}
|
||||
}
|
||||
|
||||
37
crates/frontend/src/oidc_user_store.rs
Normal file
37
crates/frontend/src/oidc_user_store.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use rustical_oidc::UserStore;
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
|
||||
pub struct OidcUserStore<AP: AuthenticationProvider>(pub Arc<AP>);
|
||||
|
||||
impl<AP: AuthenticationProvider> Clone for OidcUserStore<AP> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
|
||||
type Error = rustical_store::Error;
|
||||
|
||||
async fn user_exists(&self, id: &str) -> Result<bool, Self::Error> {
|
||||
Ok(self.0.get_principal(id).await?.is_some())
|
||||
}
|
||||
|
||||
async fn insert_user(&self, id: &str) -> Result<(), Self::Error> {
|
||||
self.0
|
||||
.insert_principal(
|
||||
User {
|
||||
id: id.to_owned(),
|
||||
displayname: None,
|
||||
principal_type: Default::default(),
|
||||
password: None,
|
||||
memberships: vec![],
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
http::{StatusCode, header},
|
||||
web::{self, Data, Path},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::Path,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::Referer;
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Addressbook, AddressbookStore, auth::User};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
@@ -14,37 +19,31 @@ struct AddressbookPage {
|
||||
}
|
||||
|
||||
pub async fn route_addressbook<AS: AddressbookStore>(
|
||||
path: Path<(String, String)>,
|
||||
store: Data<AS>,
|
||||
Path((owner, addrbook_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<AS>>,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
) -> Result<impl Responder, rustical_store::Error> {
|
||||
let (owner, addrbook_id) = path.into_inner();
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
|
||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
||||
}
|
||||
Ok(AddressbookPage {
|
||||
addressbook: store.get_addressbook(&owner, &addrbook_id, true).await?,
|
||||
}
|
||||
.respond_to(&req))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn route_addressbook_restore<AS: AddressbookStore>(
|
||||
path: Path<(String, String)>,
|
||||
req: HttpRequest,
|
||||
store: Data<AS>,
|
||||
Path((owner, addressbook_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<AS>>,
|
||||
user: User,
|
||||
) -> Result<impl Responder, rustical_store::Error> {
|
||||
let (owner, addressbook_id) = path.into_inner();
|
||||
referer: Option<TypedHeader<Referer>>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
|
||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
||||
}
|
||||
store.restore_addressbook(&owner, &addressbook_id).await?;
|
||||
Ok(match req.headers().get(header::REFERER) {
|
||||
Some(referer) => web::Redirect::to(referer.to_str().unwrap().to_owned())
|
||||
.using_status_code(StatusCode::FOUND)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body(),
|
||||
None => HttpResponse::Ok().body("Restored"),
|
||||
Ok(match referer {
|
||||
Some(referer) => Redirect::to(&referer.to_string()).into_response(),
|
||||
None => (StatusCode::CREATED, "Restored").into_response(),
|
||||
})
|
||||
}
|
||||
|
||||
104
crates/frontend/src/routes/app_token.rs
Normal file
104
crates/frontend/src/routes/app_token.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
body::Body,
|
||||
extract::Path,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use headers::{ContentType, HeaderMapExt};
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use rand::{Rng, distributions::Alphanumeric};
|
||||
use rustical_store::auth::{AuthenticationProvider, User};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn generate_app_token() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.map(char::from)
|
||||
.take(64)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "apple_configuration/template.xml")]
|
||||
pub struct AppleConfig {
|
||||
token_name: String,
|
||||
account_description: String,
|
||||
hostname: String,
|
||||
caldav_principal_url: String,
|
||||
carddav_principal_url: String,
|
||||
user: String,
|
||||
token: String,
|
||||
caldav_profile_uuid: Uuid,
|
||||
carddav_profile_uuid: Uuid,
|
||||
plist_uuid: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub(crate) struct PostAppTokenForm {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
apple: bool,
|
||||
}
|
||||
|
||||
pub async fn route_post_app_token<AP: AuthenticationProvider>(
|
||||
user: User,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Path(user_id): Path<String>,
|
||||
Host(hostname): Host,
|
||||
Form(PostAppTokenForm { apple, name }): Form<PostAppTokenForm>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
assert!(!name.is_empty());
|
||||
assert_eq!(user_id, user.id);
|
||||
let token = generate_app_token();
|
||||
auth_provider
|
||||
.add_app_token(&user.id, name.to_owned(), token.clone())
|
||||
.await?;
|
||||
if apple {
|
||||
let profile = AppleConfig {
|
||||
token_name: name,
|
||||
account_description: format!("{}@{}", &user.id, &hostname),
|
||||
hostname: hostname.clone(),
|
||||
caldav_principal_url: format!("https://{hostname}/caldav/principal/{user_id}"),
|
||||
carddav_principal_url: format!("https://{hostname}/carddav/principal/{user_id}"),
|
||||
user: user.id.to_owned(),
|
||||
token,
|
||||
caldav_profile_uuid: Uuid::new_v4(),
|
||||
carddav_profile_uuid: Uuid::new_v4(),
|
||||
plist_uuid: Uuid::new_v4(),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
let mut res = Response::builder().status(StatusCode::OK);
|
||||
let hdrs = res.headers_mut().unwrap();
|
||||
hdrs.typed_insert(
|
||||
ContentType::from_str("application/x-apple-aspen-config; charset=utf-8").unwrap(),
|
||||
);
|
||||
let filename = format!("rustical-{}.mobileconfig", user_id);
|
||||
hdrs.insert(
|
||||
header::CONTENT_DISPOSITION,
|
||||
HeaderValue::from_str(&format!(
|
||||
"attachement; filename*=UTF-8''{} filename={}",
|
||||
filename, filename
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
Ok(res.body(Body::new(profile)).unwrap())
|
||||
} else {
|
||||
Ok((StatusCode::OK, token).into_response())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn route_delete_app_token<AP: AuthenticationProvider>(
|
||||
user: User,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Path((user_id, token_id)): Path<(String, String)>,
|
||||
) -> Result<Redirect, rustical_store::Error> {
|
||||
assert_eq!(user_id, user.id);
|
||||
auth_provider.remove_app_token(&user.id, &token_id).await?;
|
||||
Ok(Redirect::to("/frontend/user"))
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
use actix_web::{
|
||||
HttpRequest, HttpResponse, Responder,
|
||||
http::{StatusCode, header},
|
||||
web::{self, Data, Path},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::Path,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::Referer;
|
||||
use http::StatusCode;
|
||||
use rustical_store::{Calendar, CalendarStore, auth::User};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
@@ -14,37 +19,31 @@ struct CalendarPage {
|
||||
}
|
||||
|
||||
pub async fn route_calendar<C: CalendarStore>(
|
||||
path: Path<(String, String)>,
|
||||
store: Data<C>,
|
||||
Path((owner, cal_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<C>>,
|
||||
user: User,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, rustical_store::Error> {
|
||||
let (owner, cal_id) = path.into_inner();
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
|
||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
||||
}
|
||||
Ok(CalendarPage {
|
||||
calendar: store.get_calendar(&owner, &cal_id).await?,
|
||||
}
|
||||
.respond_to(&req))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub async fn route_calendar_restore<CS: CalendarStore>(
|
||||
path: Path<(String, String)>,
|
||||
req: HttpRequest,
|
||||
store: Data<CS>,
|
||||
Path((owner, cal_id)): Path<(String, String)>,
|
||||
Extension(store): Extension<Arc<CS>>,
|
||||
user: User,
|
||||
) -> Result<HttpResponse, rustical_store::Error> {
|
||||
let (owner, cal_id) = path.into_inner();
|
||||
referer: Option<TypedHeader<Referer>>,
|
||||
) -> Result<Response, rustical_store::Error> {
|
||||
if !user.is_principal(&owner) {
|
||||
return Ok(HttpResponse::Unauthorized().body("Unauthorized"));
|
||||
return Ok(StatusCode::UNAUTHORIZED.into_response());
|
||||
}
|
||||
store.restore_calendar(&owner, &cal_id).await?;
|
||||
Ok(match req.headers().get(header::REFERER) {
|
||||
Some(referer) => web::Redirect::to(referer.to_str().unwrap().to_owned())
|
||||
.using_status_code(StatusCode::FOUND)
|
||||
.respond_to(&req)
|
||||
.map_into_boxed_body(),
|
||||
None => HttpResponse::Created().body("Restored"),
|
||||
Ok(match referer {
|
||||
Some(referer) => Redirect::to(&referer.to_string()).into_response(),
|
||||
None => (StatusCode::CREATED, "Restored").into_response(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,13 +5,16 @@ use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::{OriginalUri, Query},
|
||||
extract::Query,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
use http::StatusCode;
|
||||
use rustical_store::auth::AuthenticationProvider;
|
||||
use serde::Deserialize;
|
||||
use tower_sessions::Session;
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/login.html")]
|
||||
@@ -37,20 +40,17 @@ pub async fn route_get_login(
|
||||
Extension(config): Extension<FrontendConfig>,
|
||||
Extension(oidc_config): Extension<Option<OidcConfig>>,
|
||||
) -> Response {
|
||||
// let oidc_data = oidc_config
|
||||
// .as_ref()
|
||||
// .as_ref()
|
||||
// .map(|oidc_config| OidcProviderData {
|
||||
// name: &oidc_config.name,
|
||||
// redirect_url: req
|
||||
// .url_for_static(ROUTE_NAME_OIDC_LOGIN)
|
||||
// .unwrap()
|
||||
// .to_string(),
|
||||
// });
|
||||
let oidc_data = oidc_config
|
||||
.as_ref()
|
||||
.as_ref()
|
||||
.map(|oidc_config| OidcProviderData {
|
||||
name: &oidc_config.name,
|
||||
redirect_url: "/frontend/login/oidc".to_owned(),
|
||||
});
|
||||
LoginPage {
|
||||
redirect_uri,
|
||||
allow_password_login: config.allow_password_login,
|
||||
oidc_data: None,
|
||||
oidc_data,
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
@@ -66,7 +66,8 @@ pub struct PostLoginForm {
|
||||
pub async fn route_post_login<AP: AuthenticationProvider>(
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
Extension(config): Extension<FrontendConfig>,
|
||||
OriginalUri(orig_uri): OriginalUri,
|
||||
session: Session,
|
||||
Host(host): Host,
|
||||
Form(PostLoginForm {
|
||||
username,
|
||||
password,
|
||||
@@ -76,24 +77,32 @@ pub async fn route_post_login<AP: AuthenticationProvider>(
|
||||
if !config.allow_password_login {
|
||||
return StatusCode::METHOD_NOT_ALLOWED.into_response();
|
||||
}
|
||||
// Ensure that redirect_uri never goes cross-origin
|
||||
let default_redirect = "/frontend/user".to_string();
|
||||
let redirect_uri = redirect_uri.unwrap_or(default_redirect.clone());
|
||||
// let redirect_uri = orig_uri
|
||||
// .join(&redirect_uri)
|
||||
// .ok()
|
||||
// .and_then(|uri| orig_uri.make_relative(&uri))
|
||||
// .unwrap_or(default_redirect);
|
||||
// Ensure that redirect_uri never goes cross-origin
|
||||
let base_url: Url = format!("https://{host}").parse().unwrap();
|
||||
let redirect_uri = if let Some(redirect_uri) = redirect_uri {
|
||||
if let Ok(redirect_url) = base_url.join(&redirect_uri) {
|
||||
if redirect_url.origin() == base_url.origin() {
|
||||
redirect_url.path().to_owned()
|
||||
} else {
|
||||
default_redirect
|
||||
}
|
||||
} else {
|
||||
default_redirect
|
||||
}
|
||||
} else {
|
||||
default_redirect
|
||||
};
|
||||
|
||||
if let Ok(Some(user)) = auth_provider.validate_password(&username, &password).await {
|
||||
// session.insert("user", user.id).unwrap();
|
||||
session.insert("user", user.id).await.unwrap();
|
||||
Redirect::to(&redirect_uri).into_response()
|
||||
} else {
|
||||
StatusCode::UNAUTHORIZED.into_response()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn route_post_logout() -> Redirect {
|
||||
// session.remove("user");
|
||||
pub async fn route_post_logout(session: Session) -> Redirect {
|
||||
session.remove_value("user").await.unwrap();
|
||||
Redirect::to("/")
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// pub mod addressbook;
|
||||
// pub mod calendar;
|
||||
pub mod addressbook;
|
||||
pub mod app_token;
|
||||
pub mod calendar;
|
||||
pub mod login;
|
||||
pub mod user;
|
||||
|
||||
89
crates/frontend/src/routes/user.rs
Normal file
89
crates/frontend/src/routes/user.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use askama::Template;
|
||||
use askama_web::WebTemplate;
|
||||
use axum::{
|
||||
Extension,
|
||||
extract::Path,
|
||||
response::{IntoResponse, Redirect},
|
||||
};
|
||||
use axum_extra::TypedHeader;
|
||||
use headers::UserAgent;
|
||||
use http::StatusCode;
|
||||
use rustical_store::{
|
||||
Addressbook, AddressbookStore, Calendar, CalendarStore,
|
||||
auth::{AuthenticationProvider, User, user::AppToken},
|
||||
};
|
||||
|
||||
#[derive(Template, WebTemplate)]
|
||||
#[template(path = "pages/user.html")]
|
||||
pub 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>,
|
||||
pub is_apple: bool,
|
||||
}
|
||||
|
||||
pub async fn route_user_named<
|
||||
CS: CalendarStore,
|
||||
AS: AddressbookStore,
|
||||
AP: AuthenticationProvider,
|
||||
>(
|
||||
Path(user_id): Path<String>,
|
||||
Extension(cal_store): Extension<Arc<CS>>,
|
||||
Extension(addr_store): Extension<Arc<AS>>,
|
||||
Extension(auth_provider): Extension<Arc<AP>>,
|
||||
TypedHeader(user_agent): TypedHeader<UserAgent>,
|
||||
user: User,
|
||||
) -> impl IntoResponse {
|
||||
if user_id != user.id {
|
||||
return StatusCode::UNAUTHORIZED.into_response();
|
||||
}
|
||||
|
||||
let mut calendars = vec![];
|
||||
for group in user.memberships() {
|
||||
calendars.extend(cal_store.get_calendars(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut deleted_calendars = vec![];
|
||||
for group in user.memberships() {
|
||||
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut addressbooks = vec![];
|
||||
for group in user.memberships() {
|
||||
addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
|
||||
}
|
||||
|
||||
let mut deleted_addressbooks = vec![];
|
||||
for group in user.memberships() {
|
||||
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
|
||||
}
|
||||
|
||||
let is_apple = user_agent.as_str().contains("Apple") || user_agent.as_str().contains("Mac OS");
|
||||
|
||||
UserPage {
|
||||
app_tokens: auth_provider.get_app_tokens(&user.id).await.unwrap(),
|
||||
calendars,
|
||||
deleted_calendars,
|
||||
addressbooks,
|
||||
deleted_addressbooks,
|
||||
user,
|
||||
is_apple,
|
||||
}
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub async fn route_get_home(user: User) -> Redirect {
|
||||
Redirect::to(&format!("/frontend/user/{}", user.id))
|
||||
}
|
||||
|
||||
pub async fn route_root(user: Option<User>) -> Redirect {
|
||||
match user {
|
||||
Some(user) => route_get_home(user).await,
|
||||
None => Redirect::to("/frontend/login"),
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ impl AddressObject {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.id);
|
||||
hasher.update(self.get_vcf());
|
||||
format!("{:x}", hasher.finalize())
|
||||
format!("\"{:x}\"", hasher.finalize())
|
||||
}
|
||||
|
||||
pub fn get_vcf(&self) -> &str {
|
||||
|
||||
@@ -142,7 +142,7 @@ impl CalendarObject {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&self.id);
|
||||
hasher.update(self.get_ics());
|
||||
format!("{:x}", hasher.finalize())
|
||||
format!("\"{:x}\"", hasher.finalize())
|
||||
}
|
||||
|
||||
pub fn get_ics(&self) -> &str {
|
||||
|
||||
@@ -12,4 +12,6 @@ reqwest.workspace = true
|
||||
thiserror.workspace = true
|
||||
async-trait.workspace = true
|
||||
axum.workspace = true
|
||||
axum_session = "0.16"
|
||||
tower-sessions = "0.14"
|
||||
axum-extra.workspace = true
|
||||
headers.workspace = true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use openidconnect::{ClientId, ClientSecret, IssuerUrl, Scope};
|
||||
use reqwest::Url;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Default)]
|
||||
|
||||
@@ -13,6 +13,9 @@ pub enum OidcError {
|
||||
#[error(transparent)]
|
||||
OidcClaimsVerificationError(#[from] ClaimsVerificationError),
|
||||
|
||||
#[error(transparent)]
|
||||
SessionError(#[from] tower_sessions::session::Error),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(&'static str),
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
extract::{Query, Request, State},
|
||||
extract::Query,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Host;
|
||||
pub use config::OidcConfig;
|
||||
use config::UserIdClaim;
|
||||
use error::OidcError;
|
||||
@@ -12,21 +13,20 @@ use openidconnect::{
|
||||
RedirectUrl, TokenResponse, UserInfoClaims,
|
||||
core::{CoreClient, CoreGenderClaim, CoreProviderMetadata, CoreResponseType},
|
||||
};
|
||||
use reqwest::{StatusCode, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tower_sessions::Session;
|
||||
pub use user_store::UserStore;
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
mod user_store;
|
||||
|
||||
pub const ROUTE_NAME_OIDC_LOGIN: &str = "oidc_login";
|
||||
const ROUTE_NAME_OIDC_CALLBACK: &str = "oidc_callback";
|
||||
const SESSION_KEY_OIDC_STATE: &str = "oidc_state";
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OidcServiceConfig {
|
||||
pub default_redirect_route_name: &'static str,
|
||||
pub default_redirect_path: &'static str,
|
||||
pub session_key_user_id: &'static str,
|
||||
}
|
||||
|
||||
@@ -92,44 +92,48 @@ pub struct GetOidcForm {
|
||||
}
|
||||
|
||||
/// Endpoint that redirects to the authorize endpoint of the OIDC service
|
||||
// pub async fn route_post_oidc(
|
||||
// Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
||||
// State(oidc_config): State<OidcConfig>,
|
||||
// // session: Session,
|
||||
// req: Request,
|
||||
// ) -> Result<Response, OidcError> {
|
||||
// let http_client = get_http_client();
|
||||
// let oidc_client = get_oidc_client(
|
||||
// oidc_config.clone(),
|
||||
// &http_client,
|
||||
// RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?,
|
||||
// )
|
||||
// .await?;
|
||||
//
|
||||
// let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
//
|
||||
// let (auth_url, csrf_token, nonce) = oidc_client
|
||||
// .authorize_url(
|
||||
// AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||
// CsrfToken::new_random,
|
||||
// Nonce::new_random,
|
||||
// )
|
||||
// .add_scopes(oidc_config.scopes.clone())
|
||||
// .set_pkce_challenge(pkce_challenge)
|
||||
// .url();
|
||||
//
|
||||
// // session.insert(
|
||||
// // SESSION_KEY_OIDC_STATE,
|
||||
// // OidcState {
|
||||
// // state: csrf_token,
|
||||
// // nonce,
|
||||
// // pkce_verifier,
|
||||
// // redirect_uri,
|
||||
// // },
|
||||
// // )?;
|
||||
//
|
||||
// Ok(Redirect::to(auth_url.as_str()).into_response())
|
||||
// }
|
||||
pub async fn route_post_oidc(
|
||||
Extension(oidc_config): Extension<OidcConfig>,
|
||||
session: Session,
|
||||
Host(host): Host,
|
||||
Form(GetOidcForm { redirect_uri }): Form<GetOidcForm>,
|
||||
) -> Result<Response, OidcError> {
|
||||
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
||||
|
||||
let http_client = get_http_client();
|
||||
let oidc_client = get_oidc_client(
|
||||
oidc_config.clone(),
|
||||
&http_client,
|
||||
RedirectUrl::new(callback_uri)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
let (auth_url, csrf_token, nonce) = oidc_client
|
||||
.authorize_url(
|
||||
AuthenticationFlow::<CoreResponseType>::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
.add_scopes(oidc_config.scopes.clone())
|
||||
.set_pkce_challenge(pkce_challenge)
|
||||
.url();
|
||||
|
||||
session
|
||||
.insert(
|
||||
SESSION_KEY_OIDC_STATE,
|
||||
OidcState {
|
||||
state: csrf_token,
|
||||
nonce,
|
||||
pkce_verifier,
|
||||
redirect_uri,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Redirect::to(auth_url.as_str()).into_response())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AuthCallbackQuery {
|
||||
@@ -139,123 +143,111 @@ pub struct AuthCallbackQuery {
|
||||
}
|
||||
|
||||
// Handle callback from IdP page
|
||||
// pub async fn route_get_oidc_callback<US: UserStore>(
|
||||
// Extension(oidc_config): Extension<OidcConfig>,
|
||||
// session: Session,
|
||||
// Extension(user_store): Extension<US>,
|
||||
// Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
||||
// State(service_config): State<OidcServiceConfig>,
|
||||
// req: Request,
|
||||
// ) -> Result<Response, OidcError> {
|
||||
// assert_eq!(iss, oidc_config.issuer);
|
||||
// let oidc_state = session
|
||||
// .remove_as::<OidcState>(SESSION_KEY_OIDC_STATE)
|
||||
// .ok_or(OidcError::Other("No local OIDC state"))?
|
||||
// .map_err(|_| OidcError::Other("Error parsing OIDC state"))?;
|
||||
//
|
||||
// assert_eq!(oidc_state.state.secret(), &state);
|
||||
//
|
||||
// let http_client = get_http_client();
|
||||
// let oidc_client = get_oidc_client(
|
||||
// oidc_config.clone(),
|
||||
// &http_client,
|
||||
// RedirectUrl::new(req.url_for_static(ROUTE_NAME_OIDC_CALLBACK)?.to_string())?,
|
||||
// )
|
||||
// .await?;
|
||||
//
|
||||
// let token_response = oidc_client
|
||||
// .exchange_code(code)?
|
||||
// .set_pkce_verifier(oidc_state.pkce_verifier)
|
||||
// .request_async(&http_client)
|
||||
// .await
|
||||
// .map_err(|_| OidcError::Other("Error requesting token"))?;
|
||||
// let id_claims = token_response
|
||||
// .id_token()
|
||||
// .ok_or(OidcError::Other("OIDC provider did not return an ID token"))?
|
||||
// .claims(&oidc_client.id_token_verifier(), &oidc_state.nonce)?;
|
||||
//
|
||||
// let user_info_claims: UserInfoClaims<GroupAdditionalClaims, CoreGenderClaim> = oidc_client
|
||||
// .user_info(
|
||||
// token_response.access_token().clone(),
|
||||
// Some(id_claims.subject().clone()),
|
||||
// )?
|
||||
// .request_async(&http_client)
|
||||
// .await
|
||||
// .map_err(|_| OidcError::Other("Error fetching user info"))?;
|
||||
//
|
||||
// if let Some(require_group) = &oidc_config.require_group {
|
||||
// if !user_info_claims
|
||||
// .additional_claims()
|
||||
// .groups
|
||||
// .contains(require_group)
|
||||
// {
|
||||
// return Ok(HttpResponse::build(StatusCode::UNAUTHORIZED)
|
||||
// .body("User is not in an authorized group to use RustiCal"));
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let user_id = match oidc_config.claim_userid {
|
||||
// UserIdClaim::Sub => user_info_claims.subject().to_string(),
|
||||
// UserIdClaim::PreferredUsername => user_info_claims
|
||||
// .preferred_username()
|
||||
// .ok_or(OidcError::Other("Missing preferred_username claim"))?
|
||||
// .to_string(),
|
||||
// };
|
||||
//
|
||||
// match user_store.user_exists(&user_id).await {
|
||||
// Ok(false) => {
|
||||
// // User does not exist
|
||||
// if !oidc_config.allow_sign_up {
|
||||
// return Ok(HttpResponse::Unauthorized().body("User sign up disabled"));
|
||||
// }
|
||||
// // Create new user
|
||||
// if let Err(err) = user_store.insert_user(&user_id).await {
|
||||
// return Ok(err.error_response());
|
||||
// }
|
||||
// }
|
||||
// Ok(true) => {}
|
||||
// Err(err) => {
|
||||
// return Ok(err.error_response());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let default_redirect = req
|
||||
// .url_for_static(service_config.default_redirect_route_name)?
|
||||
// .to_string();
|
||||
// let redirect_uri = oidc_state.redirect_uri.unwrap_or(default_redirect.clone());
|
||||
// let redirect_uri = req
|
||||
// .full_url()
|
||||
// .join(&redirect_uri)
|
||||
// .ok()
|
||||
// .and_then(|uri| req.full_url().make_relative(&uri))
|
||||
// .unwrap_or(default_redirect);
|
||||
//
|
||||
// // Complete login flow
|
||||
// session.insert(service_config.session_key_user_id, user_id.clone())?;
|
||||
//
|
||||
// Ok(Redirect::to(redirect_uri)
|
||||
// .temporary()
|
||||
// .respond_to(&req)
|
||||
// .map_into_boxed_body())
|
||||
// }
|
||||
pub async fn route_get_oidc_callback<US: UserStore + Clone>(
|
||||
Extension(oidc_config): Extension<OidcConfig>,
|
||||
Extension(user_store): Extension<US>,
|
||||
Extension(service_config): Extension<OidcServiceConfig>,
|
||||
session: Session,
|
||||
Query(AuthCallbackQuery { code, iss, state }): Query<AuthCallbackQuery>,
|
||||
Host(host): Host,
|
||||
) -> Result<Response, OidcError> {
|
||||
let callback_uri = format!("https://{host}/frontend/login/oidc/callback");
|
||||
|
||||
// pub fn configure_oidc<US: UserStore>(
|
||||
// cfg: &mut ServiceConfig,
|
||||
// oidc_config: OidcConfig,
|
||||
// service_config: OidcServiceConfig,
|
||||
// user_store: Arc<US>,
|
||||
// ) {
|
||||
// cfg.app_data(Data::new(oidc_config))
|
||||
// .app_data(Data::new(service_config))
|
||||
// .app_data(Data::from(user_store))
|
||||
// .service(
|
||||
// web::resource("")
|
||||
// .name(ROUTE_NAME_OIDC_LOGIN)
|
||||
// .post(route_post_oidc),
|
||||
// )
|
||||
// .service(
|
||||
// web::resource("/callback")
|
||||
// .name(ROUTE_NAME_OIDC_CALLBACK)
|
||||
// .get(route_get_oidc_callback::<US>),
|
||||
// );
|
||||
// }
|
||||
assert_eq!(iss, oidc_config.issuer);
|
||||
let oidc_state = session
|
||||
.remove::<OidcState>(SESSION_KEY_OIDC_STATE)
|
||||
.await?
|
||||
.ok_or(OidcError::Other("No local OIDC state"))?;
|
||||
|
||||
assert_eq!(oidc_state.state.secret(), &state);
|
||||
|
||||
let http_client = get_http_client();
|
||||
let oidc_client = get_oidc_client(
|
||||
oidc_config.clone(),
|
||||
&http_client,
|
||||
RedirectUrl::new(callback_uri)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let token_response = oidc_client
|
||||
.exchange_code(code)?
|
||||
.set_pkce_verifier(oidc_state.pkce_verifier)
|
||||
.request_async(&http_client)
|
||||
.await
|
||||
.map_err(|_| OidcError::Other("Error requesting token"))?;
|
||||
let id_claims = token_response
|
||||
.id_token()
|
||||
.ok_or(OidcError::Other("OIDC provider did not return an ID token"))?
|
||||
.claims(&oidc_client.id_token_verifier(), &oidc_state.nonce)?;
|
||||
|
||||
let user_info_claims: UserInfoClaims<GroupAdditionalClaims, CoreGenderClaim> = oidc_client
|
||||
.user_info(
|
||||
token_response.access_token().clone(),
|
||||
Some(id_claims.subject().clone()),
|
||||
)?
|
||||
.request_async(&http_client)
|
||||
.await
|
||||
.map_err(|_| OidcError::Other("Error fetching user info"))?;
|
||||
|
||||
if let Some(require_group) = &oidc_config.require_group {
|
||||
if !user_info_claims
|
||||
.additional_claims()
|
||||
.groups
|
||||
.contains(require_group)
|
||||
{
|
||||
return Ok((
|
||||
StatusCode::UNAUTHORIZED,
|
||||
"User is not in an authorized group to use RustiCal",
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
}
|
||||
|
||||
let user_id = match oidc_config.claim_userid {
|
||||
UserIdClaim::Sub => user_info_claims.subject().to_string(),
|
||||
UserIdClaim::PreferredUsername => user_info_claims
|
||||
.preferred_username()
|
||||
.ok_or(OidcError::Other("Missing preferred_username claim"))?
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
match user_store.user_exists(&user_id).await {
|
||||
Ok(false) => {
|
||||
// User does not exist
|
||||
if !oidc_config.allow_sign_up {
|
||||
return Ok((StatusCode::UNAUTHORIZED, "User signup is disabled").into_response());
|
||||
}
|
||||
// Create new user
|
||||
if let Err(err) = user_store.insert_user(&user_id).await {
|
||||
return Ok(err.into_response());
|
||||
}
|
||||
}
|
||||
Ok(true) => {}
|
||||
Err(err) => {
|
||||
return Ok(err.into_response());
|
||||
}
|
||||
}
|
||||
|
||||
let default_redirect = service_config.default_redirect_path.to_owned();
|
||||
let base_url: Url = format!("https://{host}").parse().unwrap();
|
||||
let redirect_uri = if let Some(redirect_uri) = oidc_state.redirect_uri {
|
||||
if let Ok(redirect_url) = base_url.join(&redirect_uri) {
|
||||
if redirect_url.origin() == base_url.origin() {
|
||||
redirect_url.path().to_owned()
|
||||
} else {
|
||||
default_redirect
|
||||
}
|
||||
} else {
|
||||
default_redirect
|
||||
}
|
||||
} else {
|
||||
default_redirect
|
||||
};
|
||||
|
||||
// Complete login flow
|
||||
session
|
||||
.insert(service_config.session_key_user_id, user_id.clone())
|
||||
.await?;
|
||||
|
||||
Ok(Redirect::to(&redirect_uri).into_response())
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ rrule.workspace = true
|
||||
headers.workspace = true
|
||||
tower.workspace = true
|
||||
futures-core.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = { workspace = true }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use crate::auth::User;
|
||||
|
||||
use super::AuthenticationProvider;
|
||||
use axum::{extract::Request, response::Response};
|
||||
use futures_core::future::BoxFuture;
|
||||
@@ -8,6 +10,7 @@ use std::{
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tower::{Layer, Service};
|
||||
use tower_sessions::Session;
|
||||
use tracing::{Instrument, info_span};
|
||||
|
||||
pub struct AuthenticationLayer<AP: AuthenticationProvider> {
|
||||
@@ -71,8 +74,15 @@ where
|
||||
let ap = self.auth_provider.clone();
|
||||
let mut inner = self.inner.clone();
|
||||
|
||||
// request.extensions_mut();
|
||||
Box::pin(async move {
|
||||
if let Some(session) = request.extensions().get::<Session>() {
|
||||
if let Ok(Some(user_id)) = session.get::<String>("user").await {
|
||||
if let Ok(Some(user)) = ap.get_principal(&user_id).await {
|
||||
request.extensions_mut().insert(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(auth) = auth_header {
|
||||
let user_id = auth.username();
|
||||
let password = auth.password();
|
||||
@@ -84,6 +94,7 @@ where
|
||||
request.extensions_mut().insert(user);
|
||||
}
|
||||
}
|
||||
|
||||
let response = inner.call(request).await?;
|
||||
Ok(response)
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::FromRequestParts,
|
||||
extract::{FromRequestParts, OptionalFromRequestParts},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
@@ -8,7 +8,7 @@ use derive_more::Display;
|
||||
use http::{HeaderValue, StatusCode, header};
|
||||
use rustical_xml::ValueSerialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::{convert::Infallible, fmt::Display};
|
||||
|
||||
use crate::Secret;
|
||||
|
||||
@@ -144,3 +144,14 @@ impl<S: Send + Sync + Clone> FromRequestParts<S> for User {
|
||||
.ok_or(UnauthorizedError)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Send + Sync + Clone> OptionalFromRequestParts<S> for User {
|
||||
type Rejection = Infallible;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut http::request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Option<Self>, Self::Rejection> {
|
||||
Ok(parts.extensions.get::<Self>().cloned())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use axum::response::IntoResponse;
|
||||
use http::StatusCode;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -39,3 +40,9 @@ impl Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(self.status_code(), self.to_string()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum XmlError {
|
||||
// Syntactix errors
|
||||
// Syntactic errors
|
||||
#[error(transparent)]
|
||||
QuickXmlError(#[from] quick_xml::Error),
|
||||
#[error(transparent)]
|
||||
|
||||
62
src/app.rs
62
src/app.rs
@@ -1,14 +1,20 @@
|
||||
use axum::Router;
|
||||
use axum::response::Redirect;
|
||||
use axum::routing::get;
|
||||
use axum::extract::Request;
|
||||
use axum::response::{Redirect, Response};
|
||||
use axum::routing::{any, get};
|
||||
use rustical_caldav::caldav_router;
|
||||
use rustical_carddav::carddav_router;
|
||||
use rustical_frontend::nextcloud_login::NextcloudFlows;
|
||||
use rustical_frontend::nextcloud_login::{NextcloudFlows, nextcloud_login_router};
|
||||
use rustical_frontend::{FrontendConfig, frontend_router};
|
||||
use rustical_oidc::OidcConfig;
|
||||
use rustical_store::auth::AuthenticationProvider;
|
||||
use rustical_store::{AddressbookStore, CalendarStore, SubscriptionStore};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tower_http::classify::ServerErrorsFailureClass;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tower_sessions::MemoryStore;
|
||||
use tracing::Span;
|
||||
|
||||
use crate::config::NextcloudLoginConfig;
|
||||
|
||||
@@ -45,13 +51,14 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
||||
)
|
||||
.route(
|
||||
"/.well-known/caldav",
|
||||
get(async || Redirect::permanent("/caldav")),
|
||||
any(async || Redirect::permanent("/caldav")),
|
||||
)
|
||||
.route(
|
||||
"/.well-known/carddav",
|
||||
get(async || Redirect::permanent("/caldav")),
|
||||
any(async || Redirect::permanent("/caldav")),
|
||||
);
|
||||
|
||||
let session_store = MemoryStore::default();
|
||||
if frontend_config.enabled {
|
||||
router = router
|
||||
.nest(
|
||||
@@ -62,21 +69,42 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
|
||||
addr_store.clone(),
|
||||
frontend_config,
|
||||
oidc_config,
|
||||
session_store.clone(),
|
||||
),
|
||||
)
|
||||
.route("/", get(async || Redirect::to("/frontend")));
|
||||
}
|
||||
|
||||
router
|
||||
|
||||
// if nextcloud_login_config.enabled {
|
||||
// app = app.configure(|cfg| {
|
||||
// configure_nextcloud_login(
|
||||
// cfg,
|
||||
// nextcloud_flows_state,
|
||||
// auth_provider.clone(),
|
||||
// frontend_config.secret_key,
|
||||
// )
|
||||
// });
|
||||
// }
|
||||
if nextcloud_login_config.enabled {
|
||||
router = router.nest(
|
||||
"/index.php/login/v2",
|
||||
nextcloud_login_router(
|
||||
nextcloud_flows_state,
|
||||
auth_provider.clone(),
|
||||
session_store.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
router.layer(
|
||||
TraceLayer::new_for_http()
|
||||
.make_span_with(|request: &Request| {
|
||||
tracing::debug_span!(
|
||||
"http-request",
|
||||
status_code = tracing::field::Empty,
|
||||
otel.name =
|
||||
tracing::field::display(format!("{} {}", request.method(), request.uri())),
|
||||
)
|
||||
})
|
||||
.on_request(|_req: &Request, _span: &Span| {})
|
||||
.on_response(|response: &Response, _latency: Duration, span: &Span| {
|
||||
span.record("status_code", tracing::field::display(response.status()));
|
||||
|
||||
tracing::debug!("response generated")
|
||||
})
|
||||
.on_failure(
|
||||
|_error: ServerErrorsFailureClass, _latency: Duration, _span: &Span| {
|
||||
tracing::error!("something went wrong")
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use clap::{Parser, ValueEnum};
|
||||
use password_hash::PasswordHasher;
|
||||
use pbkdf2::Params;
|
||||
use rand::rngs::OsRng;
|
||||
use rustical_frontend::{FrontendConfig, generate_frontend_secret};
|
||||
use rustical_frontend::FrontendConfig;
|
||||
|
||||
use crate::config::{
|
||||
Config, DataStoreConfig, DavPushConfig, HttpConfig, SqliteDataStoreConfig, TracingConfig,
|
||||
@@ -23,7 +23,6 @@ pub fn cmd_gen_config(_args: GenConfigArgs) -> anyhow::Result<()> {
|
||||
}),
|
||||
tracing: TracingConfig::default(),
|
||||
frontend: FrontendConfig {
|
||||
secret_key: generate_frontend_secret(),
|
||||
enabled: true,
|
||||
allow_password_login: true,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::Result;
|
||||
use app::make_app;
|
||||
use axum::ServiceExt;
|
||||
use axum::extract::Request;
|
||||
use clap::{Parser, Subcommand};
|
||||
use commands::principals::{PrincipalsArgs, cmd_principals};
|
||||
use commands::{cmd_gen_config, cmd_pwhash};
|
||||
@@ -18,6 +20,8 @@ use rustical_store_sqlite::{SqliteStore, create_db_pool};
|
||||
use setup_tracing::setup_tracing;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::Receiver;
|
||||
use tower::Layer;
|
||||
use tower_http::normalize_path::NormalizePathLayer;
|
||||
|
||||
mod app;
|
||||
mod commands;
|
||||
@@ -113,6 +117,9 @@ async fn main() -> Result<()> {
|
||||
config.nextcloud_login.clone(),
|
||||
nextcloud_flows.clone(),
|
||||
);
|
||||
let app = ServiceExt::<Request>::into_make_service(
|
||||
NormalizePathLayer::trim_trailing_slash().layer(app),
|
||||
);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&format!(
|
||||
"{}:{}",
|
||||
|
||||
Reference in New Issue
Block a user