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}