Skip to main content

assay_auth/
ctx.rs

1//! Composed auth context — the value engine state holds for the auth
2//! module.
3//!
4//! Phase 4 wires user/session stores and (when JWT is enabled) the
5//! [`crate::jwt::JwtConfig`]. Later phases extend this with the
6//! Zanzibar store and OIDC provider registry. The struct is `Clone`
7//! because axum's `FromRef` model requires it.
8
9use std::sync::Arc;
10
11use crate::biscuit::BiscuitConfig;
12use crate::store::{SessionStore, UserStore};
13
14#[cfg(feature = "auth-recovery")]
15use crate::recovery::PasswordRecovery;
16
17#[cfg(feature = "auth-jwt")]
18use crate::external_jwt::ExternalJwtIssuer;
19#[cfg(feature = "auth-jwt")]
20use crate::jwt::JwtConfig;
21#[cfg(feature = "auth-oidc")]
22use crate::oidc::OidcRegistry;
23#[cfg(feature = "auth-oidc-provider")]
24use crate::oidc_provider::OidcProviderConfig;
25#[cfg(feature = "auth-passkey")]
26use crate::passkey::PasskeyManager;
27#[cfg(feature = "auth-zanzibar")]
28use crate::zanzibar::ZanzibarStore;
29
30#[derive(Clone)]
31#[non_exhaustive]
32pub struct AuthCtx {
33    /// Authoritative user record store. Carries password hashes,
34    /// upstream-provider links, and passkeys.
35    pub users: Arc<dyn UserStore>,
36    /// Session record store — opaque session id + CSRF token + expiry.
37    pub sessions: Arc<dyn SessionStore>,
38    #[cfg(feature = "auth-recovery")]
39    pub recovery: Option<PasswordRecovery>,
40    /// Biscuit capability-token issuer + verifier. Foundational
41    /// (always present): wraps the active root keypair loaded from
42    /// `auth.biscuit_root_keys` (or generated on first boot). Used for
43    /// share links, delegated upload caps, worker capability tokens,
44    /// edge auth, and any flow that wants offline-verifiable bearer
45    /// tokens. See [`crate::biscuit::BiscuitConfig`].
46    pub biscuit: BiscuitConfig,
47    /// JWT issuance/verification configuration. Active key + history;
48    /// see [`crate::jwt::JwtConfig`]. Present only when the
49    /// `auth-jwt` feature is enabled.
50    #[cfg(feature = "auth-jwt")]
51    pub jwt: Option<JwtConfig>,
52    /// External OIDC issuers trusted to mint JWTs the engine accepts
53    /// pass-through. Empty by default; populated by engine boot from
54    /// `[[auth.external_issuers]]` blocks in `engine.toml` via the
55    /// [`AuthCtx::with_external_issuers`] builder. See
56    /// [`crate::external_jwt::ExternalJwtIssuer`] for the verifier
57    /// shape. When non-empty, the engine boots without requiring
58    /// operator users / `admin_api_keys` — pass-through tokens are
59    /// considered sufficient identity proof.
60    ///
61    /// `Arc<[T]>` so cloning `AuthCtx` (which axum's `FromRef` does
62    /// per request that extracts it) bumps a single refcount instead
63    /// of allocating a fresh `Vec`. Each `ExternalJwtIssuer` already
64    /// owns its mutable state (the JWKS) behind its own `Arc<RwLock>`,
65    /// so the inner type doesn't need an extra `Arc` wrap.
66    ///
67    /// Field is private so future entries (per-issuer policy, claim
68    /// mappers, etc.) can be added without breaking downstream
69    /// construction. Read via [`AuthCtx::external_issuers`].
70    #[cfg(feature = "auth-jwt")]
71    external_issuers: Arc<[ExternalJwtIssuer]>,
72    /// Slug-keyed registry of discovered upstream OIDC providers.
73    /// Engine boot constructs an empty registry; admin CRUD (or seed
74    /// config) populates it. See [`crate::oidc::OidcRegistry`].
75    #[cfg(feature = "auth-oidc")]
76    pub oidc: Option<OidcRegistry>,
77    /// WebAuthn / passkey manager. Wraps a single
78    /// [`webauthn_rs::Webauthn`] built from the operator's RP config.
79    /// See [`crate::passkey::PasskeyManager`].
80    #[cfg(feature = "auth-passkey")]
81    pub passkeys: Option<PasskeyManager>,
82    /// Zanzibar / ReBAC permission store. Optional — engine boot wires
83    /// the appropriate backend (Postgres / SQLite) once the auth schema
84    /// migration has run. See [`crate::zanzibar::ZanzibarStore`] for
85    /// the trait surface; full Keto/SpiceDB feature parity (recursive
86    /// CTE walk, expand, lookup_*) lives behind it.
87    #[cfg(feature = "auth-zanzibar")]
88    pub zanzibar: Option<Arc<dyn ZanzibarStore>>,
89    /// Full OIDC provider — discovery, JWKS, /authorize, /token,
90    /// /userinfo, /revoke, /introspect, federation. Optional because a
91    /// deployment may use assay-engine purely as an OIDC client; engine
92    /// boot constructs the config once the V4 migration has run and
93    /// the upstream provider rows are loaded into the registry.
94    #[cfg(feature = "auth-oidc-provider")]
95    pub oidc_provider: Option<OidcProviderConfig>,
96}
97
98impl AuthCtx {
99    /// Construct a context from the bare minimum required by phase 4 —
100    /// user and session stores. Biscuit is initialised with an
101    /// ephemeral keypair (no DB row) so unit tests + downstream callers
102    /// that don't run engine boot can still construct an [`AuthCtx`].
103    /// Engine boot replaces the biscuit field via
104    /// [`AuthCtx::with_biscuit`] once the persistent root key has been
105    /// loaded from `auth.biscuit_root_keys`.
106    pub fn new(users: Arc<dyn UserStore>, sessions: Arc<dyn SessionStore>) -> Self {
107        Self {
108            users,
109            sessions,
110            #[cfg(feature = "auth-recovery")]
111            recovery: None,
112            biscuit: BiscuitConfig::generate_ephemeral(),
113            #[cfg(feature = "auth-jwt")]
114            jwt: None,
115            #[cfg(feature = "auth-jwt")]
116            external_issuers: Arc::from([]),
117            #[cfg(feature = "auth-oidc")]
118            oidc: None,
119            #[cfg(feature = "auth-passkey")]
120            passkeys: None,
121            #[cfg(feature = "auth-zanzibar")]
122            zanzibar: None,
123            #[cfg(feature = "auth-oidc-provider")]
124            oidc_provider: None,
125        }
126    }
127
128    #[cfg(feature = "auth-recovery")]
129    pub fn with_recovery(mut self, recovery: PasswordRecovery) -> Self {
130        self.recovery = Some(recovery);
131        self
132    }
133
134    /// Replace the JWT configuration. Used by engine boot once the
135    /// JWKS keys have been loaded from `auth.jwks_keys`.
136    #[cfg(feature = "auth-jwt")]
137    pub fn with_jwt(mut self, jwt: JwtConfig) -> Self {
138        self.jwt = Some(jwt);
139        self
140    }
141
142    /// Replace the external-issuer list. Used by engine boot after
143    /// each issuer's discovery + initial JWKS fetch completes. The
144    /// `Vec` is consumed and stored as `Arc<[T]>` so subsequent
145    /// `AuthCtx` clones share the same slice via refcount.
146    #[cfg(feature = "auth-jwt")]
147    pub fn with_external_issuers(mut self, issuers: Vec<ExternalJwtIssuer>) -> Self {
148        self.external_issuers = issuers.into();
149        self
150    }
151
152    /// Read access to the configured external issuers. Used by the
153    /// auth gate's JWT pass-through fallthrough.
154    #[cfg(feature = "auth-jwt")]
155    pub fn external_issuers(&self) -> &[ExternalJwtIssuer] {
156        &self.external_issuers
157    }
158
159    /// Replace the OIDC registry. Engine boot creates an empty registry
160    /// for unconfigured deployments; once admin CRUD lands, the same
161    /// builder runs after the seed providers are loaded.
162    #[cfg(feature = "auth-oidc")]
163    pub fn with_oidc(mut self, oidc: OidcRegistry) -> Self {
164        self.oidc = Some(oidc);
165        self
166    }
167
168    /// Replace the passkey manager. Optional — the manager owns a live
169    /// [`webauthn_rs::Webauthn`] built from the engine's RP config and
170    /// is only constructible when that config is present.
171    #[cfg(feature = "auth-passkey")]
172    pub fn with_passkeys(mut self, passkeys: PasskeyManager) -> Self {
173        self.passkeys = Some(passkeys);
174        self
175    }
176
177    /// Replace the biscuit configuration. Engine boot loads the active
178    /// root key from `auth.biscuit_root_keys` (or generates one on
179    /// first boot) and feeds the result here.
180    pub fn with_biscuit(mut self, biscuit: BiscuitConfig) -> Self {
181        self.biscuit = biscuit;
182        self
183    }
184
185    /// Replace the Zanzibar store. Engine boot constructs the
186    /// appropriate backend impl after the auth schema migration runs;
187    /// see `crates/assay-engine/src/init.rs`. Phase 6 only wires the
188    /// builder + the migration; full AuthCtx composition happens in
189    /// phase 8 alongside HTTP route mounting.
190    #[cfg(feature = "auth-zanzibar")]
191    pub fn with_zanzibar(mut self, zanzibar: Arc<dyn ZanzibarStore>) -> Self {
192        self.zanzibar = Some(zanzibar);
193        self
194    }
195
196    /// Replace the OIDC provider configuration. Engine boot constructs
197    /// the appropriate stores (PG / SQLite) after the V4 auth schema
198    /// migration runs; see `crates/assay-engine/src/init.rs`.
199    /// only wires the builder + the migrations + the placeholder
200    /// router; phase 8 weaves the resolved AuthCtx into the actual
201    /// `/authorize` and `/token` HTTP handlers.
202    #[cfg(feature = "auth-oidc-provider")]
203    pub fn with_oidc_provider(mut self, oidc_provider: OidcProviderConfig) -> Self {
204        self.oidc_provider = Some(oidc_provider);
205        self
206    }
207}