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