💾 Archived View for alchemi.dev › en › projects › kochab › files › src › user_management › user.rs captured on 2022-07-16 at 16:43:55.

View Raw

More Information

-=-=-=-=-=-=-

//! Several structs representing data about users
//!
//! This module contains any structs needed to store and retrieve data about users.  The
//! different varieties have different purposes and come from different places.
//!
//! [`User`] is the most common for of user struct, and typically comes from calling
//! [`Request::user()`](crate::types::Request::user()).  This is an enum with several
//! variants, and can be specialized into a [`NotSignedInUser`] or a [`RegisteredUser`] if
//! the user has presented a certificate.  These two subtypes have more specific
//! information, like the user's username and active certificate.
//!
//! [`RegisteredUser`] is particularly signifigant in that this is the struct used to modify
//! the data stored for almost all users.  This is accomplished through the
//! [`as_mut()`](RegisteredUser::as_mut) method.  Changes made this way must be persisted
//! using [`save()`](RegisteredUser::save()) or by dropping the user.
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sled::Transactional;

use crate::user_management::UserManager;
use crate::user_management::Result;

#[cfg(feature = "user_management_advanced")]
const ARGON2_CONFIG: argon2::Config = argon2::Config {
    ad: &[],
    hash_length: 32,
    lanes: 1,
    mem_cost: 4096,
    secret: &[],
    thread_mode: argon2::ThreadMode::Sequential,
    time_cost: 3,
    variant: argon2::Variant::Argon2id,
    version: argon2::Version::Version13,
};

#[cfg(feature = "user_management_advanced")]
lazy_static::lazy_static! {
    static ref RANDOM: ring::rand::SystemRandom = ring::rand::SystemRandom::new();
}

/// An struct corresponding to the data stored in the user tree
///
/// In order to generate a full user obj, you need to perform a lookup with a specific
/// certificate.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub (crate) struct PartialUser<UserData> {
    pub data: UserData,
    pub certificates: Vec<[u8; 32]>,
    #[cfg(feature = "user_management_advanced")]
    pub pass_hash: Option<(Vec<u8>, [u8; 32])>,
}

impl<UserData> PartialUser<UserData> {

    /// Write user data to the database
    ///
    /// This MUST be called if the user data is modified using the AsMut trait, or else
    /// changes will not be written to the database
    fn store(&self, tree: &sled::Tree, username: impl AsRef<[u8]>) -> Result<()>
    where
        UserData: Serialize
    {
        tree.insert(
            &username,
            bincode::serialize(&self)?,
        )?;
        Ok(())
    }
}

/// Any information about the connecting user
#[derive(Clone, Debug)]
pub enum User<UserData: Serialize + DeserializeOwned> {
    /// A user who is connected without using a client certificate.
    ///
    /// This could be a user who has an account but just isn't presenting a certificate at
    /// the minute, a user whose client does not support client certificates, or a user
    /// who has not yet created a certificate for the site
    Unauthenticated,

    /// A user who is connecting with a certificate that isn't connected to an account
    ///
    /// This is typically a new user who hasn't set up an account yet, or a user
    /// connecting with a new certificate that needs to be added to an existing account.
    NotSignedIn(NotSignedInUser),

    /// A user connecting with an identified account
    SignedIn(RegisteredUser<UserData>),
}

#[derive(Clone, Debug)]
/// Data about a user with a certificate not associated with an account
///
/// For more information about the user lifecycle and sign-in stages, see [`User`]
pub struct NotSignedInUser {
    pub (crate) certificate: [u8; 32],
    pub (crate) manager: UserManager,
}

impl NotSignedInUser {
    /// Register a new user with this certificate.
    ///
    /// This creates a new user & user data entry in the database with the given username.
    /// From now on, when this user logs in with this certificate, they will be
    /// automatically authenticated, and their user data automatically retrieved.
    ///
    /// # Errors
    /// The provided username must be unique, or else an error will be raised.
    ///
    /// Additional errors might occur if there is a problem writing to the database
    pub fn register<UserData: Serialize + DeserializeOwned + Default>(
        self,
        username: String,
    ) -> Result<RegisteredUser<UserData>> {
        if self.manager.users.contains_key(username.as_str())? {
            Err(super::UserManagerError::UsernameNotUnique)
        } else {
            info!("User {} registered!", username);

            let mut newser = RegisteredUser::new(
                username,
                Some(self.certificate),
                self.manager,
                PartialUser {
                    data: UserData::default(),
                    certificates: Vec::with_capacity(1),
                    #[cfg(feature = "user_management_advanced")]
                    pass_hash: None,
                },
            );

            // As a nice bonus, calling add_certificate with a user not yet in the
            // database creates the user and adds the certificate in a single transaction.
            // Because of this, we can delegate here ^^
            newser.add_certificate(self.certificate)?;

            Ok(newser)
        }
    }

    #[cfg(feature = "user_management_advanced")]
    /// Attach this certificate to an existing user
    ///
    /// Try to add this certificate to another user using a username and password.  If
    /// successful, the user this certificate is attached to will be able to automatically
    /// log in with either this certificate or any of the certificates they already had
    /// registered.
    ///
    /// This method can check the user's password to ensure that they match before
    /// registering.  If you want to skip this verification, perhaps because you've
    /// already verified that this user owns this account, then you can pass [`None`] as
    /// the password to skip the password check.
    ///
    /// This method returns the new RegisteredUser instance representing the now-attached
    /// user, or [`None`] if the username and password didn't match.
    ///
    /// Because this method both performs a bcrypt verification and a database access, it
    /// should be considered expensive.
    ///
    /// If you already have a [`RegisteredUser`] that you would like to attach a
    /// certificate to, consider using [`RegisteredUser::add_certificate()`]
    ///
    /// # Errors
    /// This will error if the user has yet to set a password.
    ///
    /// Additional errors might occur if an error occurs during database lookup and
    /// deserialization
    pub fn attach<UserData: Serialize + DeserializeOwned>(
        self,
        username: &str,
        password: Option<&[u8]>,
    ) -> Result<Option<RegisteredUser<UserData>>> {
        if let Some(mut user) = self.manager.lookup_user(username)? {
            // Perform password check, if caller wants
            if let Some(password) = password {
                if !user.check_password(password)? {
                    return Ok(None);
                }
            }

            info!("User {} attached certificate with fingerprint {:x?}", username, &self.certificate[..]);
            user.add_certificate(self.certificate)?;
            user.active_certificate = Some(self.certificate);
            Ok(Some(user))
        } else {
            Ok(None)
        }
    }
}

#[derive(Clone, Debug)]
/// Data about a logged in user
///
/// For more information about the user lifecycle and sign-in stages, see [`User`]
pub struct RegisteredUser<UserData: Serialize + DeserializeOwned> {
    username: String,
    active_certificate: Option<[u8; 32]>,
    manager: UserManager,
    inner: PartialUser<UserData>,
    /// Indicates that [`RegisteredUser::as_mut()`] has been called, but [`RegisteredUser::save()`] has not
    has_changed: bool,
}

impl<UserData: Serialize + DeserializeOwned> RegisteredUser<UserData> {

    /// Create a new user from parts
    pub (crate) fn new(
        username: String,
        active_certificate: Option<[u8; 32]>,
        manager: UserManager,
        inner: PartialUser<UserData>
    ) -> Self {
        Self {
            username,
            active_certificate,
            manager,
            inner,
            has_changed: false,
        }
    }

    /// Update the active certificate
    ///
    /// This is not to be confused with [`RegisteredUser::add_certificate`], which
    /// performs the database operations needed to register a new certificate to a user.
    /// This literally just marks the active certificate.
    pub (crate) fn with_cert(mut self, cert: [u8; 32]) -> Self {
        self.active_certificate = Some(cert);
        self
    }

    /// Get the fingerprint of the certificate that the user is currently using.
    ///
    /// If this user was retrieved by a [`UserManager::lookup_user()`], this will be
    /// [`None`].  In all other cases, this will be [`Some`].
    pub fn active_certificate(&self) -> Option<&[u8; 32]> {
        self.active_certificate.as_ref()
    }

    /// Produce a list of all certificate fingerprints registered to this account
    pub fn all_certificates(&self) -> &Vec<[u8; 32]> {
        &self.inner.certificates
    }

    /// Get the user's current username.
    ///
    /// NOTE: This is not guaranteed not to change.
    pub fn username(&self) -> &String {
        &self.username
    }

    #[cfg(feature = "user_management_advanced")]
    /// Check a password against the user's password hash
    ///
    /// # Errors
    /// An error is raised if the user has yet to set a password, or if the user's
    /// password hash is somehow malformed.
    pub fn check_password(
        &self,
        try_password: impl AsRef<[u8]>
    ) -> Result<bool> {
        if let Some((hash, salt)) = &self.inner.pass_hash {
            Ok(argon2::verify_raw(
                try_password.as_ref(),
                salt,
                hash.as_ref(),
                &ARGON2_CONFIG,
            )?)
        } else {
            Err(super::UserManagerError::PasswordNotSet)
        }
    }

    #[cfg(feature = "user_management_advanced")]
    /// Set's the password for this user
    ///
    /// By default, users have no password, meaning the cannot add any certificates beyond
    /// the one they created their account with.  However, by setting their password,
    /// users are able to add more devices to their account, and recover their account if
    /// it's lost.  Note that this will completely overwrite the users old password.
    ///
    /// Use [`RegisteredUser::check_password()`] and [`NotSignedInUser::attach()`] to check
    /// the password against another one, or to link a new certificate.
    ///
    /// Because this method uses a key derivation algorithm, this should be considered a
    /// very expensive operation.
    pub fn set_password(
        &mut self,
        password: impl AsRef<[u8]>,
    ) -> Result<()> {
        let salt: [u8; 32] = ring::rand::generate(&*RANDOM)
            .expect("Error generating random salt")
            .expose();
        self.inner.pass_hash = Some((
            argon2::hash_raw(
                password.as_ref(),
                salt.as_ref(),
                &ARGON2_CONFIG,
            )?,
            salt,
        ));
        self.has_changed = true;
        Ok(())
    }

    /// Write any updates to the user to the database.
    ///
    /// Updates caused by calling methods directly on the user do not need to be saved.
    /// This is only for changes made to the UserData.
    pub fn save(&mut self) -> Result<()>
    where
        UserData: Serialize
    {
        self.inner.store(&self.manager.users, &self.username)?;
        self.has_changed = false;
        Ok(())
    }

    /// Register a new certificate to this user
    ///
    /// This adds a new certificate to this user for use in logins.  This requires a
    /// couple database accesses, one in order to link the user to the certificate, and
    /// one in order to link the certificate to the user.
    ///
    /// If you have a [`NotSignedInUser`] and are looking for a way to link them to an
    /// existing user, consider [`NotSignedInUser::attach()`], which contains facilities for
    /// password checking and automatically performs the user lookup.
    pub fn add_certificate(&mut self, certificate: [u8; 32]) -> Result<()> {
        self.inner.certificates.push(certificate);

        let inner_serialized = bincode::serialize(&self.inner)?;

        (&self.manager.users, &self.manager.certificates)
            .transaction(|(tx_usr, tx_crt)| {
                tx_usr.insert(
                    self.username.as_str(),
                    inner_serialized.clone(),
                )?;
                tx_crt.insert(
                    &certificate,
                    self.username.as_bytes(),
                )?;
                Ok(())
            })?;

        Ok(())
    }

    #[cfg(feature = "user_management_advanced")]
    /// Check if the user has a password set
    ///
    /// Since authentication is done using client certificates, users aren't required to
    /// set a password up front.  In some cases, it may be useful to know if a user has or
    /// has not set a password yet.
    ///
    /// This returns `true` if the user has a password set, or `false` otherwise
    pub fn has_password(&self) -> bool {
        self.inner.pass_hash.is_some()
    }

    /// Get an immutable reference to the data associated with this user
    pub fn data(&self) -> &UserData {
        &self.inner.data
    }

    /// Get a mutable reference to the data associated with this user
    ///
    /// This automatically flags the user data as needing to be saved to the database,
    /// which automatically performs the action when this user falls out of scope.  If
    /// need be, you can push these changes to the database sooner by calling [`save()`]
    ///
    /// [`save()`]: Self::save()
    pub fn mut_data(&mut self) -> &mut UserData {
        self.has_changed = true;
        &mut self.inner.data
    }
}

impl<UserData: Serialize + DeserializeOwned> std::ops::Drop for RegisteredUser<UserData> {
    fn drop(&mut self) {
        if self.has_changed {
            if let Err(e) = self.save() {
                error!("Failed to save user data to database: {:?}", e);
            }
        }
    }
}

impl<UserData: Serialize + DeserializeOwned> AsRef<UserData> for RegisteredUser<UserData> {
    fn as_ref(&self) -> &UserData {
        self.data()
    }
}

impl<UserData: Serialize + DeserializeOwned> AsMut<UserData> for RegisteredUser<UserData> {
    fn as_mut(&mut self) -> &mut UserData {
        self.mut_data()
    }
}