1#![allow(missing_docs)]
10
11use std::collections::BTreeSet;
12use std::sync::Arc;
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use dashmap::DashMap;
16use kvarn::extensions::RetFut;
17use kvarn::prelude::*;
18use serde::de::DeserializeOwned;
19use serde::{Deserialize, Serialize, Serializer};
20
21use crate::{AuthData, Builder, CryptoAlgo, Validation};
22
23pub enum UserValidation<T> {
24 Unauthorized,
25 Authorized(LoginData, T),
26}
27impl<T> UserValidation<T> {
28 pub fn into_option(self) -> Option<(LoginData, T)> {
29 match self {
30 Self::Unauthorized => None,
31 Self::Authorized(l, t) => Some((l, t)),
32 }
33 }
34}
35pub struct GetFsUser<T, U> {
37 pub login: Login,
38 data: Arc<FsUserCollection<T, U>>,
39}
40impl<T, U> Clone for GetFsUser<T, U> {
41 fn clone(&self) -> Self {
42 Self {
43 login: self.login.clone(),
44 data: self.data.clone(),
45 }
46 }
47}
48impl<
49 T: DeserializeOwned + Serialize + Send + Sync,
50 U: DeserializeOwned + Serialize + Send + Sync,
51 > GetFsUser<T, U>
52{
53 pub fn get_user(
55 &self,
56 request: &FatRequest,
57 addr: SocketAddr,
58 ) -> UserValidation<dashmap::mapref::one::Ref<CompactString, User<T>>> {
59 let validation = (self.login)(request, addr);
60 match validation {
61 Validation::Unauthorized => UserValidation::Unauthorized,
62 Validation::Authorized(AuthData::Structured(v)) => {
63 let Some(user) = self.data.users.get(&v.username) else {
64 warn!("User {} is authorized but doesn't exist in the DB", v.username);
65 return UserValidation::Unauthorized;
66 };
67
68 UserValidation::Authorized(v, user)
69 }
70 _ => panic!("our AuthData is always Structured"),
71 }
72 }
73 pub fn get_user_mut(
75 &self,
76 request: &FatRequest,
77 addr: SocketAddr,
78 ) -> UserValidation<dashmap::mapref::one::RefMut<CompactString, User<T>>> {
79 let validation = (self.login)(request, addr);
80 match validation {
81 Validation::Unauthorized => UserValidation::Unauthorized,
82 Validation::Authorized(AuthData::Structured(v)) => {
83 let Some(user) = self.data.users.get_mut(&v.username) else {
84 warn!("User {} is authorized but doesn't exist in the DB", v.username);
85 return UserValidation::Unauthorized;
86 };
87
88 UserValidation::Authorized(v, user)
89 }
90 _ => panic!("our AuthData is always Structured"),
91 }
92 }
93}
94
95#[derive(Deserialize, Serialize)]
97pub struct LoginData {
98 pub username: CompactString,
99 pub email: CompactString,
100 pub admin: bool,
101 pub ctime: DateTime,
102}
103
104pub type Login = crate::LoginStatusClosure<LoginData>;
105
106#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone, Debug)]
109#[serde(transparent)]
110pub struct DateTime(u64);
111
112#[derive(Deserialize)]
113struct CreationUser {
114 username: CompactString,
115 email: CompactString,
116 password: CompactString,
117 #[serde(flatten)]
118 other: serde_json::Value,
119}
120#[derive(Deserialize, Serialize, Clone)]
121pub struct User<T> {
122 pub username: CompactString,
123 pub email: CompactString,
124 pub admin: bool,
125
126 pub data: T,
127
128 pub ctime: DateTime,
129
130 hashed_password: u128,
131 salt: [u8; 16],
132}
133impl<T> User<T> {
134 pub fn new_password(&mut self, password: &[u8]) {
135 let (hash, salt) = new_hash(password);
136 self.hashed_password = hash;
137 self.salt = salt;
138 }
139}
140#[derive(Deserialize, Serialize, Clone)]
141struct QueriedUser {
142 pub username: CompactString,
143 pub email: CompactString,
144 pub admin: bool,
145}
146#[derive(Serialize, Deserialize)]
147pub struct FsUserCollection<T, U> {
148 pub users: DashMap<CompactString, User<T>>,
149 pub email_to_user: DashMap<CompactString, CompactString>,
150 pub other_data: U,
151 #[serde(skip)]
152 pub path: CompactString,
153}
154impl<
155 T: DeserializeOwned + Serialize + Send + Sync,
156 U: DeserializeOwned + Serialize + Send + Sync,
157 > FsUserCollection<T, U>
158{
159 pub fn empty_at(path: impl AsRef<str>, other_data: U) -> Self {
160 Self {
161 users: DashMap::new(),
162 email_to_user: DashMap::new(),
163 other_data,
164
165 path: path.as_ref().to_compact_string(),
166 }
167 }
168 pub async fn read(path: impl AsRef<str>) -> Option<Result<Self, String>> {
169 let path = path.as_ref().to_compact_string();
170 let file = kvarn::read_file(&path, None).await?;
172 let me: Result<Self, _> =
173 bincode::serde::decode_from_slice(&file, bincode::config::standard()).map(|(v, _)| v);
174 match me {
175 Ok(mut me) => {
176 me.path = path;
177 Some(Ok(me))
178 }
179 Err(err) => Some(Err(format!("Failed to load the file at {path}: {err}"))),
180 }
181 }
182 pub async fn write(&self) {
183 let (data, path) = {
184 (
185 bincode::serde::encode_to_vec(self, bincode::config::standard()).unwrap(),
186 self.path.clone(),
187 )
188 };
189
190 if let Err(err) = tokio::fs::write(path.as_str(), data).await {
191 error!("Failed to write user database to {path:?}: {err}");
192 }
193 }
194
195 pub fn remove_user(&self, username: &str) -> bool {
196 let user = self.users.remove(username);
197 if let Some((_, user)) = user {
198 self.email_to_user.remove(&user.email);
199 true
200 } else {
201 false
202 }
203 }
204 #[allow(clippy::result_unit_err)]
205 pub fn change_user_password(&self, username: &str, password: &[u8]) -> Result<(), ()> {
206 let mut u = self.users.get_mut(username).ok_or(())?;
207 u.new_password(password);
208 Ok(())
209 }
210
211 pub fn map<
214 NewT: DeserializeOwned + Serialize + Send + Sync,
215 NewU: DeserializeOwned + Serialize + Send + Sync,
216 >(
217 self,
218 map_other_data: impl FnOnce(U, &DashMap<CompactString, User<NewT>>) -> NewU,
219 mut map_user_data: impl FnMut(T, &U) -> NewT,
220 ) -> FsUserCollection<NewT, NewU> {
221 let Self {
222 users,
223 email_to_user,
224 other_data,
225 path,
226 } = self;
227
228 let users = users
229 .into_iter()
230 .map(|(k, v)| {
231 let User {
232 username,
233 email,
234 admin,
235 data,
236 ctime,
237 hashed_password,
238 salt,
239 } = v;
240 let data = map_user_data(data, &other_data);
241 (
242 k,
243 User {
244 username,
245 email,
246 admin,
247 data,
248 ctime,
249 hashed_password,
250 salt,
251 },
252 )
253 })
254 .collect();
255 let other_data = map_other_data(other_data, &users);
256
257 FsUserCollection {
258 users,
259 email_to_user,
260 other_data,
261 path,
262 }
263 }
264}
265pub type CreationAllowed<T> = Arc<
267 dyn Fn(CompactString, CompactString, serde_json::Value) -> RetFut<'static, Option<T>>
268 + Send
269 + Sync,
270>;
271pub type AllowUserDeletion = Arc<
276 dyn Fn(CompactString, CompactString, CompactString, bool) -> RetFut<'static, bool>
277 + Send
278 + Sync,
279>;
280
281#[derive(Default)]
282pub struct FsIntegrationOptions {
283 pub always_admin: BTreeSet<CompactString>,
284 pub account_path: Option<CompactString>,
285 pub login_path: Option<CompactString>,
286 pub cookie_path: Option<CompactString>,
287 pub allow_user_deletion: Option<AllowUserDeletion>,
288}
289
290pub fn mount_fs_integration<
291 T: DeserializeOwned + Serialize + Send + Sync + 'static,
292 U: Serialize + DeserializeOwned + Send + Sync + 'static,
293>(
294 path: impl AsRef<str>,
295 extensions: &mut Extensions,
296 creation_allowed: CreationAllowed<T>,
297 users: Arc<FsUserCollection<T, U>>,
298 key: CryptoAlgo,
299 opts: FsIntegrationOptions,
300) -> GetFsUser<T, U> {
301 let path = path.as_ref();
302 let account_path = format_compact!(
303 "{path}{}",
304 opts.account_path.as_ref().map_or("account", |s| s.as_ref())
305 );
306 let login_path = format_compact!(
307 "{path}{}",
308 opts.login_path.as_ref().map_or("login", |s| s.as_ref())
309 );
310
311 let auth = {
312 let users = users.clone();
313 Builder::new()
314 .with_cookie_path(opts.cookie_path.as_ref().map_or(path, |s| s.as_ref()))
315 .with_auth_page_name(login_path)
316 .with_relaxed_httponly()
317 .build(
318 move |user, password, _addr, _req| {
319 let user = user.to_compact_string();
320 let password = password.to_compact_string();
321 let users = users.clone();
322 async move {
323 let user = users.users.get(&user).or_else(|| {
324 users
325 .email_to_user
326 .get(&user)
327 .and_then(|user| users.users.get(user.value()))
328 });
329 let Some(user) = user else {
330 return Validation::Unauthorized;
331 };
332
333 let hash = password_hash(password.as_bytes(), &user.salt);
334
335 if user.hashed_password != hash {
336 return Validation::Unauthorized;
337 }
338
339 Validation::Authorized(AuthData::Structured(LoginData {
340 username: user.username.clone(),
341 email: user.email.clone(),
342 admin: user.admin,
343 ctime: user.ctime.clone(),
344 }))
345 }
346 },
347 key,
348 )
349 };
350
351 let login = auth.login_status();
352 struct Ext<
353 T: DeserializeOwned + Serialize + Send + Sync,
354 U: DeserializeOwned + Serialize + Send + Sync,
355 > {
356 creation_allowed: CreationAllowed<T>,
357 users: Arc<FsUserCollection<T, U>>,
358 login: Login,
359 deletion: Option<AllowUserDeletion>,
360 always_admin: BTreeSet<CompactString>,
361 }
362 impl<
363 T: DeserializeOwned + Serialize + Send + Sync,
364 U: DeserializeOwned + Serialize + Send + Sync,
365 > kvarn::extensions::PrepareCall for Ext<T, U>
366 {
367 fn call<'a>(
368 &'a self,
369 req: &'a mut FatRequest,
370 host: &'a Host,
371 _: Option<&'a Path>,
372 addr: SocketAddr,
373 ) -> RetFut<'a, FatResponse> {
374 Box::pin(async move {
375 let Self {
376 creation_allowed,
377 users,
378 login,
379 deletion,
380 always_admin,
381 } = self;
382 match *req.method() {
383 Method::PUT => {
384 if matches!(login(req, addr), Validation::Authorized(_)) {
385 return default_error_response(
386 StatusCode::BAD_REQUEST,
387 host,
388 Some("You're already logged in!"),
389 )
390 .await;
391 }
392 let Ok(body) = req.body_mut().read_to_bytes(1024 * 8).await else {
393 return default_error_response(
394 StatusCode::BAD_REQUEST, host, Some("requires JSON body")
395 ).await;
396 };
397 let Ok(mut body): Result<CreationUser, _> = serde_json::from_slice(&body) else {
398 return default_error_response(
399 StatusCode::BAD_REQUEST, host, Some("missing parameters")
400 ).await;
401 };
402
403 body.email = body.email.to_lowercase().to_compact_string();
404
405 let contains = {
406 users.users.contains_key(&body.username)
407 || users.email_to_user.contains_key(&body.email)
408 };
409 let allow = async {
410 (creation_allowed)(
411 body.username.clone(),
412 body.email.clone(),
413 body.other,
414 )
415 .await
416 };
417 let opt = if contains { None } else { allow.await };
418 let Some(data) = opt else {
419 return default_error_response(
420 StatusCode::FORBIDDEN,
421 host,
422 Some("you aren't allowed to create an account"),
423 )
424 .await;
425 };
426
427 let (hash, salt) = new_hash(body.password.as_bytes());
428
429 let user = User {
430 username: body.username.clone(),
431 email: body.email.clone(),
432 admin: always_admin.contains(&body.username),
433
434 data,
435
436 ctime: DateTime(
437 SystemTime::now()
438 .duration_since(UNIX_EPOCH)
439 .unwrap_or(Duration::ZERO)
440 .as_millis() as u64,
441 ),
442
443 hashed_password: hash,
444 salt,
445 };
446 if users.users.contains_key(&body.username)
447 || users.email_to_user.contains_key(&body.email)
448 {
449 return default_error_response(
450 StatusCode::FORBIDDEN,
451 host,
452 Some("you aren't allowed to create an account"),
453 )
454 .await;
455 }
456 users.users.insert(body.username.clone(), user);
457 users
458 .email_to_user
459 .insert(body.email.clone(), body.username.clone());
460
461 users.write().await;
462
463 FatResponse::no_cache(Response::new(Bytes::new()))
464 }
465 Method::GET => {
466 let login = login(req, addr);
467 if !matches!(
468 login,
469 Validation::Authorized(AuthData::Structured(LoginData {
470 username: _,
471 email: _,
472 admin: true,
473 ctime: _,
474 }))
475 ) {
476 return default_error_response(StatusCode::UNAUTHORIZED, host, None)
477 .await;
478 }
479 let users = users.users.iter().map(|user| QueriedUser {
480 username: user.value().username.clone(),
481 email: user.value().email.clone(),
482 admin: user.value().admin,
483 });
484 let bytes = WriteableBytes::new();
485 let mut ser = serde_json::Serializer::new(bytes);
486 ser.collect_seq(users).unwrap();
487 let bytes = ser.into_inner();
488
489 FatResponse::no_cache(Response::new(bytes.into_inner().freeze()))
490 }
491 Method::DELETE => {
492 let Validation::Authorized(AuthData::Structured(
493 LoginData {
494 username,
495 email,
496 admin,
497 ctime: _,
498 },
499 )) = login(req, addr) else {
500 return default_error_response(StatusCode::UNAUTHORIZED, host, None)
501 .await;
502 };
503
504 let header = req
505 .headers()
506 .get("x-account")
507 .map(HeaderValue::to_str)
508 .and_then(Result::ok);
509
510 let mut target;
511
512 if let Some(header) = header {
513 if !admin {
514 return default_error_response(
515 StatusCode::UNAUTHORIZED,
516 host,
517 None,
518 )
519 .await;
520 }
521 target = header.to_compact_string();
522 } else {
523 if admin {
524 return default_error_response(
525 StatusCode::UNAUTHORIZED,
526 host,
527 Some("you can't implicitly delete your account as admin"),
528 )
529 .await;
530 }
531 target = username.clone()
532 }
533
534 if !users.users.contains_key(&target) {
535 if let Some(u) = users.email_to_user.get(&target) {
536 target = u.value().to_compact_string();
537 }
538 }
539
540 let allow = if let Some(f) = deletion {
541 f(
542 username.to_compact_string(),
543 email,
544 target.to_compact_string(),
545 admin,
546 )
547 .await
548 } else {
549 true
550 };
551 let r = if allow {
552 if users.remove_user(&target) {
553 FatResponse::no_cache(Response::new(Bytes::new()))
554 } else {
555 default_error_response(
556 StatusCode::NOT_FOUND,
557 host,
558 Some("account not found"),
559 )
560 .await
561 }
562 } else {
563 default_error_response(
564 StatusCode::UNAUTHORIZED,
565 host,
566 Some("you weren't allowed to remove your account"),
567 )
568 .await
569 };
570 users.write().await;
571 r
572 }
573 _ => default_error_response(StatusCode::METHOD_NOT_ALLOWED, host, None).await,
574 }
575 })
576 }
577 }
578 extensions.add_prepare_single(
579 account_path,
580 Box::new(Ext {
581 creation_allowed,
582 login,
583 users: users.clone(),
584 deletion: opts.allow_user_deletion,
585 always_admin: opts.always_admin,
586 }),
587 );
588 auth.mount(extensions);
589 GetFsUser {
590 login: auth.login_status(),
591 data: users,
592 }
593}
594
595fn password_hash(password: &[u8], salt: &[u8]) -> u128 {
596 let mut pass = Vec::with_capacity(password.len() + salt.len());
597 pass.extend_from_slice(password);
598 pass.extend_from_slice(salt);
599
600 xxhash_rust::xxh3::xxh3_128(&pass)
601}
602fn new_hash(password: &[u8]) -> (u128, [u8; 16]) {
603 let salt: [u8; 16] = rand::Rng::gen(&mut rand::thread_rng());
604
605 let hash = password_hash(password, &salt);
606 (hash, salt)
607}