Skip to main content

corium_protocol/
authz.rs

1//! Spike: request-scoped identity and authorization for the network surfaces.
2//!
3//! The shipped [`auth`](crate::auth) module authenticates a *connection*: its
4//! [`Authenticator`](crate::auth::Authenticator) returns a bare `bool`, so a
5//! request either passes the bearer-token gate or it does not, and no identity
6//! survives into the handler. That is enough for a single trusted operator, but
7//! it cannot express three things this spike explores:
8//!
9//! 1. **Optional, layered enforcement.** A surface may run wide open, require
10//!    only authentication, or require authentication *and* per-operation
11//!    authorization — chosen at deploy time without recompiling.
12//! 2. **External identity providers on a shared, public system.** A transactor
13//!    or peer server exposed to many tenants must accept tokens minted by
14//!    someone else (an OIDC issuer, an mTLS CA) and turn them into a local
15//!    [`Principal`], not compare them to a single static secret.
16//! 3. **Many users, different views, one server.** Because identity is bound to
17//!    the *request* (not the connection), one hosted peer can answer two
18//!    callers concurrently and give each a different authorization decision and
19//!    a different [`ViewFilter`] over the same database.
20//!
21//! The model is deliberately transport-adjacent: [`Guard`] bundles a chosen
22//! ([`IdentityProvider`], [`Authorizer`]) pair, and [`IdentityInterceptor`] is
23//! the only tonic-facing piece. A deployment that wants none of this constructs
24//! [`Guard::disabled`] and behaves exactly as today.
25//!
26//! Authn is **synchronous** and authz is **asynchronous**, on purpose:
27//! authenticating a request is local verification (a token compare, a signature
28//! check against cached keys, an mTLS subject) and it runs inside the
29//! synchronous tonic [`Interceptor`]; authorizing one may consult an external
30//! policy oracle (`OpenFGA` / `Auth0 FGA` answer a per-decision network `Check`), and
31//! it runs handler-side where awaiting is free. A provider that genuinely needs
32//! I/O (OIDC token introspection) caches results and refreshes out of band, so
33//! [`IdentityProvider::authenticate`] stays a cheap synchronous lookup.
34//!
35//! See `docs/design/auth.md` for how this maps onto the RPC surface and the
36//! migration away from the bool authenticator.
37
38use std::collections::{BTreeMap, BTreeSet};
39use std::fmt;
40use std::sync::Arc;
41
42use tonic::metadata::MetadataMap;
43use tonic::service::Interceptor;
44use tonic::{Request, Status};
45
46/// An authenticated (or anonymous) caller, carried in request extensions.
47///
48/// A principal is what an [`IdentityProvider`] produces from credentials and
49/// what an [`Authorizer`] consumes. It is intentionally a flat bag of strings
50/// so that identities from different providers (static tokens, OIDC claims,
51/// mTLS subjects) share one shape; richer typing can come later without
52/// changing the seam.
53#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct Principal {
55    /// Stable subject identifier, unique within `provider` (e.g. an OIDC
56    /// `sub`, a certificate SAN, or a static-token label).
57    pub subject: String,
58    /// Name of the [`IdentityProvider`] that vouched for this principal.
59    pub provider: String,
60    /// Roles used by role-based authorizers.
61    pub roles: BTreeSet<String>,
62    /// Arbitrary provider claims (e.g. `tenant`, `email`, `scope`).
63    pub claims: BTreeMap<String, String>,
64}
65
66impl Principal {
67    /// The unauthenticated caller. Present whenever a surface does not require
68    /// authentication, so handlers never deal with an absent identity.
69    #[must_use]
70    pub fn anonymous() -> Self {
71        Self {
72            subject: "anonymous".to_owned(),
73            provider: "anonymous".to_owned(),
74            roles: BTreeSet::new(),
75            claims: BTreeMap::new(),
76        }
77    }
78
79    /// Builds a principal with `subject` vouched for by `provider`.
80    #[must_use]
81    pub fn new(provider: impl Into<String>, subject: impl Into<String>) -> Self {
82        Self {
83            subject: subject.into(),
84            provider: provider.into(),
85            roles: BTreeSet::new(),
86            claims: BTreeMap::new(),
87        }
88    }
89
90    /// Adds a role (builder style).
91    #[must_use]
92    pub fn with_role(mut self, role: impl Into<String>) -> Self {
93        self.roles.insert(role.into());
94        self
95    }
96
97    /// Adds a claim (builder style).
98    #[must_use]
99    pub fn with_claim(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
100        self.claims.insert(key.into(), value.into());
101        self
102    }
103
104    /// Whether this is the anonymous principal.
105    #[must_use]
106    pub fn is_anonymous(&self) -> bool {
107        self.provider == "anonymous"
108    }
109
110    /// Whether the principal holds `role`.
111    #[must_use]
112    pub fn has_role(&self, role: &str) -> bool {
113        self.roles.contains(role)
114    }
115
116    /// Reads a claim by key.
117    #[must_use]
118    pub fn claim(&self, key: &str) -> Option<&str> {
119        self.claims.get(key).map(String::as_str)
120    }
121}
122
123/// Raw credentials extracted from a request, before authentication.
124///
125/// Only the bearer token is populated by [`Credentials::from_metadata`] today;
126/// `client_cert_subject` is the documented seam for mTLS, which tonic exposes
127/// on the request extensions inside the service (not in the interceptor), so a
128/// full wiring fills it in there. Keeping both here lets one provider consume
129/// either without a second extraction path.
130#[derive(Clone, Debug, Default)]
131pub struct Credentials {
132    /// Value after `Bearer ` in the `authorization` metadata, if any.
133    pub bearer: Option<String>,
134    /// Subject (SAN/CN) of a verified client certificate, if mTLS is in use.
135    pub client_cert_subject: Option<String>,
136}
137
138impl Credentials {
139    /// Extracts credentials from gRPC request metadata.
140    #[must_use]
141    pub fn from_metadata(metadata: &MetadataMap) -> Self {
142        let bearer = metadata
143            .get("authorization")
144            .and_then(|value| value.to_str().ok())
145            .and_then(|value| value.strip_prefix("Bearer "))
146            .map(str::to_owned);
147        Self {
148            bearer,
149            client_cert_subject: None,
150        }
151    }
152
153    /// Whether any credential material was presented.
154    #[must_use]
155    pub fn is_empty(&self) -> bool {
156        self.bearer.is_none() && self.client_cert_subject.is_none()
157    }
158}
159
160/// Failure of authentication or authorization.
161#[derive(Clone, Debug, thiserror::Error)]
162pub enum AuthError {
163    /// Credentials were required, missing, or invalid.
164    #[error("unauthenticated: {0}")]
165    Unauthenticated(String),
166    /// The principal is known but not permitted the requested access.
167    #[error("forbidden: {0}")]
168    Forbidden(String),
169}
170
171impl AuthError {
172    /// Maps onto the gRPC status a handler should return.
173    #[must_use]
174    pub fn to_status(&self) -> Status {
175        match self {
176            Self::Unauthenticated(message) => Status::unauthenticated(message.clone()),
177            Self::Forbidden(message) => Status::permission_denied(message.clone()),
178        }
179    }
180}
181
182/// Turns credentials into a [`Principal`], or declines.
183///
184/// The three-way return is what makes providers composable on a shared system:
185/// `Ok(Some(p))` accepts, `Ok(None)` *abstains* (these are not my credentials —
186/// let the next provider try), and `Err` *rejects* (these look like mine but are
187/// invalid — stop and fail). A provider that abstains on absent credentials lets
188/// an anonymous fallback take over when the surface allows it.
189pub trait IdentityProvider: Send + Sync + 'static {
190    /// A short name recorded in [`Principal::provider`] and logs.
191    fn name(&self) -> &str;
192
193    /// Authenticates `credentials`.
194    ///
195    /// # Errors
196    /// Returns [`AuthError::Unauthenticated`] when credentials are addressed to
197    /// this provider but fail verification.
198    fn authenticate(&self, credentials: &Credentials) -> Result<Option<Principal>, AuthError>;
199}
200
201/// Accepts everyone as [`Principal::anonymous`]. Used when a surface does not
202/// require authentication.
203#[derive(Clone, Debug, Default)]
204pub struct AllowAnonymous;
205
206impl IdentityProvider for AllowAnonymous {
207    #[allow(clippy::unnecessary_literal_bound)]
208    fn name(&self) -> &str {
209        "anonymous"
210    }
211
212    fn authenticate(&self, _credentials: &Credentials) -> Result<Option<Principal>, AuthError> {
213        Ok(Some(Principal::anonymous()))
214    }
215}
216
217/// Maps a fixed set of bearer tokens to fixed principals.
218///
219/// The successor to [`StaticToken`](crate::auth::StaticToken): instead of one
220/// shared secret gating the whole server, each token names a distinct principal,
221/// so a small multi-user deployment gets per-caller identity and authorization
222/// without an external issuer.
223#[derive(Clone, Debug, Default)]
224pub struct StaticTokens {
225    tokens: BTreeMap<String, Principal>,
226}
227
228impl StaticTokens {
229    /// An empty table; every request abstains.
230    #[must_use]
231    pub fn new() -> Self {
232        Self::default()
233    }
234
235    /// Registers `token` as `principal`.
236    #[must_use]
237    pub fn with(mut self, token: impl Into<String>, principal: Principal) -> Self {
238        self.tokens.insert(token.into(), principal);
239        self
240    }
241}
242
243impl IdentityProvider for StaticTokens {
244    #[allow(clippy::unnecessary_literal_bound)]
245    fn name(&self) -> &str {
246        "static-token"
247    }
248
249    fn authenticate(&self, credentials: &Credentials) -> Result<Option<Principal>, AuthError> {
250        // Abstain on an unknown token rather than reject: an opaque token this
251        // table does not hold may still be a valid OIDC/mTLS credential a later
252        // provider recognizes. The all-abstained outcome is the [`Guard`]'s call.
253        Ok(credentials
254            .bearer
255            .as_ref()
256            .and_then(|token| self.tokens.get(token).cloned()))
257    }
258}
259
260/// Verifies an opaque bearer token minted by an external issuer.
261///
262/// This is the seam for OIDC/JWT and similar: a real implementation validates a
263/// signature against a JWKS (or introspects the token with the issuer) and maps
264/// verified claims onto a [`Principal`]. The spike ships only the trait so the
265/// dependency choice and network calls stay out of this crate; a concrete
266/// verifier lives behind a feature flag in a later milestone.
267pub trait TokenVerifier: Send + Sync + 'static {
268    /// Verifies `token` and derives a principal from its claims.
269    ///
270    /// # Errors
271    /// Returns [`AuthError::Unauthenticated`] for a malformed, expired, or
272    /// untrusted token.
273    fn verify(&self, token: &str) -> Result<Principal, AuthError>;
274}
275
276/// Adapts a [`TokenVerifier`] into an [`IdentityProvider`].
277pub struct ExternalTokens<V: TokenVerifier> {
278    name: String,
279    verifier: V,
280}
281
282impl<V: TokenVerifier> ExternalTokens<V> {
283    /// Wraps `verifier`, labelling produced principals with `name`.
284    pub fn new(name: impl Into<String>, verifier: V) -> Self {
285        Self {
286            name: name.into(),
287            verifier,
288        }
289    }
290}
291
292impl<V: TokenVerifier> IdentityProvider for ExternalTokens<V> {
293    fn name(&self) -> &str {
294        &self.name
295    }
296
297    fn authenticate(&self, credentials: &Credentials) -> Result<Option<Principal>, AuthError> {
298        match &credentials.bearer {
299            None => Ok(None),
300            Some(token) => self.verifier.verify(token).map(Some),
301        }
302    }
303}
304
305/// Tries a list of providers in order, so one shared system can accept several
306/// issuers at once (static tokens for internal tools, OIDC for humans, mTLS for
307/// services). The first provider to accept wins; the first to reject stops the
308/// chain. If every provider abstains, the composite abstains too — whether that
309/// becomes anonymous access or a rejection is the [`Guard`]'s policy.
310#[derive(Clone)]
311pub struct CompositeProvider {
312    providers: Vec<Arc<dyn IdentityProvider>>,
313}
314
315impl CompositeProvider {
316    /// Builds a chain of providers, tried in order.
317    #[must_use]
318    pub fn new(providers: Vec<Arc<dyn IdentityProvider>>) -> Self {
319        Self { providers }
320    }
321}
322
323impl IdentityProvider for CompositeProvider {
324    #[allow(clippy::unnecessary_literal_bound)]
325    fn name(&self) -> &str {
326        "composite"
327    }
328
329    fn authenticate(&self, credentials: &Credentials) -> Result<Option<Principal>, AuthError> {
330        for provider in &self.providers {
331            if let Some(principal) = provider.authenticate(credentials)? {
332                return Ok(Some(principal));
333            }
334        }
335        Ok(None)
336    }
337}
338
339/// The operation a request wants to perform, for authorization.
340///
341/// One variant per public RPC family; the mapping from concrete RPCs is in
342/// `docs/design/auth.md`. [`Action::is_write`] and [`Action::is_admin`] let
343/// coarse policies grant by category instead of enumerating every action.
344#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
345pub enum Action {
346    /// Datalog query against a database view.
347    Query,
348    /// Pull expression against a database view.
349    Pull,
350    /// Raw datom access against a database view.
351    Datoms,
352    /// Transaction-log range read.
353    TxRange,
354    /// Live subscription to a database.
355    Subscribe,
356    /// Stats / status read.
357    Inspect,
358    /// Submit a transaction.
359    Transact,
360    /// Create a database.
361    CreateDatabase,
362    /// Delete a database.
363    DeleteDatabase,
364    /// Fork a database.
365    ForkDatabase,
366    /// List databases in the catalog.
367    ListDatabases,
368    /// Garbage-collect deleted databases or old segments.
369    GarbageCollect,
370    /// Request or reconfigure indexing.
371    ManageIndex,
372    /// Inspect or rotate a database's storage-encryption keys.
373    ManageKeys,
374}
375
376impl Action {
377    /// Whether the action mutates database state.
378    #[must_use]
379    pub fn is_write(self) -> bool {
380        matches!(self, Self::Transact)
381    }
382
383    /// Whether the action is a catalog/administration operation.
384    #[must_use]
385    pub fn is_admin(self) -> bool {
386        matches!(
387            self,
388            Self::CreateDatabase
389                | Self::DeleteDatabase
390                | Self::ForkDatabase
391                | Self::GarbageCollect
392                | Self::ManageIndex
393                | Self::ManageKeys
394        )
395    }
396}
397
398/// A specific access: an [`Action`] on an optional target database.
399#[derive(Clone, Debug, PartialEq, Eq)]
400pub struct Access {
401    /// The operation being attempted.
402    pub action: Action,
403    /// The target database, or `None` for catalog-wide operations.
404    pub database: Option<String>,
405}
406
407impl Access {
408    /// An action targeting `database`.
409    #[must_use]
410    pub fn on(action: Action, database: impl Into<String>) -> Self {
411        Self {
412            action,
413            database: Some(database.into()),
414        }
415    }
416
417    /// A catalog-wide action with no specific database.
418    #[must_use]
419    pub fn catalog(action: Action) -> Self {
420        Self {
421            action,
422            database: None,
423        }
424    }
425}
426
427/// Restricts what a principal may see within a database it is allowed to read.
428///
429/// This is the "different views, one server" seam. An [`Authorizer`] can return
430/// [`Decision::AllowFiltered`] with a filter, and the query/datom paths consult
431/// it before returning facts. The spike defines attribute-level visibility (the
432/// cheapest useful cut, enforceable in the peer's datom scan); entity- or
433/// value-predicate filtering is a documented extension of the same trait.
434pub trait ViewFilter: Send + Sync + fmt::Debug {
435    /// Whether datoms of `attribute` (a keyword ident like `:person/email`) are
436    /// visible to the principal.
437    fn attribute_visible(&self, attribute: &str) -> bool;
438}
439
440/// A [`ViewFilter`] that hides every attribute outside an allowlist.
441#[derive(Clone, Debug)]
442pub struct AttributeAllowlist {
443    allowed: BTreeSet<String>,
444}
445
446impl AttributeAllowlist {
447    /// Builds an allowlist from attribute idents.
448    pub fn new(attributes: impl IntoIterator<Item = impl Into<String>>) -> Self {
449        Self {
450            allowed: attributes.into_iter().map(Into::into).collect(),
451        }
452    }
453}
454
455impl ViewFilter for AttributeAllowlist {
456    fn attribute_visible(&self, attribute: &str) -> bool {
457        self.allowed.contains(attribute)
458    }
459}
460
461/// The outcome of an authorization check.
462#[derive(Clone)]
463pub enum Decision {
464    /// Permit the access with full visibility.
465    Allow,
466    /// Permit the access, but only through `filter`.
467    AllowFiltered(Arc<dyn ViewFilter>),
468    /// Refuse the access; the string explains why.
469    Deny(String),
470}
471
472impl fmt::Debug for Decision {
473    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
474        match self {
475            Self::Allow => formatter.write_str("Allow"),
476            Self::AllowFiltered(filter) => write!(formatter, "AllowFiltered({filter:?})"),
477            Self::Deny(reason) => write!(formatter, "Deny({reason:?})"),
478        }
479    }
480}
481
482/// Decides whether a [`Principal`] may perform an [`Access`].
483///
484/// This is **async**, unlike [`IdentityProvider`], because the interesting
485/// authorizers call out to an external policy oracle — a relationship-based
486/// service such as `OpenFGA` / `Auth0 FGA` answers a per-decision `Check(user,
487/// relation, object)` over the network, and modelling that as a blocking call
488/// would stall a runtime thread. Local authorizers ([`AllowAll`],
489/// [`PolicyAuthorizer`]) simply return without awaiting. Authorization also runs
490/// handler-side, inside the async RPC, so `.await` costs nothing structurally
491/// there — whereas authn runs in the synchronous tonic interceptor.
492#[tonic::async_trait]
493pub trait Authorizer: Send + Sync + 'static {
494    /// Renders a [`Decision`] for `principal` attempting `access`.
495    async fn authorize(&self, principal: &Principal, access: &Access) -> Decision;
496}
497
498/// Permits every access. Used when a surface requires authentication but not
499/// authorization (or none at all, paired with [`AllowAnonymous`]).
500#[derive(Clone, Debug, Default)]
501pub struct AllowAll;
502
503#[tonic::async_trait]
504impl Authorizer for AllowAll {
505    async fn authorize(&self, _principal: &Principal, _access: &Access) -> Decision {
506        Decision::Allow
507    }
508}
509
510/// A grant held by a role: which actions, over which databases, with what view.
511#[derive(Clone, Debug)]
512pub struct Grant {
513    /// Actions this grant permits. Empty means "any action".
514    pub actions: BTreeSet<ActionClass>,
515    /// Databases this grant covers. Empty means "any database".
516    pub databases: BTreeSet<String>,
517    /// Optional view restriction applied when the grant permits a read.
518    pub view: Option<Arc<dyn ViewFilter>>,
519}
520
521/// A coarse class of action, so grants need not list every [`Action`].
522#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
523pub enum ActionClass {
524    /// Any read (`Query`, `Pull`, `Datoms`, `TxRange`, `Subscribe`, `Inspect`).
525    Read,
526    /// Any write (`Transact`).
527    Write,
528    /// Any catalog/administration action.
529    Admin,
530}
531
532impl ActionClass {
533    /// The class an [`Action`] falls into.
534    #[must_use]
535    pub fn of(action: Action) -> Self {
536        if action.is_admin() {
537            Self::Admin
538        } else if action.is_write() {
539            Self::Write
540        } else {
541            Self::Read
542        }
543    }
544}
545
546impl Grant {
547    /// A grant permitting `actions` over `databases`.
548    #[must_use]
549    pub fn new(
550        actions: impl IntoIterator<Item = ActionClass>,
551        databases: impl IntoIterator<Item = impl Into<String>>,
552    ) -> Self {
553        Self {
554            actions: actions.into_iter().collect(),
555            databases: databases.into_iter().map(Into::into).collect(),
556            view: None,
557        }
558    }
559
560    /// Attaches a view restriction (builder style).
561    #[must_use]
562    pub fn with_view(mut self, view: Arc<dyn ViewFilter>) -> Self {
563        self.view = Some(view);
564        self
565    }
566
567    fn covers(&self, access: &Access) -> bool {
568        let action_ok =
569            self.actions.is_empty() || self.actions.contains(&ActionClass::of(access.action));
570        let database_ok = self.databases.is_empty()
571            || access
572                .database
573                .as_ref()
574                .is_some_and(|database| self.databases.contains(database));
575        action_ok && database_ok
576    }
577}
578
579/// A role-based authorizer: a principal's roles select [`Grant`]s, and the
580/// union of matching grants decides the access. Deny-by-default — an access no
581/// grant covers is refused.
582#[derive(Clone, Default)]
583pub struct PolicyAuthorizer {
584    roles: BTreeMap<String, Vec<Grant>>,
585}
586
587impl PolicyAuthorizer {
588    /// An empty policy that denies everything.
589    #[must_use]
590    pub fn new() -> Self {
591        Self::default()
592    }
593
594    /// Grants `grant` to holders of `role`.
595    #[must_use]
596    pub fn grant(mut self, role: impl Into<String>, grant: Grant) -> Self {
597        self.roles.entry(role.into()).or_default().push(grant);
598        self
599    }
600}
601
602#[tonic::async_trait]
603impl Authorizer for PolicyAuthorizer {
604    async fn authorize(&self, principal: &Principal, access: &Access) -> Decision {
605        let mut matched = false;
606        let mut view: Option<Arc<dyn ViewFilter>> = None;
607        for role in &principal.roles {
608            let Some(grants) = self.roles.get(role) else {
609                continue;
610            };
611            for grant in grants {
612                if grant.covers(access) {
613                    matched = true;
614                    // A read gets the narrowest requirement the matching grants
615                    // impose; the first view wins for the spike. An unfiltered
616                    // matching grant widens to full visibility.
617                    match &grant.view {
618                        Some(filter) if view.is_none() => view = Some(Arc::clone(filter)),
619                        Some(_) => {}
620                        None => return Decision::Allow,
621                    }
622                }
623            }
624        }
625        if !matched {
626            return Decision::Deny(format!(
627                "principal {:?} has no grant for {:?}",
628                principal.subject, access
629            ));
630        }
631        match view {
632            Some(filter) => Decision::AllowFiltered(filter),
633            None => Decision::Allow,
634        }
635    }
636}
637
638/// A chosen ([`IdentityProvider`], [`Authorizer`]) pair — the policy a surface
639/// enforces. One `Guard` serves every request on a surface; identity is derived
640/// per request, so a single guard handles many concurrent callers.
641///
642/// `allow_anonymous` decides what happens when no provider recognizes a
643/// request's credentials: `true` admits it as [`Principal::anonymous`] (auth is
644/// optional), `false` rejects it (auth is required). Authorization still runs on
645/// the resulting principal, so "optional authn + strict authz" is expressible —
646/// anonymous simply holds no roles.
647#[derive(Clone)]
648pub struct Guard {
649    identity: Arc<dyn IdentityProvider>,
650    authorizer: Arc<dyn Authorizer>,
651    allow_anonymous: bool,
652}
653
654impl Guard {
655    /// Builds a guard that *requires* authentication (`allow_anonymous = false`).
656    #[must_use]
657    pub fn new(identity: Arc<dyn IdentityProvider>, authorizer: Arc<dyn Authorizer>) -> Self {
658        Self {
659            identity,
660            authorizer,
661            allow_anonymous: false,
662        }
663    }
664
665    /// Sets whether unrecognized/absent credentials fall back to anonymous
666    /// (builder style).
667    #[must_use]
668    pub fn allow_anonymous(mut self, allow: bool) -> Self {
669        self.allow_anonymous = allow;
670        self
671    }
672
673    /// The wide-open policy: anonymous is accepted and everything is allowed.
674    /// Behaviourally identical to running with no auth.
675    #[must_use]
676    pub fn disabled() -> Self {
677        Self::new(Arc::new(AllowAnonymous), Arc::new(AllowAll)).allow_anonymous(true)
678    }
679
680    /// Whether this guard enforces nothing (anonymous provider + allow-all), so
681    /// callers can skip the identity plumbing on hot paths if they wish.
682    #[must_use]
683    pub fn is_disabled(&self) -> bool {
684        self.identity.name() == "anonymous"
685    }
686
687    /// Authenticates a request's credentials into a principal.
688    ///
689    /// # Errors
690    /// Propagates a provider's [`AuthError`], or returns
691    /// [`AuthError::Unauthenticated`] when no provider accepts and anonymous
692    /// access is not allowed.
693    pub fn authenticate(&self, credentials: &Credentials) -> Result<Principal, AuthError> {
694        match self.identity.authenticate(credentials)? {
695            Some(principal) => Ok(principal),
696            None if self.allow_anonymous => Ok(Principal::anonymous()),
697            None if credentials.is_empty() => Err(AuthError::Unauthenticated(
698                "authentication is required".to_owned(),
699            )),
700            None => Err(AuthError::Unauthenticated(
701                "no provider accepted the presented credentials".to_owned(),
702            )),
703        }
704    }
705
706    /// Authorizes `access` for `principal`, returning any view restriction.
707    ///
708    /// Async because [`Authorizer`] may consult an external policy oracle; local
709    /// authorizers resolve without awaiting.
710    ///
711    /// # Errors
712    /// Returns [`AuthError::Forbidden`] when the authorizer denies the access.
713    pub async fn authorize(
714        &self,
715        principal: &Principal,
716        access: &Access,
717    ) -> Result<Option<Arc<dyn ViewFilter>>, AuthError> {
718        match self.authorizer.authorize(principal, access).await {
719            Decision::Allow => Ok(None),
720            Decision::AllowFiltered(filter) => Ok(Some(filter)),
721            Decision::Deny(reason) => Err(AuthError::Forbidden(reason)),
722        }
723    }
724}
725
726/// tonic interceptor that authenticates each request and stashes the resulting
727/// [`Principal`] in the request extensions. Because it runs per request, two
728/// callers on the same connection get their own identities — the basis for
729/// multi-tenant serving. Handlers then call [`principal`] and [`Guard::authorize`]
730/// once they know the concrete [`Access`].
731#[derive(Clone)]
732pub struct IdentityInterceptor {
733    guard: Guard,
734}
735
736impl IdentityInterceptor {
737    /// Wraps `guard` for use with `InterceptedService`.
738    #[must_use]
739    pub fn new(guard: Guard) -> Self {
740        Self { guard }
741    }
742}
743
744impl Interceptor for IdentityInterceptor {
745    fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
746        let credentials = Credentials::from_metadata(request.metadata());
747        let principal = self
748            .guard
749            .authenticate(&credentials)
750            .map_err(|error| error.to_status())?;
751        request.extensions_mut().insert(principal);
752        Ok(request)
753    }
754}
755
756/// Reads the [`Principal`] an [`IdentityInterceptor`] attached to a request,
757/// defaulting to anonymous when no interceptor ran (e.g. embedded transport).
758#[must_use]
759pub fn principal<T>(request: &Request<T>) -> Principal {
760    request
761        .extensions()
762        .get::<Principal>()
763        .cloned()
764        .unwrap_or_else(Principal::anonymous)
765}
766
767#[cfg(test)]
768mod tests {
769    use super::*;
770
771    fn creds(bearer: &str) -> Credentials {
772        Credentials {
773            bearer: Some(bearer.to_owned()),
774            client_cert_subject: None,
775        }
776    }
777
778    #[tokio::test]
779    async fn disabled_guard_allows_anonymous_everything() {
780        let guard = Guard::disabled();
781        assert!(guard.is_disabled());
782        let principal = guard.authenticate(&Credentials::default()).unwrap();
783        assert!(principal.is_anonymous());
784        assert!(
785            guard
786                .authorize(&principal, &Access::on(Action::Transact, "people"))
787                .await
788                .unwrap()
789                .is_none()
790        );
791    }
792
793    #[test]
794    fn static_tokens_map_to_distinct_principals() {
795        let provider = StaticTokens::new()
796            .with(
797                "reader-secret",
798                Principal::new("static-token", "reader").with_role("reader"),
799            )
800            .with(
801                "writer-secret",
802                Principal::new("static-token", "writer").with_role("writer"),
803            );
804
805        let reader = provider.authenticate(&creds("reader-secret")).unwrap();
806        assert_eq!(reader.unwrap().subject, "reader");
807        let writer = provider.authenticate(&creds("writer-secret")).unwrap();
808        assert!(writer.unwrap().has_role("writer"));
809
810        // Absent and unknown tokens both abstain, so the table composes ahead
811        // of other issuers; the accept/reject call is the Guard's.
812        assert!(
813            provider
814                .authenticate(&Credentials::default())
815                .unwrap()
816                .is_none()
817        );
818        assert!(provider.authenticate(&creds("bogus")).unwrap().is_none());
819    }
820
821    struct FakeJwt;
822    impl TokenVerifier for FakeJwt {
823        fn verify(&self, token: &str) -> Result<Principal, AuthError> {
824            // Stand-in for signature+claim validation: "oidc:<sub>:<tenant>".
825            let mut parts = token.split(':');
826            match (parts.next(), parts.next(), parts.next()) {
827                (Some("oidc"), Some(sub), Some(tenant)) => Ok(Principal::new("oidc", sub)
828                    .with_role("reader")
829                    .with_claim("tenant", tenant)),
830                _ => Err(AuthError::Unauthenticated("bad token".to_owned())),
831            }
832        }
833    }
834
835    #[test]
836    fn external_token_verifier_seam_produces_principal() {
837        let provider = ExternalTokens::new("oidc", FakeJwt);
838        let principal = provider
839            .authenticate(&creds("oidc:alice:acme"))
840            .unwrap()
841            .unwrap();
842        assert_eq!(principal.provider, "oidc");
843        assert_eq!(principal.claim("tenant"), Some("acme"));
844        assert!(matches!(
845            provider.authenticate(&creds("garbage")),
846            Err(AuthError::Unauthenticated(_))
847        ));
848    }
849
850    #[test]
851    fn composite_provider_tries_in_order_and_abstains() {
852        let statics = StaticTokens::new().with(
853            "svc-secret",
854            Principal::new("static-token", "svc").with_role("writer"),
855        );
856        let provider = CompositeProvider::new(vec![
857            Arc::new(statics),
858            Arc::new(ExternalTokens::new("oidc", FakeJwt)),
859        ]);
860
861        // First provider accepts.
862        assert_eq!(
863            provider
864                .authenticate(&creds("svc-secret"))
865                .unwrap()
866                .unwrap()
867                .subject,
868            "svc"
869        );
870        // Static table abstains, so the request falls through to OIDC.
871        assert_eq!(
872            provider
873                .authenticate(&creds("oidc:bob:beta"))
874                .unwrap()
875                .unwrap()
876                .provider,
877            "oidc"
878        );
879        // Absent credentials: both abstain, composite abstains.
880        assert!(
881            provider
882                .authenticate(&Credentials::default())
883                .unwrap()
884                .is_none()
885        );
886
887        // A guard requiring auth turns the abstain into a rejection; the same
888        // provider under an anonymous-allowing guard yields anonymous.
889        let strict = Guard::new(Arc::new(provider.clone()), Arc::new(AllowAll));
890        assert!(matches!(
891            strict.authenticate(&Credentials::default()),
892            Err(AuthError::Unauthenticated(_))
893        ));
894        let open = Guard::new(Arc::new(provider), Arc::new(AllowAll)).allow_anonymous(true);
895        assert!(
896            open.authenticate(&Credentials::default())
897                .unwrap()
898                .is_anonymous()
899        );
900    }
901
902    fn sample_policy() -> PolicyAuthorizer {
903        PolicyAuthorizer::new()
904            .grant("reader", Grant::new([ActionClass::Read], ["people"]))
905            .grant(
906                "writer",
907                Grant::new([ActionClass::Read, ActionClass::Write], ["people"]),
908            )
909            .grant(
910                "admin",
911                Grant::new([ActionClass::Admin], Vec::<String>::new()),
912            )
913    }
914
915    #[tokio::test]
916    async fn policy_authorizer_enforces_actions_and_databases() {
917        let policy = sample_policy();
918        let reader = Principal::new("t", "r").with_role("reader");
919        let writer = Principal::new("t", "w").with_role("writer");
920        let admin = Principal::new("t", "a").with_role("admin");
921
922        // Reader may query people but not transact, and not touch other dbs.
923        assert!(matches!(
924            policy
925                .authorize(&reader, &Access::on(Action::Query, "people"))
926                .await,
927            Decision::Allow
928        ));
929        assert!(matches!(
930            policy
931                .authorize(&reader, &Access::on(Action::Transact, "people"))
932                .await,
933            Decision::Deny(_)
934        ));
935        assert!(matches!(
936            policy
937                .authorize(&reader, &Access::on(Action::Query, "secrets"))
938                .await,
939            Decision::Deny(_)
940        ));
941
942        // Writer may transact people; admin may create any database.
943        assert!(matches!(
944            policy
945                .authorize(&writer, &Access::on(Action::Transact, "people"))
946                .await,
947            Decision::Allow
948        ));
949        assert!(matches!(
950            policy
951                .authorize(&admin, &Access::catalog(Action::CreateDatabase))
952                .await,
953            Decision::Allow
954        ));
955        // Admin grant is admin-only: no read of people.
956        assert!(matches!(
957            policy
958                .authorize(&admin, &Access::on(Action::Query, "people"))
959                .await,
960            Decision::Deny(_)
961        ));
962    }
963
964    #[tokio::test]
965    async fn per_principal_view_filter_gives_different_views() {
966        // Two tenants read the same database through different attribute views.
967        let acme_view: Arc<dyn ViewFilter> = Arc::new(AttributeAllowlist::new([
968            ":person/name",
969            ":person/acme-note",
970        ]));
971        let beta_view: Arc<dyn ViewFilter> = Arc::new(AttributeAllowlist::new([
972            ":person/name",
973            ":person/beta-note",
974        ]));
975        let policy = PolicyAuthorizer::new()
976            .grant(
977                "acme",
978                Grant::new([ActionClass::Read], ["people"]).with_view(acme_view),
979            )
980            .grant(
981                "beta",
982                Grant::new([ActionClass::Read], ["people"]).with_view(beta_view),
983            );
984        let guard = Guard::new(Arc::new(AllowAnonymous), Arc::new(policy));
985
986        let acme = Principal::new("oidc", "alice").with_role("acme");
987        let beta = Principal::new("oidc", "bob").with_role("beta");
988        let acme_filter = guard
989            .authorize(&acme, &Access::on(Action::Query, "people"))
990            .await
991            .unwrap()
992            .expect("acme is filtered");
993        let beta_filter = guard
994            .authorize(&beta, &Access::on(Action::Query, "people"))
995            .await
996            .unwrap()
997            .expect("beta is filtered");
998
999        assert!(acme_filter.attribute_visible(":person/name"));
1000        assert!(acme_filter.attribute_visible(":person/acme-note"));
1001        assert!(!acme_filter.attribute_visible(":person/beta-note"));
1002
1003        assert!(beta_filter.attribute_visible(":person/beta-note"));
1004        assert!(!beta_filter.attribute_visible(":person/acme-note"));
1005    }
1006
1007    #[test]
1008    fn interceptor_attaches_principal_to_extensions() {
1009        let guard = Guard::new(
1010            Arc::new(StaticTokens::new().with(
1011                "reader-secret",
1012                Principal::new("static-token", "reader").with_role("reader"),
1013            )),
1014            Arc::new(AllowAll),
1015        );
1016        let mut interceptor = IdentityInterceptor::new(guard);
1017
1018        let mut request = Request::new(());
1019        request
1020            .metadata_mut()
1021            .insert("authorization", "Bearer reader-secret".parse().unwrap());
1022        let request = interceptor.call(request).unwrap();
1023        assert_eq!(principal(&request).subject, "reader");
1024
1025        // A bad token is rejected before reaching any handler.
1026        let mut bad = Request::new(());
1027        bad.metadata_mut()
1028            .insert("authorization", "Bearer nope".parse().unwrap());
1029        assert_eq!(
1030            interceptor.call(bad).unwrap_err().code(),
1031            tonic::Code::Unauthenticated
1032        );
1033    }
1034
1035    #[tokio::test]
1036    async fn one_guard_serves_two_tenants_with_different_authority() {
1037        // A single shared guard: static-token issuer + role policy, no anonymous.
1038        let provider = CompositeProvider::new(vec![Arc::new(
1039            StaticTokens::new()
1040                .with(
1041                    "acme-writer",
1042                    Principal::new("static-token", "acme-svc").with_role("acme"),
1043                )
1044                .with(
1045                    "beta-reader",
1046                    Principal::new("static-token", "beta-user").with_role("beta"),
1047                ),
1048        )]);
1049        let policy = PolicyAuthorizer::new()
1050            .grant(
1051                "acme",
1052                Grant::new([ActionClass::Read, ActionClass::Write], ["acme-db"]),
1053            )
1054            .grant("beta", Grant::new([ActionClass::Read], ["beta-db"]));
1055        let guard = Guard::new(Arc::new(provider), Arc::new(policy));
1056
1057        let acme = guard.authenticate(&creds("acme-writer")).unwrap();
1058        let beta = guard.authenticate(&creds("beta-reader")).unwrap();
1059
1060        // acme may write its own db; beta may not read acme's db.
1061        assert!(
1062            guard
1063                .authorize(&acme, &Access::on(Action::Transact, "acme-db"))
1064                .await
1065                .is_ok()
1066        );
1067        assert!(
1068            guard
1069                .authorize(&beta, &Access::on(Action::Query, "acme-db"))
1070                .await
1071                .is_err()
1072        );
1073        assert!(
1074            guard
1075                .authorize(&beta, &Access::on(Action::Query, "beta-db"))
1076                .await
1077                .is_ok()
1078        );
1079        // beta is read-only even on its own db.
1080        assert!(
1081            guard
1082                .authorize(&beta, &Access::on(Action::Transact, "beta-db"))
1083                .await
1084                .is_err()
1085        );
1086    }
1087
1088    /// Stand-in for an external relationship-based oracle (`OpenFGA` / `Auth0 FGA`):
1089    /// `authorize` awaits a "network" `Check` before deciding. Demonstrates that
1090    /// the async `Authorizer` seam is dyn-compatible and composes with `Guard`.
1091    struct FakeOracle {
1092        allowed: BTreeSet<(String, Action)>,
1093    }
1094
1095    #[tonic::async_trait]
1096    impl Authorizer for FakeOracle {
1097        async fn authorize(&self, principal: &Principal, access: &Access) -> Decision {
1098            // Simulate the round-trip to the policy service.
1099            tokio::task::yield_now().await;
1100            let key = (principal.subject.clone(), access.action);
1101            if self.allowed.contains(&key) {
1102                Decision::Allow
1103            } else {
1104                Decision::Deny("oracle check failed".to_owned())
1105            }
1106        }
1107    }
1108
1109    #[tokio::test]
1110    async fn external_async_oracle_authorizer() {
1111        let oracle = FakeOracle {
1112            allowed: [("alice".to_owned(), Action::Query)].into_iter().collect(),
1113        };
1114        let guard = Guard::new(Arc::new(AllowAnonymous), Arc::new(oracle));
1115        let alice = Principal::new("oidc", "alice");
1116        let bob = Principal::new("oidc", "bob");
1117
1118        assert!(
1119            guard
1120                .authorize(&alice, &Access::on(Action::Query, "people"))
1121                .await
1122                .is_ok()
1123        );
1124        assert!(
1125            guard
1126                .authorize(&alice, &Access::on(Action::Transact, "people"))
1127                .await
1128                .is_err()
1129        );
1130        assert!(
1131            guard
1132                .authorize(&bob, &Access::on(Action::Query, "people"))
1133                .await
1134                .is_err()
1135        );
1136    }
1137}