Skip to main content

klieo_auth_common/
identity.rs

1//! Verified caller identity returned by an
2//! [`Authenticator`](crate::Authenticator).
3
4use std::collections::HashSet;
5
6/// Verified caller identity returned by an
7/// [`Authenticator`](crate::Authenticator).
8///
9/// Wraps an opaque principal string (typically a JWT subject claim,
10/// service-account name, or peer agent id depending on the authenticator)
11/// plus an optional set of authorisation scopes lifted from the credential
12/// (e.g. JWT `scope` / `scp` claim). Handler authors can pattern-match on
13/// [`Identity::as_str`] or [`Identity::has_scope`] to make per-method
14/// authorisation decisions; scope-gating authenticators (e.g.
15/// `klieo_auth_oauth::OAuthAuthenticator`) consult [`Identity::scopes`]
16/// inside their `authorize_method` impl.
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18#[non_exhaustive]
19pub struct Identity {
20    principal: String,
21    // BTreeSet would give Hash for free, but the working set is small (≤ a
22    // handful of scopes) so we keep HashSet for O(1) `has_scope` and pay the
23    // ordering cost in the Hash impl below.
24    scopes: ScopeSet,
25}
26
27/// Wrapper that gives `HashSet<String>` a deterministic `Hash` impl so the
28/// outer `Identity` can derive `Hash` without losing membership semantics.
29#[derive(Debug, Clone, Default, PartialEq, Eq)]
30#[non_exhaustive]
31pub struct ScopeSet(HashSet<String>);
32
33impl std::hash::Hash for ScopeSet {
34    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
35        let mut sorted: Vec<&String> = self.0.iter().collect();
36        sorted.sort();
37        for scope in sorted {
38            scope.hash(state);
39        }
40    }
41}
42
43impl Identity {
44    /// Wrap a verified principal with no scopes.
45    pub fn new(value: impl Into<String>) -> Self {
46        Self {
47            principal: value.into(),
48            scopes: ScopeSet::default(),
49        }
50    }
51
52    /// Wrap a verified principal together with the scopes lifted from
53    /// the credential. Scope-gating authenticators populate this so
54    /// [`Authenticator::authorize_method`](crate::Authenticator::authorize_method)
55    /// can consult the set without re-decoding the token.
56    pub fn with_scopes(value: impl Into<String>, scopes: HashSet<String>) -> Self {
57        Self {
58            principal: value.into(),
59            scopes: ScopeSet(scopes),
60        }
61    }
62
63    /// Sentinel identity returned by
64    /// [`AllowAnonymous`](crate::AllowAnonymous). Handlers can detect this
65    /// and refuse to act for mutating methods.
66    pub fn anonymous() -> Self {
67        Self {
68            principal: "anonymous".into(),
69            scopes: ScopeSet::default(),
70        }
71    }
72
73    /// Borrow the underlying principal string.
74    pub fn as_str(&self) -> &str {
75        &self.principal
76    }
77
78    /// Returns `true` iff this identity was produced by
79    /// [`AllowAnonymous`](crate::AllowAnonymous).
80    pub fn is_anonymous(&self) -> bool {
81        self.principal == "anonymous"
82    }
83
84    /// Borrow the credential scopes attached to this identity. Empty for
85    /// identities built via [`Identity::new`] / [`Identity::anonymous`].
86    pub fn scopes(&self) -> &HashSet<String> {
87        &self.scopes.0
88    }
89
90    /// Returns `true` iff `scope` is present in the credential's scope set.
91    pub fn has_scope(&self, scope: &str) -> bool {
92        self.scopes.0.contains(scope)
93    }
94}