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}
373
374impl Action {
375    /// Whether the action mutates database state.
376    #[must_use]
377    pub fn is_write(self) -> bool {
378        matches!(self, Self::Transact)
379    }
380
381    /// Whether the action is a catalog/administration operation.
382    #[must_use]
383    pub fn is_admin(self) -> bool {
384        matches!(
385            self,
386            Self::CreateDatabase
387                | Self::DeleteDatabase
388                | Self::ForkDatabase
389                | Self::GarbageCollect
390                | Self::ManageIndex
391        )
392    }
393}
394
395/// A specific access: an [`Action`] on an optional target database.
396#[derive(Clone, Debug, PartialEq, Eq)]
397pub struct Access {
398    /// The operation being attempted.
399    pub action: Action,
400    /// The target database, or `None` for catalog-wide operations.
401    pub database: Option<String>,
402}
403
404impl Access {
405    /// An action targeting `database`.
406    #[must_use]
407    pub fn on(action: Action, database: impl Into<String>) -> Self {
408        Self {
409            action,
410            database: Some(database.into()),
411        }
412    }
413
414    /// A catalog-wide action with no specific database.
415    #[must_use]
416    pub fn catalog(action: Action) -> Self {
417        Self {
418            action,
419            database: None,
420        }
421    }
422}
423
424/// Restricts what a principal may see within a database it is allowed to read.
425///
426/// This is the "different views, one server" seam. An [`Authorizer`] can return
427/// [`Decision::AllowFiltered`] with a filter, and the query/datom paths consult
428/// it before returning facts. The spike defines attribute-level visibility (the
429/// cheapest useful cut, enforceable in the peer's datom scan); entity- or
430/// value-predicate filtering is a documented extension of the same trait.
431pub trait ViewFilter: Send + Sync + fmt::Debug {
432    /// Whether datoms of `attribute` (a keyword ident like `:person/email`) are
433    /// visible to the principal.
434    fn attribute_visible(&self, attribute: &str) -> bool;
435}
436
437/// A [`ViewFilter`] that hides every attribute outside an allowlist.
438#[derive(Clone, Debug)]
439pub struct AttributeAllowlist {
440    allowed: BTreeSet<String>,
441}
442
443impl AttributeAllowlist {
444    /// Builds an allowlist from attribute idents.
445    pub fn new(attributes: impl IntoIterator<Item = impl Into<String>>) -> Self {
446        Self {
447            allowed: attributes.into_iter().map(Into::into).collect(),
448        }
449    }
450}
451
452impl ViewFilter for AttributeAllowlist {
453    fn attribute_visible(&self, attribute: &str) -> bool {
454        self.allowed.contains(attribute)
455    }
456}
457
458/// The outcome of an authorization check.
459#[derive(Clone)]
460pub enum Decision {
461    /// Permit the access with full visibility.
462    Allow,
463    /// Permit the access, but only through `filter`.
464    AllowFiltered(Arc<dyn ViewFilter>),
465    /// Refuse the access; the string explains why.
466    Deny(String),
467}
468
469impl fmt::Debug for Decision {
470    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
471        match self {
472            Self::Allow => formatter.write_str("Allow"),
473            Self::AllowFiltered(filter) => write!(formatter, "AllowFiltered({filter:?})"),
474            Self::Deny(reason) => write!(formatter, "Deny({reason:?})"),
475        }
476    }
477}
478
479/// Decides whether a [`Principal`] may perform an [`Access`].
480///
481/// This is **async**, unlike [`IdentityProvider`], because the interesting
482/// authorizers call out to an external policy oracle — a relationship-based
483/// service such as `OpenFGA` / `Auth0 FGA` answers a per-decision `Check(user,
484/// relation, object)` over the network, and modelling that as a blocking call
485/// would stall a runtime thread. Local authorizers ([`AllowAll`],
486/// [`PolicyAuthorizer`]) simply return without awaiting. Authorization also runs
487/// handler-side, inside the async RPC, so `.await` costs nothing structurally
488/// there — whereas authn runs in the synchronous tonic interceptor.
489#[tonic::async_trait]
490pub trait Authorizer: Send + Sync + 'static {
491    /// Renders a [`Decision`] for `principal` attempting `access`.
492    async fn authorize(&self, principal: &Principal, access: &Access) -> Decision;
493}
494
495/// Permits every access. Used when a surface requires authentication but not
496/// authorization (or none at all, paired with [`AllowAnonymous`]).
497#[derive(Clone, Debug, Default)]
498pub struct AllowAll;
499
500#[tonic::async_trait]
501impl Authorizer for AllowAll {
502    async fn authorize(&self, _principal: &Principal, _access: &Access) -> Decision {
503        Decision::Allow
504    }
505}
506
507/// A grant held by a role: which actions, over which databases, with what view.
508#[derive(Clone, Debug)]
509pub struct Grant {
510    /// Actions this grant permits. Empty means "any action".
511    pub actions: BTreeSet<ActionClass>,
512    /// Databases this grant covers. Empty means "any database".
513    pub databases: BTreeSet<String>,
514    /// Optional view restriction applied when the grant permits a read.
515    pub view: Option<Arc<dyn ViewFilter>>,
516}
517
518/// A coarse class of action, so grants need not list every [`Action`].
519#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
520pub enum ActionClass {
521    /// Any read (`Query`, `Pull`, `Datoms`, `TxRange`, `Subscribe`, `Inspect`).
522    Read,
523    /// Any write (`Transact`).
524    Write,
525    /// Any catalog/administration action.
526    Admin,
527}
528
529impl ActionClass {
530    /// The class an [`Action`] falls into.
531    #[must_use]
532    pub fn of(action: Action) -> Self {
533        if action.is_admin() {
534            Self::Admin
535        } else if action.is_write() {
536            Self::Write
537        } else {
538            Self::Read
539        }
540    }
541}
542
543impl Grant {
544    /// A grant permitting `actions` over `databases`.
545    #[must_use]
546    pub fn new(
547        actions: impl IntoIterator<Item = ActionClass>,
548        databases: impl IntoIterator<Item = impl Into<String>>,
549    ) -> Self {
550        Self {
551            actions: actions.into_iter().collect(),
552            databases: databases.into_iter().map(Into::into).collect(),
553            view: None,
554        }
555    }
556
557    /// Attaches a view restriction (builder style).
558    #[must_use]
559    pub fn with_view(mut self, view: Arc<dyn ViewFilter>) -> Self {
560        self.view = Some(view);
561        self
562    }
563
564    fn covers(&self, access: &Access) -> bool {
565        let action_ok =
566            self.actions.is_empty() || self.actions.contains(&ActionClass::of(access.action));
567        let database_ok = self.databases.is_empty()
568            || access
569                .database
570                .as_ref()
571                .is_some_and(|database| self.databases.contains(database));
572        action_ok && database_ok
573    }
574}
575
576/// A role-based authorizer: a principal's roles select [`Grant`]s, and the
577/// union of matching grants decides the access. Deny-by-default — an access no
578/// grant covers is refused.
579#[derive(Clone, Default)]
580pub struct PolicyAuthorizer {
581    roles: BTreeMap<String, Vec<Grant>>,
582}
583
584impl PolicyAuthorizer {
585    /// An empty policy that denies everything.
586    #[must_use]
587    pub fn new() -> Self {
588        Self::default()
589    }
590
591    /// Grants `grant` to holders of `role`.
592    #[must_use]
593    pub fn grant(mut self, role: impl Into<String>, grant: Grant) -> Self {
594        self.roles.entry(role.into()).or_default().push(grant);
595        self
596    }
597}
598
599#[tonic::async_trait]
600impl Authorizer for PolicyAuthorizer {
601    async fn authorize(&self, principal: &Principal, access: &Access) -> Decision {
602        let mut matched = false;
603        let mut view: Option<Arc<dyn ViewFilter>> = None;
604        for role in &principal.roles {
605            let Some(grants) = self.roles.get(role) else {
606                continue;
607            };
608            for grant in grants {
609                if grant.covers(access) {
610                    matched = true;
611                    // A read gets the narrowest requirement the matching grants
612                    // impose; the first view wins for the spike. An unfiltered
613                    // matching grant widens to full visibility.
614                    match &grant.view {
615                        Some(filter) if view.is_none() => view = Some(Arc::clone(filter)),
616                        Some(_) => {}
617                        None => return Decision::Allow,
618                    }
619                }
620            }
621        }
622        if !matched {
623            return Decision::Deny(format!(
624                "principal {:?} has no grant for {:?}",
625                principal.subject, access
626            ));
627        }
628        match view {
629            Some(filter) => Decision::AllowFiltered(filter),
630            None => Decision::Allow,
631        }
632    }
633}
634
635/// A chosen ([`IdentityProvider`], [`Authorizer`]) pair — the policy a surface
636/// enforces. One `Guard` serves every request on a surface; identity is derived
637/// per request, so a single guard handles many concurrent callers.
638///
639/// `allow_anonymous` decides what happens when no provider recognizes a
640/// request's credentials: `true` admits it as [`Principal::anonymous`] (auth is
641/// optional), `false` rejects it (auth is required). Authorization still runs on
642/// the resulting principal, so "optional authn + strict authz" is expressible —
643/// anonymous simply holds no roles.
644#[derive(Clone)]
645pub struct Guard {
646    identity: Arc<dyn IdentityProvider>,
647    authorizer: Arc<dyn Authorizer>,
648    allow_anonymous: bool,
649}
650
651impl Guard {
652    /// Builds a guard that *requires* authentication (`allow_anonymous = false`).
653    #[must_use]
654    pub fn new(identity: Arc<dyn IdentityProvider>, authorizer: Arc<dyn Authorizer>) -> Self {
655        Self {
656            identity,
657            authorizer,
658            allow_anonymous: false,
659        }
660    }
661
662    /// Sets whether unrecognized/absent credentials fall back to anonymous
663    /// (builder style).
664    #[must_use]
665    pub fn allow_anonymous(mut self, allow: bool) -> Self {
666        self.allow_anonymous = allow;
667        self
668    }
669
670    /// The wide-open policy: anonymous is accepted and everything is allowed.
671    /// Behaviourally identical to running with no auth.
672    #[must_use]
673    pub fn disabled() -> Self {
674        Self::new(Arc::new(AllowAnonymous), Arc::new(AllowAll)).allow_anonymous(true)
675    }
676
677    /// Whether this guard enforces nothing (anonymous provider + allow-all), so
678    /// callers can skip the identity plumbing on hot paths if they wish.
679    #[must_use]
680    pub fn is_disabled(&self) -> bool {
681        self.identity.name() == "anonymous"
682    }
683
684    /// Authenticates a request's credentials into a principal.
685    ///
686    /// # Errors
687    /// Propagates a provider's [`AuthError`], or returns
688    /// [`AuthError::Unauthenticated`] when no provider accepts and anonymous
689    /// access is not allowed.
690    pub fn authenticate(&self, credentials: &Credentials) -> Result<Principal, AuthError> {
691        match self.identity.authenticate(credentials)? {
692            Some(principal) => Ok(principal),
693            None if self.allow_anonymous => Ok(Principal::anonymous()),
694            None if credentials.is_empty() => Err(AuthError::Unauthenticated(
695                "authentication is required".to_owned(),
696            )),
697            None => Err(AuthError::Unauthenticated(
698                "no provider accepted the presented credentials".to_owned(),
699            )),
700        }
701    }
702
703    /// Authorizes `access` for `principal`, returning any view restriction.
704    ///
705    /// Async because [`Authorizer`] may consult an external policy oracle; local
706    /// authorizers resolve without awaiting.
707    ///
708    /// # Errors
709    /// Returns [`AuthError::Forbidden`] when the authorizer denies the access.
710    pub async fn authorize(
711        &self,
712        principal: &Principal,
713        access: &Access,
714    ) -> Result<Option<Arc<dyn ViewFilter>>, AuthError> {
715        match self.authorizer.authorize(principal, access).await {
716            Decision::Allow => Ok(None),
717            Decision::AllowFiltered(filter) => Ok(Some(filter)),
718            Decision::Deny(reason) => Err(AuthError::Forbidden(reason)),
719        }
720    }
721}
722
723/// tonic interceptor that authenticates each request and stashes the resulting
724/// [`Principal`] in the request extensions. Because it runs per request, two
725/// callers on the same connection get their own identities — the basis for
726/// multi-tenant serving. Handlers then call [`principal`] and [`Guard::authorize`]
727/// once they know the concrete [`Access`].
728#[derive(Clone)]
729pub struct IdentityInterceptor {
730    guard: Guard,
731}
732
733impl IdentityInterceptor {
734    /// Wraps `guard` for use with `InterceptedService`.
735    #[must_use]
736    pub fn new(guard: Guard) -> Self {
737        Self { guard }
738    }
739}
740
741impl Interceptor for IdentityInterceptor {
742    fn call(&mut self, mut request: Request<()>) -> Result<Request<()>, Status> {
743        let credentials = Credentials::from_metadata(request.metadata());
744        let principal = self
745            .guard
746            .authenticate(&credentials)
747            .map_err(|error| error.to_status())?;
748        request.extensions_mut().insert(principal);
749        Ok(request)
750    }
751}
752
753/// Reads the [`Principal`] an [`IdentityInterceptor`] attached to a request,
754/// defaulting to anonymous when no interceptor ran (e.g. embedded transport).
755#[must_use]
756pub fn principal<T>(request: &Request<T>) -> Principal {
757    request
758        .extensions()
759        .get::<Principal>()
760        .cloned()
761        .unwrap_or_else(Principal::anonymous)
762}
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767
768    fn creds(bearer: &str) -> Credentials {
769        Credentials {
770            bearer: Some(bearer.to_owned()),
771            client_cert_subject: None,
772        }
773    }
774
775    #[tokio::test]
776    async fn disabled_guard_allows_anonymous_everything() {
777        let guard = Guard::disabled();
778        assert!(guard.is_disabled());
779        let principal = guard.authenticate(&Credentials::default()).unwrap();
780        assert!(principal.is_anonymous());
781        assert!(
782            guard
783                .authorize(&principal, &Access::on(Action::Transact, "people"))
784                .await
785                .unwrap()
786                .is_none()
787        );
788    }
789
790    #[test]
791    fn static_tokens_map_to_distinct_principals() {
792        let provider = StaticTokens::new()
793            .with(
794                "reader-secret",
795                Principal::new("static-token", "reader").with_role("reader"),
796            )
797            .with(
798                "writer-secret",
799                Principal::new("static-token", "writer").with_role("writer"),
800            );
801
802        let reader = provider.authenticate(&creds("reader-secret")).unwrap();
803        assert_eq!(reader.unwrap().subject, "reader");
804        let writer = provider.authenticate(&creds("writer-secret")).unwrap();
805        assert!(writer.unwrap().has_role("writer"));
806
807        // Absent and unknown tokens both abstain, so the table composes ahead
808        // of other issuers; the accept/reject call is the Guard's.
809        assert!(
810            provider
811                .authenticate(&Credentials::default())
812                .unwrap()
813                .is_none()
814        );
815        assert!(provider.authenticate(&creds("bogus")).unwrap().is_none());
816    }
817
818    struct FakeJwt;
819    impl TokenVerifier for FakeJwt {
820        fn verify(&self, token: &str) -> Result<Principal, AuthError> {
821            // Stand-in for signature+claim validation: "oidc:<sub>:<tenant>".
822            let mut parts = token.split(':');
823            match (parts.next(), parts.next(), parts.next()) {
824                (Some("oidc"), Some(sub), Some(tenant)) => Ok(Principal::new("oidc", sub)
825                    .with_role("reader")
826                    .with_claim("tenant", tenant)),
827                _ => Err(AuthError::Unauthenticated("bad token".to_owned())),
828            }
829        }
830    }
831
832    #[test]
833    fn external_token_verifier_seam_produces_principal() {
834        let provider = ExternalTokens::new("oidc", FakeJwt);
835        let principal = provider
836            .authenticate(&creds("oidc:alice:acme"))
837            .unwrap()
838            .unwrap();
839        assert_eq!(principal.provider, "oidc");
840        assert_eq!(principal.claim("tenant"), Some("acme"));
841        assert!(matches!(
842            provider.authenticate(&creds("garbage")),
843            Err(AuthError::Unauthenticated(_))
844        ));
845    }
846
847    #[test]
848    fn composite_provider_tries_in_order_and_abstains() {
849        let statics = StaticTokens::new().with(
850            "svc-secret",
851            Principal::new("static-token", "svc").with_role("writer"),
852        );
853        let provider = CompositeProvider::new(vec![
854            Arc::new(statics),
855            Arc::new(ExternalTokens::new("oidc", FakeJwt)),
856        ]);
857
858        // First provider accepts.
859        assert_eq!(
860            provider
861                .authenticate(&creds("svc-secret"))
862                .unwrap()
863                .unwrap()
864                .subject,
865            "svc"
866        );
867        // Static table abstains, so the request falls through to OIDC.
868        assert_eq!(
869            provider
870                .authenticate(&creds("oidc:bob:beta"))
871                .unwrap()
872                .unwrap()
873                .provider,
874            "oidc"
875        );
876        // Absent credentials: both abstain, composite abstains.
877        assert!(
878            provider
879                .authenticate(&Credentials::default())
880                .unwrap()
881                .is_none()
882        );
883
884        // A guard requiring auth turns the abstain into a rejection; the same
885        // provider under an anonymous-allowing guard yields anonymous.
886        let strict = Guard::new(Arc::new(provider.clone()), Arc::new(AllowAll));
887        assert!(matches!(
888            strict.authenticate(&Credentials::default()),
889            Err(AuthError::Unauthenticated(_))
890        ));
891        let open = Guard::new(Arc::new(provider), Arc::new(AllowAll)).allow_anonymous(true);
892        assert!(
893            open.authenticate(&Credentials::default())
894                .unwrap()
895                .is_anonymous()
896        );
897    }
898
899    fn sample_policy() -> PolicyAuthorizer {
900        PolicyAuthorizer::new()
901            .grant("reader", Grant::new([ActionClass::Read], ["people"]))
902            .grant(
903                "writer",
904                Grant::new([ActionClass::Read, ActionClass::Write], ["people"]),
905            )
906            .grant(
907                "admin",
908                Grant::new([ActionClass::Admin], Vec::<String>::new()),
909            )
910    }
911
912    #[tokio::test]
913    async fn policy_authorizer_enforces_actions_and_databases() {
914        let policy = sample_policy();
915        let reader = Principal::new("t", "r").with_role("reader");
916        let writer = Principal::new("t", "w").with_role("writer");
917        let admin = Principal::new("t", "a").with_role("admin");
918
919        // Reader may query people but not transact, and not touch other dbs.
920        assert!(matches!(
921            policy
922                .authorize(&reader, &Access::on(Action::Query, "people"))
923                .await,
924            Decision::Allow
925        ));
926        assert!(matches!(
927            policy
928                .authorize(&reader, &Access::on(Action::Transact, "people"))
929                .await,
930            Decision::Deny(_)
931        ));
932        assert!(matches!(
933            policy
934                .authorize(&reader, &Access::on(Action::Query, "secrets"))
935                .await,
936            Decision::Deny(_)
937        ));
938
939        // Writer may transact people; admin may create any database.
940        assert!(matches!(
941            policy
942                .authorize(&writer, &Access::on(Action::Transact, "people"))
943                .await,
944            Decision::Allow
945        ));
946        assert!(matches!(
947            policy
948                .authorize(&admin, &Access::catalog(Action::CreateDatabase))
949                .await,
950            Decision::Allow
951        ));
952        // Admin grant is admin-only: no read of people.
953        assert!(matches!(
954            policy
955                .authorize(&admin, &Access::on(Action::Query, "people"))
956                .await,
957            Decision::Deny(_)
958        ));
959    }
960
961    #[tokio::test]
962    async fn per_principal_view_filter_gives_different_views() {
963        // Two tenants read the same database through different attribute views.
964        let acme_view: Arc<dyn ViewFilter> = Arc::new(AttributeAllowlist::new([
965            ":person/name",
966            ":person/acme-note",
967        ]));
968        let beta_view: Arc<dyn ViewFilter> = Arc::new(AttributeAllowlist::new([
969            ":person/name",
970            ":person/beta-note",
971        ]));
972        let policy = PolicyAuthorizer::new()
973            .grant(
974                "acme",
975                Grant::new([ActionClass::Read], ["people"]).with_view(acme_view),
976            )
977            .grant(
978                "beta",
979                Grant::new([ActionClass::Read], ["people"]).with_view(beta_view),
980            );
981        let guard = Guard::new(Arc::new(AllowAnonymous), Arc::new(policy));
982
983        let acme = Principal::new("oidc", "alice").with_role("acme");
984        let beta = Principal::new("oidc", "bob").with_role("beta");
985        let acme_filter = guard
986            .authorize(&acme, &Access::on(Action::Query, "people"))
987            .await
988            .unwrap()
989            .expect("acme is filtered");
990        let beta_filter = guard
991            .authorize(&beta, &Access::on(Action::Query, "people"))
992            .await
993            .unwrap()
994            .expect("beta is filtered");
995
996        assert!(acme_filter.attribute_visible(":person/name"));
997        assert!(acme_filter.attribute_visible(":person/acme-note"));
998        assert!(!acme_filter.attribute_visible(":person/beta-note"));
999
1000        assert!(beta_filter.attribute_visible(":person/beta-note"));
1001        assert!(!beta_filter.attribute_visible(":person/acme-note"));
1002    }
1003
1004    #[test]
1005    fn interceptor_attaches_principal_to_extensions() {
1006        let guard = Guard::new(
1007            Arc::new(StaticTokens::new().with(
1008                "reader-secret",
1009                Principal::new("static-token", "reader").with_role("reader"),
1010            )),
1011            Arc::new(AllowAll),
1012        );
1013        let mut interceptor = IdentityInterceptor::new(guard);
1014
1015        let mut request = Request::new(());
1016        request
1017            .metadata_mut()
1018            .insert("authorization", "Bearer reader-secret".parse().unwrap());
1019        let request = interceptor.call(request).unwrap();
1020        assert_eq!(principal(&request).subject, "reader");
1021
1022        // A bad token is rejected before reaching any handler.
1023        let mut bad = Request::new(());
1024        bad.metadata_mut()
1025            .insert("authorization", "Bearer nope".parse().unwrap());
1026        assert_eq!(
1027            interceptor.call(bad).unwrap_err().code(),
1028            tonic::Code::Unauthenticated
1029        );
1030    }
1031
1032    #[tokio::test]
1033    async fn one_guard_serves_two_tenants_with_different_authority() {
1034        // A single shared guard: static-token issuer + role policy, no anonymous.
1035        let provider = CompositeProvider::new(vec![Arc::new(
1036            StaticTokens::new()
1037                .with(
1038                    "acme-writer",
1039                    Principal::new("static-token", "acme-svc").with_role("acme"),
1040                )
1041                .with(
1042                    "beta-reader",
1043                    Principal::new("static-token", "beta-user").with_role("beta"),
1044                ),
1045        )]);
1046        let policy = PolicyAuthorizer::new()
1047            .grant(
1048                "acme",
1049                Grant::new([ActionClass::Read, ActionClass::Write], ["acme-db"]),
1050            )
1051            .grant("beta", Grant::new([ActionClass::Read], ["beta-db"]));
1052        let guard = Guard::new(Arc::new(provider), Arc::new(policy));
1053
1054        let acme = guard.authenticate(&creds("acme-writer")).unwrap();
1055        let beta = guard.authenticate(&creds("beta-reader")).unwrap();
1056
1057        // acme may write its own db; beta may not read acme's db.
1058        assert!(
1059            guard
1060                .authorize(&acme, &Access::on(Action::Transact, "acme-db"))
1061                .await
1062                .is_ok()
1063        );
1064        assert!(
1065            guard
1066                .authorize(&beta, &Access::on(Action::Query, "acme-db"))
1067                .await
1068                .is_err()
1069        );
1070        assert!(
1071            guard
1072                .authorize(&beta, &Access::on(Action::Query, "beta-db"))
1073                .await
1074                .is_ok()
1075        );
1076        // beta is read-only even on its own db.
1077        assert!(
1078            guard
1079                .authorize(&beta, &Access::on(Action::Transact, "beta-db"))
1080                .await
1081                .is_err()
1082        );
1083    }
1084
1085    /// Stand-in for an external relationship-based oracle (`OpenFGA` / `Auth0 FGA`):
1086    /// `authorize` awaits a "network" `Check` before deciding. Demonstrates that
1087    /// the async `Authorizer` seam is dyn-compatible and composes with `Guard`.
1088    struct FakeOracle {
1089        allowed: BTreeSet<(String, Action)>,
1090    }
1091
1092    #[tonic::async_trait]
1093    impl Authorizer for FakeOracle {
1094        async fn authorize(&self, principal: &Principal, access: &Access) -> Decision {
1095            // Simulate the round-trip to the policy service.
1096            tokio::task::yield_now().await;
1097            let key = (principal.subject.clone(), access.action);
1098            if self.allowed.contains(&key) {
1099                Decision::Allow
1100            } else {
1101                Decision::Deny("oracle check failed".to_owned())
1102            }
1103        }
1104    }
1105
1106    #[tokio::test]
1107    async fn external_async_oracle_authorizer() {
1108        let oracle = FakeOracle {
1109            allowed: [("alice".to_owned(), Action::Query)].into_iter().collect(),
1110        };
1111        let guard = Guard::new(Arc::new(AllowAnonymous), Arc::new(oracle));
1112        let alice = Principal::new("oidc", "alice");
1113        let bob = Principal::new("oidc", "bob");
1114
1115        assert!(
1116            guard
1117                .authorize(&alice, &Access::on(Action::Query, "people"))
1118                .await
1119                .is_ok()
1120        );
1121        assert!(
1122            guard
1123                .authorize(&alice, &Access::on(Action::Transact, "people"))
1124                .await
1125                .is_err()
1126        );
1127        assert!(
1128            guard
1129                .authorize(&bob, &Access::on(Action::Query, "people"))
1130                .await
1131                .is_err()
1132        );
1133    }
1134}