gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **The SSO half of the storage seam.** A control plane with an identity
//! table answers [`crate::seam::ControlPlane::sso`] with one of these; one
//! without (a bare test plane) answers `None`, and every `Sso` verb is then
//! `UNIMPLEMENTED` rather than a guess.
//!
//! The rows cross as proto messages where one exists; the asks are plain
//! structs. Nothing here carries a secret except [`SsoPlane::client_secret`]'s
//! answer, which never leaves gunnar.

use gunnar_proto::{DirectoryUser, Principal, ScimToken, SsoProvider};

use crate::seam::ControlResult;

/// What happens to the stored OIDC client secret.
#[derive(Clone, PartialEq, Eq)]
pub enum SecretChange {
    Keep,
    Clear,
    Set(String),
}

impl std::fmt::Debug for SecretChange {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(match self {
            SecretChange::Keep => "Keep",
            SecretChange::Clear => "Clear",
            SecretChange::Set(_) => "Set(<redacted>)",
        })
    }
}

/// The room a NEW employee holds (computed by the service from the account's
/// entitlement and usage, as `Accounts.EnrollEmployee` does).
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Room {
    pub room_bytes: u64,
    pub cap: Option<u64>,
    pub stored: u64,
}

/// One verified SSO login, to commit.
#[derive(Clone)]
pub struct SsoLoginAsk {
    pub account: String,
    /// The OIDC issuer or the SAML IdP entity id.
    pub issuer: String,
    /// The IdP's subject (OIDC `sub`, the SAML NameID or mapped attribute).
    pub subject: String,
    /// The directory user name (normalized lowercase).
    pub user_name: String,
    pub display_name: String,
    pub email: String,
    /// The IdP asserted the e-mail is verified (OIDC `email_verified`).
    pub email_verified: bool,
    /// The IdP's stable ids for the person (`sub`/`oid`, the SAML NameID): a
    /// provisioned user whose `externalId` is one of them is the admin's
    /// binding (A34-2).
    pub idp_ids: Vec<String>,
    pub room: Room,
    /// The session key (one OpenSSH line) and its expiry; `None` when a
    /// second factor is owed first.
    pub session: Option<(String, i64)>,
    pub now_unix_ms: i64,
}

impl std::fmt::Debug for SsoLoginAsk {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SsoLoginAsk")
            .field("account", &self.account)
            .field("user_name", &self.user_name)
            .finish_non_exhaustive()
    }
}

/// A directory row as the seam carries it.
#[derive(Debug, Clone, PartialEq)]
pub struct DirUser {
    pub user: DirectoryUser,
    /// Removed from the directory (SCIM DELETE): kept gated, never listed.
    pub deleted: bool,
}

/// What a committed SSO login answers.
#[derive(Debug, Clone)]
pub struct SsoLoginOutcome {
    pub principal: Principal,
    pub user: DirUser,
    pub created: bool,
    pub claimed: bool,
    /// How a provisioned user was claimed (`external_id`, `verified_email`);
    /// empty when none was.
    pub claimed_by: String,
    /// The enrolled session key's fingerprint, when one was asked.
    pub fingerprint: Option<String>,
}

/// A NEW employee from the directory (SCIM create, a CSV row).
#[derive(Debug, Clone)]
pub struct ProvisionAsk {
    pub account: String,
    pub user_name: String,
    pub display_name: String,
    pub email: String,
    pub external_id: String,
    pub active: bool,
    /// `scim` or `csv`.
    pub source: &'static str,
    pub room: Room,
    pub actor: String,
    pub now_unix_ms: i64,
}

/// A change to a directory user; `None` leaves a field.
#[derive(Debug, Clone, Default)]
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 as the seam carries it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GroupRow {
    pub id: String,
    pub account: String,
    pub display_name: String,
    pub external_id: String,
    pub members: Vec<String>,
    pub created_at_unix_ms: i64,
}

/// **The SSO storage seam.** Errors are the seam's: `NotFound`,
/// `AlreadyExists` (a user name taken — SCIM `uniqueness`), `PermissionDenied`
/// (a login the table refuses), `FailedPrecondition` (no room for a new
/// employee), `InvalidArgument`.
pub trait SsoPlane: Send + Sync {
    /// An account's provider, `has_client_secret` filled, no secret.
    fn provider(&self, account: &str) -> ControlResult<Option<SsoProvider>>;
    /// Store a provider (validated by the service) and change the secret.
    fn set_provider(
        &self,
        provider: SsoProvider,
        secret: SecretChange,
        actor: &str,
    ) -> ControlResult<SsoProvider>;
    fn remove_provider(&self, account: &str) -> ControlResult<bool>;
    /// **Whether any account has a provider configured** — an existence bit
    /// for the login page's company field (`DiscoverResponse.any_provider`).
    fn any_provider(&self) -> ControlResult<bool>;
    fn account_for_domain(&self, domain: &str) -> ControlResult<Option<String>>;
    /// The unsealed OIDC client secret, for the token request only.
    fn client_secret(&self, account: &str) -> ControlResult<Option<String>>;
    fn login(&self, ask: SsoLoginAsk) -> ControlResult<SsoLoginOutcome>;
    /// The account's directory, deleted rows included.
    fn directory(&self, account: &str) -> ControlResult<Vec<DirUser>>;
    fn provision(&self, ask: ProvisionAsk) -> ControlResult<DirUser>;
    /// Returns the row and whether `active` changed.
    fn update_user(
        &self,
        account: &str,
        principal_id: &str,
        patch: UserPatch,
        actor: &str,
    ) -> ControlResult<(DirUser, bool)>;
    fn delete_user(&self, account: &str, principal_id: &str, actor: &str)
        -> ControlResult<DirUser>;
    fn create_scim_token(
        &self,
        account: &str,
        id: &str,
        label: &str,
        secret_hash: &str,
        created_by: &str,
    ) -> ControlResult<ScimToken>;
    fn scim_tokens(&self, account: &str) -> ControlResult<Vec<ScimToken>>;
    fn revoke_scim_token(&self, account: &str, id: &str) -> ControlResult<bool>;
    /// The account a bearer's hash provisions (and its use noted).
    fn scim_account(&self, secret_hash: &str) -> ControlResult<Option<String>>;
    fn groups(&self, account: &str) -> ControlResult<Vec<GroupRow>>;
    fn put_group(&self, group: GroupRow) -> ControlResult<GroupRow>;
    fn remove_group(&self, account: &str, id: &str) -> ControlResult<bool>;
    /// **`require_sso`**: the sentence a passkey-only login of this principal
    /// is refused with, or `None` (not an employee, or its account does not
    /// require SSO). The account admin is never an employee.
    fn passkey_login_refused(&self, principal_id: &str) -> ControlResult<Option<String>>;
    /// The principal's passkeys (ids), for a second factor's
    /// `allowCredentials`.
    fn passkeys_of(&self, principal_id: &str) -> ControlResult<Vec<Vec<u8>>>;
}