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}