kvarn_auth/
integrations.rs

1//! Integrations with databases to get started more quickly.
2//!
3//! # Support
4//!
5//! - [x] Custom, built-in format
6//! - [ ] SQL?
7//! - [ ] Icelk's DB?
8
9#![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}
35/// Gets the user from a request.
36pub 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    /// Get the user and it's data.
54    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    /// Get the user and it's data as a mutable reference.
74    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/// Data used when logging in.
96#[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/// JS's `+(new Date())`:
107/// Milliseconds since epoch.
108#[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        // `tokio-uring` support
171        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    /// Change types of all data. Useful for "upgrading" the data scheme
212    /// Please keep in mind that the `other_data` passwed to `map_user_data` is not upgraded
213    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}
265/// `Arc<async Fn(username, email) -> Option<User data>>`, disallowed if `None`
266pub type CreationAllowed<T> = Arc<
267    dyn Fn(CompactString, CompactString, serde_json::Value) -> RetFut<'static, Option<T>>
268        + Send
269        + Sync,
270>;
271/// `Arc<async Fn(user, email, target_user_to_be_deleted, user_is_admin) -> delete?>`
272///
273/// Is only called if the deletion would normally be allowed (user tries to delete self, or user
274/// is admin).
275pub 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}