klieo-auth-common 2.1.0

Shared authentication traits and types for klieo HTTP transports
Documentation
//! Verified caller identity returned by an
//! [`Authenticator`](crate::Authenticator).

use std::collections::HashSet;

/// Verified caller identity returned by an
/// [`Authenticator`](crate::Authenticator).
///
/// Wraps an opaque principal string (typically a JWT subject claim,
/// service-account name, or peer agent id depending on the authenticator)
/// plus an optional set of authorisation scopes lifted from the credential
/// (e.g. JWT `scope` / `scp` claim). Handler authors can pattern-match on
/// [`Identity::as_str`] or [`Identity::has_scope`] to make per-method
/// authorisation decisions; scope-gating authenticators (e.g.
/// `klieo_auth_oauth::OAuthAuthenticator`) consult [`Identity::scopes`]
/// inside their `authorize_method` impl.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Identity {
    principal: String,
    // BTreeSet would give Hash for free, but the working set is small (≤ a
    // handful of scopes) so we keep HashSet for O(1) `has_scope` and pay the
    // ordering cost in the Hash impl below.
    scopes: ScopeSet,
}

/// Wrapper that gives `HashSet<String>` a deterministic `Hash` impl so the
/// outer `Identity` can derive `Hash` without losing membership semantics.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScopeSet(HashSet<String>);

impl std::hash::Hash for ScopeSet {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        let mut sorted: Vec<&String> = self.0.iter().collect();
        sorted.sort();
        for scope in sorted {
            scope.hash(state);
        }
    }
}

impl Identity {
    /// Wrap a verified principal with no scopes.
    pub fn new(value: impl Into<String>) -> Self {
        Self {
            principal: value.into(),
            scopes: ScopeSet::default(),
        }
    }

    /// Wrap a verified principal together with the scopes lifted from
    /// the credential. Scope-gating authenticators populate this so
    /// [`Authenticator::authorize_method`](crate::Authenticator::authorize_method)
    /// can consult the set without re-decoding the token.
    pub fn with_scopes(value: impl Into<String>, scopes: HashSet<String>) -> Self {
        Self {
            principal: value.into(),
            scopes: ScopeSet(scopes),
        }
    }

    /// Sentinel identity returned by
    /// [`AllowAnonymous`](crate::AllowAnonymous). Handlers can detect this
    /// and refuse to act for mutating methods.
    pub fn anonymous() -> Self {
        Self {
            principal: "anonymous".into(),
            scopes: ScopeSet::default(),
        }
    }

    /// Borrow the underlying principal string.
    pub fn as_str(&self) -> &str {
        &self.principal
    }

    /// Returns `true` iff this identity was produced by
    /// [`AllowAnonymous`](crate::AllowAnonymous).
    pub fn is_anonymous(&self) -> bool {
        self.principal == "anonymous"
    }

    /// Borrow the credential scopes attached to this identity. Empty for
    /// identities built via [`Identity::new`] / [`Identity::anonymous`].
    pub fn scopes(&self) -> &HashSet<String> {
        &self.scopes.0
    }

    /// Returns `true` iff `scope` is present in the credential's scope set.
    pub fn has_scope(&self, scope: &str) -> bool {
        self.scopes.0.contains(scope)
    }
}