ctoolbox/storage/
user.rs

1//! User management: creation, deletion, login, metadata.
2//! This *tries* to lock the user during write operations so it won't end up in
3//! inconsistent states, etc. but that doesn't currently seem to work reliably.
4
5use crate::storage::db::{
6    delete_user_auth, delete_user_display_name, delete_user_id,
7    delete_user_kek_params, delete_user_picture, delete_user_pubkey,
8    delete_user_uuid, delete_user_wrapped_dek, get_user_auth, get_user_id,
9    get_user_kek_params, get_user_name, get_user_pictures, get_user_pubkey,
10    get_user_wrapped_dek, put_user_auth, put_user_id, put_user_kek_params,
11    put_user_uuid, put_user_wrapped_dek,
12};
13use crate::storage::graph::Graph;
14use crate::storage::pc_settings::ensure_pc_settings;
15use crate::storage::user::auth::{KekParams, Secret};
16use crate::utilities::password::{Password, hash, verify};
17use crate::utilities::resource_lock::{Lock, ResourceLock};
18use crate::{bail_if_none, error, get_storage_dir, json, log};
19use anyhow::{Context, Result, anyhow, bail};
20use serde::{Deserialize, Serialize, de::DeserializeOwned};
21use std::collections::HashMap;
22use std::ffi::OsString;
23use std::fs;
24use std::path::Path;
25
26pub mod auth;
27
28pub const TEST_USER_PASS: &str =
29    "test_password_523ed9db-1885-439d-83d5-61c869e5223d";
30pub const TEST_USER_PHC: &str = "$argon2id$v=19$m=19456,t=2,p=1$RXiQwP6rcmLh98qumvyu0g$Pc9dem17Dgwz6uoiCuceMh+VYQxZ8WqSK36gfpAZYXY";
31
32#[derive(Debug)]
33pub struct NameAndIdLock {
34    pub name_lock: ResourceLock,
35    pub id_lock: ResourceLock,
36}
37
38impl Lock for NameAndIdLock {}
39
40impl NameAndIdLock {
41    pub fn new(name_lock: ResourceLock, id_lock: ResourceLock) -> Self {
42        Self { name_lock, id_lock }
43    }
44}
45
46#[derive(Debug, Default, Serialize, Deserialize)]
47pub struct UserLocalConfig {
48    pub default_store_location: OsString,
49}
50
51#[derive(Debug, Default, Serialize)]
52pub struct UserPublicInfo {
53    local_id: u64,
54    name: String,
55    display_name: Option<Vec<u8>>,
56    uuid: Vec<u8>,
57    picture: Option<Vec<u8>>,
58    #[serde(skip)]
59    lock: Option<ResourceLock>,
60}
61
62impl UserPublicInfo {
63    pub fn get_by_name(name: &str) -> Result<Option<Self>> {
64        let _name_lock = ResourceLock::acquire(USER_NAME_LOCK, &name)?;
65        let mut user_info = UserPublicInfo::default();
66        let id = get_user_id_by_name(name);
67        let Some(id) = id else {
68            return Ok(None);
69        };
70        let user_info_lock = bail_if_none!(
71            ResourceLock::acquire(USER_PUBLIC_INFO_LOCK, &id).ok()
72        );
73        // Re-check ID  to ensure mapping stability
74        let id_check = bail_if_none!(get_user_id_by_name(name));
75        if id != id_check {
76            bail!(format!(
77                "User ID changed during get_by_name for '{name}': {id} -> {id_check}"
78            ));
79        }
80        user_info.lock = Some(user_info_lock);
81        user_info.local_id = id;
82        // Get display name and picture  if needed in future
83        user_info.name = name.to_string();
84        if let Some(picture) = get_user_picture_by_id(id) {
85            user_info.picture = Some(picture);
86        }
87        Ok(Some(user_info))
88    }
89
90    pub fn get_by_id(id: u64) -> Option<Self> {
91        let mut user_info = UserPublicInfo::default();
92        let user_info_lock =
93            ResourceLock::acquire(USER_PUBLIC_INFO_LOCK, &id).ok()?;
94        user_info.lock = Some(user_info_lock);
95        user_info.local_id = id;
96
97        // Perform DB lookups , after acquiring public info lock (consistent ordering: never acquire other locks while holding DB lock).
98        user_info.name = get_user_name(id)?;
99
100        if let Some(picture) = { get_user_picture_by_id(id) } {
101            user_info.picture = Some(picture);
102        }
103        Some(user_info)
104    }
105
106    pub fn local_id(&self) -> u64 {
107        self.local_id
108    }
109    pub fn name(&self) -> &str {
110        &self.name
111    }
112    pub fn display_name(&self) -> Option<&[u8]> {
113        self.display_name.as_deref()
114    }
115    pub fn uuid(&self) -> &Vec<u8> {
116        &self.uuid
117    }
118    pub fn user_picture(&self) -> Option<&[u8]> {
119        self.picture.as_deref()
120    }
121
122    pub fn list_all() -> Result<Vec<Self>> {
123        let user_ids = get_all_user_ids()?;
124        let mut users = Vec::new();
125        for id in user_ids {
126            if let Some(user) = UserPublicInfo::get_by_id(id) {
127                users.push(user);
128            }
129        }
130        Ok(users)
131    }
132}
133
134#[derive(Debug, Default)]
135pub struct User {
136    public_info: UserPublicInfo,
137    remote_id: Option<u64>,
138    local_config: UserLocalConfig,
139    graphs: Vec<Graph>,
140    dek: Option<Secret>,
141    lock: Option<NameAndIdLock>, // Guard that keeps user locked while User is alive.
142}
143
144const USER_LOCK: &str = "user";
145const USER_NAME_LOCK: &str = "user_name";
146const USER_PUBLIC_INFO_LOCK: &str = "user_public_info";
147
148impl User {
149    pub fn name(&self) -> String {
150        self.public_info.name.clone()
151    }
152
153    pub fn local_id(&self) -> u64 {
154        self.public_info.local_id
155    }
156
157    pub fn remote_id(&self) -> Option<u64> {
158        self.remote_id
159    }
160
161    pub fn display_name(&self) -> Option<&[u8]> {
162        self.public_info.display_name.as_deref()
163    }
164
165    pub fn user_picture(&self) -> Option<&[u8]> {
166        self.public_info.picture.as_deref()
167    }
168
169    pub fn uuid(&self) -> &Vec<u8> {
170        &self.public_info.uuid
171    }
172
173    /// Create a new local user.
174    pub fn create(name: &str, password: &Password) -> Result<Self> {
175        // 1) Acquire name lock first to serialize operations on this user name.
176        let name_lock = ResourceLock::acquire(USER_NAME_LOCK, &name)?;
177
178        error!(format!("Creating user '{name}'"));
179        ensure_base_layout().context("Failed to ensure base layout")?;
180
181        // 2) Check existence.
182        if get_user_id_by_name(name).is_some() {
183            return Err(anyhow!("User '{name}' already exists"));
184        }
185
186        // 3) Allocate a new user id using the file-based atomic counter.
187        let user_id = next_user_id().context("Unable to allocate user id")?;
188
189        // 4) Acquire per-user lock (exclusive) for entire creation.
190        let id_lock = lock_by_id(user_id)?;
191        let user_lock = NameAndIdLock { name_lock, id_lock };
192
193        let (kek, kek_params) = auth::derive_kek(password);
194        let dek = auth::generate_dek();
195        let phc = hash(password)?;
196
197        // Wrap DEK under KEK with aad=user_id
198        // AAD = "additional authenticated data" in AEAD, used to avoid reusing
199        // the key with different user ID, etc.
200        // So, to allow the user record to be copied between local machine and
201        // server, also allocate a UUID for the user and use that as the AAD.
202        // The UUID must be persistent when the user record is copied (unlike
203        // the local user ID).
204
205        // Allocate UUID for AAD
206        let uuid = uuid::Uuid::new_v4();
207        let uuid = uuid.as_bytes();
208
209        let aad = uuid;
210        let wrapped_dek = auth::wrap_key(&kek, &dek, aad);
211
212        // 5) Persist all user metadata.
213        let root = get_storage_dir().context("No storage dir")?;
214        let users_dir = root.join("users");
215        fs::create_dir_all(&users_dir).ok();
216
217        put_user_id(name, user_id)?;
218        put_user_uuid(user_id, uuid)?;
219        put_user_auth(user_id, &phc)?;
220        put_user_kek_params(user_id, json!(kek_params).as_bytes())?;
221        put_user_wrapped_dek(user_id, &wrapped_dek)?;
222
223        ensure_pc_settings().context("Failed to ensure pc_settings.json")?;
224
225        {
226            let root = get_storage_dir().context("No storage dir")?;
227            let graphs_dir = root.join("graphs").join(user_id.to_string());
228            fs::create_dir_all(&graphs_dir).with_context(|| {
229                format!("Failed to create graphs dir {}", graphs_dir.display())
230            })?;
231        }
232
233        let mut user = User::default();
234        user.public_info.local_id = user_id;
235        user.public_info.name = name.to_string();
236        user.public_info.uuid = uuid.to_vec();
237        user.local_config = user.local_config();
238        user.dek = Some(Secret::new(dek));
239        user.lock = Some(user_lock);
240
241        Ok(user)
242    }
243
244    /// Delete this user
245    pub fn delete(&self) -> Result<()> {
246        ensure_base_layout().context("Failed to ensure base layout")?;
247        let user_id = self.local_id();
248        let name = self.name();
249
250        // If caller already holds locks (e.g., from create), respect them and only serialize DB work.
251        if self.lock.is_some() {
252            Self::_delete_user(user_id, &name)
253        } else {
254            // Acquire locks in consistent order: name -> id -> db
255            let _name_lock = ResourceLock::acquire(USER_NAME_LOCK, &name)?;
256            let _id_lock = lock_by_id(user_id)?;
257
258            Self::_delete_user(user_id, &name)
259        }
260    }
261
262    /// Delete a user by name
263    pub fn delete_by_name(name: &str) -> Result<()> {
264        ensure_base_layout().context("Failed to ensure base layout")?;
265        log!("Deleting user '{name}'");
266
267        // Acquire locks in consistent order: name -> id -> db
268        let name_lock = ResourceLock::acquire(USER_NAME_LOCK, &name)?;
269
270        let user_id = get_user_id_by_name(name)
271            .ok_or_else(|| anyhow!("User ID not found for name {name}"))?;
272
273        let _id_lock = lock_by_id(user_id)?;
274
275        // Perform deletion .
276        let res = {
277            // Keep name_lock in scope to maintain consistent ownership while deleting mapping.
278            let _keep_name_lock = &name_lock;
279            Self::_delete_user(user_id, name)
280        };
281
282        // Verify deletion .
283        {
284            if get_user_id_by_name(name).is_some() {
285                bail!("Failed to delete user {name} with id {user_id}");
286            }
287        }
288        res
289    }
290
291    /// Internal (no-DB-lock) deletion logic extracted to avoid double-locking.
292    /// Do NOT call this without holding the DB lock. This function assumes the caller holds:
293    /// - `USER_NAME_LOCK` (for `name`)
294    /// - `USER_LOCK` (for `user_id`)
295    fn _delete_user(user_id: u64, name: &str) -> Result<()> {
296        let root = get_storage_dir().context("No storage dir")?;
297
298        delete_user_uuid(user_id)?;
299        delete_user_auth(user_id)?;
300        delete_user_picture(user_id)?;
301        delete_user_display_name(user_id)?;
302        delete_user_kek_params(user_id)?;
303        delete_user_wrapped_dek(user_id)?;
304        delete_user_pubkey(user_id)?;
305        delete_user_id(name, user_id)?;
306
307        // Remove user's graphs directory (filesystem, not DB)
308        let graphs_dir = root.join("graphs").join(user_id.to_string());
309        if graphs_dir.exists() {
310            fs::remove_dir_all(&graphs_dir).with_context(|| {
311                format!("Failed to remove graphs dir {graphs_dir:?}")
312            })?;
313        }
314        Ok(())
315    }
316
317    /// Login: Verify password, derive KEK, verify DEK.
318    /// TODO: Possibly this should also log in to the server and start syncing any new user data.
319    pub fn login(
320        public_info: UserPublicInfo,
321        password: &Password,
322    ) -> Result<Self> {
323        ensure_base_layout().context("Failed to ensure base layout")?;
324
325        if public_info.local_id == 0 {
326            return Err(anyhow!(
327                "UserPublicInfo.local_id was 0. Provide a valid user_id or use UserPublicInfo::get_by_name."
328            ));
329        }
330
331        let user_id = public_info.local_id;
332
333        // Acquire per-user lock for consistency while reading user metadata.
334        // Lock ordering: id lock first, DB lock only around DB calls.
335        let _user_lock = lock_by_id(user_id)?;
336
337        let (phc, kek_params, wrapped_dek, uuid) = {
338            let phc = get_user_password_by_id(user_id).ok_or_else(|| {
339                anyhow!("Auth entry for user_id {user_id} not found")
340            })?;
341
342            let kek_params = get_user_kek_params_by_id(user_id)
343                .and_then(|bytes| {
344                    serde_json::from_slice::<KekParams>(&bytes).ok()
345                })
346                .ok_or_else(|| {
347                    anyhow!("KEK params not found for user_id {user_id}")
348                })?;
349
350            let wrapped_dek =
351                get_user_wrapped_dek_by_id(user_id).ok_or_else(|| {
352                    anyhow!("Wrapped DEK not found for user_id {user_id}")
353                })?;
354
355            // Make sure we have UUID in public_info for AAD.
356            if public_info.uuid.is_empty() {
357                // If missing, try to reload public fields if needed in the future.
358            }
359
360            (phc, kek_params, wrapped_dek, public_info.uuid.clone())
361        };
362
363        if !(verify(password, &phc)?) {
364            return Err(anyhow!("Invalid password"));
365        }
366
367        // Derive KEK. If the auth module exposes a derive-with-params, it should be used here.
368        // Fallback to existing derive to preserve behavior.
369        let (kek, _ignored_params) = auth::derive_kek(password);
370
371        let dek = auth::unwrap_key(&kek, &wrapped_dek, &uuid)
372            .context("Failed to unwrap DEK")?;
373
374        let mut user = User::default();
375        user.public_info = public_info;
376        user.local_config = user.local_config();
377        user.dek = Some(Secret::new(dek));
378        // We intentionally do not hold the user lock beyond login.
379        Ok(user)
380    }
381
382    pub fn local_config(&self) -> UserLocalConfig {
383        let cache_dir = get_storage_dir().unwrap();
384        let user_cache_dir = std::path::Path::new(&cache_dir).join(self.name());
385        std::fs::create_dir_all(&user_cache_dir)
386            .expect("Failed to create per-user cache directory");
387        let config_path = user_cache_dir.join("local_config");
388        if !config_path.exists() {
389            let default_config = UserLocalConfig {
390                default_store_location: cache_dir.into_os_string(),
391            };
392            let json = serde_json::to_string_pretty(&default_config).unwrap();
393            std::fs::write(&config_path, json)
394                .expect("Failed to write default local_config");
395            return default_config;
396        }
397        let json = std::fs::read_to_string(&config_path)
398            .expect("Failed to read local_config");
399        serde_json::from_str(&json).expect("Failed to deserialize local_config")
400    }
401
402    pub fn create_graph(&mut self, label: &str) -> Result<&Graph> {
403        let new_graph_id = if self.graphs.is_empty() {
404            1
405        } else {
406            self.graphs
407                .iter()
408                .map(|g| g.graph_id)
409                .max()
410                .ok_or_else(|| anyhow!("Failed to get max graph id"))?
411                + 1
412        };
413        let new_graph = Graph::new(new_graph_id, label, self);
414        self.graphs.push(new_graph);
415        self.graphs
416            .last()
417            .ok_or_else(|| anyhow!("Failed to get max graph id"))
418    }
419
420    pub fn get_graph_count(&self) -> usize {
421        self.graphs.len()
422    }
423
424    pub fn get_graph_by_id(&self, id: u32) -> Option<&Graph> {
425        self.graphs.iter().find(|g| g.graph_id == id)
426    }
427}
428
429pub fn lock_by_name(
430    name: &str,
431) -> Result<(ResourceLock, Option<ResourceLock>)> {
432    // Always acquire name lock first
433    let name_lock = ResourceLock::acquire(USER_NAME_LOCK, &name)?;
434    // Then, resolve id (if any)
435    let maybe_user = UserPublicInfo::get_by_name(name)?;
436    let mut id_lock = None;
437    if let Some(user) = maybe_user {
438        let id = user.local_id;
439        id_lock = Some(lock_by_id(id)?);
440        // Double-check mapping hasn't changed while acquiring id lock
441        let user_check = UserPublicInfo::get_by_name(name)?;
442        if let Some(user_check) = user_check {
443            if id != user_check.local_id {
444                log!(format!(
445                    "User ID changed during get_by_name for '{}': {} -> {}",
446                    name, id, user_check.local_id
447                ));
448                bail!("User ID changed during lock acquisition");
449            }
450        }
451    }
452    Ok((name_lock, id_lock))
453}
454
455pub fn lock_by_id(user_id: u64) -> Result<ResourceLock> {
456    let id_lock = ResourceLock::acquire(USER_LOCK, &user_id.to_string())?;
457    Ok(id_lock)
458}
459
460fn get_user_id_by_name(name: &str) -> Option<u64> {
461    get_user_id(name)
462}
463
464fn get_user_password_by_id(user_id: u64) -> Option<String> {
465    get_user_auth(user_id)
466}
467
468fn get_user_picture_by_id(user_id: u64) -> Option<Vec<u8>> {
469    get_user_pictures(user_id)
470}
471
472fn get_user_kek_params_by_id(user_id: u64) -> Option<Vec<u8>> {
473    get_user_kek_params(user_id)
474}
475
476fn get_user_wrapped_dek_by_id(user_id: u64) -> Option<Vec<u8>> {
477    get_user_wrapped_dek(user_id)
478}
479
480fn get_user_pubkey_by_id(user_id: u64) -> Option<Vec<u8>> {
481    get_user_pubkey(user_id)
482}
483
484pub fn get_all_user_ids() -> Result<Vec<u64>> {
485    ensure_base_layout().context("Failed to ensure base layout")?;
486
487    crate::storage::db::get_all_user_ids()
488}
489
490// --------- Internal helpers ----------
491
492fn ensure_base_layout() -> Result<()> {
493    let root = get_storage_dir().context("No storage dir")?;
494    let config_dir = root.join("config");
495    let users_dir = root.join("users");
496    let graphs_dir = root.join("graphs");
497    fs::create_dir_all(&config_dir)?;
498    fs::create_dir_all(&users_dir)?;
499    fs::create_dir_all(&graphs_dir)?;
500    Ok(())
501}
502
503/// Returns the next available user ID, guaranteed to be monotonically increasing and never reused.
504/// Uses a persistent file to store the last allocated user ID.
505fn next_user_id() -> Result<u64> {
506    use fs2::FileExt;
507    use std::fs::OpenOptions;
508    use std::io::{Read, Seek, Write};
509
510    let root = get_storage_dir().context("No storage dir")?;
511    let last_id_path = root.join("users").join("last_user_id");
512
513    // Open the file (create if not exists) for read/write. Do NOT truncate.
514    let mut file = OpenOptions::new()
515        .read(true)
516        .write(true)
517        .truncate(false)
518        .create(true)
519        .open(&last_id_path)
520        .with_context(|| {
521            format!("Failed to open {}", last_id_path.display())
522        })?;
523
524    // Acquire exclusive lock for atomicity.
525    file.lock_exclusive().with_context(|| {
526        format!("Failed to lock {}", last_id_path.display())
527    })?;
528
529    // Seek to start and read the current content.
530    file.seek(std::io::SeekFrom::Start(0))?;
531    let mut content = String::new();
532    file.read_to_string(&mut content)?;
533    let last_id = if content.trim().is_empty() {
534        0
535    } else {
536        content.trim().parse::<u64>()?
537    };
538
539    // Increment and handle overflow.
540    let next_id = last_id
541        .checked_add(1)
542        .ok_or_else(|| anyhow!("User ID overflow"))?;
543
544    // Seek to start, truncate, and write the new value.
545    file.seek(std::io::SeekFrom::Start(0))?;
546    file.set_len(0)?; // Truncate the file.
547    file.write_all(next_id.to_string().as_bytes())
548        .with_context(|| {
549            format!("Failed to write {}", last_id_path.display())
550        })?;
551
552    // Unlock is automatic on drop.
553    Ok(next_id)
554}
555
556fn read_json_map<T: DeserializeOwned>(
557    path: &Path,
558) -> Result<HashMap<String, T>> {
559    if !path.exists() {
560        return Ok(HashMap::new());
561    }
562    let data = fs::read(path)?;
563    if data.is_empty() {
564        return Ok(HashMap::new());
565    }
566    let map =
567        serde_json::from_slice::<HashMap<String, T>>(&data).unwrap_or_default();
568    Ok(map)
569}
570
571fn write_json_map<T: Serialize>(
572    path: &Path,
573    map: &HashMap<String, T>,
574) -> Result<()> {
575    let tmp = serde_json::to_vec_pretty(map)?;
576    let tmp_path = path.with_extension("tmp");
577    fs::write(&tmp_path, tmp)?;
578    fs::rename(tmp_path, path)?;
579    Ok(())
580}
581
582#[cfg(test)]
583pub fn get_test_user(name: &str) -> User {
584    use crate::debug;
585
586    let _ = lock_by_name(name).expect("Could not lock name");
587    debug!(
588        "Thread {} acquired lock for test user '{}'",
589        std::thread::current().name().unwrap_or("unnamed"),
590        name
591    );
592    User::delete_by_name(name).ok();
593
594    assert!(
595        !get_user_id_by_name(name).is_some(),
596        "Failed to delete test user."
597    );
598    debug!(
599        "Thread {} sees that test user not exists '{}'",
600        std::thread::current().name().unwrap_or("unnamed"),
601        name
602    );
603
604    let password = get_test_password();
605    let user = User::create(name, &password).expect(
606        format!(
607            "User creation failed {}",
608            std::thread::current().name().unwrap_or("unnamed")
609        )
610        .as_str(),
611    );
612    debug!(
613        "Thread {} was able to create user '{}'",
614        std::thread::current().name().unwrap_or("unnamed"),
615        name
616    );
617
618    user
619}
620
621#[cfg(test)]
622fn get_test_password() -> Password {
623    Password::from_string(TEST_USER_PASS)
624}
625
626#[cfg(test)]
627#[allow(clippy::unwrap_in_result, clippy::panic_in_result_fn)]
628mod tests {
629    use super::*;
630    use crate::{bail_if_none, debug};
631
632    fn get_test_user(name: &str) -> User {
633        super::get_test_user(name)
634    }
635
636    #[crate::ctb_test]
637    fn test_user_name_and_local_id() {
638        let user = User {
639            public_info: UserPublicInfo {
640                local_id: 42,
641                name: "alice".to_string(),
642                ..Default::default()
643            },
644            ..Default::default()
645        };
646        assert_eq!(user.name(), "alice");
647        assert_eq!(user.local_id(), 42);
648    }
649
650    #[crate::ctb_test]
651    fn test_user_picture_none() {
652        let user = User {
653            public_info: UserPublicInfo {
654                picture: None,
655                ..Default::default()
656            },
657            ..Default::default()
658        };
659        assert!(user.user_picture().is_none());
660    }
661
662    #[crate::ctb_test]
663    fn test_user_picture_some() {
664        let pic = vec![1, 2, 3];
665        let user = User {
666            public_info: UserPublicInfo {
667                picture: Some(pic.clone()),
668                ..Default::default()
669            },
670            ..Default::default()
671        };
672        assert_eq!(user.user_picture(), Some(pic.as_slice()));
673    }
674
675    #[crate::ctb_test]
676    fn test_create_and_get_graph_by_id() -> Result<()> {
677        let mut user = User {
678            public_info: UserPublicInfo {
679                local_id: 1,
680                name: "test_graph".to_string(),
681                ..Default::default()
682            },
683            ..Default::default()
684        };
685        let label = "test_graph_label";
686        let graph = user.create_graph(label)?;
687        assert_eq!(graph.label, label);
688        let id = graph.graph_id;
689        let fetched = bail_if_none!(user.get_graph_by_id(id));
690        assert_eq!(fetched.label, label);
691        Ok(())
692    }
693
694    #[crate::ctb_test]
695    fn test_get_graph_by_id_none() {
696        let user = User::default();
697        assert!(user.get_graph_by_id(12345).is_none());
698    }
699
700    #[crate::ctb_test]
701    fn test_get_all_user_ids() -> Result<()> {
702        let user = get_test_user(format!("{}_1", function_name!()).as_str());
703        let user2 = get_test_user(format!("{}_2", function_name!()).as_str());
704        let ids = get_all_user_ids()?;
705        assert!(ids.contains(&user.local_id()));
706        assert!(ids.contains(&user2.local_id()));
707        user.delete()?;
708        user2.delete()?;
709        Ok(())
710    }
711
712    #[crate::ctb_test]
713    fn test_create_and_login_user() -> Result<()> {
714        let name = function_name!();
715        User::delete_by_name(name).ok(); // .ok() swallows error
716
717        let password = Password::from_string(TEST_USER_PASS);
718        let user = super::get_test_user(name);
719        assert_eq!(user.name(), name);
720
721        let public_info = bail_if_none!(UserPublicInfo::get_by_name(name)?);
722        let logged_in = User::login(public_info, &password)?;
723        assert_eq!(logged_in.name(), name);
724
725        // Hold the lock until cleanup is done
726        logged_in.delete()?;
727        Ok(())
728    }
729
730    #[crate::ctb_test]
731    fn test_create_and_delete_user() -> Result<()> {
732        let user = get_test_user(function_name!());
733        user.delete()?;
734        assert!(get_user_id_by_name(&user.name()).is_none());
735        debug!(get_user_password_by_id(user.local_id()));
736        assert!(get_user_password_by_id(user.local_id()).is_none());
737        assert!(get_user_picture_by_id(user.local_id()).is_none());
738        assert!(get_user_kek_params_by_id(user.local_id()).is_none());
739        assert!(get_user_wrapped_dek_by_id(user.local_id()).is_none());
740        assert!(get_user_pubkey_by_id(user.local_id()).is_none());
741        assert!(!fs::exists(
742            get_storage_dir()?
743                .join("graphs")
744                .join(user.local_id().to_string())
745        )?);
746        Ok(())
747    }
748
749    #[crate::ctb_test]
750    fn test_create_duplicate_user_fails() -> Result<()> {
751        let name = function_name!();
752        let _ = lock_by_name(name).expect("Could not lock name");
753        let user = get_test_user(name);
754        drop(user);
755        let res = User::create(name, &get_test_password());
756        assert!(res.is_err());
757        get_test_user(name).delete()?;
758        Ok(())
759    }
760
761    #[crate::ctb_test]
762    fn test_login_invalid_password_fails() -> Result<()> {
763        let user = get_test_user(function_name!());
764
765        // Clone the picture if it exists, to avoid holding a reference after
766        // user is dropped. Not sure if there's an easier way to do this.
767        let picture = user.user_picture();
768        let new_picture: Option<Vec<u8>> = if picture.is_none() {
769            None
770        } else {
771            Some(
772                user.user_picture()
773                    .ok_or(anyhow::anyhow!("Failed to get user picture"))?
774                    .to_vec(),
775            )
776        };
777
778        let new_public_info = UserPublicInfo {
779            local_id: user.local_id(),
780            name: user.name().clone(),
781            display_name: user.display_name().clone().map(|v| v.to_vec()),
782            uuid: user.uuid().clone(),
783            picture: new_picture,
784            lock: None,
785        };
786
787        // Drop the original user (and lock) before attempting login
788        drop(user);
789
790        let res = User::login(
791            new_public_info,
792            &Password::from_string("wrong_password"),
793        );
794        assert!(res.is_err());
795
796        Ok(())
797    }
798
799    #[crate::ctb_test]
800    fn test_local_config_roundtrip() -> Result<()> {
801        let user = get_test_user(function_name!());
802
803        let config = user.local_config();
804        assert_eq!(
805            config.default_store_location,
806            get_storage_dir()?.into_os_string()
807        );
808        user.delete()?;
809        Ok(())
810    }
811}