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}