Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-identity-directory/src/model.rs

//! **The directory's rows and the store seam** (A34 M-6): what a directory
//! user and group are, and the one trait a product implements over its own
//! storage. Every call is scoped to ONE account: the product builds a store
//! for the account a SCIM bearer or an admin names, with the room, the actor
//! and the clock of that call inside it.

/// A directory user as the shared code sees it.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct User {
    /// The employee's principal id (the SCIM `id`).
    pub principal_id: String,
    /// The normalized user name ([`crate::names::normalize_user_name`]).
    pub user_name: String,
    pub display_name: String,
    pub email: String,
    /// The IdP's own id for the person (SCIM `externalId`).
    pub external_id: String,
    pub active: bool,
    pub created_at_unix_ms: i64,
    /// Removed from the directory (SCIM DELETE): kept gated, never listed.
    pub deleted: bool,
}

/// Where a directory user came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Source {
    Scim,
    Csv,
}

impl Source {
    pub fn as_str(self) -> &'static str {
        match self {
            Source::Scim => "scim",
            Source::Csv => "csv",
        }
    }
}

/// A NEW employee from the directory (a SCIM create, a CSV row).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewUser {
    pub user_name: String,
    pub display_name: String,
    pub email: String,
    pub external_id: String,
    pub active: bool,
    pub source: Source,
}

/// A change to a directory user; `None` leaves a field.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UserPatch {
    pub user_name: Option<String>,
    pub display_name: Option<String>,
    pub email: Option<String>,
    pub external_id: Option<String>,
    pub active: Option<bool>,
}

/// A SCIM group, kept as the directory sent it; its members are the
/// account's own principal ids.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Group {
    pub id: String,
    pub display_name: String,
    pub external_id: String,
    pub members: Vec<String>,
    pub created_at_unix_ms: i64,
}

/// A refusal the store made, by kind (each is one SCIM status).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StoreError {
    /// 404.
    NotFound(String),
    /// 409 `uniqueness`: the user name is taken.
    AlreadyExists(String),
    /// 400 `invalidValue`.
    Invalid(String),
    /// 409: no room for a new employee, an id that cannot be had.
    Precondition(String),
    /// 403.
    Denied(String),
    /// 429.
    Exhausted(String),
    /// 500.
    Internal(String),
}

impl std::fmt::Display for StoreError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let (StoreError::NotFound(m)
        | StoreError::AlreadyExists(m)
        | StoreError::Invalid(m)
        | StoreError::Precondition(m)
        | StoreError::Denied(m)
        | StoreError::Exhausted(m)
        | StoreError::Internal(m)) = self;
        f.write_str(m)
    }
}

impl std::error::Error for StoreError {}

/// **One account's directory, over the product's storage.**
///
/// A deactivation (an update to `active: false`, a delete) must end the
/// person's sessions in the same write; the shared code names the principal
/// in its answer ([`crate::import::Imported::ended_sessions`], the SCIM
/// effect) so the console signs them out too (A34-9).
pub trait DirectoryStore {
    /// The account's directory, deleted rows included.
    fn users(&self) -> Result<Vec<User>, StoreError>;
    /// Provision a NEW employee; a user name the directory holds is
    /// [`StoreError::AlreadyExists`].
    fn provision(&self, user: &NewUser) -> Result<User, StoreError>;
    /// Change a user. Returns the row and whether `active` changed.
    fn update_user(&self, principal_id: &str, patch: &UserPatch)
        -> Result<(User, bool), StoreError>;
    /// Remove a user from the directory (deactivated, no longer listed).
    fn delete_user(&self, principal_id: &str) -> Result<User, StoreError>;
    fn groups(&self) -> Result<Vec<Group>, StoreError>;
    fn put_group(&self, group: Group) -> Result<Group, StoreError>;
    fn remove_group(&self, id: &str) -> Result<bool, StoreError>;
    /// A fresh, unguessable group id (the product owns the randomness).
    fn new_group_id(&self) -> String;
    /// The clock of this call.
    fn now_unix_ms(&self) -> i64;
    /// **Apply a whole import in ONE write** (A34-17): every create and
    /// update of the plan, or none of them. A dry run (`commit: false`) runs
    /// the same checks (room for every new employee, free ids, free names)
    /// and writes nothing.
    #[cfg(feature = "csv")]
    fn apply_import(
        &self,
        plan: &crate::import::ImportPlan,
        commit: bool,
    ) -> Result<(), StoreError>;
}