Skip to main content

reddb_server/auth/
store.rs

1//! AuthStore -- manages users, sessions, and API keys in memory.
2//!
3//! Password hashing delegates to the existing Argon2id implementation in
4//! `crate::storage::encryption::argon2id`.  Token generation uses the
5//! OS CSPRNG (`crate::crypto::os_random`) plus SHA-256.
6#![deny(clippy::disallowed_methods)]
7
8use std::borrow::Cow;
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, RwLock};
12
13use crate::crypto::os_random;
14use crate::crypto::sha256::sha256;
15use crate::storage::encryption::argon2id::{derive_key, Argon2Params};
16use crate::storage::engine::pager::Pager;
17
18use super::column_policy_gate::{ColumnAccessRequest, ColumnPolicyGate, ColumnPolicyOutcome};
19use super::enforcement_mode::{legacy_rbac_decision, PolicyEnforcementMode};
20use super::managed_policy::{ManagedPolicyDecision, ManagedPolicyGate, PolicyOp};
21use super::policies::{self as iam_policies, EvalContext, Policy, ResourceRef, SimulationOutcome};
22use super::privileges::{
23    check_grant, Action, AuthzContext, AuthzError, Grant, GrantPrincipal, GrantsView,
24    PermissionCache, Resource, UserAttributes,
25};
26use super::registry::ConfigRegistry;
27use super::vault::{KeyPair, Vault, VaultState};
28use super::{now_ms, ApiKey, AuthConfig, AuthError, Role, Session, User, UserId};
29use crate::runtime::control_events::{
30    ControlEvent, ControlEventConfig, ControlEventCtx, ControlEventLedger, EventKind, Outcome,
31    Sensitivity,
32};
33
34// ---------------------------------------------------------------------------
35// PrincipalRef + SimCtx — IAM policy attachments
36// ---------------------------------------------------------------------------
37
38/// Principal targeted by `attach_policy` / `detach_policy`.
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub enum PrincipalRef {
41    User(UserId),
42    Group(String),
43}
44
45/// Reserved IAM group that every principal belongs to. Used by the
46/// GRANT-to-PUBLIC compatibility layer.
47pub const PUBLIC_IAM_GROUP: &str = "__public__";
48
49/// Optional context overrides for `simulate` — anything not set falls back
50/// to a default value when the kernel evaluates the request.
51#[derive(Debug, Clone, Default)]
52pub struct SimCtx {
53    pub current_tenant: Option<String>,
54    pub peer_ip: Option<std::net::IpAddr>,
55    pub mfa_present: bool,
56    pub now_ms: Option<u128>,
57}
58
59/// Control-event dependencies for audited IAM policy mutations.
60///
61/// The existing `put_policy` / `delete_policy` / attach / detach
62/// methods remain unaudited for bootstrap and synthetic GRANT internals.
63/// Public mutation surfaces that need evidence pass this context to the
64/// `*_with_control_events` siblings.
65pub struct PolicyMutationControl<'a> {
66    pub ctx: &'a ControlEventCtx<'a>,
67    pub ledger: &'a dyn ControlEventLedger,
68    pub config: ControlEventConfig,
69    pub registry: Option<&'a ConfigRegistry>,
70    pub actor: &'a UserId,
71    pub eval_ctx: &'a EvalContext,
72}
73
74/// Control-event dependencies for audited user lifecycle mutations.
75pub struct UserLifecycleControl<'a> {
76    pub ctx: &'a ControlEventCtx<'a>,
77    pub ledger: &'a dyn ControlEventLedger,
78    pub config: ControlEventConfig,
79}
80
81#[derive(Clone)]
82struct AuthStoreControlEvents {
83    ledger: Arc<dyn ControlEventLedger>,
84    config: ControlEventConfig,
85}
86
87// ---------------------------------------------------------------------------
88// BootstrapResult
89// ---------------------------------------------------------------------------
90
91/// Result of a successful bootstrap operation.
92///
93/// The `certificate` is the hex-encoded string the admin must save --
94/// it is the ONLY way to unseal the vault after a restart.
95pub struct BootstrapResult {
96    pub user: User,
97    pub api_key: ApiKey,
98    /// Certificate hex string.  `None` when vault is not configured.
99    pub certificate: Option<String>,
100}
101
102impl std::fmt::Debug for BootstrapResult {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        f.debug_struct("BootstrapResult")
105            .field("user", &self.user)
106            .field("api_key", &"<redacted>")
107            .field(
108                "certificate",
109                &self.certificate.as_ref().map(|_| "<redacted>"),
110            )
111            .finish()
112    }
113}
114
115// ---------------------------------------------------------------------------
116// AuthStore
117// ---------------------------------------------------------------------------
118
119/// Central in-process authority for auth state.
120///
121/// All mutations are guarded by `RwLock`s so the store is `Send + Sync`.
122pub struct AuthStore {
123    /// `(tenant_id, username) -> User`. Tenant scoping is built into the
124    /// key so `alice@acme` and `alice@globex` are distinct identities.
125    users: RwLock<HashMap<UserId, User>>,
126    sessions: RwLock<HashMap<String, Session>>,
127    /// key-string -> (owner UserId, role)
128    api_key_index: RwLock<HashMap<String, (UserId, Role)>>,
129    /// Once true, bootstrap() is permanently sealed.
130    bootstrapped: AtomicBool,
131    config: AuthConfig,
132    /// Optional encrypted vault for persisting auth state to pager pages.
133    vault: RwLock<Option<Vault>>,
134    /// Reference to the pager for vault page I/O.
135    pager: Option<Arc<Pager>>,
136    /// Existing operator credential; bootstrap must preserve this seal.
137    provided_certificate: Option<String>,
138    /// Certificate-based keypair for token signing and vault seal.
139    /// Populated after bootstrap or after restoring from a sealed vault.
140    keypair: RwLock<Option<KeyPair>>,
141    /// Encrypted key-value store for arbitrary secrets.
142    /// Persisted to vault alongside users/api_keys.
143    vault_kv: RwLock<HashMap<String, String>>,
144    secret_key_init: std::sync::Mutex<()>,
145    /// Plain (non-encrypted) user key-value store for `$kv.*` / `SET KV`
146    /// (#1602). Deliberately isolated from `vault_kv` (encrypted secrets)
147    /// and from the runtime config store — three independent flat-maps,
148    /// no mixing. Backs the `kv:read` / `kv:write` IAM-gated resolver.
149    plain_kv: RwLock<HashMap<String, String>>,
150    /// Per-user GRANT rows. Persisted via `vault_kv` under the
151    /// `red.acl.grants.<tenant>/<user>` key prefix so existing snapshot
152    /// logic keeps working without modification. See `privileges` module
153    /// for the resolution algorithm.
154    grants: RwLock<HashMap<UserId, Vec<Grant>>>,
155    /// PUBLIC grants — apply to every authenticated principal.
156    public_grants: RwLock<Vec<Grant>>,
157    /// PG-style account attributes (`VALID UNTIL`, `CONNECTION LIMIT`,
158    /// `search_path`). Keyed by `UserId`. Persisted under
159    /// `red.acl.attrs.<tenant>/<user>`.
160    user_attributes: RwLock<HashMap<UserId, UserAttributes>>,
161    /// Live session count per user — used by `CONNECTION LIMIT`
162    /// enforcement on login. Bumped at authenticate, decremented at
163    /// session revoke / expiry.
164    session_count_by_user: RwLock<HashMap<UserId, u32>>,
165    /// Pre-resolved (resource, action) cache built per-user so the
166    /// hot path skips a linear scan of the user's grants on every
167    /// statement. Invalidated on GRANT / REVOKE / ALTER USER.
168    permission_cache: RwLock<HashMap<UserId, PermissionCache>>,
169    /// IAM-style policies, keyed by id. Persisted under
170    /// `red.iam.policies`. The kernel in `super::policies` owns the
171    /// Policy type — this map just deduplicates and shares.
172    policies: RwLock<HashMap<String, Arc<Policy>>>,
173    /// Per-user policy attachments — ordered list of policy ids.
174    /// Persisted under `red.iam.attachments.users`.
175    user_attachments: RwLock<HashMap<UserId, Vec<String>>>,
176    /// Per-group policy attachments. Users join groups through
177    /// `UserAttributes::groups`; effective policies resolve group
178    /// attachments before user-direct attachments.
179    group_attachments: RwLock<HashMap<String, Vec<String>>>,
180    /// Cached effective `Vec<Arc<Policy>>` per user. Invalidated on
181    /// any policy mutation that affects the user's attachments.
182    iam_effective_cache: RwLock<HashMap<UserId, Vec<Arc<Policy>>>>,
183    /// Once any IAM policy is installed, authorization switches to the
184    /// IAM evaluator and stays deny-by-default even if policies are
185    /// later deleted. Persisted under `red.iam.enabled`.
186    iam_authorization_enabled: AtomicBool,
187    /// Selects the fallback behaviour when the IAM evaluator returns
188    /// `DefaultDeny`. See [`PolicyEnforcementMode`] and #712 / S5A.
189    /// Default is [`PolicyEnforcementMode::default_existing_install`]
190    /// (`LegacyRbac`) so an existing install upgrading past #712
191    /// keeps its current authorization posture until the operator
192    /// flips it explicitly. The bootstrap path sets this to
193    /// `PolicyOnly` for fresh installs; the boot-time config loader
194    /// supersedes both when a `red.config.policy.enforcement_mode`
195    /// value is present in the configured value store.
196    enforcement_mode: RwLock<PolicyEnforcementMode>,
197    /// Once-per-boot flag used by [`AuthStore::take_legacy_rbac_warn_once`]
198    /// to deliver exactly one `warn`-level log line when the store is
199    /// running under [`PolicyEnforcementMode::LegacyRbac`]. The boot
200    /// path checks the flag and, if it can claim it, emits the line.
201    legacy_rbac_boot_warn_emitted: AtomicBool,
202    /// `(tenant, role) → HashSet<CollectionId>` cache used by the AI
203    /// pipeline (issue #119). Distinct from `permission_cache`, which
204    /// is keyed by `UserId` and answers `(resource, action)` lookups —
205    /// this cache answers the inverse "what collections is this scope
206    /// allowed to read?" query that `AuthorizedSearch` uses to
207    /// pre-filter SEARCH SIMILAR / SEARCH CONTEXT candidates before any
208    /// similarity score is computed. Entries TTL out at 60s and are
209    /// invalidated explicitly on GRANT/REVOKE/CREATE POLICY/DROP
210    /// POLICY/DROP COLLECTION.
211    visible_collections_cache: super::scope_cache::AuthCache,
212    control_events: RwLock<Option<AuthStoreControlEvents>>,
213}
214
215// Use fast-but-safe Argon2id params for auth hashing (smaller than the
216// default 64 MB so that user-management RPCs respond quickly).
217fn auth_argon2_params() -> Argon2Params {
218    Argon2Params {
219        m_cost: 4 * 1024, // 4 MB
220        t_cost: 3,
221        p: 1,
222        tag_len: 32,
223    }
224}
225
226fn policy_control_fields(
227    policy_id: &str,
228    policy: Option<&Policy>,
229    principal: Option<&PrincipalRef>,
230) -> HashMap<String, Sensitivity> {
231    let mut fields = HashMap::new();
232    fields.insert("policy_id".to_string(), Sensitivity::raw(policy_id));
233    if let Some(policy) = policy {
234        let effects = policy
235            .statements
236            .iter()
237            .map(|s| match s.effect {
238                iam_policies::Effect::Allow => "allow",
239                iam_policies::Effect::Deny => "deny",
240            })
241            .collect::<Vec<_>>()
242            .join(",");
243        let actions = policy
244            .statements
245            .iter()
246            .flat_map(|s| s.actions.iter())
247            .map(policy_action_pattern)
248            .collect::<Vec<_>>()
249            .join(",");
250        let resources = policy
251            .statements
252            .iter()
253            .flat_map(|s| s.resources.iter())
254            .map(policy_resource_pattern)
255            .collect::<Vec<_>>()
256            .join(",");
257        fields.insert("effect".to_string(), Sensitivity::raw(effects));
258        fields.insert("action".to_string(), Sensitivity::raw(actions));
259        fields.insert("resource".to_string(), Sensitivity::raw(resources));
260        if let Some(attrs) = policy_principal_attrs(policy) {
261            fields.insert("principal_attrs".to_string(), Sensitivity::raw(attrs));
262        }
263    }
264    if let Some(principal) = principal {
265        fields.insert(
266            "principal".to_string(),
267            Sensitivity::raw(policy_principal_label(principal)),
268        );
269    }
270    fields
271}
272
273fn policy_action_pattern(pattern: &iam_policies::ActionPattern) -> String {
274    match pattern {
275        iam_policies::ActionPattern::Exact(s) => s.clone(),
276        iam_policies::ActionPattern::Wildcard => "*".to_string(),
277        iam_policies::ActionPattern::Prefix(s) => format!("{s}:*"),
278    }
279}
280
281fn policy_resource_pattern(pattern: &iam_policies::ResourcePattern) -> String {
282    match pattern {
283        iam_policies::ResourcePattern::Exact { kind, name } => format!("{kind}:{name}"),
284        iam_policies::ResourcePattern::Glob(s) => s.clone(),
285        iam_policies::ResourcePattern::Wildcard => "*".to_string(),
286    }
287}
288
289fn policy_principal_attrs(policy: &Policy) -> Option<String> {
290    let mut attrs = Vec::new();
291    for statement in &policy.statements {
292        let Some(condition) = &statement.condition else {
293            continue;
294        };
295        if let Some(value) = condition.platform_scoped {
296            attrs.push(format!("platform_scoped={value}"));
297        }
298        if let Some(value) = condition.mfa {
299            attrs.push(format!("mfa={value}"));
300        }
301        if let Some(value) = condition.tenant_match {
302            attrs.push(format!("tenant_match={value}"));
303        }
304    }
305    if attrs.is_empty() {
306        None
307    } else {
308        Some(attrs.join(","))
309    }
310}
311
312fn policy_principal_label(principal: &PrincipalRef) -> String {
313    match principal {
314        PrincipalRef::User(uid) => format!("user:{uid}"),
315        PrincipalRef::Group(group) => format!("group:{group}"),
316    }
317}
318
319fn default_user_lifecycle_ctx<'a>() -> ControlEventCtx<'a> {
320    ControlEventCtx {
321        actor: crate::runtime::control_events::ActorRef::Anonymous,
322        scope: None,
323        request_id: None,
324        trace_id: None,
325    }
326}
327
328fn bootstrap_user_lifecycle_ctx<'a>() -> ControlEventCtx<'a> {
329    ControlEventCtx {
330        actor: crate::runtime::control_events::ActorRef::System("bootstrap"),
331        scope: None,
332        request_id: None,
333        trace_id: None,
334    }
335}
336
337fn user_resource(id: &UserId) -> String {
338    format!("user:{id}")
339}
340
341fn api_key_resource(api_key_id: &str) -> String {
342    format!("apikey:{api_key_id}")
343}
344
345/// Hex digest that identifies an API key in control events and, with the
346/// `rkh_` prefix, at rest. Accepts either the secret or the stored id, so
347/// every entry point can take whichever the caller holds.
348fn api_key_id(key: &str) -> String {
349    match key.strip_prefix("rkh_") {
350        Some(hex) => hex.to_string(),
351        None => hex::encode(sha256(key.as_bytes())),
352    }
353}
354
355/// Stored form of an API key: `rkh_<sha256(secret)>`. The secret is only
356/// ever compared by hashing it and looking the result up.
357pub(crate) fn api_key_credential_id(token: &str) -> String {
358    format!("rkh_{}", api_key_id(token))
359}
360
361fn password_evidence() -> Sensitivity {
362    Sensitivity::redacted()
363}
364
365fn user_control_fields(
366    id: &UserId,
367    role: Option<Role>,
368    enabled: Option<bool>,
369    include_password: bool,
370) -> HashMap<String, Sensitivity> {
371    let mut fields = HashMap::new();
372    fields.insert(
373        "username".to_string(),
374        Sensitivity::raw(id.username.clone()),
375    );
376    fields.insert(
377        "tenant_id".to_string(),
378        Sensitivity::raw(id.tenant.clone().unwrap_or_default()),
379    );
380    if let Some(role) = role {
381        fields.insert("role".to_string(), Sensitivity::raw(role.as_str()));
382    }
383    if let Some(enabled) = enabled {
384        fields.insert("enabled".to_string(), Sensitivity::raw(enabled.to_string()));
385    }
386    if include_password {
387        fields.insert("password".to_string(), password_evidence());
388    }
389    fields
390}
391
392fn api_key_control_fields(
393    id: &UserId,
394    role: Role,
395    api_key_id: &str,
396) -> HashMap<String, Sensitivity> {
397    let mut fields = user_control_fields(id, Some(role), None, false);
398    fields.insert("api_key_id".to_string(), Sensitivity::raw(api_key_id));
399    fields.insert("api_key".to_string(), Sensitivity::redacted());
400    fields
401}
402
403fn user_error_is_denied(err: &AuthError) -> bool {
404    !matches!(err, AuthError::Internal(_))
405}
406
407impl AuthStore {
408    // -----------------------------------------------------------------
409    // Construction
410    // -----------------------------------------------------------------
411
412    pub fn new(config: AuthConfig) -> Self {
413        Self {
414            users: RwLock::new(HashMap::new()),
415            sessions: RwLock::new(HashMap::new()),
416            api_key_index: RwLock::new(HashMap::new()),
417            bootstrapped: AtomicBool::new(false),
418            config,
419            vault: RwLock::new(None),
420            pager: None,
421            provided_certificate: None,
422            keypair: RwLock::new(None),
423            vault_kv: RwLock::new(HashMap::new()),
424            secret_key_init: std::sync::Mutex::new(()),
425            plain_kv: RwLock::new(HashMap::new()),
426            grants: RwLock::new(HashMap::new()),
427            public_grants: RwLock::new(Vec::new()),
428            user_attributes: RwLock::new(HashMap::new()),
429            session_count_by_user: RwLock::new(HashMap::new()),
430            permission_cache: RwLock::new(HashMap::new()),
431            policies: RwLock::new(HashMap::new()),
432            user_attachments: RwLock::new(HashMap::new()),
433            group_attachments: RwLock::new(HashMap::new()),
434            iam_effective_cache: RwLock::new(HashMap::new()),
435            iam_authorization_enabled: AtomicBool::new(false),
436            enforcement_mode: RwLock::new(PolicyEnforcementMode::default_existing_install()),
437            legacy_rbac_boot_warn_emitted: AtomicBool::new(false),
438            visible_collections_cache: super::scope_cache::AuthCache::new(
439                super::scope_cache::DEFAULT_TTL,
440            ),
441            control_events: RwLock::new(None),
442        }
443    }
444
445    pub fn configure_control_events(
446        &self,
447        ledger: Arc<dyn ControlEventLedger>,
448        config: ControlEventConfig,
449    ) {
450        *self
451            .control_events
452            .write()
453            .unwrap_or_else(|e| e.into_inner()) = Some(AuthStoreControlEvents { ledger, config });
454    }
455
456    fn configured_control_events(&self) -> Option<AuthStoreControlEvents> {
457        self.control_events
458            .read()
459            .unwrap_or_else(|e| e.into_inner())
460            .clone()
461    }
462
463    /// Create an `AuthStore` backed by encrypted vault pages inside the
464    /// main `.rdb` database file.
465    ///
466    /// If vault pages already exist, their contents are loaded and
467    /// restored into the in-memory store.  All subsequent mutations are
468    /// automatically persisted to the vault pages via the pager.
469    pub fn with_vault(config: AuthConfig, pager: Arc<Pager>) -> Result<Self, AuthError> {
470        let certificate = crate::utils::env_with_file_fallback("REDDB_CERTIFICATE");
471        let passphrase = crate::utils::env_with_file_fallback("REDDB_VAULT_PASSPHRASE");
472        match (certificate, passphrase) {
473            (Some(_), Some(_)) => Err(AuthError::Internal(
474                "set either REDDB_CERTIFICATE or REDDB_VAULT_PASSPHRASE, not both".into(),
475            )),
476            (None, Some(passphrase)) => Self::with_vault_passphrase(config, pager, &passphrase),
477            (certificate, None) => {
478                Self::with_vault_optional_certificate(config, pager, certificate.as_deref())
479            }
480        }
481    }
482
483    pub fn with_vault_certificate(
484        config: AuthConfig,
485        pager: Arc<Pager>,
486        certificate_hex: &str,
487    ) -> Result<Self, AuthError> {
488        Self::with_vault_optional_certificate(config, pager, Some(certificate_hex))
489    }
490
491    pub fn with_vault_passphrase(
492        config: AuthConfig,
493        pager: Arc<Pager>,
494        passphrase: &str,
495    ) -> Result<Self, AuthError> {
496        let mut store = Self::new(config);
497        let vault = Vault::with_passphrase(&pager, passphrase)?;
498        if let Some(state) = vault.load(&pager)? {
499            store.restore_from_vault(state);
500        }
501        *store.vault.write().map_err(lock_err)? = Some(vault);
502        store.pager = Some(pager);
503        Ok(store)
504    }
505
506    fn with_vault_optional_certificate(
507        config: AuthConfig,
508        pager: Arc<Pager>,
509        certificate_hex: Option<&str>,
510    ) -> Result<Self, AuthError> {
511        let mut store = Self::new(config);
512
513        // Restore persisted state if vault pages exist. A fresh database does
514        // not need a temporary seal: bootstrap generates the certificate and
515        // installs the first real vault atomically.
516        if Vault::has_saved_state(&pager) {
517            let vault = match certificate_hex {
518                Some(certificate) => Vault::with_certificate(&pager, certificate)?,
519                None => Vault::open(&pager)?,
520            };
521            if let Some(state) = vault.load(&pager)? {
522                store.restore_from_vault(state);
523            }
524            *store.vault.write().unwrap_or_else(|e| e.into_inner()) = Some(vault);
525        } else if let Some(certificate) = certificate_hex {
526            let vault = Vault::with_certificate(&pager, certificate)?;
527            *store.vault.write().unwrap_or_else(|e| e.into_inner()) = Some(vault);
528        }
529
530        store.pager = Some(pager);
531        store.provided_certificate =
532            certificate_hex.map(|certificate| certificate.trim().to_ascii_lowercase());
533        Ok(store)
534    }
535
536    pub fn config(&self) -> &AuthConfig {
537        &self.config
538    }
539
540    pub fn is_enabled(&self) -> bool {
541        self.config.enabled
542    }
543
544    /// Returns true when no users exist yet and bootstrap hasn't been sealed.
545    pub fn needs_bootstrap(&self) -> bool {
546        !self.bootstrapped.load(Ordering::Acquire)
547            && self.users.read().map(|u| u.is_empty()).unwrap_or(true)
548    }
549
550    /// Internal: read-locked lookup by `UserId`.
551    fn get_user_cloned(&self, id: &UserId) -> Option<User> {
552        self.users.read().ok().and_then(|m| m.get(id).cloned())
553    }
554
555    /// Whether bootstrap has already been performed (sealed).
556    pub fn is_bootstrapped(&self) -> bool {
557        self.bootstrapped.load(Ordering::Acquire)
558    }
559
560    /// Bootstrap the first admin user. One-shot, irreversible.
561    ///
562    /// Uses an atomic compare-exchange to guarantee that even under
563    /// concurrent calls, only the first one succeeds. Once sealed,
564    /// all subsequent calls fail immediately -- there is no undo.
565    ///
566    /// When a vault/pager is configured, a certificate-based keypair is
567    /// generated and the vault is re-encrypted with the certificate-derived
568    /// key.  The certificate hex string is returned in `BootstrapResult`
569    /// so the admin can save it.
570    pub fn bootstrap(&self, username: &str, password: &str) -> Result<BootstrapResult, AuthError> {
571        if let Some(configured) = self.configured_control_events() {
572            let ctx = bootstrap_user_lifecycle_ctx();
573            let control = UserLifecycleControl {
574                ctx: &ctx,
575                ledger: configured.ledger.as_ref(),
576                config: configured.config,
577            };
578            self.bootstrap_with_control_events_inner(username, password, &control)
579        } else {
580            self.bootstrap_unaudited(username, password)
581        }
582    }
583
584    pub fn bootstrap_with_control_events(
585        &self,
586        username: &str,
587        password: &str,
588        ctx: &ControlEventCtx<'_>,
589        ledger: &dyn ControlEventLedger,
590        config: ControlEventConfig,
591    ) -> Result<BootstrapResult, AuthError> {
592        let system_ctx = ControlEventCtx {
593            actor: crate::runtime::control_events::ActorRef::System("bootstrap"),
594            scope: ctx.scope.clone(),
595            request_id: ctx.request_id.clone(),
596            trace_id: ctx.trace_id.clone(),
597        };
598        let control = UserLifecycleControl {
599            ctx: &system_ctx,
600            ledger,
601            config,
602        };
603        self.bootstrap_with_control_events_inner(username, password, &control)
604    }
605
606    /// Backwards-compatible bootstrap entry point for older callers
607    /// that used to ask for an operator-owned first admin. Authorization
608    /// is now policy-first, so this creates the same ordinary platform
609    /// admin as [`Self::bootstrap`].
610    pub fn bootstrap_system_admin(
611        &self,
612        username: &str,
613        password: &str,
614    ) -> Result<BootstrapResult, AuthError> {
615        self.bootstrap(username, password)
616    }
617
618    fn bootstrap_unaudited(
619        &self,
620        username: &str,
621        password: &str,
622    ) -> Result<BootstrapResult, AuthError> {
623        // Atomic seal: only the first caller wins.
624        if self
625            .bootstrapped
626            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
627            .is_err()
628        {
629            return Err(AuthError::Forbidden(
630                "bootstrap already completed — sealed permanently".to_string(),
631            ));
632        }
633
634        // Double-check users are actually empty (belt and suspenders).
635        {
636            let users = self.users.read().map_err(lock_err)?;
637            if !users.is_empty() {
638                return Err(AuthError::Forbidden(
639                    "bootstrap already completed — users exist".to_string(),
640                ));
641            }
642        }
643
644        let user = self.create_user_in_tenant_unaudited(None, username, password, Role::Admin)?;
645        let key = self.create_api_key_in_tenant_unaudited(
646            None,
647            username,
648            "bootstrap",
649            Role::Admin,
650            None,
651        )?;
652
653        // Token signing gets a fresh master secret. Preserve an existing
654        // certificate/passphrase seal instead of replacing the operator's key.
655        let credential_result = (|| -> Result<Option<String>, AuthError> {
656            let certificate = if let Some(ref pager) = self.pager {
657                let keypair = KeyPair::generate();
658                let mut vault = self.vault.write().map_err(lock_err)?;
659                let certificate = if vault.is_none() {
660                    let certificate = keypair.certificate_hex();
661                    *vault = Some(Vault::with_certificate_bytes(pager, &keypair.certificate)?);
662                    Some(certificate)
663                } else {
664                    self.provided_certificate.clone()
665                };
666                *self.keypair.write().map_err(lock_err)? = Some(keypair);
667                drop(vault);
668                self.ensure_vault_secret_key_result()?;
669                certificate
670            } else {
671                None
672            };
673            *self.enforcement_mode.write().map_err(lock_err)? =
674                PolicyEnforcementMode::default_fresh_bootstrap();
675            self.vault_kv.write().map_err(lock_err)?.insert(
676                "red.config.policy.enforcement_mode".into(),
677                PolicyEnforcementMode::default_fresh_bootstrap()
678                    .as_str()
679                    .into(),
680            );
681            self.persist_to_vault_result()?;
682            Ok(certificate)
683        })();
684        let certificate = match credential_result {
685            Ok(certificate) => certificate,
686            Err(err) => {
687                self.rollback_bootstrap(&UserId::from_parts(None, username));
688                return Err(err);
689            }
690        };
691
692        Ok(BootstrapResult {
693            user,
694            api_key: key,
695            certificate,
696        })
697    }
698
699    fn bootstrap_with_control_events_inner(
700        &self,
701        username: &str,
702        password: &str,
703        control: &UserLifecycleControl<'_>,
704    ) -> Result<BootstrapResult, AuthError> {
705        let id = UserId::from_parts(None, username);
706        match self.bootstrap_unaudited(username, password) {
707            Ok(result) => {
708                let event_result = self.emit_user_lifecycle_allowed(
709                    control,
710                    EventKind::UserCreate,
711                    "user.create",
712                    &id,
713                    user_control_fields(&id, Some(Role::Admin), Some(true), true),
714                );
715                if let Err(err) = event_result {
716                    self.rollback_bootstrap(&id);
717                    return Err(err);
718                }
719                Ok(result)
720            }
721            Err(err) => {
722                self.emit_user_lifecycle_outcome(
723                    control,
724                    if user_error_is_denied(&err) {
725                        Outcome::Denied
726                    } else {
727                        Outcome::Error
728                    },
729                    EventKind::UserCreate,
730                    "user.create",
731                    &id,
732                    Some(err.to_string()),
733                    user_control_fields(&id, Some(Role::Admin), Some(true), true),
734                );
735                Err(err)
736            }
737        }
738    }
739
740    /// Auto-bootstrap from environment variables if no users exist.
741    ///
742    /// Checks `REDDB_USERNAME` and `REDDB_PASSWORD`. If both are set and
743    /// the user store is empty, creates the first admin user automatically.
744    /// This mirrors the Docker pattern (`MYSQL_ROOT_PASSWORD`, etc.).
745    ///
746    /// Returns `Some(BootstrapResult)` if bootstrapped, `None` if skipped.
747    pub fn bootstrap_from_env(&self) -> Option<BootstrapResult> {
748        if !self.needs_bootstrap() {
749            return None;
750        }
751
752        let username = crate::utils::env_with_file_fallback("REDDB_USERNAME")?;
753        let password = crate::utils::env_with_file_fallback("REDDB_PASSWORD")?;
754
755        if username.is_empty() || password.is_empty() {
756            return None;
757        }
758
759        match self.bootstrap(&username, &password) {
760            Ok(result) => {
761                // F-04: `username` is REDDB_USERNAME — operator-supplied
762                // (env), but still routed through the LogField escaper
763                // because env strings cross trust boundaries in some
764                // deployment models (k8s downward API, Vault sidecar,
765                // external secret operator). See ADR 0010.
766                tracing::info!(
767                    username = %reddb_wire::audit_safe_log_field(&username),
768                    "bootstrapped admin user from REDDB_USERNAME/REDDB_PASSWORD"
769                );
770                if let Some(ref cert) = result.certificate {
771                    deliver_bootstrap_certificate(cert);
772                }
773                Some(result)
774            }
775            Err(e) => {
776                tracing::error!(err = %e, "env bootstrap failed");
777                None
778            }
779        }
780    }
781
782    // -----------------------------------------------------------------
783    // Vault persistence
784    // -----------------------------------------------------------------
785
786    /// Persist the current auth state to the vault pages (if configured).
787    pub(crate) fn persist_to_vault_result(&self) -> Result<(), AuthError> {
788        let vault_guard = self.vault.read().unwrap_or_else(|e| e.into_inner());
789        if let (Some(ref vault), Some(ref pager)) = (&*vault_guard, &self.pager) {
790            let state = self.snapshot();
791            vault.save(pager, &state)?;
792        }
793        Ok(())
794    }
795
796    /// Persist the current auth state to the vault pages (if configured).
797    ///
798    /// Legacy auth mutations still treat in-memory state as authoritative.
799    /// New secret-management paths use the `try_` methods below so callers
800    /// get a hard error if the vault write fails.
801    fn persist_to_vault(&self) {
802        if let Err(e) = self.persist_to_vault_result() {
803            tracing::error!(err = %e, "vault persist failed");
804            // Issue #205 — vault persist is the secret-rotation
805            // serialization point: when this fails, freshly rotated
806            // credentials live only in memory and a process restart
807            // loses them. Operator-grade event so the operator can
808            // intervene before the next restart.
809            crate::telemetry::operator_event::OperatorEvent::SecretRotationFailed {
810                secret_ref: "auth_vault".to_string(),
811                error: e.to_string(),
812            }
813            .emit_global();
814        }
815    }
816
817    /// True when this store has an encrypted vault and pager wired in.
818    pub fn is_vault_backed(&self) -> bool {
819        self.pager.is_some()
820            && self
821                .vault
822                .read()
823                .map(|guard| guard.is_some())
824                .unwrap_or(false)
825    }
826
827    // -----------------------------------------------------------------
828    // Vault KV — encrypted key-value store for arbitrary secrets
829    // -----------------------------------------------------------------
830
831    /// Read a value from the vault KV store. Returns `None` if not set.
832    pub fn vault_kv_get(&self, key: &str) -> Option<String> {
833        self.vault_kv
834            .read()
835            .ok()
836            .and_then(|kv| kv.get(key).cloned())
837    }
838
839    /// Snapshot vault KV values for statement-local secret resolution.
840    pub fn vault_kv_snapshot(&self) -> HashMap<String, String> {
841        self.vault_kv
842            .read()
843            .map(|kv| kv.clone())
844            .unwrap_or_default()
845    }
846
847    /// Export vault KV as an encrypted logical blob for JSONL dump/restore.
848    /// Returns `None` when the vault has no KV entries.
849    pub fn vault_kv_export_encrypted(&self) -> Result<Option<String>, AuthError> {
850        if !self.is_vault_backed() {
851            return Err(AuthError::Forbidden(
852                "vault KV export requires an enabled, unsealed vault".to_string(),
853            ));
854        }
855        let kv = self.vault_kv_snapshot();
856        if kv.is_empty() {
857            return Ok(None);
858        }
859
860        let vault_guard = self.vault.read().map_err(lock_err)?;
861        let vault = vault_guard.as_ref().ok_or_else(|| {
862            AuthError::Forbidden("vault KV export requires an enabled, unsealed vault".to_string())
863        })?;
864        let state = VaultState {
865            users: Vec::new(),
866            api_keys: Vec::new(),
867            bootstrapped: false,
868            master_secret: None,
869            kv,
870        };
871        Ok(Some(vault.seal_logical_export(&state)?))
872    }
873
874    /// Merge imported vault KV entries and fail if the encrypted vault
875    /// write cannot be made durable.
876    pub fn vault_kv_try_import(
877        &self,
878        entries: HashMap<String, String>,
879    ) -> Result<usize, AuthError> {
880        if !self.is_vault_backed() {
881            return Err(AuthError::Forbidden(
882                "vault KV import requires an enabled, unsealed vault".to_string(),
883            ));
884        }
885        if entries.is_empty() {
886            return Ok(0);
887        }
888
889        let mut previous = HashMap::new();
890        {
891            let mut kv = self.vault_kv.write().map_err(lock_err)?;
892            for (key, value) in &entries {
893                previous.insert(key.clone(), kv.insert(key.clone(), value.clone()));
894            }
895        }
896
897        if let Err(err) = self.persist_to_vault_result() {
898            if let Ok(mut kv) = self.vault_kv.write() {
899                for (key, old) in previous {
900                    match old {
901                        Some(value) => {
902                            kv.insert(key, value);
903                        }
904                        None => {
905                            kv.remove(&key);
906                        }
907                    }
908                }
909            }
910            return Err(err);
911        }
912
913        Ok(entries.len())
914    }
915
916    /// Import false placeholders for secrets whose encrypted payload
917    /// could not be decrypted during logical restore.
918    pub fn vault_kv_try_import_placeholders(&self, keys: &[String]) -> Result<usize, AuthError> {
919        let entries = keys
920            .iter()
921            .map(|key| (key.clone(), "false".to_string()))
922            .collect();
923        self.vault_kv_try_import(entries)
924    }
925
926    /// Write a value to the vault KV store, persisting to disk.
927    pub fn vault_kv_set(&self, key: String, value: String) {
928        if let Ok(mut kv) = self.vault_kv.write() {
929            kv.insert(key, value);
930        }
931        self.persist_to_vault();
932    }
933
934    /// Write a value to the vault KV store and fail if the vault write
935    /// cannot be made durable.
936    pub fn vault_kv_try_set(&self, key: String, value: String) -> Result<(), AuthError> {
937        if !self.is_vault_backed() {
938            return Err(AuthError::Forbidden(
939                "SET SECRET requires an enabled, unsealed vault".to_string(),
940            ));
941        }
942
943        let previous = {
944            let mut kv = self.vault_kv.write().map_err(lock_err)?;
945            kv.insert(key.clone(), value)
946        };
947
948        if let Err(err) = self.persist_to_vault_result() {
949            if let Ok(mut kv) = self.vault_kv.write() {
950                match previous {
951                    Some(value) => {
952                        kv.insert(key, value);
953                    }
954                    None => {
955                        kv.remove(&key);
956                    }
957                }
958            }
959            return Err(err);
960        }
961
962        Ok(())
963    }
964
965    /// Delete a value from the vault KV store. Returns true if it existed.
966    pub fn vault_kv_delete(&self, key: &str) -> bool {
967        let existed = self
968            .vault_kv
969            .write()
970            .map(|mut kv| kv.remove(key).is_some())
971            .unwrap_or(false);
972        if existed {
973            self.persist_to_vault();
974        }
975        existed
976    }
977
978    /// Delete a value from the vault KV store and fail if the vault write
979    /// cannot be made durable.
980    pub fn vault_kv_try_delete(&self, key: &str) -> Result<bool, AuthError> {
981        if !self.is_vault_backed() {
982            return Err(AuthError::Forbidden(
983                "DELETE SECRET requires an enabled, unsealed vault".to_string(),
984            ));
985        }
986
987        let removed = {
988            let mut kv = self.vault_kv.write().map_err(lock_err)?;
989            kv.remove(key)
990        };
991
992        if removed.is_none() {
993            return Ok(false);
994        }
995
996        if let Err(err) = self.persist_to_vault_result() {
997            if let Ok(mut kv) = self.vault_kv.write() {
998                if let Some(value) = removed {
999                    kv.insert(key.to_string(), value);
1000                }
1001            }
1002            return Err(err);
1003        }
1004
1005        Ok(true)
1006    }
1007
1008    /// List all keys in the vault KV store.
1009    pub fn vault_kv_keys(&self) -> Vec<String> {
1010        self.vault_kv
1011            .read()
1012            .map(|kv| kv.keys().cloned().collect())
1013            .unwrap_or_default()
1014    }
1015
1016    // -----------------------------------------------------------------
1017    // Plain KV — non-encrypted user key-value store (#1602)
1018    //
1019    // A sibling to the vault KV above, but stored in its own flat-map
1020    // with no encryption. Backs `$kv.*` / `SET KV` / `DELETE KV` and is
1021    // gated by `kv:read` / `kv:write` at the runtime layer. Kept fully
1022    // separate from `vault_kv` (secrets) and the config store.
1023    // -----------------------------------------------------------------
1024
1025    /// Read a value from the plain KV store. Returns `None` if not set.
1026    pub fn plain_kv_get(&self, key: &str) -> Option<String> {
1027        self.plain_kv
1028            .read()
1029            .ok()
1030            .and_then(|kv| kv.get(key).cloned())
1031    }
1032
1033    /// Snapshot plain KV values for statement-local resolution.
1034    pub fn plain_kv_snapshot(&self) -> HashMap<String, String> {
1035        self.plain_kv
1036            .read()
1037            .map(|kv| kv.clone())
1038            .unwrap_or_default()
1039    }
1040
1041    /// Write a value to the plain KV store.
1042    pub fn plain_kv_set(&self, key: String, value: String) {
1043        if let Ok(mut kv) = self.plain_kv.write() {
1044            kv.insert(key, value);
1045        }
1046    }
1047
1048    /// Delete a value from the plain KV store. Returns true if it existed.
1049    pub fn plain_kv_delete(&self, key: &str) -> bool {
1050        self.plain_kv
1051            .write()
1052            .map(|mut kv| kv.remove(key).is_some())
1053            .unwrap_or(false)
1054    }
1055
1056    /// List all keys in the plain KV store.
1057    pub fn plain_kv_keys(&self) -> Vec<String> {
1058        self.plain_kv
1059            .read()
1060            .map(|kv| kv.keys().cloned().collect())
1061            .unwrap_or_default()
1062    }
1063
1064    /// Convenience: get the 32-byte secret key for Value::Secret encryption.
1065    /// Generated on first boot and stored at `red.secret.aes_key`.
1066    pub fn vault_secret_key(&self) -> Option<[u8; 32]> {
1067        let hex_str = self.vault_kv_get("red.secret.aes_key")?;
1068        let bytes = hex::decode(hex_str).ok()?;
1069        if bytes.len() == 32 {
1070            let mut key = [0u8; 32];
1071            key.copy_from_slice(&bytes);
1072            Some(key)
1073        } else {
1074            None
1075        }
1076    }
1077
1078    /// Generate and store the AES-256 secret key on first boot if not present.
1079    pub fn ensure_vault_secret_key(&self) {
1080        if let Err(err) = self.ensure_vault_secret_key_result() {
1081            tracing::error!(error = %err, "vault secret key initialization failed");
1082        }
1083    }
1084
1085    pub(crate) fn ensure_vault_secret_key_result(&self) -> Result<(), AuthError> {
1086        let _guard = self.secret_key_init.lock().map_err(lock_err)?;
1087        if self.vault_secret_key().is_some() {
1088            return Ok(());
1089        }
1090        if self.vault_kv_get("red.secret.aes_key").is_some() {
1091            return Err(AuthError::Internal(
1092                "stored vault secret key is invalid".into(),
1093            ));
1094        }
1095        let value = hex::encode(random_bytes(32));
1096        if self.is_vault_backed() {
1097            self.vault_kv_try_set("red.secret.aes_key".into(), value)
1098        } else {
1099            self.vault_kv
1100                .write()
1101                .map_err(lock_err)?
1102                .insert("red.secret.aes_key".into(), value);
1103            Ok(())
1104        }
1105    }
1106
1107    /// Take a snapshot of the current auth state for vault serialization.
1108    fn snapshot(&self) -> VaultState {
1109        let users_guard = self.users.read().unwrap_or_else(|e| e.into_inner());
1110        let users: Vec<User> = users_guard.values().cloned().collect();
1111
1112        // Collect (owner UserId, api_key) pairs from all users so a
1113        // tenant-scoped owner can be reattached on restore.
1114        let mut api_keys = Vec::new();
1115        for user in &users {
1116            let owner = UserId::from_parts(user.tenant_id.as_deref(), &user.username);
1117            for key in &user.api_keys {
1118                api_keys.push((owner.clone(), key.clone()));
1119            }
1120        }
1121
1122        // Include the master secret if a keypair is loaded.
1123        let master_secret = self
1124            .keypair
1125            .read()
1126            .ok()
1127            .and_then(|guard| guard.as_ref().map(|kp| kp.master_secret.clone()));
1128
1129        let kv = self.vault_kv.read().map(|m| m.clone()).unwrap_or_default();
1130
1131        VaultState {
1132            users,
1133            api_keys,
1134            bootstrapped: self.bootstrapped.load(Ordering::Acquire),
1135            master_secret,
1136            kv,
1137        }
1138    }
1139
1140    /// Restore the in-memory auth state from a vault snapshot.
1141    fn restore_from_vault(&mut self, state: VaultState) {
1142        // Restore bootstrap seal.
1143        if state.bootstrapped {
1144            self.bootstrapped.store(true, Ordering::Release);
1145        }
1146
1147        // Restore keypair from master secret (if present).
1148        if let Some(secret) = state.master_secret {
1149            let kp = KeyPair::from_master_secret(secret);
1150            if let Ok(mut guard) = self.keypair.write() {
1151                *guard = Some(kp);
1152            }
1153        }
1154
1155        // Restore KV store.
1156        if let Ok(mut kv) = self.vault_kv.write() {
1157            *kv = state.kv;
1158        }
1159
1160        // Restore users.
1161        let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
1162        let mut idx = self
1163            .api_key_index
1164            .write()
1165            .unwrap_or_else(|e| e.into_inner());
1166
1167        for mut user in state.users {
1168            let id = UserId::from_parts(user.tenant_id.as_deref(), &user.username);
1169            // A vault written before keys were hashed at rest holds the
1170            // secret itself. Hash it on the way in; the next persist
1171            // rewrites the line and the secret is gone from disk.
1172            for key in &mut user.api_keys {
1173                if !key.key.starts_with("rkh_") {
1174                    key.key = api_key_credential_id(&key.key);
1175                }
1176            }
1177            // Register API keys in the index.
1178            for key in &user.api_keys {
1179                idx.insert(key.key.clone(), (id.clone(), key.role));
1180            }
1181            users.insert(id, user);
1182        }
1183        drop(idx);
1184        drop(users);
1185
1186        self.rehydrate_acl();
1187        self.rehydrate_iam();
1188    }
1189
1190    // -----------------------------------------------------------------
1191    // User management
1192    // -----------------------------------------------------------------
1193
1194    /// Create a new platform-scoped user (`tenant_id = None`).
1195    ///
1196    /// For tenant-scoped creation, use [`Self::create_user_in_tenant`].
1197    pub fn create_user(
1198        &self,
1199        username: &str,
1200        password: &str,
1201        role: Role,
1202    ) -> Result<User, AuthError> {
1203        self.create_user_in_tenant(None, username, password, role)
1204    }
1205
1206    pub fn create_user_with_control_events(
1207        &self,
1208        username: &str,
1209        password: &str,
1210        role: Role,
1211        ctx: &ControlEventCtx<'_>,
1212        ledger: &dyn ControlEventLedger,
1213        config: ControlEventConfig,
1214    ) -> Result<User, AuthError> {
1215        self.create_user_in_tenant_with_control_events(
1216            None, username, password, role, ctx, ledger, config,
1217        )
1218    }
1219
1220    /// Create a user under the given tenant scope. `tenant_id == None`
1221    /// produces a platform-wide user. `(tenant, username)` is the
1222    /// uniqueness key — the same `username` may exist independently
1223    /// under multiple tenants.
1224    pub fn create_user_in_tenant(
1225        &self,
1226        tenant_id: Option<&str>,
1227        username: &str,
1228        password: &str,
1229        role: Role,
1230    ) -> Result<User, AuthError> {
1231        if let Some(configured) = self.configured_control_events() {
1232            let ctx = default_user_lifecycle_ctx();
1233            let control = UserLifecycleControl {
1234                ctx: &ctx,
1235                ledger: configured.ledger.as_ref(),
1236                config: configured.config,
1237            };
1238            self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1239        } else {
1240            self.create_user_in_tenant_unaudited(tenant_id, username, password, role)
1241        }
1242    }
1243
1244    #[allow(clippy::too_many_arguments)]
1245    pub fn create_user_in_tenant_with_control_events(
1246        &self,
1247        tenant_id: Option<&str>,
1248        username: &str,
1249        password: &str,
1250        role: Role,
1251        ctx: &ControlEventCtx<'_>,
1252        ledger: &dyn ControlEventLedger,
1253        config: ControlEventConfig,
1254    ) -> Result<User, AuthError> {
1255        let control = UserLifecycleControl {
1256            ctx,
1257            ledger,
1258            config,
1259        };
1260        self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1261    }
1262
1263    pub fn create_admin_user(
1264        &self,
1265        username: &str,
1266        password: &str,
1267        role: Role,
1268        tenant_id: Option<&str>,
1269    ) -> Result<User, AuthError> {
1270        if let Some(configured) = self.configured_control_events() {
1271            let ctx = default_user_lifecycle_ctx();
1272            let control = UserLifecycleControl {
1273                ctx: &ctx,
1274                ledger: configured.ledger.as_ref(),
1275                config: configured.config,
1276            };
1277            self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1278        } else {
1279            self.create_user_in_tenant_unaudited(tenant_id, username, password, role)
1280        }
1281    }
1282
1283    fn create_user_in_tenant_unaudited(
1284        &self,
1285        tenant_id: Option<&str>,
1286        username: &str,
1287        password: &str,
1288        role: Role,
1289    ) -> Result<User, AuthError> {
1290        let id = UserId::from_parts(tenant_id, username);
1291        let mut users = self.users.write().map_err(lock_err)?;
1292        if users.contains_key(&id) {
1293            return Err(AuthError::UserExists(id.to_string()));
1294        }
1295
1296        let now = now_ms();
1297        let user = User {
1298            username: username.to_string(),
1299            tenant_id: tenant_id.map(|s| s.to_string()),
1300            password_hash: hash_password(password),
1301            scram_verifier: Some(make_scram_verifier(password)),
1302            role,
1303            api_keys: Vec::new(),
1304            created_at: now,
1305            updated_at: now,
1306            enabled: true,
1307        };
1308        users.insert(id, user.clone());
1309        drop(users); // release lock before vault I/O
1310        self.persist_to_vault();
1311        Ok(user)
1312    }
1313
1314    fn create_user_in_tenant_controlled(
1315        &self,
1316        tenant_id: Option<&str>,
1317        username: &str,
1318        password: &str,
1319        role: Role,
1320        control: &UserLifecycleControl<'_>,
1321    ) -> Result<User, AuthError> {
1322        let id = UserId::from_parts(tenant_id, username);
1323        match self.create_user_in_tenant_unaudited(tenant_id, username, password, role) {
1324            Ok(user) => {
1325                if let Err(err) = self.emit_user_lifecycle_allowed(
1326                    control,
1327                    EventKind::UserCreate,
1328                    "user.create",
1329                    &id,
1330                    user_control_fields(&id, Some(role), Some(true), true),
1331                ) {
1332                    self.rollback_create_user(&id);
1333                    return Err(err);
1334                }
1335                Ok(user)
1336            }
1337            Err(err) => {
1338                self.emit_user_lifecycle_outcome(
1339                    control,
1340                    if user_error_is_denied(&err) {
1341                        Outcome::Denied
1342                    } else {
1343                        Outcome::Error
1344                    },
1345                    EventKind::UserCreate,
1346                    "user.create",
1347                    &id,
1348                    Some(err.to_string()),
1349                    user_control_fields(&id, Some(role), Some(true), true),
1350                );
1351                Err(err)
1352            }
1353        }
1354    }
1355
1356    /// Look up a user's SCRAM verifier by full `UserId`.
1357    ///
1358    /// The wire handshake passes the tenant resolved from the session
1359    /// (or `None` for the bootstrap admin) so cross-tenant collisions
1360    /// never authenticate the wrong identity.
1361    pub fn lookup_scram_verifier(&self, id: &UserId) -> Option<crate::auth::scram::ScramVerifier> {
1362        // Synthetic platform-owner principal must never authenticate.
1363        if crate::auth::self_lock_guard::is_synthetic_principal(&id.username) {
1364            return None;
1365        }
1366        let users = self.users.read().ok()?;
1367        // A disabled account keeps its verifier (re-enable restores it)
1368        // but must not authenticate; returning `None` here makes the
1369        // handshake use the dummy verifier, so timing stays uniform.
1370        users
1371            .get(id)
1372            .filter(|u| u.enabled)
1373            .and_then(|u| u.scram_verifier.clone())
1374    }
1375
1376    /// Backwards-compatible shim for the v2 wire bootstrap path: looks
1377    /// up a user by username assuming the platform (`tenant=None`)
1378    /// scope. Use this only where the handshake hasn't yet learned the
1379    /// caller's tenant.
1380    pub fn lookup_scram_verifier_global(
1381        &self,
1382        username: &str,
1383    ) -> Option<crate::auth::scram::ScramVerifier> {
1384        self.lookup_scram_verifier(&UserId::platform(username))
1385    }
1386
1387    /// Return all users (password hashes redacted).
1388    ///
1389    /// The synthetic [`crate::auth::self_lock_guard::PLATFORM_OWNER_USERNAME`]
1390    /// principal is filtered out — it is a policy-graph anchor, not an
1391    /// operator-visible account.
1392    pub fn list_users(&self) -> Vec<User> {
1393        let users = match self.users.read() {
1394            Ok(g) => g,
1395            Err(_) => return Vec::new(),
1396        };
1397        users
1398            .values()
1399            .filter(|u| !crate::auth::self_lock_guard::is_synthetic_principal(&u.username))
1400            .map(|u| User {
1401                password_hash: String::new(), // redacted
1402                ..u.clone()
1403            })
1404            .collect()
1405    }
1406
1407    /// Return users restricted to a tenant scope.
1408    ///
1409    /// `tenant_filter`:
1410    ///   - `None` listing in `Some(None)` — only platform users
1411    ///   - `Some(Some("acme"))` — only users in tenant `acme`
1412    ///   - `None` argument — all users (admin-only callers)
1413    pub fn list_users_scoped(&self, tenant_filter: Option<Option<&str>>) -> Vec<User> {
1414        let users = match self.users.read() {
1415            Ok(g) => g,
1416            Err(_) => return Vec::new(),
1417        };
1418        users
1419            .values()
1420            .filter(|u| match tenant_filter {
1421                None => true,
1422                Some(t) => u.tenant_id.as_deref() == t,
1423            })
1424            .filter(|u| !crate::auth::self_lock_guard::is_synthetic_principal(&u.username))
1425            .map(|u| User {
1426                password_hash: String::new(), // redacted
1427                ..u.clone()
1428            })
1429            .collect()
1430    }
1431
1432    /// Resource shape used by IAM policies that govern user lifecycle
1433    /// mutations. Platform users are addressed as `user:<username>`;
1434    /// tenant users are addressed as `user:tenant/<tenant>/<username>`
1435    /// by the existing policy resource qualifier.
1436    pub fn user_lifecycle_resource(uid: &UserId) -> ResourceRef {
1437        let resource = ResourceRef::new("user", uid.username.clone());
1438        match &uid.tenant {
1439            Some(tenant) => resource.with_tenant(tenant.clone()),
1440            None => resource,
1441        }
1442    }
1443
1444    pub fn eval_context_for_principal(
1445        &self,
1446        principal: &UserId,
1447        role: Role,
1448        current_tenant: Option<String>,
1449    ) -> EvalContext {
1450        EvalContext {
1451            principal_tenant: principal.tenant.clone(),
1452            current_tenant,
1453            peer_ip: None,
1454            mfa_present: false,
1455            now_ms: now_ms(),
1456            principal_is_admin_role: role == Role::Admin,
1457            principal_is_platform_scoped: principal.tenant.is_none(),
1458        }
1459    }
1460
1461    /// Policy gate for user lifecycle mutations.
1462    ///
1463    /// This is intentionally separate from the store mutation methods:
1464    /// bootstrap and manifest loading can still seed users directly,
1465    /// while public surfaces must authorize `user:*` before calling the
1466    /// mutator. Explicit Deny wins even in `LegacyRbac` mode.
1467    pub fn check_user_lifecycle_authz(
1468        &self,
1469        actor: &UserId,
1470        actor_role: Role,
1471        action: &str,
1472        target: &UserId,
1473    ) -> bool {
1474        let resource = Self::user_lifecycle_resource(target);
1475        let ctx = self.eval_context_for_principal(actor, actor_role, target.tenant.clone());
1476        self.check_policy_authz_with_role(actor, action, &resource, &ctx, actor_role)
1477    }
1478
1479    /// Returns `true` only when an IAM policy explicitly denies the
1480    /// action on the target. `DefaultDeny` (no matching policy) is
1481    /// treated as permitted, so this is safe to call after the caller
1482    /// has already verified the actor's role — it only blocks protected
1483    /// principals (e.g. the cloud-preset `red_admin` guardrail).
1484    pub fn has_explicit_user_lifecycle_deny(
1485        &self,
1486        actor: &UserId,
1487        actor_role: Role,
1488        action: &str,
1489        target: &UserId,
1490    ) -> bool {
1491        let resource = Self::user_lifecycle_resource(target);
1492        let ctx = self.eval_context_for_principal(actor, actor_role, target.tenant.clone());
1493        let pols = self.effective_policies(actor);
1494        let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
1495        matches!(
1496            iam_policies::evaluate(&refs, action, &resource, &ctx),
1497            iam_policies::Decision::Deny { .. }
1498        )
1499    }
1500
1501    pub fn get_user(&self, tenant_id: Option<&str>, username: &str) -> Option<User> {
1502        let id = UserId::from_parts(tenant_id, username);
1503        self.get_user_cloned(&id).map(|u| User {
1504            password_hash: String::new(),
1505            ..u
1506        })
1507    }
1508
1509    /// Delete a platform-scoped user (`tenant_id = None`) and revoke
1510    /// all of their API keys + sessions.
1511    ///
1512    /// For tenant-scoped deletes, use [`Self::delete_user_in_tenant`].
1513    pub fn delete_user(&self, username: &str) -> Result<(), AuthError> {
1514        self.delete_user_in_tenant(None, username)
1515    }
1516
1517    pub fn delete_user_with_control_events(
1518        &self,
1519        username: &str,
1520        ctx: &ControlEventCtx<'_>,
1521        ledger: &dyn ControlEventLedger,
1522        config: ControlEventConfig,
1523    ) -> Result<(), AuthError> {
1524        self.delete_user_in_tenant_with_control_events(None, username, ctx, ledger, config)
1525    }
1526
1527    /// Delete a user identified by `(tenant_id, username)` and revoke
1528    /// all of their API keys + sessions.
1529    pub fn delete_user_in_tenant(
1530        &self,
1531        tenant_id: Option<&str>,
1532        username: &str,
1533    ) -> Result<(), AuthError> {
1534        if let Some(configured) = self.configured_control_events() {
1535            let ctx = default_user_lifecycle_ctx();
1536            let control = UserLifecycleControl {
1537                ctx: &ctx,
1538                ledger: configured.ledger.as_ref(),
1539                config: configured.config,
1540            };
1541            self.delete_user_in_tenant_controlled(tenant_id, username, &control)
1542        } else {
1543            self.delete_user_in_tenant_unaudited(tenant_id, username)
1544        }
1545    }
1546
1547    pub fn delete_user_in_tenant_with_control_events(
1548        &self,
1549        tenant_id: Option<&str>,
1550        username: &str,
1551        ctx: &ControlEventCtx<'_>,
1552        ledger: &dyn ControlEventLedger,
1553        config: ControlEventConfig,
1554    ) -> Result<(), AuthError> {
1555        let control = UserLifecycleControl {
1556            ctx,
1557            ledger,
1558            config,
1559        };
1560        self.delete_user_in_tenant_controlled(tenant_id, username, &control)
1561    }
1562
1563    fn delete_user_in_tenant_unaudited(
1564        &self,
1565        tenant_id: Option<&str>,
1566        username: &str,
1567    ) -> Result<(), AuthError> {
1568        let id = UserId::from_parts(tenant_id, username);
1569        let mut users = self.users.write().map_err(lock_err)?;
1570        let user = users
1571            .remove(&id)
1572            .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1573
1574        // Remove API key index entries.
1575        if let Ok(mut idx) = self.api_key_index.write() {
1576            for api_key in &user.api_keys {
1577                idx.remove(&api_key.key);
1578            }
1579        }
1580
1581        // Remove sessions belonging to this user (match on tenant+username
1582        // so we don't tear down a same-named user in another tenant).
1583        if let Ok(mut sessions) = self.sessions.write() {
1584            sessions
1585                .retain(|_, s| !(s.username == username && s.tenant_id.as_deref() == tenant_id));
1586        }
1587
1588        self.persist_to_vault();
1589        Ok(())
1590    }
1591
1592    fn delete_user_in_tenant_controlled(
1593        &self,
1594        tenant_id: Option<&str>,
1595        username: &str,
1596        control: &UserLifecycleControl<'_>,
1597    ) -> Result<(), AuthError> {
1598        let id = UserId::from_parts(tenant_id, username);
1599        let rollback = self.user_delete_rollback_snapshot(&id, tenant_id, username);
1600        match self.delete_user_in_tenant_unaudited(tenant_id, username) {
1601            Ok(()) => {
1602                if let Err(err) = self.emit_user_lifecycle_allowed(
1603                    control,
1604                    EventKind::UserDelete,
1605                    "user.delete",
1606                    &id,
1607                    user_control_fields(&id, rollback.as_ref().map(|r| r.0.role), None, false),
1608                ) {
1609                    if let Some((user, sessions)) = rollback {
1610                        self.restore_deleted_user(&id, user, sessions);
1611                    }
1612                    return Err(err);
1613                }
1614                Ok(())
1615            }
1616            Err(err) => {
1617                self.emit_user_lifecycle_outcome(
1618                    control,
1619                    if user_error_is_denied(&err) {
1620                        Outcome::Denied
1621                    } else {
1622                        Outcome::Error
1623                    },
1624                    EventKind::UserDelete,
1625                    "user.delete",
1626                    &id,
1627                    Some(err.to_string()),
1628                    user_control_fields(&id, rollback.as_ref().map(|r| r.0.role), None, false),
1629                );
1630                Err(err)
1631            }
1632        }
1633    }
1634
1635    /// Change password (requires the old password). Defaults to
1636    /// platform tenant; use [`Self::change_password_in_tenant`] for
1637    /// scoped users.
1638    pub fn change_password(
1639        &self,
1640        username: &str,
1641        old_password: &str,
1642        new_password: &str,
1643    ) -> Result<(), AuthError> {
1644        self.change_password_in_tenant(None, username, old_password, new_password)
1645    }
1646
1647    pub fn change_password_with_control_events(
1648        &self,
1649        username: &str,
1650        old_password: &str,
1651        new_password: &str,
1652        ctx: &ControlEventCtx<'_>,
1653        ledger: &dyn ControlEventLedger,
1654        config: ControlEventConfig,
1655    ) -> Result<(), AuthError> {
1656        self.change_password_in_tenant_with_control_events(
1657            None,
1658            username,
1659            old_password,
1660            new_password,
1661            ctx,
1662            ledger,
1663            config,
1664        )
1665    }
1666
1667    #[allow(clippy::too_many_arguments)]
1668    pub fn change_password_in_tenant(
1669        &self,
1670        tenant_id: Option<&str>,
1671        username: &str,
1672        old_password: &str,
1673        new_password: &str,
1674    ) -> Result<(), AuthError> {
1675        if let Some(configured) = self.configured_control_events() {
1676            let ctx = default_user_lifecycle_ctx();
1677            let control = UserLifecycleControl {
1678                ctx: &ctx,
1679                ledger: configured.ledger.as_ref(),
1680                config: configured.config,
1681            };
1682            self.change_password_in_tenant_controlled(
1683                tenant_id,
1684                username,
1685                old_password,
1686                new_password,
1687                &control,
1688            )
1689        } else {
1690            self.change_password_in_tenant_unaudited(
1691                tenant_id,
1692                username,
1693                old_password,
1694                new_password,
1695            )
1696        }
1697    }
1698
1699    #[allow(clippy::too_many_arguments)]
1700    pub fn change_password_in_tenant_with_control_events(
1701        &self,
1702        tenant_id: Option<&str>,
1703        username: &str,
1704        old_password: &str,
1705        new_password: &str,
1706        ctx: &ControlEventCtx<'_>,
1707        ledger: &dyn ControlEventLedger,
1708        config: ControlEventConfig,
1709    ) -> Result<(), AuthError> {
1710        let control = UserLifecycleControl {
1711            ctx,
1712            ledger,
1713            config,
1714        };
1715        self.change_password_in_tenant_controlled(
1716            tenant_id,
1717            username,
1718            old_password,
1719            new_password,
1720            &control,
1721        )
1722    }
1723
1724    fn change_password_in_tenant_unaudited(
1725        &self,
1726        tenant_id: Option<&str>,
1727        username: &str,
1728        old_password: &str,
1729        new_password: &str,
1730    ) -> Result<(), AuthError> {
1731        let id = UserId::from_parts(tenant_id, username);
1732        let mut users = self.users.write().map_err(lock_err)?;
1733        let user = users
1734            .get_mut(&id)
1735            .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1736
1737        if !verify_password(old_password, &user.password_hash) {
1738            return Err(AuthError::InvalidCredentials);
1739        }
1740
1741        user.password_hash = hash_password(new_password);
1742        user.scram_verifier = Some(make_scram_verifier(new_password));
1743        user.updated_at = now_ms();
1744        drop(users); // release lock before vault I/O
1745        self.persist_to_vault();
1746        Ok(())
1747    }
1748
1749    /// Administrative password reset (`ALTER USER … PASSWORD '…'`): no
1750    /// old-password check. Rehashes, regenerates the SCRAM verifier and
1751    /// revokes every live session of the account so a rotated credential
1752    /// cannot keep an existing session alive. Callers gate authorization.
1753    pub fn set_password_by_admin(&self, id: &UserId, new_password: &str) -> Result<(), AuthError> {
1754        if new_password.is_empty() {
1755            return Err(AuthError::Forbidden("password cannot be empty".to_string()));
1756        }
1757        {
1758            let mut users = self.users.write().map_err(lock_err)?;
1759            let user = users
1760                .get_mut(id)
1761                .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1762            user.password_hash = hash_password(new_password);
1763            user.scram_verifier = Some(make_scram_verifier(new_password));
1764            user.updated_at = now_ms();
1765        }
1766        self.purge_sessions_for(id);
1767        self.persist_to_vault();
1768        Ok(())
1769    }
1770
1771    fn change_password_in_tenant_controlled(
1772        &self,
1773        tenant_id: Option<&str>,
1774        username: &str,
1775        old_password: &str,
1776        new_password: &str,
1777        control: &UserLifecycleControl<'_>,
1778    ) -> Result<(), AuthError> {
1779        let id = UserId::from_parts(tenant_id, username);
1780        let previous = self.get_user_cloned(&id);
1781        match self.change_password_in_tenant_unaudited(
1782            tenant_id,
1783            username,
1784            old_password,
1785            new_password,
1786        ) {
1787            Ok(()) => {
1788                if let Err(err) = self.emit_user_lifecycle_allowed(
1789                    control,
1790                    EventKind::UserUpdate,
1791                    "user.update",
1792                    &id,
1793                    user_control_fields(
1794                        &id,
1795                        previous.as_ref().map(|u| u.role),
1796                        previous.as_ref().map(|u| u.enabled),
1797                        true,
1798                    ),
1799                ) {
1800                    self.restore_user_snapshot(&id, previous);
1801                    return Err(err);
1802                }
1803                Ok(())
1804            }
1805            Err(err) => {
1806                self.emit_user_lifecycle_outcome(
1807                    control,
1808                    if user_error_is_denied(&err) {
1809                        Outcome::Denied
1810                    } else {
1811                        Outcome::Error
1812                    },
1813                    EventKind::UserUpdate,
1814                    "user.update",
1815                    &id,
1816                    Some(err.to_string()),
1817                    user_control_fields(
1818                        &id,
1819                        previous.as_ref().map(|u| u.role),
1820                        previous.as_ref().map(|u| u.enabled),
1821                        true,
1822                    ),
1823                );
1824                Err(err)
1825            }
1826        }
1827    }
1828
1829    /// Change a user's role (admin-only operation). Defaults to platform
1830    /// tenant; use [`Self::change_role_in_tenant`] for scoped users.
1831    pub fn change_role(&self, username: &str, new_role: Role) -> Result<(), AuthError> {
1832        self.change_role_in_tenant(None, username, new_role)
1833    }
1834
1835    pub fn change_role_in_tenant(
1836        &self,
1837        tenant_id: Option<&str>,
1838        username: &str,
1839        new_role: Role,
1840    ) -> Result<(), AuthError> {
1841        if let Some(configured) = self.configured_control_events() {
1842            let ctx = default_user_lifecycle_ctx();
1843            let control = UserLifecycleControl {
1844                ctx: &ctx,
1845                ledger: configured.ledger.as_ref(),
1846                config: configured.config,
1847            };
1848            self.change_role_in_tenant_controlled(tenant_id, username, new_role, &control)
1849        } else {
1850            self.change_role_in_tenant_unaudited(tenant_id, username, new_role)
1851        }
1852    }
1853
1854    pub fn change_role_in_tenant_with_control_events(
1855        &self,
1856        tenant_id: Option<&str>,
1857        username: &str,
1858        new_role: Role,
1859        ctx: &ControlEventCtx<'_>,
1860        ledger: &dyn ControlEventLedger,
1861        config: ControlEventConfig,
1862    ) -> Result<(), AuthError> {
1863        let control = UserLifecycleControl {
1864            ctx,
1865            ledger,
1866            config,
1867        };
1868        self.change_role_in_tenant_controlled(tenant_id, username, new_role, &control)
1869    }
1870
1871    fn change_role_in_tenant_unaudited(
1872        &self,
1873        tenant_id: Option<&str>,
1874        username: &str,
1875        new_role: Role,
1876    ) -> Result<(), AuthError> {
1877        let id = UserId::from_parts(tenant_id, username);
1878        let mut users = self.users.write().map_err(lock_err)?;
1879        let user = users
1880            .get_mut(&id)
1881            .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1882
1883        let prior_role = user.role;
1884        user.role = new_role;
1885        user.updated_at = now_ms();
1886
1887        // Issue #205 — promotion to Admin is an operator-grade event:
1888        // the new role grants destructive capabilities (DROP, ALTER,
1889        // GRANT) that an operator must observe out-of-band even when
1890        // the auth path itself is healthy.
1891        if new_role == Role::Admin && prior_role != Role::Admin {
1892            crate::telemetry::operator_event::OperatorEvent::AdminCapabilityGranted {
1893                granted_to: id.to_string(),
1894                capability: "Role::Admin".to_string(),
1895                granted_by: "auth_store::change_role".to_string(),
1896            }
1897            .emit_global();
1898        }
1899
1900        // Downgrade any API keys that now exceed the user's role.
1901        for key in &mut user.api_keys {
1902            if key.role > new_role {
1903                key.role = new_role;
1904            }
1905        }
1906
1907        // Update the api_key_index as well.
1908        if let Ok(mut idx) = self.api_key_index.write() {
1909            for key in &user.api_keys {
1910                if let Some(entry) = idx.get_mut(&key.key) {
1911                    entry.1 = key.role;
1912                }
1913            }
1914        }
1915
1916        // Sessions snapshot the role at login. A change in either direction
1917        // must not keep riding on a token minted under the old one — a
1918        // demotion would otherwise stay ineffective for the session TTL.
1919        if prior_role != new_role {
1920            self.purge_sessions_for(&id);
1921        }
1922
1923        self.persist_to_vault();
1924        Ok(())
1925    }
1926
1927    fn change_role_in_tenant_controlled(
1928        &self,
1929        tenant_id: Option<&str>,
1930        username: &str,
1931        new_role: Role,
1932        control: &UserLifecycleControl<'_>,
1933    ) -> Result<(), AuthError> {
1934        let id = UserId::from_parts(tenant_id, username);
1935        let previous = self.get_user_cloned(&id);
1936        match self.change_role_in_tenant_unaudited(tenant_id, username, new_role) {
1937            Ok(()) => {
1938                if let Err(err) = self.emit_user_lifecycle_allowed(
1939                    control,
1940                    EventKind::UserUpdate,
1941                    "user.update",
1942                    &id,
1943                    user_control_fields(
1944                        &id,
1945                        Some(new_role),
1946                        previous.as_ref().map(|u| u.enabled),
1947                        false,
1948                    ),
1949                ) {
1950                    self.restore_user_snapshot(&id, previous);
1951                    return Err(err);
1952                }
1953                Ok(())
1954            }
1955            Err(err) => {
1956                self.emit_user_lifecycle_outcome(
1957                    control,
1958                    if user_error_is_denied(&err) {
1959                        Outcome::Denied
1960                    } else {
1961                        Outcome::Error
1962                    },
1963                    EventKind::UserUpdate,
1964                    "user.update",
1965                    &id,
1966                    Some(err.to_string()),
1967                    user_control_fields(
1968                        &id,
1969                        Some(new_role),
1970                        previous.as_ref().map(|u| u.enabled),
1971                        false,
1972                    ),
1973                );
1974                Err(err)
1975            }
1976        }
1977    }
1978
1979    // -----------------------------------------------------------------
1980    // Authentication (login)
1981    // -----------------------------------------------------------------
1982
1983    /// Verify credentials for a platform-tenant user (`tenant_id = None`)
1984    /// and create a session. For tenant-scoped login use
1985    /// [`Self::authenticate_in_tenant`].
1986    ///
1987    /// When a keypair is available (certificate-based seal), session tokens
1988    /// are signed with the master secret so the server can verify they were
1989    /// genuinely issued by this vault instance.
1990    pub fn authenticate(&self, username: &str, password: &str) -> Result<Session, AuthError> {
1991        self.authenticate_in_tenant(None, username, password)
1992    }
1993
1994    /// Verify credentials for `(tenant_id, username, password)` and
1995    /// create a session. Tenant-aware: `alice@acme` and `alice@globex`
1996    /// authenticate independently.
1997    ///
1998    /// Every outcome is recorded in the control-event ledger when one is
1999    /// configured: login was the one credential decision with no chained
2000    /// trace, so a brute-force run or a login from a freshly re-enabled
2001    /// account was invisible to an audit of the store. Under a
2002    /// compliance-mode ledger a login that cannot be recorded is refused.
2003    pub fn authenticate_in_tenant(
2004        &self,
2005        tenant_id: Option<&str>,
2006        username: &str,
2007        password: &str,
2008    ) -> Result<Session, AuthError> {
2009        let id = UserId::from_parts(tenant_id, username);
2010        match self.authenticate_in_tenant_unaudited(tenant_id, username, password) {
2011            Ok(session) => {
2012                if let Err(err) = self.emit_auth_event(
2013                    EventKind::AuthLogin,
2014                    Outcome::Allowed,
2015                    &id,
2016                    Some(session.role),
2017                    None,
2018                ) {
2019                    if let Ok(mut sessions) = self.sessions.write() {
2020                        sessions.remove(&session.token);
2021                    }
2022                    return Err(err);
2023                }
2024                Ok(session)
2025            }
2026            Err(err) => {
2027                let outcome = if matches!(err, AuthError::InvalidCredentials) {
2028                    Outcome::Denied
2029                } else {
2030                    Outcome::Error
2031                };
2032                // Already refusing; a ledger failure cannot make it worse.
2033                let _ = self.emit_auth_event(
2034                    EventKind::AuthLoginFailed,
2035                    outcome,
2036                    &id,
2037                    None,
2038                    Some(&err.to_string()),
2039                );
2040                Err(err)
2041            }
2042        }
2043    }
2044
2045    /// Record one authentication decision. `Err` only when the ledger is
2046    /// configured to require persistence and refused the event.
2047    fn emit_auth_event(
2048        &self,
2049        kind: EventKind,
2050        outcome: Outcome,
2051        id: &UserId,
2052        role: Option<Role>,
2053        reason: Option<&str>,
2054    ) -> Result<(), AuthError> {
2055        let Some(configured) = self.configured_control_events() else {
2056            return Ok(());
2057        };
2058        let ctx = ControlEventCtx {
2059            actor: crate::runtime::control_events::ActorRef::User(id),
2060            scope: id.tenant.as_deref().map(Cow::Borrowed),
2061            request_id: None,
2062            trace_id: None,
2063        };
2064        let mut fields: HashMap<String, Sensitivity> = HashMap::new();
2065        fields.insert("principal".to_string(), Sensitivity::raw(id.to_string()));
2066        if let Some(role) = role {
2067            fields.insert("role".to_string(), Sensitivity::raw(role.as_str()));
2068        }
2069        let event = ControlEvent {
2070            kind,
2071            outcome,
2072            action: Cow::Borrowed(kind.as_str()),
2073            resource: Some(format!("principal:{id}")),
2074            reason: reason.map(str::to_string),
2075            matched_policy_id: None,
2076            fields,
2077        };
2078        match configured.ledger.emit(&ctx, event) {
2079            Ok(_) => Ok(()),
2080            Err(err) if configured.config.require_persistence() => Err(AuthError::Internal(
2081                format!("control-event ledger refused {}: {err}", kind.as_str()),
2082            )),
2083            Err(err) => {
2084                tracing::warn!(
2085                    target: "reddb::security",
2086                    kind = kind.as_str(),
2087                    error = %err,
2088                    "control-event ledger refused an auth event"
2089                );
2090                Ok(())
2091            }
2092        }
2093    }
2094
2095    fn authenticate_in_tenant_unaudited(
2096        &self,
2097        tenant_id: Option<&str>,
2098        username: &str,
2099        password: &str,
2100    ) -> Result<Session, AuthError> {
2101        // The synthetic platform-owner principal exists only as a
2102        // policy-graph anchor (see `crate::auth::self_lock_guard`); it
2103        // must never authenticate via any transport.
2104        if crate::auth::self_lock_guard::is_synthetic_principal(username) {
2105            return Err(AuthError::InvalidCredentials);
2106        }
2107        let id = UserId::from_parts(tenant_id, username);
2108        let users = self.users.read().map_err(lock_err)?;
2109        let user = users.get(&id).ok_or(AuthError::InvalidCredentials)?;
2110
2111        if !user.enabled {
2112            return Err(AuthError::InvalidCredentials);
2113        }
2114
2115        let stored_hash = user.password_hash.clone();
2116        if !verify_password(password, &stored_hash) {
2117            return Err(AuthError::InvalidCredentials);
2118        }
2119
2120        // Generate token: signed if keypair is available, random otherwise.
2121        let token = match self.keypair.read().ok().and_then(|g| {
2122            g.as_ref().map(|kp| {
2123                let token_id = random_hex(16);
2124                let sig = kp.sign(format!("session:{}", token_id).as_bytes());
2125                // Take first 16 bytes of signature for compact token.
2126                format!("rs_{}{}", token_id, hex::encode(&sig[..16]))
2127            })
2128        }) {
2129            Some(signed_token) => signed_token,
2130            None => generate_session_token(),
2131        };
2132
2133        let now = now_ms();
2134        let session = Session {
2135            token,
2136            username: username.to_string(),
2137            tenant_id: user.tenant_id.clone(),
2138            role: user.role,
2139            created_at: now,
2140            expires_at: now + (self.config.session_ttl_secs as u128 * 1000),
2141        };
2142
2143        drop(users); // release read lock before acquiring write
2144
2145        let mut sessions = self.sessions.write().map_err(lock_err)?;
2146        sessions.insert(session.token.clone(), session.clone());
2147        drop(sessions);
2148
2149        // Transparently upgrade a hash produced by the retired in-tree KDF.
2150        // This is the only point where the verified plaintext is in hand.
2151        if password_hash_needs_upgrade(&stored_hash) {
2152            let upgraded = hash_password(password);
2153            let mut rehashed = false;
2154            if let Ok(mut users) = self.users.write() {
2155                if let Some(user) = users.get_mut(&id) {
2156                    // Skip if a concurrent password change already moved it.
2157                    if user.password_hash == stored_hash {
2158                        user.password_hash = upgraded;
2159                        rehashed = true;
2160                    }
2161                }
2162            }
2163            if rehashed {
2164                self.persist_to_vault();
2165            }
2166        }
2167
2168        Ok(session)
2169    }
2170
2171    // -----------------------------------------------------------------
2172    // Token validation
2173    // -----------------------------------------------------------------
2174
2175    /// Validate a token (session or API key).
2176    ///
2177    /// Returns `(username, role)` if valid, `None` otherwise. Tenant
2178    /// scope is dropped here for compatibility with the bulk of the
2179    /// existing caller surface (routing, gRPC control, redwire). Use
2180    /// [`Self::validate_token_full`] when the caller needs the
2181    /// resolved `UserId` (e.g. to pin `CURRENT_TENANT()`).
2182    pub fn validate_token(&self, token: &str) -> Option<(String, Role)> {
2183        self.validate_token_full(token)
2184            .map(|(id, role)| (id.username, role))
2185    }
2186
2187    /// Tenant-aware token validation. Returns the resolved `UserId`
2188    /// (which carries the tenant) and the granted `Role`.
2189    pub fn validate_token_full(&self, token: &str) -> Option<(UserId, Role)> {
2190        let resolved = self.resolve_token_unchecked(token)?;
2191        // Every credential is re-checked against the live user record so
2192        // `ALTER USER … DISABLE` / `VALID UNTIL` take effect immediately for
2193        // sessions and API keys, not only at password login.
2194        if !self.principal_is_active(&resolved.0) {
2195            return None;
2196        }
2197        Some(resolved)
2198    }
2199
2200    /// Token → `(UserId, Role)` without consulting the owning user record.
2201    fn resolve_token_unchecked(&self, token: &str) -> Option<(UserId, Role)> {
2202        // Try session tokens first.
2203        if token.starts_with("rs_") {
2204            if let Ok(sessions) = self.sessions.read() {
2205                if let Some(session) = sessions.get(token) {
2206                    let now = now_ms();
2207                    if now < session.expires_at {
2208                        return Some((
2209                            UserId::from_parts(session.tenant_id.as_deref(), &session.username),
2210                            session.role,
2211                        ));
2212                    }
2213                }
2214            }
2215            return None;
2216        }
2217
2218        // Try API keys: the index is keyed by the credential id, so the
2219        // presented secret is hashed and never compared as text.
2220        if token.starts_with("rk_") {
2221            let id = api_key_credential_id(token);
2222            let (owner, role) = self
2223                .api_key_index
2224                .read()
2225                .ok()
2226                .and_then(|idx| idx.get(&id).cloned())?;
2227            if self.api_key_expired(&owner, &id) {
2228                return None;
2229            }
2230            return Some((owner, role));
2231        }
2232
2233        None
2234    }
2235
2236    /// Whether the key `id` owned by `owner` has passed its `expires_at`.
2237    /// A key the owner no longer lists counts as expired.
2238    fn api_key_expired(&self, owner: &UserId, id: &str) -> bool {
2239        let Ok(users) = self.users.read() else {
2240            return true;
2241        };
2242        users
2243            .get(owner)
2244            .and_then(|user| user.api_keys.iter().find(|key| key.key == id))
2245            .map(|key| key.expires_at.is_some_and(|deadline| now_ms() >= deadline))
2246            .unwrap_or(true)
2247    }
2248
2249    /// The current role of `id` when the account is active, else `None`.
2250    ///
2251    /// Callers holding a long-lived credential (the browser refresh cookie)
2252    /// use this to re-derive the principal's role at use time rather than
2253    /// trusting a role captured when the credential was minted.
2254    pub fn active_user_role(&self, id: &UserId) -> Option<Role> {
2255        if !self.principal_is_active(id) {
2256            return None;
2257        }
2258        self.users.read().ok()?.get(id).map(|user| user.role)
2259    }
2260
2261    /// Whether `id` names an enabled account whose `VALID UNTIL` (if any)
2262    /// has not passed. Unknown accounts are inactive.
2263    fn principal_is_active(&self, id: &UserId) -> bool {
2264        let enabled = match self.users.read() {
2265            Ok(users) => users.get(id).map(|u| u.enabled).unwrap_or(false),
2266            Err(_) => false,
2267        };
2268        if !enabled {
2269            return false;
2270        }
2271        match self.user_attributes(id).valid_until {
2272            Some(deadline) => now_ms() < deadline,
2273            None => true,
2274        }
2275    }
2276
2277    // -----------------------------------------------------------------
2278    // API Key management
2279    // -----------------------------------------------------------------
2280
2281    /// Create a persistent API key for a platform-tenant user.
2282    ///
2283    /// For tenant-scoped users use [`Self::create_api_key_in_tenant`].
2284    pub fn create_api_key(
2285        &self,
2286        username: &str,
2287        name: &str,
2288        role: Role,
2289    ) -> Result<ApiKey, AuthError> {
2290        self.create_api_key_in_tenant(None, username, name, role)
2291    }
2292
2293    pub fn create_api_key_with_control_events(
2294        &self,
2295        username: &str,
2296        name: &str,
2297        role: Role,
2298        ctx: &ControlEventCtx<'_>,
2299        ledger: &dyn ControlEventLedger,
2300        config: ControlEventConfig,
2301    ) -> Result<ApiKey, AuthError> {
2302        self.create_api_key_in_tenant_with_control_events(
2303            None, username, name, role, ctx, ledger, config,
2304        )
2305    }
2306
2307    pub fn create_api_key_in_tenant(
2308        &self,
2309        tenant_id: Option<&str>,
2310        username: &str,
2311        name: &str,
2312        role: Role,
2313    ) -> Result<ApiKey, AuthError> {
2314        self.create_api_key_in_tenant_expiring(tenant_id, username, name, role, None)
2315    }
2316
2317    /// Create an API key that stops validating at `expires_at` (ms since
2318    /// epoch). The returned value carries the secret; nothing else does.
2319    pub fn create_api_key_in_tenant_expiring(
2320        &self,
2321        tenant_id: Option<&str>,
2322        username: &str,
2323        name: &str,
2324        role: Role,
2325        expires_at: Option<u128>,
2326    ) -> Result<ApiKey, AuthError> {
2327        if let Some(configured) = self.configured_control_events() {
2328            let ctx = default_user_lifecycle_ctx();
2329            let control = UserLifecycleControl {
2330                ctx: &ctx,
2331                ledger: configured.ledger.as_ref(),
2332                config: configured.config,
2333            };
2334            self.create_api_key_in_tenant_controlled(
2335                tenant_id, username, name, role, expires_at, &control,
2336            )
2337        } else {
2338            self.create_api_key_in_tenant_unaudited(tenant_id, username, name, role, expires_at)
2339        }
2340    }
2341
2342    #[allow(clippy::too_many_arguments)]
2343    pub fn create_api_key_in_tenant_with_control_events(
2344        &self,
2345        tenant_id: Option<&str>,
2346        username: &str,
2347        name: &str,
2348        role: Role,
2349        ctx: &ControlEventCtx<'_>,
2350        ledger: &dyn ControlEventLedger,
2351        config: ControlEventConfig,
2352    ) -> Result<ApiKey, AuthError> {
2353        let control = UserLifecycleControl {
2354            ctx,
2355            ledger,
2356            config,
2357        };
2358        self.create_api_key_in_tenant_controlled(tenant_id, username, name, role, None, &control)
2359    }
2360
2361    fn create_api_key_in_tenant_unaudited(
2362        &self,
2363        tenant_id: Option<&str>,
2364        username: &str,
2365        name: &str,
2366        role: Role,
2367        expires_at: Option<u128>,
2368    ) -> Result<ApiKey, AuthError> {
2369        let id = UserId::from_parts(tenant_id, username);
2370        let mut users = self.users.write().map_err(lock_err)?;
2371        let user = users
2372            .get_mut(&id)
2373            .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
2374
2375        // The key's role cannot exceed the user's role.
2376        if role > user.role {
2377            return Err(AuthError::RoleExceeded {
2378                requested: role,
2379                ceiling: user.role,
2380            });
2381        }
2382
2383        let secret = generate_api_key();
2384        let stored = ApiKey {
2385            key: api_key_credential_id(&secret),
2386            name: name.to_string(),
2387            role,
2388            created_at: now_ms(),
2389            expires_at,
2390        };
2391
2392        user.api_keys.push(stored.clone());
2393        user.updated_at = now_ms();
2394
2395        // Update the index.
2396        if let Ok(mut idx) = self.api_key_index.write() {
2397            idx.insert(stored.key.clone(), (id.clone(), stored.role));
2398        }
2399
2400        drop(users); // release lock before vault I/O
2401        self.persist_to_vault();
2402        // The secret leaves the store exactly once, here.
2403        Ok(ApiKey {
2404            key: secret,
2405            ..stored
2406        })
2407    }
2408
2409    fn create_api_key_in_tenant_controlled(
2410        &self,
2411        tenant_id: Option<&str>,
2412        username: &str,
2413        name: &str,
2414        role: Role,
2415        expires_at: Option<u128>,
2416        control: &UserLifecycleControl<'_>,
2417    ) -> Result<ApiKey, AuthError> {
2418        let id = UserId::from_parts(tenant_id, username);
2419        match self.create_api_key_in_tenant_unaudited(tenant_id, username, name, role, expires_at) {
2420            Ok(api_key) => {
2421                let key_id = api_key_id(&api_key.key);
2422                if let Err(err) = self.emit_api_key_allowed(
2423                    control,
2424                    EventKind::ApiKeyCreate,
2425                    "apikey.create",
2426                    &key_id,
2427                    api_key_control_fields(&id, role, &key_id),
2428                ) {
2429                    self.rollback_create_api_key(&id, &api_key.key);
2430                    return Err(err);
2431                }
2432                Ok(api_key)
2433            }
2434            Err(err) => {
2435                self.emit_user_lifecycle_outcome(
2436                    control,
2437                    if user_error_is_denied(&err) {
2438                        Outcome::Denied
2439                    } else {
2440                        Outcome::Error
2441                    },
2442                    EventKind::ApiKeyCreate,
2443                    "apikey.create",
2444                    &id,
2445                    Some(err.to_string()),
2446                    user_control_fields(&id, Some(role), None, false),
2447                );
2448                Err(err)
2449            }
2450        }
2451    }
2452
2453    /// Resolve the tenant-aware owner of an API key without exposing its record.
2454    pub fn api_key_owner(&self, key: &str) -> Option<UserId> {
2455        let key_id = api_key_credential_id(key);
2456        self.api_key_index
2457            .read()
2458            .ok()
2459            .and_then(|index| index.get(&key_id).map(|(id, _)| id.clone()))
2460    }
2461
2462    /// Revoke (delete) an API key.
2463    pub fn revoke_api_key(&self, key: &str) -> Result<(), AuthError> {
2464        if let Some(configured) = self.configured_control_events() {
2465            let ctx = default_user_lifecycle_ctx();
2466            let control = UserLifecycleControl {
2467                ctx: &ctx,
2468                ledger: configured.ledger.as_ref(),
2469                config: configured.config,
2470            };
2471            self.revoke_api_key_controlled(key, &control)
2472        } else {
2473            self.revoke_api_key_unaudited(key)
2474        }
2475    }
2476
2477    pub fn revoke_api_key_with_control_events(
2478        &self,
2479        key: &str,
2480        ctx: &ControlEventCtx<'_>,
2481        ledger: &dyn ControlEventLedger,
2482        config: ControlEventConfig,
2483    ) -> Result<(), AuthError> {
2484        let control = UserLifecycleControl {
2485            ctx,
2486            ledger,
2487            config,
2488        };
2489        self.revoke_api_key_controlled(key, &control)
2490    }
2491
2492    fn revoke_api_key_unaudited(&self, key: &str) -> Result<(), AuthError> {
2493        let key_id = api_key_credential_id(key);
2494        let key = key_id.as_str();
2495        let mut users = self.users.write().map_err(lock_err)?;
2496
2497        // Find which user owns this key (look up by the api_key_index
2498        // first; fall back to a scan for legacy state restored before
2499        // the index was reseeded).
2500        let owner_id: UserId = {
2501            if let Ok(idx) = self.api_key_index.read() {
2502                if let Some((id, _)) = idx.get(key) {
2503                    id.clone()
2504                } else {
2505                    return Err(AuthError::KeyNotFound(key.to_string()));
2506                }
2507            } else {
2508                let owner = users
2509                    .iter()
2510                    .find(|(_, u)| u.api_keys.iter().any(|k| k.key == key));
2511                match owner {
2512                    Some((id, _)) => id.clone(),
2513                    None => return Err(AuthError::KeyNotFound(key.to_string())),
2514                }
2515            }
2516        };
2517
2518        let user = users
2519            .get_mut(&owner_id)
2520            .ok_or_else(|| AuthError::KeyNotFound(key.to_string()))?;
2521        user.api_keys.retain(|k| k.key != key);
2522        user.updated_at = now_ms();
2523
2524        // Remove from index.
2525        if let Ok(mut idx) = self.api_key_index.write() {
2526            idx.remove(key);
2527        }
2528
2529        self.persist_to_vault();
2530        Ok(())
2531    }
2532
2533    fn revoke_api_key_controlled(
2534        &self,
2535        key: &str,
2536        control: &UserLifecycleControl<'_>,
2537    ) -> Result<(), AuthError> {
2538        let key_id = api_key_id(key);
2539        let rollback = self.api_key_rollback_snapshot(key);
2540        let id = rollback
2541            .as_ref()
2542            .map(|r| r.0.clone())
2543            .unwrap_or_else(|| UserId::from_parts(None, ""));
2544        let role = rollback.as_ref().map(|r| r.1.role).unwrap_or(Role::Read);
2545        match self.revoke_api_key_unaudited(key) {
2546            Ok(()) => {
2547                if let Err(err) = self.emit_api_key_allowed(
2548                    control,
2549                    EventKind::ApiKeyRevoke,
2550                    "apikey.revoke",
2551                    &key_id,
2552                    api_key_control_fields(&id, role, &key_id),
2553                ) {
2554                    if let Some((owner, api_key)) = rollback {
2555                        self.restore_api_key(&owner, api_key);
2556                    }
2557                    return Err(err);
2558                }
2559                Ok(())
2560            }
2561            Err(err) => {
2562                self.emit_user_lifecycle_outcome(
2563                    control,
2564                    if user_error_is_denied(&err) {
2565                        Outcome::Denied
2566                    } else {
2567                        Outcome::Error
2568                    },
2569                    EventKind::ApiKeyRevoke,
2570                    "apikey.revoke",
2571                    &id,
2572                    Some(err.to_string()),
2573                    api_key_control_fields(&id, role, &key_id),
2574                );
2575                Err(err)
2576            }
2577        }
2578    }
2579
2580    // -----------------------------------------------------------------
2581    // Session management
2582    // -----------------------------------------------------------------
2583
2584    /// Revoke a session token.
2585    pub fn revoke_session(&self, token: &str) {
2586        if let Ok(mut sessions) = self.sessions.write() {
2587            sessions.remove(token);
2588        }
2589    }
2590
2591    /// Purge expired sessions (housekeeping).
2592    pub fn purge_expired_sessions(&self) -> usize {
2593        let now = now_ms();
2594        if let Ok(mut sessions) = self.sessions.write() {
2595            let before = sessions.len();
2596            sessions.retain(|_, s| s.expires_at > now);
2597            return before - sessions.len();
2598        }
2599        0
2600    }
2601
2602    // -----------------------------------------------------------------
2603    // Granular RBAC — GRANT / REVOKE
2604    //
2605    // The privilege engine lives in `super::privileges`. These helpers
2606    // are the AuthStore facade: they keep an in-memory map of grants per
2607    // user (plus a `public_grants` list), persist additions/removals to
2608    // the existing `vault_kv` store, and rebuild the per-user
2609    // `PermissionCache` so the hot path stays O(1).
2610    //
2611    // Persistence design: rather than extend the snapshot/restore
2612    // pipeline (Agent #2's territory) we serialise grants and account
2613    // attributes to the vault KV store. That gives us atomic write +
2614    // encrypted-at-rest semantics for free without touching the
2615    // existing USER/KEY/KV serializer paths. On restart `rehydrate_acl`
2616    // reads these KV entries back into the in-memory maps.
2617    // -----------------------------------------------------------------
2618
2619    /// Persist a grant. Returns `Forbidden` when the granting user is
2620    /// not Admin or attempts a cross-tenant grant.
2621    pub fn grant(
2622        &self,
2623        granter: &UserId,
2624        granter_role: Role,
2625        principal: GrantPrincipal,
2626        resource: Resource,
2627        actions: Vec<Action>,
2628        with_grant_option: bool,
2629        tenant: Option<String>,
2630    ) -> Result<(), AuthError> {
2631        if granter_role != Role::Admin {
2632            return Err(AuthError::Forbidden(format!(
2633                "GRANT requires Admin role; granter `{}` has `{:?}`",
2634                granter, granter_role
2635            )));
2636        }
2637
2638        // Cross-tenant guard: a tenant-scoped admin cannot mint grants
2639        // outside their tenant. Platform admin (tenant=None) may grant
2640        // anywhere.
2641        if granter.tenant.is_some() && granter.tenant != tenant {
2642            return Err(AuthError::Forbidden(format!(
2643                "cross-tenant GRANT denied: granter tenant `{:?}` != grant tenant `{:?}`",
2644                granter.tenant, tenant
2645            )));
2646        }
2647
2648        let mut actions_set = std::collections::BTreeSet::new();
2649        for a in actions {
2650            actions_set.insert(a);
2651        }
2652        let g = Grant {
2653            principal: principal.clone(),
2654            resource,
2655            actions: actions_set,
2656            with_grant_option,
2657            granted_by: granter.to_string(),
2658            granted_at: now_ms(),
2659            tenant,
2660            columns: None,
2661        };
2662
2663        match &principal {
2664            GrantPrincipal::User(uid) => {
2665                self.grants
2666                    .write()
2667                    .unwrap_or_else(|e| e.into_inner())
2668                    .entry(uid.clone())
2669                    .or_default()
2670                    .push(g.clone());
2671                self.invalidate_permission_cache(Some(uid));
2672            }
2673            GrantPrincipal::Public => {
2674                self.public_grants
2675                    .write()
2676                    .unwrap_or_else(|e| e.into_inner())
2677                    .push(g.clone());
2678                self.invalidate_permission_cache(None);
2679            }
2680            GrantPrincipal::Group(_) => {
2681                return Err(AuthError::Forbidden(
2682                    "GROUP principals are not yet supported; use a USER or PUBLIC".to_string(),
2683                ));
2684            }
2685        }
2686
2687        // Issue #119: a fresh grant changes the visible-collections set
2688        // for `(tenant, role)` callers under the same tenant. Drop those
2689        // cache entries so the next AI command sees the new SELECT
2690        // privilege immediately.
2691        self.invalidate_visible_collections_for_tenant(g.tenant.as_deref());
2692
2693        self.persist_acl_to_kv();
2694        Ok(())
2695    }
2696
2697    /// Drop matching grants from a principal. Returns the number of
2698    /// grants removed.
2699    pub fn revoke(
2700        &self,
2701        granter: &UserId,
2702        granter_role: Role,
2703        principal: &GrantPrincipal,
2704        resource: &Resource,
2705        actions: &[Action],
2706    ) -> Result<usize, AuthError> {
2707        if granter_role != Role::Admin {
2708            return Err(AuthError::Forbidden(format!(
2709                "REVOKE requires Admin role; granter `{}` has `{:?}`",
2710                granter, granter_role
2711            )));
2712        }
2713
2714        // Cross-tenant guard, mirroring GRANT: a tenant-scoped admin can only
2715        // touch grants of principals in its own tenant. The target tenant
2716        // comes from the statement text, so without this a tenant admin could
2717        // strip another tenant's grants. Platform admin (tenant=None) may
2718        // revoke anywhere.
2719        if let GrantPrincipal::User(uid) = principal {
2720            if granter.tenant.is_some() && granter.tenant != uid.tenant {
2721                return Err(AuthError::Forbidden(format!(
2722                    "cross-tenant REVOKE denied: granter tenant `{:?}` != principal tenant `{:?}`",
2723                    granter.tenant, uid.tenant
2724                )));
2725            }
2726        }
2727
2728        let removed = match principal {
2729            GrantPrincipal::User(uid) => {
2730                let mut g = self.grants.write().unwrap_or_else(|e| e.into_inner());
2731                let before = g.get(uid).map(|v| v.len()).unwrap_or(0);
2732                if let Some(list) = g.get_mut(uid) {
2733                    list.retain(|gr| {
2734                        !(gr.resource == *resource
2735                            && (actions.iter().any(|a| gr.actions.contains(a))
2736                                || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2737                    });
2738                }
2739                let after = g.get(uid).map(|v| v.len()).unwrap_or(0);
2740                drop(g);
2741                self.invalidate_permission_cache(Some(uid));
2742                before - after
2743            }
2744            GrantPrincipal::Public => {
2745                let mut p = self
2746                    .public_grants
2747                    .write()
2748                    .unwrap_or_else(|e| e.into_inner());
2749                let before = p.len();
2750                p.retain(|gr| {
2751                    !(gr.resource == *resource
2752                        && (actions.iter().any(|a| gr.actions.contains(a))
2753                            || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2754                });
2755                let after = p.len();
2756                drop(p);
2757                self.invalidate_permission_cache(None);
2758                before - after
2759            }
2760            GrantPrincipal::Group(_) => 0,
2761        };
2762
2763        if removed > 0 {
2764            // Issue #119: REVOKE may shrink the visible-collections set
2765            // for any `(tenant, role)` slot. We don't know the exact
2766            // tenant when the principal is `Public`, so a `Public`
2767            // revoke clears the whole cache; user revokes scope to the
2768            // user's tenant.
2769            match principal {
2770                GrantPrincipal::User(uid) => {
2771                    self.invalidate_visible_collections_for_tenant(uid.tenant.as_deref());
2772                }
2773                GrantPrincipal::Public | GrantPrincipal::Group(_) => {
2774                    self.invalidate_visible_collections_cache();
2775                }
2776            }
2777            self.persist_acl_to_kv();
2778        }
2779        Ok(removed)
2780    }
2781
2782    /// Compute the set of collection ids a given `(tenant, role)`
2783    /// scope can read, consulting the explicit grant table. The result
2784    /// is cached for `super::scope_cache::DEFAULT_TTL` (60s) and
2785    /// invalidated on every GRANT/REVOKE/policy/collection mutation
2786    /// that could change the answer.
2787    ///
2788    /// `all_collections` is the full list of collection ids known to
2789    /// the storage layer. The runtime hands it in so this module stays
2790    /// decoupled from the storage crate. Each collection passes through
2791    /// `check_grant(SELECT)` under a synthetic `(principal, role,
2792    /// tenant)` view. The cache key includes principal because direct
2793    /// grants can differ between users that share the same tenant and
2794    /// role.
2795    pub fn visible_collections_for_scope(
2796        &self,
2797        tenant: Option<&str>,
2798        role: Role,
2799        principal: &str,
2800        all_collections: &[String],
2801    ) -> std::collections::HashSet<String> {
2802        let key = super::scope_cache::ScopeKey::new(tenant, principal, role);
2803        if let Some(hit) = self.visible_collections_cache.get(&key) {
2804            return hit;
2805        }
2806        // Slow path: walk every collection through `check_grant`. We
2807        // build the AuthzContext once, then reuse it per resource.
2808        let ctx = AuthzContext {
2809            principal,
2810            effective_role: role,
2811            tenant,
2812        };
2813        let mut visible = std::collections::HashSet::new();
2814        for collection in all_collections {
2815            let resource = Resource::table_from_name(collection);
2816            if self.check_grant(&ctx, Action::Select, &resource).is_ok() {
2817                visible.insert(collection.clone());
2818            }
2819        }
2820        self.visible_collections_cache.insert(key, visible.clone());
2821        visible
2822    }
2823
2824    /// Stats probe required by issue #119 — exposes hit/miss counts and
2825    /// invalidations for the visible-collections cache so metrics
2826    /// pipelines can compute a hit rate.
2827    pub fn auth_cache_stats(&self) -> super::scope_cache::AuthCacheStats {
2828        self.visible_collections_cache.stats()
2829    }
2830
2831    /// Drop every cached `(tenant, role)` entry. Called from CREATE
2832    /// POLICY / DROP POLICY / DROP COLLECTION paths where the affected
2833    /// tenant set is unknown.
2834    pub fn invalidate_visible_collections_cache(&self) {
2835        self.visible_collections_cache.invalidate_all();
2836    }
2837
2838    /// Drop cached entries for one tenant. Called from GRANT / REVOKE
2839    /// where the principal's tenant is known.
2840    pub fn invalidate_visible_collections_for_tenant(&self, tenant: Option<&str>) {
2841        self.visible_collections_cache.invalidate_tenant(tenant);
2842    }
2843
2844    /// Snapshot of every grant the principal effectively has, including
2845    /// `Public` grants. Audit / introspection helper.
2846    pub fn effective_grants(&self, uid: &UserId) -> Vec<Grant> {
2847        let mut out = Vec::new();
2848        if let Ok(g) = self.grants.read() {
2849            if let Some(list) = g.get(uid) {
2850                out.extend(list.iter().cloned());
2851            }
2852        }
2853        if let Ok(p) = self.public_grants.read() {
2854            out.extend(p.iter().cloned());
2855        }
2856        out
2857    }
2858
2859    /// Run a privilege check using the in-memory grant tables. Returns
2860    /// `Ok(())` on allow, `Err(AuthzError)` on deny.
2861    pub fn check_grant(
2862        &self,
2863        ctx: &AuthzContext<'_>,
2864        action: Action,
2865        resource: &Resource,
2866    ) -> Result<(), AuthzError> {
2867        if ctx.effective_role == Role::Admin {
2868            return Ok(());
2869        }
2870
2871        let uid = UserId::from_parts(ctx.tenant, ctx.principal);
2872
2873        // Fast path: per-user pre-resolved cache.
2874        if let Ok(cache) = self.permission_cache.read() {
2875            if let Some(pc) = cache.get(&uid) {
2876                if pc.allows(resource, action) {
2877                    return Ok(());
2878                }
2879            }
2880        }
2881
2882        // Slow path: linear scan + rebuild cache as a side-effect.
2883        let user_grants = self
2884            .grants
2885            .read()
2886            .ok()
2887            .and_then(|g| g.get(&uid).cloned())
2888            .unwrap_or_default();
2889        let any_user_grants = self
2890            .grants
2891            .read()
2892            .ok()
2893            .map(|g| g.values().any(|list| !list.is_empty()))
2894            .unwrap_or(false);
2895        let public_grants = self
2896            .public_grants
2897            .read()
2898            .ok()
2899            .map(|p| p.clone())
2900            .unwrap_or_default();
2901        if user_grants.is_empty() && public_grants.is_empty() && any_user_grants {
2902            return Err(AuthzError::PermissionDenied {
2903                action,
2904                resource: resource.clone(),
2905                principal: ctx.principal.to_string(),
2906            });
2907        }
2908        let view = GrantsView {
2909            user_grants: &user_grants,
2910            public_grants: &public_grants,
2911        };
2912        let result = check_grant(ctx, action, resource, &view);
2913
2914        if result.is_ok() {
2915            let pc = PermissionCache::build(&user_grants, &public_grants);
2916            if let Ok(mut cache) = self.permission_cache.write() {
2917                cache.insert(uid, pc);
2918            }
2919        }
2920        result
2921    }
2922
2923    // -----------------------------------------------------------------
2924    // ALTER USER attributes (VALID UNTIL, CONNECTION LIMIT, etc.)
2925    // -----------------------------------------------------------------
2926
2927    /// Replace the attribute record for `uid`.
2928    pub fn set_user_attributes(
2929        &self,
2930        uid: &UserId,
2931        attrs: UserAttributes,
2932    ) -> Result<(), AuthError> {
2933        let users = self.users.read().map_err(lock_err)?;
2934        if !users.contains_key(uid) {
2935            return Err(AuthError::UserNotFound(uid.to_string()));
2936        }
2937        drop(users);
2938
2939        self.user_attributes
2940            .write()
2941            .unwrap_or_else(|e| e.into_inner())
2942            .insert(uid.clone(), attrs);
2943        self.invalidate_iam_cache(Some(uid));
2944        self.persist_acl_to_kv();
2945        Ok(())
2946    }
2947
2948    /// Read the attributes for `uid`. Returns `Default::default()` for
2949    /// users that have never been altered.
2950    pub fn user_attributes(&self, uid: &UserId) -> UserAttributes {
2951        self.user_attributes
2952            .read()
2953            .ok()
2954            .and_then(|m| m.get(uid).cloned())
2955            .unwrap_or_default()
2956    }
2957
2958    pub fn add_user_to_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
2959        if group.trim().is_empty() {
2960            return Err(AuthError::Forbidden("group name cannot be empty".into()));
2961        }
2962        let mut attrs = self.user_attributes(uid);
2963        if !attrs.groups.iter().any(|g| g == group) {
2964            attrs.groups.push(group.to_string());
2965            attrs.groups.sort();
2966        }
2967        self.set_user_attributes(uid, attrs)
2968    }
2969
2970    pub fn remove_user_from_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
2971        let mut attrs = self.user_attributes(uid);
2972        attrs.groups.retain(|g| g != group);
2973        self.set_user_attributes(uid, attrs)
2974    }
2975
2976    /// Toggle `User.enabled` without rotating credentials.
2977    pub fn set_user_enabled(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
2978        if let Some(configured) = self.configured_control_events() {
2979            let ctx = default_user_lifecycle_ctx();
2980            let control = UserLifecycleControl {
2981                ctx: &ctx,
2982                ledger: configured.ledger.as_ref(),
2983                config: configured.config,
2984            };
2985            self.set_user_enabled_controlled(uid, enabled, &control)
2986        } else {
2987            self.set_user_enabled_unaudited(uid, enabled)
2988        }
2989    }
2990
2991    pub fn disable_user(
2992        &self,
2993        username: &str,
2994        ctx: &ControlEventCtx<'_>,
2995        ledger: &dyn ControlEventLedger,
2996        config: ControlEventConfig,
2997    ) -> Result<(), AuthError> {
2998        self.disable_user_in_tenant(None, username, ctx, ledger, config)
2999    }
3000
3001    pub fn disable_user_in_tenant(
3002        &self,
3003        tenant_id: Option<&str>,
3004        username: &str,
3005        ctx: &ControlEventCtx<'_>,
3006        ledger: &dyn ControlEventLedger,
3007        config: ControlEventConfig,
3008    ) -> Result<(), AuthError> {
3009        let uid = UserId::from_parts(tenant_id, username);
3010        let control = UserLifecycleControl {
3011            ctx,
3012            ledger,
3013            config,
3014        };
3015        self.set_user_enabled_controlled(&uid, false, &control)
3016    }
3017
3018    fn set_user_enabled_unaudited(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
3019        let mut users = self.users.write().map_err(lock_err)?;
3020        let user = users
3021            .get_mut(uid)
3022            .ok_or_else(|| AuthError::UserNotFound(uid.to_string()))?;
3023        user.enabled = enabled;
3024        user.updated_at = now_ms();
3025        drop(users);
3026        if !enabled {
3027            self.purge_sessions_for(uid);
3028        }
3029        self.persist_to_vault();
3030        Ok(())
3031    }
3032
3033    /// Drop every live session belonging to `uid` (tenant + username match
3034    /// so a same-named user in another tenant is untouched).
3035    fn purge_sessions_for(&self, uid: &UserId) {
3036        if let Ok(mut sessions) = self.sessions.write() {
3037            sessions.retain(|_, s| !(s.username == uid.username && s.tenant_id == uid.tenant));
3038        }
3039    }
3040
3041    fn set_user_enabled_controlled(
3042        &self,
3043        uid: &UserId,
3044        enabled: bool,
3045        control: &UserLifecycleControl<'_>,
3046    ) -> Result<(), AuthError> {
3047        let previous = self.get_user_cloned(uid);
3048        let kind = if enabled {
3049            EventKind::UserUpdate
3050        } else {
3051            EventKind::UserDisable
3052        };
3053        let action = if enabled {
3054            "user.update"
3055        } else {
3056            "user.disable"
3057        };
3058        match self.set_user_enabled_unaudited(uid, enabled) {
3059            Ok(()) => {
3060                if let Err(err) = self.emit_user_lifecycle_allowed(
3061                    control,
3062                    kind,
3063                    action,
3064                    uid,
3065                    user_control_fields(
3066                        uid,
3067                        previous.as_ref().map(|u| u.role),
3068                        Some(enabled),
3069                        false,
3070                    ),
3071                ) {
3072                    self.restore_user_snapshot(uid, previous);
3073                    return Err(err);
3074                }
3075                Ok(())
3076            }
3077            Err(err) => {
3078                self.emit_user_lifecycle_outcome(
3079                    control,
3080                    if user_error_is_denied(&err) {
3081                        Outcome::Denied
3082                    } else {
3083                        Outcome::Error
3084                    },
3085                    kind,
3086                    action,
3087                    uid,
3088                    Some(err.to_string()),
3089                    user_control_fields(
3090                        uid,
3091                        previous.as_ref().map(|u| u.role),
3092                        Some(enabled),
3093                        false,
3094                    ),
3095                );
3096                Err(err)
3097            }
3098        }
3099    }
3100
3101    fn emit_user_lifecycle_allowed(
3102        &self,
3103        control: &UserLifecycleControl<'_>,
3104        kind: EventKind,
3105        action: &'static str,
3106        id: &UserId,
3107        fields: HashMap<String, Sensitivity>,
3108    ) -> Result<(), AuthError> {
3109        self.emit_user_lifecycle_event(control, Outcome::Allowed, kind, action, id, None, fields)
3110    }
3111
3112    fn emit_api_key_allowed(
3113        &self,
3114        control: &UserLifecycleControl<'_>,
3115        kind: EventKind,
3116        action: &'static str,
3117        api_key_id: &str,
3118        fields: HashMap<String, Sensitivity>,
3119    ) -> Result<(), AuthError> {
3120        self.emit_control_event(
3121            control,
3122            Outcome::Allowed,
3123            kind,
3124            action,
3125            Some(api_key_resource(api_key_id)),
3126            None,
3127            fields,
3128        )
3129    }
3130
3131    #[allow(clippy::too_many_arguments)]
3132    fn emit_user_lifecycle_outcome(
3133        &self,
3134        control: &UserLifecycleControl<'_>,
3135        outcome: Outcome,
3136        kind: EventKind,
3137        action: &'static str,
3138        id: &UserId,
3139        reason: Option<String>,
3140        fields: HashMap<String, Sensitivity>,
3141    ) {
3142        let _ = self.emit_user_lifecycle_event(control, outcome, kind, action, id, reason, fields);
3143    }
3144
3145    #[allow(clippy::too_many_arguments)]
3146    fn emit_user_lifecycle_event(
3147        &self,
3148        control: &UserLifecycleControl<'_>,
3149        outcome: Outcome,
3150        kind: EventKind,
3151        action: &'static str,
3152        id: &UserId,
3153        reason: Option<String>,
3154        fields: HashMap<String, Sensitivity>,
3155    ) -> Result<(), AuthError> {
3156        self.emit_control_event(
3157            control,
3158            outcome,
3159            kind,
3160            action,
3161            Some(user_resource(id)),
3162            reason,
3163            fields,
3164        )
3165    }
3166
3167    #[allow(clippy::too_many_arguments)]
3168    fn emit_control_event(
3169        &self,
3170        control: &UserLifecycleControl<'_>,
3171        outcome: Outcome,
3172        kind: EventKind,
3173        action: &'static str,
3174        resource: Option<String>,
3175        reason: Option<String>,
3176        fields: HashMap<String, Sensitivity>,
3177    ) -> Result<(), AuthError> {
3178        let event = ControlEvent {
3179            kind,
3180            outcome,
3181            action: Cow::Borrowed(action),
3182            resource,
3183            reason,
3184            matched_policy_id: None,
3185            fields,
3186        };
3187        match control.ledger.emit(control.ctx, event) {
3188            Ok(_) => Ok(()),
3189            Err(err) if control.config.require_persistence() => {
3190                Err(AuthError::Internal(err.to_string()))
3191            }
3192            Err(_) => Ok(()),
3193        }
3194    }
3195
3196    fn rollback_create_user(&self, id: &UserId) {
3197        let removed = self
3198            .users
3199            .write()
3200            .unwrap_or_else(|e| e.into_inner())
3201            .remove(id);
3202        if let Some(user) = removed {
3203            self.remove_api_key_index_entries(&user);
3204        }
3205        self.persist_to_vault();
3206    }
3207
3208    fn rollback_bootstrap(&self, id: &UserId) {
3209        self.bootstrapped.store(false, Ordering::Release);
3210        self.rollback_create_user(id);
3211    }
3212
3213    fn restore_user_snapshot(&self, id: &UserId, previous: Option<User>) {
3214        match previous {
3215            Some(user) => {
3216                self.users
3217                    .write()
3218                    .unwrap_or_else(|e| e.into_inner())
3219                    .insert(id.clone(), user.clone());
3220                self.index_user_api_keys(id, &user);
3221            }
3222            None => {
3223                self.rollback_create_user(id);
3224                return;
3225            }
3226        }
3227        self.persist_to_vault();
3228    }
3229
3230    fn user_delete_rollback_snapshot(
3231        &self,
3232        id: &UserId,
3233        tenant_id: Option<&str>,
3234        username: &str,
3235    ) -> Option<(User, Vec<(String, Session)>)> {
3236        let user = self.get_user_cloned(id)?;
3237        let sessions = self
3238            .sessions
3239            .read()
3240            .map(|sessions| {
3241                sessions
3242                    .iter()
3243                    .filter(|(_, session)| {
3244                        session.username == username && session.tenant_id.as_deref() == tenant_id
3245                    })
3246                    .map(|(token, session)| (token.clone(), session.clone()))
3247                    .collect()
3248            })
3249            .unwrap_or_default();
3250        Some((user, sessions))
3251    }
3252
3253    fn restore_deleted_user(&self, id: &UserId, user: User, sessions: Vec<(String, Session)>) {
3254        self.users
3255            .write()
3256            .unwrap_or_else(|e| e.into_inner())
3257            .insert(id.clone(), user.clone());
3258        self.index_user_api_keys(id, &user);
3259        if !sessions.is_empty() {
3260            let mut guard = self.sessions.write().unwrap_or_else(|e| e.into_inner());
3261            for (token, session) in sessions {
3262                guard.insert(token, session);
3263            }
3264        }
3265        self.persist_to_vault();
3266    }
3267
3268    fn rollback_create_api_key(&self, id: &UserId, key: &str) {
3269        let key_id = api_key_credential_id(key);
3270        let key = key_id.as_str();
3271        if let Ok(mut users) = self.users.write() {
3272            if let Some(user) = users.get_mut(id) {
3273                user.api_keys.retain(|api_key| api_key.key != key);
3274                user.updated_at = now_ms();
3275            }
3276        }
3277        if let Ok(mut idx) = self.api_key_index.write() {
3278            idx.remove(key);
3279        }
3280        self.persist_to_vault();
3281    }
3282
3283    fn api_key_rollback_snapshot(&self, key: &str) -> Option<(UserId, ApiKey)> {
3284        let key = api_key_credential_id(key);
3285        let users = self.users.read().ok()?;
3286        users.iter().find_map(|(id, user)| {
3287            user.api_keys
3288                .iter()
3289                .find(|api_key| api_key.key == key)
3290                .cloned()
3291                .map(|api_key| (id.clone(), api_key))
3292        })
3293    }
3294
3295    fn restore_api_key(&self, id: &UserId, api_key: ApiKey) {
3296        if let Ok(mut users) = self.users.write() {
3297            if let Some(user) = users.get_mut(id) {
3298                if !user
3299                    .api_keys
3300                    .iter()
3301                    .any(|candidate| candidate.key == api_key.key)
3302                {
3303                    user.api_keys.push(api_key.clone());
3304                    user.updated_at = now_ms();
3305                }
3306            }
3307        }
3308        if let Ok(mut idx) = self.api_key_index.write() {
3309            idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
3310        }
3311        self.persist_to_vault();
3312    }
3313
3314    fn remove_api_key_index_entries(&self, user: &User) {
3315        if let Ok(mut idx) = self.api_key_index.write() {
3316            for api_key in &user.api_keys {
3317                idx.remove(&api_key.key);
3318            }
3319        }
3320    }
3321
3322    fn index_user_api_keys(&self, id: &UserId, user: &User) {
3323        if let Ok(mut idx) = self.api_key_index.write() {
3324            for api_key in &user.api_keys {
3325                idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
3326            }
3327        }
3328    }
3329
3330    // -----------------------------------------------------------------
3331    // Login-side enforcement (HTTP path)
3332    // -----------------------------------------------------------------
3333
3334    /// Authenticate with VALID UNTIL / CONNECTION LIMIT enforcement.
3335    /// Wraps `authenticate_in_tenant` and additionally:
3336    ///   * rejects logins after `valid_until`,
3337    ///   * rejects logins when the live session count would exceed the
3338    ///     `connection_limit` attribute.
3339    pub fn authenticate_with_attrs(
3340        &self,
3341        tenant_id: Option<&str>,
3342        username: &str,
3343        password: &str,
3344    ) -> Result<Session, AuthError> {
3345        let uid = UserId::from_parts(tenant_id, username);
3346        let attrs = self.user_attributes(&uid);
3347
3348        if let Some(deadline) = attrs.valid_until {
3349            if now_ms() >= deadline {
3350                return Err(AuthError::Forbidden(format!(
3351                    "account `{}` expired (VALID UNTIL exceeded)",
3352                    uid
3353                )));
3354            }
3355        }
3356
3357        if let Some(limit) = attrs.connection_limit {
3358            let current = self
3359                .session_count_by_user
3360                .read()
3361                .ok()
3362                .and_then(|m| m.get(&uid).copied())
3363                .unwrap_or(0);
3364            if current >= limit {
3365                return Err(AuthError::Forbidden(format!(
3366                    "account `{}` exceeded CONNECTION LIMIT ({})",
3367                    uid, limit
3368                )));
3369            }
3370        }
3371
3372        let session = self.authenticate_in_tenant(tenant_id, username, password)?;
3373
3374        if let Ok(mut counts) = self.session_count_by_user.write() {
3375            *counts.entry(uid).or_insert(0) += 1;
3376        }
3377        Ok(session)
3378    }
3379
3380    /// Decrement the live-session count for `uid`. Call from session
3381    /// revoke / expiry paths so CONNECTION LIMIT stays accurate.
3382    pub fn decrement_session_count(&self, uid: &UserId) {
3383        if let Ok(mut counts) = self.session_count_by_user.write() {
3384            if let Some(c) = counts.get_mut(uid) {
3385                *c = c.saturating_sub(1);
3386            }
3387        }
3388    }
3389
3390    // -----------------------------------------------------------------
3391    // ACL persistence — vault_kv backed
3392    // -----------------------------------------------------------------
3393
3394    /// Re-read the ACL state from `vault_kv`. Call after vault load /
3395    /// restore so the in-memory maps reflect the persisted data.
3396    pub fn rehydrate_acl(&self) {
3397        let kv_snapshot: Vec<(String, String)> = self
3398            .vault_kv
3399            .read()
3400            .map(|kv| {
3401                kv.iter()
3402                    .filter(|(k, _)| {
3403                        k.starts_with("red.acl.grants.")
3404                            || k.starts_with("red.acl.attrs.")
3405                            || k == &"red.acl.public_grants"
3406                    })
3407                    .map(|(k, v)| (k.clone(), v.clone()))
3408                    .collect()
3409            })
3410            .unwrap_or_default();
3411
3412        for (k, v) in kv_snapshot {
3413            if k == "red.acl.public_grants" {
3414                if let Some(parsed) = decode_grants_blob(&v) {
3415                    *self
3416                        .public_grants
3417                        .write()
3418                        .unwrap_or_else(|e| e.into_inner()) = parsed;
3419                }
3420            } else if let Some(suffix) = k.strip_prefix("red.acl.grants.") {
3421                if let Some(uid) = decode_uid(suffix) {
3422                    if let Some(mut parsed) = decode_grants_blob(&v) {
3423                        // Restore the principal field — the on-disk
3424                        // line stores only resource+action shape.
3425                        for g in parsed.iter_mut() {
3426                            g.principal = GrantPrincipal::User(uid.clone());
3427                        }
3428                        self.grants
3429                            .write()
3430                            .unwrap_or_else(|e| e.into_inner())
3431                            .insert(uid, parsed);
3432                    }
3433                }
3434            } else if let Some(suffix) = k.strip_prefix("red.acl.attrs.") {
3435                if let Some(uid) = decode_uid(suffix) {
3436                    if let Some(parsed) = decode_attrs_blob(&v) {
3437                        self.user_attributes
3438                            .write()
3439                            .unwrap_or_else(|e| e.into_inner())
3440                            .insert(uid, parsed);
3441                    }
3442                }
3443            }
3444        }
3445
3446        self.permission_cache
3447            .write()
3448            .unwrap_or_else(|e| e.into_inner())
3449            .clear();
3450    }
3451
3452    /// Snapshot every ACL change back into the vault KV store.
3453    fn persist_acl_to_kv(&self) {
3454        let public = self
3455            .public_grants
3456            .read()
3457            .ok()
3458            .map(|p| encode_grants_blob(&p))
3459            .unwrap_or_default();
3460        self.vault_kv_set("red.acl.public_grants".to_string(), public);
3461
3462        let snapshot: Vec<(UserId, Vec<Grant>)> = self
3463            .grants
3464            .read()
3465            .ok()
3466            .map(|g| g.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3467            .unwrap_or_default();
3468        for (uid, list) in snapshot {
3469            let key = format!("red.acl.grants.{}", encode_uid(&uid));
3470            let val = encode_grants_blob(&list);
3471            self.vault_kv_set(key, val);
3472        }
3473
3474        let attrs_snapshot: Vec<(UserId, UserAttributes)> = self
3475            .user_attributes
3476            .read()
3477            .ok()
3478            .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3479            .unwrap_or_default();
3480        for (uid, attrs) in attrs_snapshot {
3481            let key = format!("red.acl.attrs.{}", encode_uid(&uid));
3482            let val = encode_attrs_blob(&attrs);
3483            self.vault_kv_set(key, val);
3484        }
3485    }
3486
3487    fn invalidate_permission_cache(&self, uid: Option<&UserId>) {
3488        if let Ok(mut cache) = self.permission_cache.write() {
3489            match uid {
3490                Some(u) => {
3491                    cache.remove(u);
3492                }
3493                None => cache.clear(),
3494            }
3495        }
3496    }
3497
3498    // -----------------------------------------------------------------
3499    // IAM policies — put / delete / attach / detach / simulate
3500    //
3501    // The kernel in `super::policies` owns the Policy type and the
3502    // evaluator. AuthStore handles persistence + per-user cache + the
3503    // GRANT translation layer (synthetic `_grant_*` policies).
3504    // -----------------------------------------------------------------
3505
3506    pub fn put_policy_with_control_events(
3507        &self,
3508        p: Policy,
3509        control: &PolicyMutationControl<'_>,
3510    ) -> Result<(), AuthError> {
3511        let policy_id = p.id.clone();
3512        let kind = if self.get_policy(&policy_id).is_some() {
3513            EventKind::PolicyUpdate
3514        } else {
3515            EventKind::PolicyCreate
3516        };
3517
3518        if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
3519            let err = AuthError::Forbidden(format!("policy id `{}` is reserved", p.id));
3520            self.emit_policy_error(
3521                control,
3522                kind,
3523                "policy:put",
3524                &policy_id,
3525                Some(&p),
3526                None,
3527                &err,
3528            );
3529            return Err(err);
3530        }
3531
3532        if let Some(ManagedPolicyDecision::Deny {
3533            entry_id,
3534            matched_action,
3535            matched_resource,
3536            reason,
3537            ..
3538        }) = self.managed_policy_decision(&policy_id, PolicyOp::Put, control)
3539        {
3540            self.emit_policy_denied(
3541                control,
3542                kind,
3543                "policy:put",
3544                &policy_id,
3545                Some(&p),
3546                None,
3547                &reason.to_string(),
3548                Some(entry_id),
3549                Some((matched_action, matched_resource)),
3550            );
3551            return Err(Self::managed_policy_error(&policy_id, &reason.to_string()));
3552        }
3553
3554        let previous = self.get_policy(&policy_id);
3555        let was_enabled = self.iam_authorization_enabled();
3556        match self.put_policy_internal(p.clone()) {
3557            Ok(()) => match self.emit_policy_allowed(
3558                control,
3559                kind,
3560                "policy:put",
3561                &policy_id,
3562                Some(&p),
3563                None,
3564                None,
3565            ) {
3566                Ok(()) => Ok(()),
3567                Err(err) => {
3568                    self.restore_policy_put(&policy_id, previous, was_enabled);
3569                    Err(err)
3570                }
3571            },
3572            Err(err) => {
3573                self.emit_policy_error(
3574                    control,
3575                    kind,
3576                    "policy:put",
3577                    &policy_id,
3578                    Some(&p),
3579                    None,
3580                    &err,
3581                );
3582                Err(err)
3583            }
3584        }
3585    }
3586
3587    pub fn delete_policy_with_control_events(
3588        &self,
3589        id: &str,
3590        control: &PolicyMutationControl<'_>,
3591    ) -> Result<(), AuthError> {
3592        let existing = self.get_policy(id);
3593        if let Some(ManagedPolicyDecision::Deny {
3594            entry_id,
3595            matched_action,
3596            matched_resource,
3597            reason,
3598            ..
3599        }) = self.managed_policy_decision(id, PolicyOp::Drop, control)
3600        {
3601            self.emit_policy_denied(
3602                control,
3603                EventKind::PolicyDelete,
3604                "policy:drop",
3605                id,
3606                existing.as_deref(),
3607                None,
3608                &reason.to_string(),
3609                Some(entry_id),
3610                Some((matched_action, matched_resource)),
3611            );
3612            return Err(Self::managed_policy_error(id, &reason.to_string()));
3613        }
3614
3615        let user_attachments = self
3616            .user_attachments
3617            .read()
3618            .map(|m| m.clone())
3619            .unwrap_or_default();
3620        let group_attachments = self
3621            .group_attachments
3622            .read()
3623            .map(|m| m.clone())
3624            .unwrap_or_default();
3625        let was_enabled = self.iam_authorization_enabled();
3626        match self.delete_policy(id) {
3627            Ok(()) => match self.emit_policy_allowed(
3628                control,
3629                EventKind::PolicyDelete,
3630                "policy:drop",
3631                id,
3632                existing.as_deref(),
3633                None,
3634                None,
3635            ) {
3636                Ok(()) => Ok(()),
3637                Err(err) => {
3638                    if let Some(policy) = existing {
3639                        self.restore_policy_delete(
3640                            id,
3641                            policy,
3642                            user_attachments,
3643                            group_attachments,
3644                            was_enabled,
3645                        );
3646                    }
3647                    Err(err)
3648                }
3649            },
3650            Err(err) => {
3651                self.emit_policy_error(
3652                    control,
3653                    EventKind::PolicyDelete,
3654                    "policy:drop",
3655                    id,
3656                    existing.as_deref(),
3657                    None,
3658                    &err,
3659                );
3660                Err(err)
3661            }
3662        }
3663    }
3664
3665    pub fn attach_policy_with_control_events(
3666        &self,
3667        principal: PrincipalRef,
3668        policy_id: &str,
3669        control: &PolicyMutationControl<'_>,
3670    ) -> Result<(), AuthError> {
3671        let existing = self.get_policy(policy_id);
3672        if let Some(ManagedPolicyDecision::Deny {
3673            entry_id,
3674            matched_action,
3675            matched_resource,
3676            reason,
3677            ..
3678        }) = self.managed_policy_decision(policy_id, PolicyOp::Attach, control)
3679        {
3680            self.emit_policy_denied(
3681                control,
3682                EventKind::PolicyAttach,
3683                "policy:attach",
3684                policy_id,
3685                existing.as_deref(),
3686                Some(&principal),
3687                &reason.to_string(),
3688                Some(entry_id),
3689                Some((matched_action, matched_resource)),
3690            );
3691            return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3692        }
3693
3694        if let Some(err_msg) = self.check_self_lock_invariant_for_attach(policy_id) {
3695            self.emit_policy_denied(
3696                control,
3697                EventKind::PolicyAttach,
3698                "policy:attach",
3699                policy_id,
3700                existing.as_deref(),
3701                Some(&principal),
3702                &err_msg,
3703                None,
3704                None,
3705            );
3706            return Err(AuthError::Forbidden(err_msg));
3707        }
3708
3709        let user_attachments = self
3710            .user_attachments
3711            .read()
3712            .map(|m| m.clone())
3713            .unwrap_or_default();
3714        let group_attachments = self
3715            .group_attachments
3716            .read()
3717            .map(|m| m.clone())
3718            .unwrap_or_default();
3719        match self.attach_policy(principal.clone(), policy_id) {
3720            Ok(()) => match self.emit_policy_allowed(
3721                control,
3722                EventKind::PolicyAttach,
3723                "policy:attach",
3724                policy_id,
3725                existing.as_deref(),
3726                Some(&principal),
3727                None,
3728            ) {
3729                Ok(()) => Ok(()),
3730                Err(err) => {
3731                    self.restore_policy_attachments(user_attachments, group_attachments);
3732                    Err(err)
3733                }
3734            },
3735            Err(err) => {
3736                self.emit_policy_error(
3737                    control,
3738                    EventKind::PolicyAttach,
3739                    "policy:attach",
3740                    policy_id,
3741                    existing.as_deref(),
3742                    Some(&principal),
3743                    &err,
3744                );
3745                Err(err)
3746            }
3747        }
3748    }
3749
3750    pub fn detach_policy_with_control_events(
3751        &self,
3752        principal: PrincipalRef,
3753        policy_id: &str,
3754        control: &PolicyMutationControl<'_>,
3755    ) -> Result<(), AuthError> {
3756        let existing = self.get_policy(policy_id);
3757        if let Some(ManagedPolicyDecision::Deny {
3758            entry_id,
3759            matched_action,
3760            matched_resource,
3761            reason,
3762            ..
3763        }) = self.managed_policy_decision(policy_id, PolicyOp::Detach, control)
3764        {
3765            self.emit_policy_denied(
3766                control,
3767                EventKind::PolicyDetach,
3768                "policy:detach",
3769                policy_id,
3770                existing.as_deref(),
3771                Some(&principal),
3772                &reason.to_string(),
3773                Some(entry_id),
3774                Some((matched_action, matched_resource)),
3775            );
3776            return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3777        }
3778        let Some(existing_policy) = existing else {
3779            let err = AuthError::Forbidden(format!("policy `{policy_id}` not found"));
3780            self.emit_policy_error(
3781                control,
3782                EventKind::PolicyDetach,
3783                "policy:detach",
3784                policy_id,
3785                None,
3786                Some(&principal),
3787                &err,
3788            );
3789            return Err(err);
3790        };
3791
3792        let user_attachments = self
3793            .user_attachments
3794            .read()
3795            .map(|m| m.clone())
3796            .unwrap_or_default();
3797        let group_attachments = self
3798            .group_attachments
3799            .read()
3800            .map(|m| m.clone())
3801            .unwrap_or_default();
3802        match self.detach_policy(principal.clone(), policy_id) {
3803            Ok(()) => match self.emit_policy_allowed(
3804                control,
3805                EventKind::PolicyDetach,
3806                "policy:detach",
3807                policy_id,
3808                Some(existing_policy.as_ref()),
3809                Some(&principal),
3810                None,
3811            ) {
3812                Ok(()) => Ok(()),
3813                Err(err) => {
3814                    self.restore_policy_attachments(user_attachments, group_attachments);
3815                    Err(err)
3816                }
3817            },
3818            Err(err) => {
3819                self.emit_policy_error(
3820                    control,
3821                    EventKind::PolicyDetach,
3822                    "policy:detach",
3823                    policy_id,
3824                    Some(existing_policy.as_ref()),
3825                    Some(&principal),
3826                    &err,
3827                );
3828                Err(err)
3829            }
3830        }
3831    }
3832
3833    fn managed_policy_decision(
3834        &self,
3835        policy_id: &str,
3836        op: PolicyOp,
3837        control: &PolicyMutationControl<'_>,
3838    ) -> Option<ManagedPolicyDecision> {
3839        control.registry.map(|registry| {
3840            ManagedPolicyGate::new(registry).check_mutation(
3841                self,
3842                control.actor,
3843                control.eval_ctx,
3844                policy_id,
3845                op,
3846            )
3847        })
3848    }
3849
3850    /// Run the [`crate::auth::self_lock_guard`] invariant against the
3851    /// current policy set. Returns `Some(error_message)` when an attach
3852    /// must be refused, `None` when the system would remain unlockable.
3853    ///
3854    /// `_policy_id` is informational — the invariant is on the full
3855    /// graph and doesn't currently special-case the new entry.
3856    fn check_self_lock_invariant_for_attach(&self, _policy_id: &str) -> Option<String> {
3857        use crate::auth::self_lock_guard::{
3858            check_self_lock_invariant, format_block_error, InvariantOutcome,
3859        };
3860        let policies: Vec<Arc<Policy>> = match self.policies.read() {
3861            Ok(map) => map.values().cloned().collect(),
3862            Err(_) => return None,
3863        };
3864        let outcome = check_self_lock_invariant(&policies);
3865        match &outcome {
3866            InvariantOutcome::Ok => None,
3867            InvariantOutcome::Blocked { .. } => format_block_error(&outcome),
3868        }
3869    }
3870
3871    fn managed_policy_error(policy_id: &str, reason: &str) -> AuthError {
3872        AuthError::Forbidden(format!(
3873            "managed policy mutation blocked for `{policy_id}`: {reason}"
3874        ))
3875    }
3876
3877    fn emit_policy_allowed(
3878        &self,
3879        control: &PolicyMutationControl<'_>,
3880        kind: EventKind,
3881        action: &'static str,
3882        policy_id: &str,
3883        policy: Option<&Policy>,
3884        principal: Option<&PrincipalRef>,
3885        matched_policy_id: Option<String>,
3886    ) -> Result<(), AuthError> {
3887        let event = ControlEvent {
3888            kind,
3889            outcome: Outcome::Allowed,
3890            action: std::borrow::Cow::Borrowed(action),
3891            resource: Some(format!("policy:{policy_id}")),
3892            reason: None,
3893            matched_policy_id,
3894            fields: policy_control_fields(policy_id, policy, principal),
3895        };
3896        match control.ledger.emit(control.ctx, event) {
3897            Ok(_) => Ok(()),
3898            Err(err) if control.config.require_persistence() => {
3899                Err(AuthError::Internal(err.to_string()))
3900            }
3901            Err(_) => Ok(()),
3902        }
3903    }
3904
3905    #[allow(clippy::too_many_arguments)]
3906    fn emit_policy_denied(
3907        &self,
3908        control: &PolicyMutationControl<'_>,
3909        kind: EventKind,
3910        action: &'static str,
3911        policy_id: &str,
3912        policy: Option<&Policy>,
3913        principal: Option<&PrincipalRef>,
3914        reason: &str,
3915        matched_policy_id: Option<String>,
3916        matched: Option<(String, String)>,
3917    ) {
3918        let mut fields = policy_control_fields(policy_id, policy, principal);
3919        if let Some((matched_action, matched_resource)) = matched {
3920            fields.insert(
3921                "matched_action".to_string(),
3922                Sensitivity::raw(matched_action),
3923            );
3924            fields.insert(
3925                "matched_resource".to_string(),
3926                Sensitivity::raw(matched_resource),
3927            );
3928        }
3929        let event = ControlEvent {
3930            kind,
3931            outcome: Outcome::Denied,
3932            action: std::borrow::Cow::Borrowed(action),
3933            resource: Some(format!("policy:{policy_id}")),
3934            reason: Some(reason.to_string()),
3935            matched_policy_id,
3936            fields,
3937        };
3938        let _ = control.ledger.emit(control.ctx, event);
3939    }
3940
3941    fn emit_policy_error(
3942        &self,
3943        control: &PolicyMutationControl<'_>,
3944        kind: EventKind,
3945        action: &'static str,
3946        policy_id: &str,
3947        policy: Option<&Policy>,
3948        principal: Option<&PrincipalRef>,
3949        err: &AuthError,
3950    ) {
3951        let event = ControlEvent {
3952            kind,
3953            outcome: Outcome::Error,
3954            action: std::borrow::Cow::Borrowed(action),
3955            resource: Some(format!("policy:{policy_id}")),
3956            reason: Some(err.to_string()),
3957            matched_policy_id: None,
3958            fields: policy_control_fields(policy_id, policy, principal),
3959        };
3960        let _ = control.ledger.emit(control.ctx, event);
3961    }
3962
3963    fn restore_policy_put(
3964        &self,
3965        policy_id: &str,
3966        previous: Option<Arc<Policy>>,
3967        was_enabled: bool,
3968    ) {
3969        let mut policies = self.policies.write().unwrap_or_else(|e| e.into_inner());
3970        match previous {
3971            Some(policy) => {
3972                policies.insert(policy_id.to_string(), policy);
3973            }
3974            None => {
3975                policies.remove(policy_id);
3976            }
3977        }
3978        drop(policies);
3979        self.iam_authorization_enabled
3980            .store(was_enabled, Ordering::Release);
3981        self.iam_effective_cache
3982            .write()
3983            .unwrap_or_else(|e| e.into_inner())
3984            .clear();
3985        self.invalidate_visible_collections_cache();
3986        self.persist_iam_to_kv();
3987    }
3988
3989    fn restore_policy_delete(
3990        &self,
3991        policy_id: &str,
3992        policy: Arc<Policy>,
3993        user_attachments: HashMap<UserId, Vec<String>>,
3994        group_attachments: HashMap<String, Vec<String>>,
3995        was_enabled: bool,
3996    ) {
3997        self.policies
3998            .write()
3999            .unwrap_or_else(|e| e.into_inner())
4000            .insert(policy_id.to_string(), policy);
4001        self.restore_policy_attachments(user_attachments, group_attachments);
4002        self.iam_authorization_enabled
4003            .store(was_enabled, Ordering::Release);
4004        self.persist_iam_to_kv();
4005    }
4006
4007    fn restore_policy_attachments(
4008        &self,
4009        user_attachments: HashMap<UserId, Vec<String>>,
4010        group_attachments: HashMap<String, Vec<String>>,
4011    ) {
4012        *self
4013            .user_attachments
4014            .write()
4015            .unwrap_or_else(|e| e.into_inner()) = user_attachments;
4016        *self
4017            .group_attachments
4018            .write()
4019            .unwrap_or_else(|e| e.into_inner()) = group_attachments;
4020        self.iam_effective_cache
4021            .write()
4022            .unwrap_or_else(|e| e.into_inner())
4023            .clear();
4024        self.invalidate_visible_collections_cache();
4025        self.persist_iam_to_kv();
4026    }
4027
4028    /// Insert or replace a policy by id. Rejects synthetic ids
4029    /// (`_grant_*` / `_default_*`) so callers can't hand-write them
4030    /// from the public API. Use `put_policy_internal` for synthetic
4031    /// inserts.
4032    pub fn put_policy(&self, p: Policy) -> Result<(), AuthError> {
4033        if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
4034            return Err(AuthError::Forbidden(format!(
4035                "policy id `{}` is reserved",
4036                p.id
4037            )));
4038        }
4039        self.put_policy_internal(p)
4040    }
4041
4042    /// Internal put bypassing the synthetic-namespace guard. Used by
4043    /// the GRANT translation layer; exposed publicly so integration
4044    /// tests can register synthetic `_grant_*` policies without going
4045    /// through the SQL frontend.
4046    pub fn put_policy_internal(&self, p: Policy) -> Result<(), AuthError> {
4047        p.validate()
4048            .map_err(|e| AuthError::Forbidden(format!("invalid policy `{}`: {e}", p.id)))?;
4049        let id = p.id.clone();
4050        self.policies
4051            .write()
4052            .unwrap_or_else(|e| e.into_inner())
4053            .insert(id, Arc::new(p));
4054        self.iam_authorization_enabled
4055            .store(true, Ordering::Release);
4056        self.iam_effective_cache
4057            .write()
4058            .unwrap_or_else(|e| e.into_inner())
4059            .clear();
4060        // Issue #119: a policy mutation can change the visible-
4061        // collections answer for any (tenant, role); we don't know
4062        // which up-front, so blow the whole cache.
4063        self.invalidate_visible_collections_cache();
4064        self.persist_iam_to_kv();
4065        Ok(())
4066    }
4067
4068    /// Whether the IAM evaluator should be authoritative for runtime
4069    /// authorization. This flips on the first policy write and remains
4070    /// on after deletes so dropping all policies leaves the instance in
4071    /// default-deny rather than silently returning to role fallback.
4072    pub fn iam_authorization_enabled(&self) -> bool {
4073        self.iam_authorization_enabled.load(Ordering::Acquire)
4074    }
4075
4076    /// Remove a policy and any attachments referencing it.
4077    pub fn delete_policy(&self, id: &str) -> Result<(), AuthError> {
4078        let removed = self
4079            .policies
4080            .write()
4081            .unwrap_or_else(|e| e.into_inner())
4082            .remove(id)
4083            .is_some();
4084        if !removed {
4085            return Err(AuthError::Forbidden(format!("policy `{id}` not found")));
4086        }
4087        // Detach from every user / group.
4088        if let Ok(mut ua) = self.user_attachments.write() {
4089            for ids in ua.values_mut() {
4090                ids.retain(|p| p != id);
4091            }
4092            ua.retain(|_, v| !v.is_empty());
4093        }
4094        if let Ok(mut ga) = self.group_attachments.write() {
4095            for ids in ga.values_mut() {
4096                ids.retain(|p| p != id);
4097            }
4098            ga.retain(|_, v| !v.is_empty());
4099        }
4100        self.iam_effective_cache
4101            .write()
4102            .unwrap_or_else(|e| e.into_inner())
4103            .clear();
4104        // Issue #119: dropping a policy can shrink any caller's visible
4105        // set; clear the (tenant, role) cache so AI commands re-resolve.
4106        self.invalidate_visible_collections_cache();
4107        self.persist_iam_to_kv();
4108        Ok(())
4109    }
4110
4111    /// List all policies (id-sorted for deterministic output).
4112    pub fn list_policies(&self) -> Vec<Arc<Policy>> {
4113        let map = match self.policies.read() {
4114            Ok(g) => g,
4115            Err(_) => return Vec::new(),
4116        };
4117        let mut out: Vec<Arc<Policy>> = map.values().cloned().collect();
4118        out.sort_by(|a, b| a.id.cmp(&b.id));
4119        out
4120    }
4121
4122    /// Fetch a single policy by id.
4123    pub fn get_policy(&self, id: &str) -> Option<Arc<Policy>> {
4124        self.policies.read().ok().and_then(|m| m.get(id).cloned())
4125    }
4126
4127    /// List policies directly attached to a group.
4128    pub fn group_policies(&self, group: &str) -> Vec<Arc<Policy>> {
4129        let policies = self.policies.read();
4130        let attachments = self.group_attachments.read();
4131        let mut out = Vec::new();
4132        if let (Ok(p_map), Ok(ga_map)) = (policies, attachments) {
4133            if let Some(ids) = ga_map.get(group) {
4134                for id in ids {
4135                    if let Some(p) = p_map.get(id) {
4136                        out.push(p.clone());
4137                    }
4138                }
4139            }
4140        }
4141        out.sort_by(|a, b| a.id.cmp(&b.id));
4142        out
4143    }
4144
4145    /// Delete synthetic policies produced by SQL GRANT translation.
4146    /// REVOKE uses this to keep the IAM lane and the legacy grant table
4147    /// in lock-step.
4148    pub fn delete_synthetic_grant_policies(
4149        &self,
4150        principal: &GrantPrincipal,
4151        resource: &Resource,
4152        actions: &[Action],
4153    ) -> usize {
4154        let attached = match principal {
4155            GrantPrincipal::User(uid) => self
4156                .user_attachments
4157                .read()
4158                .ok()
4159                .and_then(|m| m.get(uid).cloned())
4160                .unwrap_or_default(),
4161            GrantPrincipal::Group(group) => self
4162                .group_attachments
4163                .read()
4164                .ok()
4165                .and_then(|m| m.get(group).cloned())
4166                .unwrap_or_default(),
4167            GrantPrincipal::Public => self
4168                .group_attachments
4169                .read()
4170                .ok()
4171                .and_then(|m| m.get(PUBLIC_IAM_GROUP).cloned())
4172                .unwrap_or_default(),
4173        };
4174        if attached.is_empty() {
4175            return 0;
4176        }
4177
4178        let mut delete_ids = Vec::new();
4179        if let Ok(policies) = self.policies.read() {
4180            for id in attached {
4181                let Some(policy) = policies.get(&id) else {
4182                    continue;
4183                };
4184                if !policy.id.starts_with("_grant_") {
4185                    continue;
4186                }
4187                if synthetic_grant_matches(policy, resource, actions) {
4188                    delete_ids.push(policy.id.clone());
4189                }
4190            }
4191        }
4192
4193        let mut deleted = 0usize;
4194        for id in delete_ids {
4195            if self.delete_policy(&id).is_ok() {
4196                deleted += 1;
4197            }
4198        }
4199        deleted
4200    }
4201
4202    /// Apply the `REDDB_POLICY_BREAK_GLASS` recovery path: install or
4203    /// refresh the unlock policy, ensure the synthetic platform-owner
4204    /// user record exists, rebind the unlock policy to it, and emit a
4205    /// `policy.break_glass` audit event. Idempotent across reboots.
4206    ///
4207    /// `boot_ts_ms` is recorded in the audit event so operators can
4208    /// correlate the recovery with the boot transcript.
4209    pub fn apply_policy_break_glass(&self, boot_ts_ms: u128) -> Result<(), AuthError> {
4210        use crate::auth::self_lock_guard::{
4211            break_glass_audit_fields, unlock_policy, PLATFORM_OWNER_UNLOCK_POLICY_ID,
4212            PLATFORM_OWNER_USERNAME,
4213        };
4214
4215        // (1) Install or refresh the unlock policy. `put_policy_internal`
4216        // bypasses the synthetic-namespace guard, lets us replace any
4217        // tampered persisted copy, and persists to vault on success.
4218        let policy = unlock_policy();
4219        self.put_policy_internal(policy)?;
4220
4221        // (2) Ensure the synthetic platform-owner user record exists.
4222        // We bypass the public user-creation API to keep this user
4223        // immutable, disabled, and absent from operator listings even
4224        // if our filter ever regresses.
4225        let owner_id = UserId::platform(PLATFORM_OWNER_USERNAME);
4226        {
4227            let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
4228            users.entry(owner_id.clone()).or_insert_with(|| User {
4229                username: PLATFORM_OWNER_USERNAME.to_string(),
4230                tenant_id: None,
4231                // No usable password; even if `authenticate` were
4232                // wired to accept the principal it would fail
4233                // verification. Defence in depth: `enabled = false`
4234                // and the username filter both block it.
4235                password_hash: String::new(),
4236                scram_verifier: None,
4237                role: Role::Admin,
4238                api_keys: Vec::new(),
4239                created_at: boot_ts_ms,
4240                updated_at: boot_ts_ms,
4241                enabled: false,
4242            });
4243        }
4244
4245        // (3) Rebind the unlock policy to the synthetic principal.
4246        // `attach_policy` is idempotent (the helper checks for an
4247        // existing binding before appending).
4248        self.attach_policy(
4249            PrincipalRef::User(owner_id.clone()),
4250            PLATFORM_OWNER_UNLOCK_POLICY_ID,
4251        )?;
4252
4253        // (4) Emit a loud audit event. If the control-event ledger
4254        // isn't configured (e.g. unit tests without runtime wiring),
4255        // log a warn and proceed — recovery must not depend on audit
4256        // wiring being live.
4257        let fields_raw = break_glass_audit_fields(boot_ts_ms);
4258        let mut fields: HashMap<String, Sensitivity> = HashMap::new();
4259        for (k, v) in fields_raw {
4260            fields.insert(k, Sensitivity::raw(v));
4261        }
4262        if let Some(configured) = self.configured_control_events() {
4263            let ctx = ControlEventCtx {
4264                actor: crate::runtime::control_events::ActorRef::System("break_glass"),
4265                scope: None,
4266                request_id: None,
4267                trace_id: None,
4268            };
4269            let event = ControlEvent {
4270                kind: EventKind::PolicyBreakGlass,
4271                outcome: Outcome::Allowed,
4272                action: std::borrow::Cow::Borrowed("policy:break_glass"),
4273                resource: Some(format!("policy:{PLATFORM_OWNER_UNLOCK_POLICY_ID}")),
4274                reason: Some("REDDB_POLICY_BREAK_GLASS=1 at boot".into()),
4275                matched_policy_id: None,
4276                fields,
4277            };
4278            let _ = configured.ledger.emit(&ctx, event);
4279        }
4280        tracing::warn!(
4281            policy_id = PLATFORM_OWNER_UNLOCK_POLICY_ID,
4282            principal = PLATFORM_OWNER_USERNAME,
4283            boot_ts_ms = %boot_ts_ms,
4284            "REDDB_POLICY_BREAK_GLASS=1 — installed/refreshed platform-owner unlock policy and rebound to synthetic principal"
4285        );
4286        Ok(())
4287    }
4288
4289    /// Attach a policy to a user or group. Returns an error if the
4290    /// policy id doesn't exist.
4291    pub fn attach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
4292        if !self
4293            .policies
4294            .read()
4295            .map(|m| m.contains_key(policy_id))
4296            .unwrap_or(false)
4297        {
4298            return Err(AuthError::Forbidden(format!(
4299                "policy `{policy_id}` not found"
4300            )));
4301        }
4302        match &principal {
4303            PrincipalRef::User(uid) => {
4304                let mut ua = self
4305                    .user_attachments
4306                    .write()
4307                    .unwrap_or_else(|e| e.into_inner());
4308                let list = ua.entry(uid.clone()).or_default();
4309                if !list.iter().any(|p| p == policy_id) {
4310                    list.push(policy_id.to_string());
4311                }
4312                drop(ua);
4313                self.invalidate_iam_cache(Some(uid));
4314            }
4315            PrincipalRef::Group(g) => {
4316                let mut ga = self
4317                    .group_attachments
4318                    .write()
4319                    .unwrap_or_else(|e| e.into_inner());
4320                let list = ga.entry(g.clone()).or_default();
4321                if !list.iter().any(|p| p == policy_id) {
4322                    list.push(policy_id.to_string());
4323                }
4324                drop(ga);
4325                self.invalidate_iam_cache(None);
4326            }
4327        }
4328        self.persist_iam_to_kv();
4329        Ok(())
4330    }
4331
4332    /// Remove a policy attachment from a user or group.
4333    pub fn detach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
4334        match &principal {
4335            PrincipalRef::User(uid) => {
4336                if let Ok(mut ua) = self.user_attachments.write() {
4337                    if let Some(list) = ua.get_mut(uid) {
4338                        list.retain(|p| p != policy_id);
4339                        if list.is_empty() {
4340                            ua.remove(uid);
4341                        }
4342                    }
4343                }
4344                self.invalidate_iam_cache(Some(uid));
4345            }
4346            PrincipalRef::Group(g) => {
4347                if let Ok(mut ga) = self.group_attachments.write() {
4348                    if let Some(list) = ga.get_mut(g) {
4349                        list.retain(|p| p != policy_id);
4350                        if list.is_empty() {
4351                            ga.remove(g);
4352                        }
4353                    }
4354                }
4355                self.invalidate_iam_cache(None);
4356            }
4357        }
4358        self.persist_iam_to_kv();
4359        Ok(())
4360    }
4361
4362    /// Resolve the ordered list of effective policies for a user:
4363    /// group attachments first (least specific), then user
4364    /// attachments (most specific). Cached per user.
4365    pub fn effective_policies(&self, user: &UserId) -> Vec<Arc<Policy>> {
4366        if let Ok(cache) = self.iam_effective_cache.read() {
4367            if let Some(hit) = cache.get(user) {
4368                return hit.clone();
4369            }
4370        }
4371        let policies = self.policies.read();
4372        let user_attachments = self.user_attachments.read();
4373        let group_attachments = self.group_attachments.read();
4374        let mut groups = self
4375            .user_attributes
4376            .read()
4377            .ok()
4378            .and_then(|m| m.get(user).map(|attrs| attrs.groups.clone()))
4379            .unwrap_or_default();
4380        groups.insert(0, PUBLIC_IAM_GROUP.to_string());
4381        let mut out: Vec<Arc<Policy>> = Vec::new();
4382        if let (Ok(p_map), Ok(ua_map), Ok(ga_map)) = (policies, user_attachments, group_attachments)
4383        {
4384            for group in groups {
4385                if let Some(ids) = ga_map.get(&group) {
4386                    for id in ids {
4387                        if let Some(p) = p_map.get(id) {
4388                            out.push(p.clone());
4389                        }
4390                    }
4391                }
4392            }
4393            if let Some(ids) = ua_map.get(user) {
4394                for id in ids {
4395                    if let Some(p) = p_map.get(id) {
4396                        out.push(p.clone());
4397                    }
4398                }
4399            }
4400        }
4401        if let Ok(mut cache) = self.iam_effective_cache.write() {
4402            cache.insert(user.clone(), out.clone());
4403        }
4404        out
4405    }
4406
4407    /// Run the policy simulator for a principal. Synthesises an
4408    /// `EvalContext` from the user record + caller-supplied extras.
4409    pub fn simulate(
4410        &self,
4411        principal: &UserId,
4412        action: &str,
4413        resource: &ResourceRef,
4414        ctx_extras: SimCtx,
4415    ) -> SimulationOutcome {
4416        let user_role = self
4417            .users
4418            .read()
4419            .ok()
4420            .and_then(|u| u.get(principal).map(|u| u.role));
4421        let principal_is_admin_role = user_role == Some(Role::Admin);
4422        let now = ctx_extras.now_ms.unwrap_or_else(now_ms);
4423        let ctx = EvalContext {
4424            principal_tenant: principal.tenant.clone(),
4425            current_tenant: ctx_extras.current_tenant,
4426            peer_ip: ctx_extras.peer_ip,
4427            mfa_present: ctx_extras.mfa_present,
4428            now_ms: now,
4429            principal_is_admin_role,
4430            principal_is_platform_scoped: principal.tenant.is_none(),
4431        };
4432        let pols = self.effective_policies(principal);
4433        let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4434        iam_policies::simulate(&refs, action, resource, &ctx)
4435    }
4436
4437    /// Production hot-path policy evaluation. Returns `true` on Allow
4438    /// / AdminBypass, `false` on Deny / DefaultDeny.
4439    ///
4440    /// This entry point is **strict**: it never consults the
4441    /// [`PolicyEnforcementMode`] fallback. Governance APIs that gate
4442    /// admin-tier mutations (managed-config writes, registry
4443    /// supersedes, managed-policy lifecycle) call this so they cannot
4444    /// accidentally pick up the lenient `LegacyRbac` posture. Runtime
4445    /// hot-path callers that should respect the mode call
4446    /// [`AuthStore::check_policy_authz_with_role`] instead.
4447    pub fn check_policy_authz(
4448        &self,
4449        principal: &UserId,
4450        action: &str,
4451        resource: &ResourceRef,
4452        ctx: &EvalContext,
4453    ) -> bool {
4454        let pols = self.effective_policies(principal);
4455        let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4456        let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4457        matches!(
4458            decision,
4459            iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass
4460        )
4461    }
4462
4463    /// Mode-aware policy evaluation for runtime SQL/HTTP/wire surfaces.
4464    ///
4465    /// Returns `true` on `Allow`/`AdminBypass`, `false` on an explicit
4466    /// `Deny`. On `DefaultDeny` (no statement matched) the result
4467    /// depends on the active [`PolicyEnforcementMode`]:
4468    ///
4469    /// * `LegacyRbac` — defer to
4470    ///   [`legacy_rbac_decision`][super::enforcement_mode::legacy_rbac_decision]
4471    ///   using the caller's `role`. This preserves the pre-#712
4472    ///   behaviour where a principal with no attached policy is gated
4473    ///   only by their role.
4474    /// * `PolicyOnly` — return `false`. A principal needs an explicit
4475    ///   matching `Allow` to be authorized.
4476    ///
4477    /// An explicit `Deny` always wins, irrespective of mode and role.
4478    pub fn check_policy_authz_with_role(
4479        &self,
4480        principal: &UserId,
4481        action: &str,
4482        resource: &ResourceRef,
4483        ctx: &EvalContext,
4484        role: Role,
4485    ) -> bool {
4486        let pols = self.effective_policies(principal);
4487        let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4488        let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4489        match decision {
4490            iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass => true,
4491            iam_policies::Decision::Deny { .. } => false,
4492            iam_policies::Decision::DefaultDeny => match self.enforcement_mode() {
4493                PolicyEnforcementMode::LegacyRbac => legacy_rbac_decision(role, action),
4494                PolicyEnforcementMode::PolicyOnly => false,
4495            },
4496        }
4497    }
4498
4499    // -----------------------------------------------------------------
4500    // PolicyEnforcementMode (#712)
4501    // -----------------------------------------------------------------
4502
4503    /// Read the active enforcement mode. Cheap (a single `RwLock` read);
4504    /// safe to call on the hot path.
4505    pub fn enforcement_mode(&self) -> PolicyEnforcementMode {
4506        *self
4507            .enforcement_mode
4508            .read()
4509            .unwrap_or_else(|e| e.into_inner())
4510    }
4511
4512    /// Overwrite the active enforcement mode and persist it to the
4513    /// vault KV so the new value survives a restart. Returns the
4514    /// previous value so callers logging a transition (e.g. the
4515    /// boot-time loader, the S5B migration command) can record the
4516    /// before/after.
4517    pub fn set_enforcement_mode(&self, mode: PolicyEnforcementMode) -> PolicyEnforcementMode {
4518        let prev = {
4519            let mut guard = self
4520                .enforcement_mode
4521                .write()
4522                .unwrap_or_else(|e| e.into_inner());
4523            let prev = *guard;
4524            *guard = mode;
4525            prev
4526        };
4527        self.vault_kv_set(
4528            "red.config.policy.enforcement_mode".to_string(),
4529            mode.as_str().to_string(),
4530        );
4531        prev
4532    }
4533
4534    /// Claim the "emit the one-time legacy-RBAC boot warning" token.
4535    /// Returns `true` on the first call after construction (or after a
4536    /// process restart) **iff** the active mode is `LegacyRbac`;
4537    /// returns `false` on every subsequent call so the boot path can
4538    /// guarantee the warning is logged at most once per boot.
4539    pub fn take_legacy_rbac_warn_once(&self) -> bool {
4540        if self.enforcement_mode() != PolicyEnforcementMode::LegacyRbac {
4541            return false;
4542        }
4543        !self
4544            .legacy_rbac_boot_warn_emitted
4545            .swap(true, Ordering::AcqRel)
4546    }
4547
4548    /// Evaluate a resolved table projection through the column policy
4549    /// gate. Query paths should pass already-resolved column names; this
4550    /// helper intentionally does not parse SQL projection syntax.
4551    pub fn check_column_projection_authz(
4552        &self,
4553        principal: &UserId,
4554        request: &ColumnAccessRequest,
4555        ctx: &EvalContext,
4556    ) -> ColumnPolicyOutcome {
4557        let pols = self.effective_policies(principal);
4558        let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4559        ColumnPolicyGate::new(&refs).evaluate(request, ctx)
4560    }
4561
4562    fn invalidate_iam_cache(&self, uid: Option<&UserId>) {
4563        if let Ok(mut cache) = self.iam_effective_cache.write() {
4564            match uid {
4565                Some(u) => {
4566                    cache.remove(u);
4567                }
4568                None => cache.clear(),
4569            }
4570        }
4571    }
4572
4573    /// Drop every effective-policy cache entry. Called from execution
4574    /// paths that mutate policies/attachments without knowing which
4575    /// users will be affected.
4576    pub fn invalidate_all_iam_cache(&self) {
4577        self.invalidate_iam_cache(None);
4578    }
4579
4580    // -----------------------------------------------------------------
4581    // IAM persistence — vault_kv backed under `red.iam.*` keys
4582    // -----------------------------------------------------------------
4583
4584    /// Reload IAM state (policies + attachments) from the vault KV.
4585    /// Replaces the legacy `rehydrate_acl` reader — pre-1.0 we drop
4586    /// the old `red.acl.*` blob format entirely.
4587    pub fn rehydrate_iam(&self) {
4588        let mut enabled = self
4589            .vault_kv_get("red.iam.enabled")
4590            .map(|v| v == "true")
4591            .unwrap_or(false);
4592        // Policies — single JSON object keyed by id.
4593        if let Some(blob) = self.vault_kv_get("red.iam.policies") {
4594            if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4595                if let Some(obj) = val.as_object() {
4596                    let mut map = HashMap::new();
4597                    for (id, body) in obj.iter() {
4598                        let s = body.to_string_compact();
4599                        if let Ok(p) = Policy::from_json_str(&s) {
4600                            map.insert(id.clone(), Arc::new(p));
4601                        }
4602                    }
4603                    if !map.is_empty() {
4604                        enabled = true;
4605                    }
4606                    *self.policies.write().unwrap_or_else(|e| e.into_inner()) = map;
4607                }
4608            }
4609        }
4610        // User attachments.
4611        if let Some(blob) = self.vault_kv_get("red.iam.attachments.users") {
4612            if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4613                if let Some(obj) = val.as_object() {
4614                    let mut map: HashMap<UserId, Vec<String>> = HashMap::new();
4615                    for (encoded_uid, ids_v) in obj.iter() {
4616                        let Some(uid) = decode_uid(encoded_uid) else {
4617                            continue;
4618                        };
4619                        if let Some(arr) = ids_v.as_array() {
4620                            let ids: Vec<String> = arr
4621                                .iter()
4622                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
4623                                .collect();
4624                            map.insert(uid, ids);
4625                        }
4626                    }
4627                    *self
4628                        .user_attachments
4629                        .write()
4630                        .unwrap_or_else(|e| e.into_inner()) = map;
4631                }
4632            }
4633        }
4634        // Group attachments.
4635        if let Some(blob) = self.vault_kv_get("red.iam.attachments.groups") {
4636            if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4637                if let Some(obj) = val.as_object() {
4638                    let mut map: HashMap<String, Vec<String>> = HashMap::new();
4639                    for (g, ids_v) in obj.iter() {
4640                        if let Some(arr) = ids_v.as_array() {
4641                            let ids: Vec<String> = arr
4642                                .iter()
4643                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
4644                                .collect();
4645                            map.insert(g.clone(), ids);
4646                        }
4647                    }
4648                    *self
4649                        .group_attachments
4650                        .write()
4651                        .unwrap_or_else(|e| e.into_inner()) = map;
4652                }
4653            }
4654        }
4655        self.iam_authorization_enabled
4656            .store(enabled, Ordering::Release);
4657        self.invalidate_iam_cache(None);
4658
4659        // #712 / S5A: load persisted enforcement mode, if any. Absence
4660        // of the key means an existing install upgrading past #712 —
4661        // stay on the lenient default written at construction time.
4662        // Fresh installs receive `PolicyOnly` via the bootstrap path
4663        // (`mark_bootstrap_complete`) before the first restart, so this
4664        // hydrate path picks it up on every subsequent boot.
4665        if let Some(stored) = self.vault_kv_get("red.config.policy.enforcement_mode") {
4666            if let Some(mode) = PolicyEnforcementMode::parse(&stored) {
4667                *self
4668                    .enforcement_mode
4669                    .write()
4670                    .unwrap_or_else(|e| e.into_inner()) = mode;
4671            }
4672        }
4673
4674        // #712 / S5A: legacy_rbac is a transitional posture; emit one
4675        // boot-time warning so operators see the migration nudge in
4676        // their logs. The flag inside take_legacy_rbac_warn_once
4677        // guarantees we don't spam reload cycles.
4678        if self.take_legacy_rbac_warn_once() {
4679            tracing::warn!(
4680                target: "reddb::auth::enforcement_mode",
4681                mode = "legacy_rbac",
4682                hard_version = crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4683                "policy enforcement_mode=legacy_rbac (transitional). Run \
4684                 `MIGRATE POLICY MODE TO 'policy_only'` (next slice S5B) \
4685                 before {} to flip this install over to the strict posture.",
4686                crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4687            );
4688        }
4689    }
4690
4691    /// Snapshot policies + attachments into the vault KV. Called
4692    /// after every mutation.
4693    fn persist_iam_to_kv(&self) {
4694        let enabled = if self.iam_authorization_enabled() {
4695            "true"
4696        } else {
4697            "false"
4698        };
4699        self.vault_kv_set("red.iam.enabled".to_string(), enabled.to_string());
4700
4701        // Policies: `{ "<id>": <policy_json>, ... }`
4702        let policies_obj = {
4703            let map = self.policies.read().unwrap_or_else(|e| e.into_inner());
4704            let mut obj = crate::serde_json::Map::new();
4705            for (id, p) in map.iter() {
4706                let s = p.to_json_string();
4707                if let Ok(v) = crate::serde_json::from_str::<crate::serde_json::Value>(&s) {
4708                    obj.insert(id.clone(), v);
4709                }
4710            }
4711            crate::serde_json::Value::Object(obj).to_string_compact()
4712        };
4713        self.vault_kv_set("red.iam.policies".to_string(), policies_obj);
4714
4715        // User attachments: `{ "<encoded_uid>": [ "<policy_id>", ... ], ... }`
4716        let users_obj = {
4717            let map = self
4718                .user_attachments
4719                .read()
4720                .unwrap_or_else(|e| e.into_inner());
4721            let mut obj = crate::serde_json::Map::new();
4722            for (uid, ids) in map.iter() {
4723                let arr = crate::serde_json::Value::Array(
4724                    ids.iter()
4725                        .map(|s| crate::serde_json::Value::String(s.clone()))
4726                        .collect(),
4727                );
4728                obj.insert(encode_uid(uid), arr);
4729            }
4730            crate::serde_json::Value::Object(obj).to_string_compact()
4731        };
4732        self.vault_kv_set("red.iam.attachments.users".to_string(), users_obj);
4733
4734        // Group attachments.
4735        let groups_obj = {
4736            let map = self
4737                .group_attachments
4738                .read()
4739                .unwrap_or_else(|e| e.into_inner());
4740            let mut obj = crate::serde_json::Map::new();
4741            for (g, ids) in map.iter() {
4742                let arr = crate::serde_json::Value::Array(
4743                    ids.iter()
4744                        .map(|s| crate::serde_json::Value::String(s.clone()))
4745                        .collect(),
4746                );
4747                obj.insert(g.clone(), arr);
4748            }
4749            crate::serde_json::Value::Object(obj).to_string_compact()
4750        };
4751        self.vault_kv_set("red.iam.attachments.groups".to_string(), groups_obj);
4752    }
4753}
4754
4755fn synthetic_grant_matches(policy: &Policy, resource: &Resource, actions: &[Action]) -> bool {
4756    policy.statements.iter().any(|st| {
4757        st.effect == crate::auth::policies::Effect::Allow
4758            && st.condition.is_none()
4759            && grant_actions_overlap(&st.actions, actions)
4760            && grant_resource_matches(&st.resources, resource)
4761    })
4762}
4763
4764fn grant_actions_overlap(
4765    patterns: &[crate::auth::policies::ActionPattern],
4766    actions: &[Action],
4767) -> bool {
4768    if actions.contains(&Action::All) {
4769        return true;
4770    }
4771    patterns.iter().any(|pat| match pat {
4772        crate::auth::policies::ActionPattern::Wildcard => true,
4773        crate::auth::policies::ActionPattern::Exact(s) => {
4774            actions.iter().any(|a| s.eq_ignore_ascii_case(a.as_str()))
4775        }
4776        crate::auth::policies::ActionPattern::Prefix(_) => false,
4777    })
4778}
4779
4780fn grant_resource_matches(
4781    patterns: &[crate::auth::policies::ResourcePattern],
4782    resource: &Resource,
4783) -> bool {
4784    let expected = grant_resource_pattern(resource);
4785    patterns.iter().any(|pat| pat == &expected)
4786}
4787
4788fn grant_resource_pattern(resource: &Resource) -> crate::auth::policies::ResourcePattern {
4789    use crate::auth::policies::ResourcePattern;
4790
4791    match resource {
4792        Resource::Database => ResourcePattern::Glob("table:*".to_string()),
4793        Resource::Schema(s) => ResourcePattern::Glob(format!("table:{s}.*")),
4794        Resource::Table { schema, table } => ResourcePattern::Exact {
4795            kind: "table".to_string(),
4796            name: match schema {
4797                Some(s) => format!("{s}.{table}"),
4798                None => table.clone(),
4799            },
4800        },
4801        Resource::Function { schema, name } => ResourcePattern::Exact {
4802            kind: "function".to_string(),
4803            name: match schema {
4804                Some(s) => format!("{s}.{name}"),
4805                None => name.clone(),
4806            },
4807        },
4808    }
4809}
4810
4811// ===========================================================================
4812// ACL serialization helpers — line-oriented, human-readable so an
4813// operator inspecting the vault dump can spot misconfigurations.
4814//
4815// Format (one record per line):
4816//   GRANT|<resource>|<actions_csv>|<with_grant_option>|<tenant_or_*>|<granted_by>|<granted_at>
4817//   ATTR|<valid_until>|<connection_limit>|<search_path>
4818//
4819// Resources are encoded as:
4820//   db                          → Database
4821//   schema:<name>               → Schema(name)
4822//   table:<schema_or_*>:<name>  → Table { schema, table }
4823//   func:<schema_or_*>:<name>   → Function { schema, name }
4824// ===========================================================================
4825
4826fn encode_uid(uid: &UserId) -> String {
4827    match &uid.tenant {
4828        Some(t) => format!("{}/{}", t, uid.username),
4829        None => format!("*/{}", uid.username),
4830    }
4831}
4832
4833fn decode_uid(s: &str) -> Option<UserId> {
4834    let (tenant, username) = s.split_once('/')?;
4835    Some(if tenant == "*" {
4836        UserId::platform(username)
4837    } else {
4838        UserId::scoped(tenant, username)
4839    })
4840}
4841
4842fn encode_resource(r: &Resource) -> String {
4843    match r {
4844        Resource::Database => "db".into(),
4845        Resource::Schema(s) => format!("schema:{}", s),
4846        Resource::Table { schema, table } => {
4847            format!("table:{}:{}", schema.as_deref().unwrap_or("*"), table)
4848        }
4849        Resource::Function { schema, name } => {
4850            format!("func:{}:{}", schema.as_deref().unwrap_or("*"), name)
4851        }
4852    }
4853}
4854
4855fn decode_resource(s: &str) -> Option<Resource> {
4856    if s == "db" {
4857        return Some(Resource::Database);
4858    }
4859    if let Some(rest) = s.strip_prefix("schema:") {
4860        return Some(Resource::Schema(rest.to_string()));
4861    }
4862    if let Some(rest) = s.strip_prefix("table:") {
4863        let (schema, table) = rest.split_once(':')?;
4864        return Some(Resource::Table {
4865            schema: if schema == "*" {
4866                None
4867            } else {
4868                Some(schema.to_string())
4869            },
4870            table: table.to_string(),
4871        });
4872    }
4873    if let Some(rest) = s.strip_prefix("func:") {
4874        let (schema, name) = rest.split_once(':')?;
4875        return Some(Resource::Function {
4876            schema: if schema == "*" {
4877                None
4878            } else {
4879                Some(schema.to_string())
4880            },
4881            name: name.to_string(),
4882        });
4883    }
4884    None
4885}
4886
4887fn encode_grants_blob(grants: &[Grant]) -> String {
4888    let mut out = String::new();
4889    for g in grants {
4890        let actions: Vec<&str> = g.actions.iter().map(|a| a.as_str()).collect();
4891        out.push_str(&format!(
4892            "GRANT|{}|{}|{}|{}|{}|{}\n",
4893            encode_resource(&g.resource),
4894            actions.join(","),
4895            g.with_grant_option,
4896            g.tenant.as_deref().unwrap_or("*"),
4897            g.granted_by,
4898            g.granted_at,
4899        ));
4900    }
4901    out
4902}
4903
4904fn decode_grants_blob(s: &str) -> Option<Vec<Grant>> {
4905    let mut out = Vec::new();
4906    for line in s.lines() {
4907        if line.is_empty() {
4908            continue;
4909        }
4910        let parts: Vec<&str> = line.split('|').collect();
4911        if parts.len() != 7 || parts[0] != "GRANT" {
4912            return None;
4913        }
4914        let resource = decode_resource(parts[1])?;
4915        let mut actions = std::collections::BTreeSet::new();
4916        for token in parts[2].split(',') {
4917            if let Some(a) = Action::from_keyword(token) {
4918                actions.insert(a);
4919            }
4920        }
4921        let with_grant_option = parts[3] == "true";
4922        let tenant = if parts[4] == "*" {
4923            None
4924        } else {
4925            Some(parts[4].to_string())
4926        };
4927        let granted_by = parts[5].to_string();
4928        let granted_at: u128 = parts[6].parse().unwrap_or(0);
4929        out.push(Grant {
4930            // Principal field is reconstructed by the loader from the
4931            // storage-key prefix; default to `Public` here.
4932            principal: GrantPrincipal::Public,
4933            resource,
4934            actions,
4935            with_grant_option,
4936            granted_by,
4937            granted_at,
4938            tenant,
4939            columns: None,
4940        });
4941    }
4942    Some(out)
4943}
4944
4945fn encode_attrs_blob(a: &UserAttributes) -> String {
4946    let valid = a
4947        .valid_until
4948        .map(|t| t.to_string())
4949        .unwrap_or_else(|| "*".into());
4950    let limit = a
4951        .connection_limit
4952        .map(|l| l.to_string())
4953        .unwrap_or_else(|| "*".into());
4954    let path = a.search_path.clone().unwrap_or_else(|| "*".into());
4955    let groups = if a.groups.is_empty() {
4956        "*".to_string()
4957    } else {
4958        a.groups.join(",")
4959    };
4960    format!("ATTR|{}|{}|{}|{}\n", valid, limit, path, groups)
4961}
4962
4963fn decode_attrs_blob(s: &str) -> Option<UserAttributes> {
4964    let line = s.lines().next()?;
4965    let parts: Vec<&str> = line.split('|').collect();
4966    if !(parts.len() == 4 || parts.len() == 5) || parts[0] != "ATTR" {
4967        return None;
4968    }
4969    let groups = if parts.get(4).copied().unwrap_or("*") == "*" {
4970        Vec::new()
4971    } else {
4972        parts[4]
4973            .split(',')
4974            .filter(|g| !g.is_empty())
4975            .map(|g| g.to_string())
4976            .collect()
4977    };
4978    Some(UserAttributes {
4979        valid_until: if parts[1] == "*" {
4980            None
4981        } else {
4982            parts[1].parse().ok()
4983        },
4984        connection_limit: if parts[2] == "*" {
4985            None
4986        } else {
4987            parts[2].parse().ok()
4988        },
4989        search_path: if parts[3] == "*" {
4990            None
4991        } else {
4992            Some(parts[3].to_string())
4993        },
4994        groups,
4995    })
4996}
4997
4998// ===========================================================================
4999// Password hashing
5000// ===========================================================================
5001
5002/// Derive a SCRAM-SHA-256 verifier for a fresh user / password
5003/// rotation. Salt is 16 random bytes; iter is the engine default
5004/// (`scram::DEFAULT_ITER`). Stored alongside the Argon2 password
5005/// hash so HTTP login + v2 SCRAM can both authenticate the same
5006/// user.
5007fn make_scram_verifier(password: &str) -> crate::auth::scram::ScramVerifier {
5008    let salt = random_bytes(16);
5009    crate::auth::scram::ScramVerifier::from_password(
5010        password,
5011        salt,
5012        crate::auth::scram::DEFAULT_ITER,
5013    )
5014}
5015
5016/// Argon2id cost parameters for password hashing, at the OWASP floor
5017/// (19 MiB, t=2, p=1). The previous in-tree KDF ran at 4 MiB / t=3, which
5018/// is below that floor; the parameters are encoded in the PHC string so
5019/// they can be raised again without another format change.
5020const PASSWORD_M_COST_KIB: u32 = 19 * 1024;
5021const PASSWORD_T_COST: u32 = 2;
5022const PASSWORD_P_COST: u32 = 1;
5023const PASSWORD_TAG_LEN: usize = 32;
5024
5025fn password_hasher() -> argon2::Argon2<'static> {
5026    let params = argon2::Params::new(
5027        PASSWORD_M_COST_KIB,
5028        PASSWORD_T_COST,
5029        PASSWORD_P_COST,
5030        Some(PASSWORD_TAG_LEN),
5031    )
5032    .expect("Argon2id parameters are constant and valid");
5033    argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
5034}
5035
5036/// Hash a password with Argon2id (RFC 9106) via the RustCrypto `argon2`
5037/// crate, encoded as a PHC string:
5038/// `$argon2id$v=19$m=19456,t=2,p=1$<salt_b64>$<hash_b64>`.
5039///
5040/// The previous in-tree implementation was not RFC 9106 — its `G` mixing
5041/// function added the block indices where the specification calls for the
5042/// `2·lo32(a)·lo32(b)` term, and its compression function was ad hoc — so
5043/// its memory-hardness was unknown. Hashes in the old
5044/// `argon2id$<salt_hex>$<hash_hex>` form still verify (see
5045/// [`verify_password`]) and are transparently upgraded on the owner's next
5046/// successful login.
5047pub(crate) fn hash_password(password: &str) -> String {
5048    use argon2::PasswordHasher;
5049    // The salt comes from the OS CSPRNG inside the hasher and, like
5050    // `random_bytes`, fails closed rather than degrading.
5051    password_hasher()
5052        .hash_password(password.as_bytes())
5053        .expect("Argon2id hashing cannot fail for valid parameters and a working CSPRNG")
5054        .to_string()
5055}
5056
5057/// Whether `stored_hash` was produced by the retired in-tree KDF and should
5058/// be re-hashed the next time its owner authenticates.
5059pub(crate) fn password_hash_needs_upgrade(stored_hash: &str) -> bool {
5060    stored_hash.starts_with("argon2id$")
5061}
5062
5063/// Verify a password against either encoding: the current PHC string, or a
5064/// legacy `argon2id$<salt_hex>$<hash_hex>` produced by the retired in-tree
5065/// KDF. Legacy hashes stay verifiable so an upgrade does not lock every
5066/// account out; [`password_hash_needs_upgrade`] drives the rehash.
5067pub(crate) fn verify_password(password: &str, stored_hash: &str) -> bool {
5068    if let Some(rest) = stored_hash.strip_prefix("argon2id$") {
5069        return verify_legacy_password(password, rest);
5070    }
5071    use argon2::{PasswordHash, PasswordVerifier};
5072    let Ok(parsed) = stored_hash.parse::<PasswordHash>() else {
5073        return false;
5074    };
5075    password_hasher()
5076        .verify_password(password.as_bytes(), &parsed)
5077        .is_ok()
5078}
5079
5080/// Verify against the retired in-tree KDF, whose stored form is
5081/// `<salt_hex>$<hash_hex>` (the `argon2id$` prefix is already stripped).
5082fn verify_legacy_password(password: &str, salt_and_hash: &str) -> bool {
5083    let Some((salt_hex, hash_hex)) = salt_and_hash.split_once('$') else {
5084        return false;
5085    };
5086    let (Ok(salt), Ok(expected)) = (hex::decode(salt_hex), hex::decode(hash_hex)) else {
5087        return false;
5088    };
5089    let computed = derive_key(password.as_bytes(), &salt, &auth_argon2_params());
5090    constant_time_eq(&computed, &expected)
5091}
5092
5093/// Constant-time byte comparison — one implementation for the crate.
5094fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
5095    crate::crypto::constant_time_eq(a, b)
5096}
5097
5098// ===========================================================================
5099// Token generation
5100// ===========================================================================
5101
5102fn generate_session_token() -> String {
5103    format!("rs_{}", hex::encode(random_bytes(32)))
5104}
5105
5106fn generate_api_key() -> String {
5107    format!("rk_{}", hex::encode(random_bytes(32)))
5108}
5109
5110/// Generate `n` random bytes and return as a hex string.
5111fn random_hex(n: usize) -> String {
5112    hex::encode(random_bytes(n))
5113}
5114
5115/// Generate `n` cryptographically random bytes using the OS CSPRNG,
5116/// then mix with SHA-256 for domain separation.
5117pub(crate) fn random_bytes(n: usize) -> Vec<u8> {
5118    let mut buf = vec![0u8; n.max(32)];
5119    // Fail closed: these bytes become session tokens, API keys, salts and
5120    // AES-GCM nonces. A predictable fallback (the old time-derived seed)
5121    // is strictly worse than refusing the operation.
5122    if let Err(err) = os_random::fill_bytes(&mut buf) {
5123        panic!("OS CSPRNG unavailable; refusing to generate credential material: {err}");
5124    }
5125    // SHA-256 mix to ensure uniform distribution.
5126    let digest = sha256(&buf);
5127    if n <= 32 {
5128        digest[..n].to_vec()
5129    } else {
5130        // Chain SHA-256 for longer outputs (unusual but supported).
5131        let mut out = Vec::with_capacity(n);
5132        let mut prev = digest;
5133        while out.len() < n {
5134            out.extend_from_slice(&prev[..std::cmp::min(32, n - out.len())]);
5135            prev = sha256(&prev);
5136        }
5137        out
5138    }
5139}
5140
5141// ===========================================================================
5142// Helpers
5143// ===========================================================================
5144
5145fn lock_err<T>(_: T) -> AuthError {
5146    AuthError::Internal("lock poisoned".to_string())
5147}
5148
5149// ===========================================================================
5150// Tests
5151// ===========================================================================
5152
5153#[cfg(test)]
5154mod tests {
5155    use super::*;
5156
5157    fn test_config() -> AuthConfig {
5158        AuthConfig {
5159            enabled: true,
5160            session_ttl_secs: 60,
5161            require_auth: true,
5162            auto_encrypt_storage: false,
5163            vault_enabled: false,
5164            cert: Default::default(),
5165            oauth: Default::default(),
5166        }
5167    }
5168
5169    #[test]
5170    fn test_create_and_list_users() {
5171        let store = AuthStore::new(test_config());
5172        store.create_user("alice", "pass1", Role::Admin).unwrap();
5173        store.create_user("bob", "pass2", Role::Read).unwrap();
5174
5175        let users = store.list_users();
5176        assert_eq!(users.len(), 2);
5177        // Password hashes should be redacted.
5178        for u in &users {
5179            assert!(u.password_hash.is_empty());
5180        }
5181    }
5182
5183    #[test]
5184    fn test_create_duplicate_user() {
5185        let store = AuthStore::new(test_config());
5186        store.create_user("alice", "pass", Role::Admin).unwrap();
5187        let err = store.create_user("alice", "pass2", Role::Read).unwrap_err();
5188        assert!(matches!(err, AuthError::UserExists(_)));
5189    }
5190
5191    struct RecordingLedger(std::sync::Mutex<Vec<(String, String)>>);
5192
5193    impl crate::runtime::control_events::ControlEventLedger for RecordingLedger {
5194        fn emit(
5195            &self,
5196            _ctx: &ControlEventCtx<'_>,
5197            event: ControlEvent,
5198        ) -> Result<
5199            crate::runtime::control_events::EventId,
5200            crate::runtime::control_events::ControlEventError,
5201        > {
5202            self.0.lock().unwrap().push((
5203                event.kind.as_str().to_string(),
5204                event.outcome.as_str().to_string(),
5205            ));
5206            Ok(crate::runtime::control_events::EventId("evt".to_string()))
5207        }
5208    }
5209
5210    #[test]
5211    fn logins_land_in_the_control_event_ledger() {
5212        let store = AuthStore::new(test_config());
5213        store.create_user("dave", "pw", Role::Read).unwrap();
5214        let ledger = std::sync::Arc::new(RecordingLedger(std::sync::Mutex::new(Vec::new())));
5215        store.configure_control_events(
5216            ledger.clone(),
5217            crate::runtime::control_events::ControlEventConfig::default(),
5218        );
5219        store.authenticate("dave", "pw").expect("login");
5220        store
5221            .authenticate("dave", "nope")
5222            .expect_err("bad password");
5223        store
5224            .authenticate("nobody", "pw")
5225            .expect_err("unknown user");
5226        let seen = ledger.0.lock().unwrap().clone();
5227        let allowed = Outcome::Allowed.as_str().to_string();
5228        let denied = Outcome::Denied.as_str().to_string();
5229        assert!(
5230            seen.contains(&("auth.login".to_string(), allowed)),
5231            "success not recorded: {seen:?}"
5232        );
5233        assert_eq!(
5234            seen.iter()
5235                .filter(|(k, o)| k == "auth.login_failed" && *o == denied)
5236                .count(),
5237            2,
5238            "both refusals must be recorded: {seen:?}"
5239        );
5240    }
5241
5242    #[test]
5243    fn api_keys_are_hashed_at_rest_and_expire() {
5244        let store = AuthStore::new(test_config());
5245        store.create_user("erin", "pw", Role::Write).unwrap();
5246        let key = store
5247            .create_api_key_in_tenant_expiring(None, "erin", "ci", Role::Read, Some(now_ms() + 60))
5248            .unwrap();
5249        assert!(key.key.starts_with("rk_"));
5250
5251        let stored = store
5252            .users
5253            .read()
5254            .unwrap()
5255            .get(&UserId::from_parts(None, "erin"))
5256            .unwrap()
5257            .api_keys[0]
5258            .clone();
5259        assert!(stored.key.starts_with("rkh_"), "{}", stored.key);
5260        assert_ne!(stored.key, key.key, "the secret must not be stored");
5261        assert_eq!(stored.key, api_key_credential_id(&key.key));
5262
5263        assert!(store.validate_token(&key.key).is_some());
5264        std::thread::sleep(std::time::Duration::from_millis(80));
5265        assert!(
5266            store.validate_token(&key.key).is_none(),
5267            "an expired key must not validate"
5268        );
5269
5270        // Revocation accepts the stored id as well as the secret.
5271        store.revoke_api_key(&stored.key).unwrap();
5272        assert!(store.revoke_api_key(&key.key).is_err());
5273    }
5274
5275    #[test]
5276    fn revoke_is_clamped_to_the_granter_tenant() {
5277        use crate::auth::privileges::{Action, GrantPrincipal, Resource};
5278        let store = AuthStore::new(test_config());
5279        let acme_admin = UserId::from_parts(Some("acme"), "adm");
5280        let globex_user = UserId::from_parts(Some("globex"), "u");
5281        let err = store
5282            .revoke(
5283                &acme_admin,
5284                Role::Admin,
5285                &GrantPrincipal::User(globex_user),
5286                &Resource::table_from_name("orders"),
5287                &[Action::Select],
5288            )
5289            .expect_err("a tenant admin must not touch another tenant's grants");
5290        assert!(err.to_string().contains("cross-tenant"), "{err}");
5291        // Platform admin may.
5292        store
5293            .revoke(
5294                &UserId::from_parts(None, "platform"),
5295                Role::Admin,
5296                &GrantPrincipal::User(UserId::from_parts(Some("globex"), "u")),
5297                &Resource::table_from_name("orders"),
5298                &[Action::Select],
5299            )
5300            .expect("platform admin revokes anywhere");
5301    }
5302
5303    #[test]
5304    fn role_change_revokes_sessions_minted_under_the_old_role() {
5305        let store = AuthStore::new(test_config());
5306        store.create_user("carol", "pw", Role::Write).unwrap();
5307        let session = store.authenticate("carol", "pw").unwrap();
5308        assert!(store.validate_token(&session.token).is_some());
5309        store.change_role("carol", Role::Read).unwrap();
5310        assert!(
5311            store.validate_token(&session.token).is_none(),
5312            "a demotion must not keep riding on the old session"
5313        );
5314    }
5315
5316    #[test]
5317    fn test_authenticate_and_validate() {
5318        let store = AuthStore::new(test_config());
5319        store.create_user("alice", "secret", Role::Write).unwrap();
5320
5321        let session = store.authenticate("alice", "secret").unwrap();
5322        assert!(session.token.starts_with("rs_"));
5323
5324        let (username, role) = store.validate_token(&session.token).unwrap();
5325        assert_eq!(username, "alice");
5326        assert_eq!(role, Role::Write);
5327    }
5328
5329    #[test]
5330    fn test_authenticate_wrong_password() {
5331        let store = AuthStore::new(test_config());
5332        store.create_user("alice", "secret", Role::Read).unwrap();
5333
5334        let err = store.authenticate("alice", "wrong").unwrap_err();
5335        assert!(matches!(err, AuthError::InvalidCredentials));
5336    }
5337
5338    #[test]
5339    fn test_api_key_lifecycle() {
5340        let store = AuthStore::new(test_config());
5341        store.create_user("alice", "pass", Role::Admin).unwrap();
5342
5343        let key = store
5344            .create_api_key("alice", "ci-token", Role::Write)
5345            .unwrap();
5346        assert!(key.key.starts_with("rk_"));
5347
5348        let (username, role) = store.validate_token(&key.key).unwrap();
5349        assert_eq!(username, "alice");
5350        assert_eq!(role, Role::Write);
5351
5352        store.revoke_api_key(&key.key).unwrap();
5353        assert!(store.validate_token(&key.key).is_none());
5354    }
5355
5356    #[test]
5357    fn test_api_key_role_exceeded() {
5358        let store = AuthStore::new(test_config());
5359        store.create_user("bob", "pass", Role::Read).unwrap();
5360
5361        let err = store
5362            .create_api_key("bob", "escalate", Role::Admin)
5363            .unwrap_err();
5364        assert!(matches!(err, AuthError::RoleExceeded { .. }));
5365    }
5366
5367    #[test]
5368    fn test_change_password() {
5369        let store = AuthStore::new(test_config());
5370        store.create_user("alice", "old", Role::Write).unwrap();
5371
5372        store.change_password("alice", "old", "new").unwrap();
5373
5374        // Old password should fail.
5375        assert!(store.authenticate("alice", "old").is_err());
5376        // New password should succeed.
5377        assert!(store.authenticate("alice", "new").is_ok());
5378    }
5379
5380    #[test]
5381    fn test_change_role() {
5382        let store = AuthStore::new(test_config());
5383        store.create_user("alice", "pass", Role::Admin).unwrap();
5384        store.create_api_key("alice", "key1", Role::Admin).unwrap();
5385
5386        store.change_role("alice", Role::Read).unwrap();
5387
5388        // User's role should be Read now.
5389        let users = store.list_users();
5390        let alice = users.iter().find(|u| u.username == "alice").unwrap();
5391        assert_eq!(alice.role, Role::Read);
5392
5393        // API keys should have been downgraded.
5394        assert_eq!(alice.api_keys[0].role, Role::Read);
5395    }
5396
5397    #[test]
5398    fn test_admin_user_mutations_are_policy_controlled_not_flag_guarded() {
5399        let store = AuthStore::new(test_config());
5400        store
5401            .create_admin_user("system", "pass", Role::Admin, None)
5402            .unwrap();
5403
5404        let uid = UserId::platform("system");
5405        store.change_password("system", "pass", "new").unwrap();
5406        store.change_role("system", Role::Read).unwrap();
5407        store.set_user_enabled(&uid, false).unwrap();
5408
5409        let key = store
5410            .create_api_key("system", "rotation", Role::Read)
5411            .unwrap();
5412        // The account is disabled at this point: every credential it owns
5413        // must stop validating, API keys included (they used to be resolved
5414        // straight from the index without consulting the user record).
5415        assert!(store.validate_token(&key.key).is_none());
5416        store.set_user_enabled(&uid, true).unwrap();
5417        assert!(store.validate_token(&key.key).is_some());
5418        store.revoke_api_key(&key.key).unwrap();
5419        assert!(store.validate_token(&key.key).is_none());
5420        store.delete_user("system").unwrap();
5421    }
5422
5423    #[test]
5424    fn test_user_lifecycle_deny_policy_blocks_cloud_admin_deletion() {
5425        use crate::auth::policies::Policy;
5426
5427        let store = AuthStore::new(test_config());
5428        store
5429            .create_user("cloud-admin", "pass", Role::Admin)
5430            .unwrap();
5431        store
5432            .create_user("customer-admin", "pass", Role::Admin)
5433            .unwrap();
5434
5435        store
5436            .put_policy(
5437                Policy::from_json_str(
5438                    r#"{
5439                        "id": "customer-admin-allow-all",
5440                        "version": 1,
5441                        "statements": [{
5442                            "effect": "allow",
5443                            "actions": ["*"],
5444                            "resources": ["*"]
5445                        }]
5446                    }"#,
5447                )
5448                .unwrap(),
5449            )
5450            .unwrap();
5451        store
5452            .put_policy(
5453                Policy::from_json_str(
5454                    r#"{
5455                        "id": "cloud-admin-protection",
5456                        "version": 1,
5457                        "statements": [{
5458                            "effect": "deny",
5459                            "actions": [
5460                                "user:delete",
5461                                "user:disable",
5462                                "user:password:change",
5463                                "user:role:update"
5464                            ],
5465                            "resources": ["user:cloud-admin"]
5466                        }]
5467                    }"#,
5468                )
5469                .unwrap(),
5470            )
5471            .unwrap();
5472        let customer = UserId::platform("customer-admin");
5473        store
5474            .attach_policy(
5475                PrincipalRef::User(customer.clone()),
5476                "customer-admin-allow-all",
5477            )
5478            .unwrap();
5479        store
5480            .attach_policy(
5481                PrincipalRef::User(customer.clone()),
5482                "cloud-admin-protection",
5483            )
5484            .unwrap();
5485
5486        let cloud_admin = UserId::platform("cloud-admin");
5487        assert!(!store.check_user_lifecycle_authz(
5488            &customer,
5489            Role::Admin,
5490            "user:delete",
5491            &cloud_admin,
5492        ));
5493        assert!(!store.check_user_lifecycle_authz(
5494            &customer,
5495            Role::Admin,
5496            "user:disable",
5497            &cloud_admin,
5498        ));
5499        assert!(!store.check_user_lifecycle_authz(
5500            &customer,
5501            Role::Admin,
5502            "user:password:change",
5503            &cloud_admin,
5504        ));
5505        assert!(!store.check_user_lifecycle_authz(
5506            &customer,
5507            Role::Admin,
5508            "user:role:update",
5509            &cloud_admin,
5510        ));
5511
5512        let another_user = UserId::platform("someone-else");
5513        assert!(store.check_user_lifecycle_authz(
5514            &customer,
5515            Role::Admin,
5516            "user:delete",
5517            &another_user,
5518        ));
5519    }
5520
5521    #[test]
5522    fn test_regular_user_mutations_still_work() {
5523        let store = AuthStore::new(test_config());
5524        store.create_user("alice", "old", Role::Admin).unwrap();
5525
5526        let uid = UserId::platform("alice");
5527        store.set_user_enabled(&uid, false).unwrap();
5528        assert!(matches!(
5529            store.authenticate("alice", "old"),
5530            Err(AuthError::InvalidCredentials)
5531        ));
5532
5533        store.set_user_enabled(&uid, true).unwrap();
5534        store.change_password("alice", "old", "new").unwrap();
5535        store.change_role("alice", Role::Read).unwrap();
5536        store.delete_user("alice").unwrap();
5537        assert!(matches!(
5538            store.authenticate("alice", "new"),
5539            Err(AuthError::InvalidCredentials)
5540        ));
5541    }
5542
5543    #[test]
5544    fn test_delete_user() {
5545        let store = AuthStore::new(test_config());
5546        store.create_user("alice", "pass", Role::Admin).unwrap();
5547        let key = store.create_api_key("alice", "key1", Role::Read).unwrap();
5548        let session = store.authenticate("alice", "pass").unwrap();
5549
5550        store.delete_user("alice").unwrap();
5551
5552        assert!(store.validate_token(&key.key).is_none());
5553        assert!(store.validate_token(&session.token).is_none());
5554        assert!(store.list_users().is_empty());
5555    }
5556
5557    #[test]
5558    fn test_revoke_session() {
5559        let store = AuthStore::new(test_config());
5560        store.create_user("alice", "pass", Role::Read).unwrap();
5561        let session = store.authenticate("alice", "pass").unwrap();
5562
5563        store.revoke_session(&session.token);
5564        assert!(store.validate_token(&session.token).is_none());
5565    }
5566
5567    #[test]
5568    fn test_password_hash_format() {
5569        // PHC string: `$argon2id$v=19$m=19456,t=2,p=1$<salt_b64>$<hash_b64>`.
5570        // Encoding the parameters is what lets the cost be raised later
5571        // without another format change — the retired in-tree form stored
5572        // only salt and tag, so its 4 MiB / t=3 cost was frozen.
5573        let hash = hash_password("test");
5574        assert!(
5575            hash.starts_with("$argon2id$v=19$"),
5576            "expected a PHC-encoded Argon2id hash, got: {hash}"
5577        );
5578        assert!(
5579            hash.contains(&format!(
5580                "m={PASSWORD_M_COST_KIB},t={PASSWORD_T_COST},p={PASSWORD_P_COST}"
5581            )),
5582            "the PHC string must carry the cost parameters: {hash}"
5583        );
5584        assert!(verify_password("test", &hash));
5585        assert!(!verify_password("wrong", &hash));
5586        assert!(
5587            !password_hash_needs_upgrade(&hash),
5588            "a freshly minted hash must not be flagged for upgrade"
5589        );
5590    }
5591
5592    #[test]
5593    fn legacy_password_hashes_still_verify_and_upgrade_on_login() {
5594        // Records written by the retired in-tree KDF must keep verifying —
5595        // otherwise the upgrade locks every existing account out — and must
5596        // be re-hashed with the real KDF once the plaintext is in hand.
5597        let legacy = {
5598            let salt = random_bytes(16);
5599            let hash = derive_key(b"hunter2", &salt, &auth_argon2_params());
5600            format!("argon2id${}${}", hex::encode(&salt), hex::encode(&hash))
5601        };
5602        assert!(verify_password("hunter2", &legacy));
5603        assert!(!verify_password("wrong", &legacy));
5604        assert!(password_hash_needs_upgrade(&legacy));
5605
5606        let store = AuthStore::new(test_config());
5607        store
5608            .create_user("erin", "placeholder", Role::Write)
5609            .unwrap();
5610        {
5611            let mut users = store.users.write().unwrap();
5612            users
5613                .get_mut(&UserId::platform("erin"))
5614                .unwrap()
5615                .password_hash = legacy.clone();
5616        }
5617
5618        store
5619            .authenticate("erin", "hunter2")
5620            .expect("a legacy hash must still authenticate");
5621
5622        let stored = store
5623            .users
5624            .read()
5625            .unwrap()
5626            .get(&UserId::platform("erin"))
5627            .unwrap()
5628            .password_hash
5629            .clone();
5630        assert!(
5631            stored.starts_with("$argon2id$v=19$"),
5632            "a successful login must upgrade the stored hash, got: {stored}"
5633        );
5634        assert!(store.authenticate("erin", "hunter2").is_ok());
5635    }
5636
5637    #[test]
5638    fn test_constant_time_eq() {
5639        assert!(constant_time_eq(b"hello", b"hello"));
5640        assert!(!constant_time_eq(b"hello", b"world"));
5641        assert!(!constant_time_eq(b"short", b"longer"));
5642    }
5643
5644    #[test]
5645    fn test_bootstrap_seals_permanently() {
5646        let store = AuthStore::new(test_config());
5647
5648        assert!(store.needs_bootstrap());
5649        assert!(!store.is_bootstrapped());
5650
5651        // First bootstrap succeeds
5652        let result = store.bootstrap("admin", "secret");
5653        assert!(result.is_ok());
5654        let br = result.unwrap();
5655        assert_eq!(br.user.username, "admin");
5656        assert_eq!(br.user.role, Role::Admin);
5657        assert!(br.api_key.key.starts_with("rk_"));
5658        // No vault configured, so no certificate.
5659        assert!(br.certificate.is_none());
5660
5661        // Sealed now
5662        assert!(!store.needs_bootstrap());
5663        assert!(store.is_bootstrapped());
5664
5665        // Second bootstrap fails -- sealed permanently
5666        let result = store.bootstrap("admin2", "secret2");
5667        assert!(result.is_err());
5668        let err = result.unwrap_err();
5669        assert!(err.to_string().contains("sealed permanently"));
5670
5671        // Only 1 user exists (the first one)
5672        assert_eq!(store.list_users().len(), 1);
5673        assert_eq!(store.list_users()[0].username, "admin");
5674    }
5675
5676    #[test]
5677    fn test_bootstrap_after_manual_user_creation() {
5678        let store = AuthStore::new(test_config());
5679
5680        // Create a user manually first
5681        store.create_user("existing", "pass", Role::Read).unwrap();
5682
5683        // Bootstrap sees the seal hasn't been set but users exist
5684        // The atomic seal fires first, then the users check catches it
5685        assert!(!store.needs_bootstrap()); // users exist → false
5686    }
5687
5688    // ---------------------------------------------------------------
5689    // Tenant scoping
5690    // ---------------------------------------------------------------
5691
5692    #[test]
5693    fn test_same_username_two_tenants_distinct() {
5694        let store = AuthStore::new(test_config());
5695        store
5696            .create_user_in_tenant(Some("acme"), "alice", "pw-acme", Role::Write)
5697            .unwrap();
5698        store
5699            .create_user_in_tenant(Some("globex"), "alice", "pw-globex", Role::Read)
5700            .unwrap();
5701
5702        // Two distinct users.
5703        let users = store.list_users();
5704        assert_eq!(users.len(), 2);
5705
5706        // Each verifies its own password under its own tenant.
5707        assert!(store
5708            .authenticate_in_tenant(Some("acme"), "alice", "pw-acme")
5709            .is_ok());
5710        assert!(store
5711            .authenticate_in_tenant(Some("globex"), "alice", "pw-globex")
5712            .is_ok());
5713
5714        // Cross-tenant credentials are rejected.
5715        assert!(store
5716            .authenticate_in_tenant(Some("acme"), "alice", "pw-globex")
5717            .is_err());
5718        assert!(store
5719            .authenticate_in_tenant(Some("globex"), "alice", "pw-acme")
5720            .is_err());
5721    }
5722
5723    #[test]
5724    fn test_session_carries_tenant() {
5725        let store = AuthStore::new(test_config());
5726        store
5727            .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
5728            .unwrap();
5729        let session = store
5730            .authenticate_in_tenant(Some("acme"), "alice", "pw")
5731            .unwrap();
5732        assert_eq!(session.tenant_id.as_deref(), Some("acme"));
5733
5734        let (id, role) = store.validate_token_full(&session.token).unwrap();
5735        assert_eq!(id.tenant.as_deref(), Some("acme"));
5736        assert_eq!(id.username, "alice");
5737        assert_eq!(role, Role::Admin);
5738    }
5739
5740    #[test]
5741    fn test_platform_user_has_no_tenant() {
5742        let store = AuthStore::new(test_config());
5743        store.create_user("admin", "pw", Role::Admin).unwrap();
5744        let session = store.authenticate("admin", "pw").unwrap();
5745        assert!(session.tenant_id.is_none());
5746
5747        let (id, _) = store.validate_token_full(&session.token).unwrap();
5748        assert!(id.tenant.is_none());
5749    }
5750
5751    #[test]
5752    fn test_lookup_scram_verifier_global_resolves_platform() {
5753        let store = AuthStore::new(test_config());
5754        store.create_user("admin", "pw", Role::Admin).unwrap();
5755        store
5756            .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
5757            .unwrap();
5758
5759        // The global helper picks the platform-tenant user only.
5760        let v = store.lookup_scram_verifier_global("admin");
5761        assert!(v.is_some());
5762
5763        // The tenant-scoped user has its own verifier.
5764        let v_acme = store.lookup_scram_verifier(&UserId::scoped("acme", "admin"));
5765        assert!(v_acme.is_some());
5766
5767        // The two verifiers carry independent salts.
5768        assert_ne!(v.unwrap().salt, v_acme.unwrap().salt);
5769    }
5770
5771    #[test]
5772    fn test_delete_in_tenant_does_not_touch_other_tenant() {
5773        let store = AuthStore::new(test_config());
5774        store
5775            .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
5776            .unwrap();
5777        store
5778            .create_user_in_tenant(Some("globex"), "alice", "pw", Role::Admin)
5779            .unwrap();
5780
5781        store.delete_user_in_tenant(Some("acme"), "alice").unwrap();
5782
5783        // Globex still alive.
5784        assert!(store
5785            .authenticate_in_tenant(Some("globex"), "alice", "pw")
5786            .is_ok());
5787        // Acme gone.
5788        assert!(store
5789            .authenticate_in_tenant(Some("acme"), "alice", "pw")
5790            .is_err());
5791    }
5792
5793    #[test]
5794    fn test_user_id_display() {
5795        assert_eq!(UserId::platform("admin").to_string(), "admin");
5796        assert_eq!(UserId::scoped("acme", "alice").to_string(), "acme/alice");
5797    }
5798
5799    // -----------------------------------------------------------------
5800    // PolicyEnforcementMode wiring (#712 / S5A)
5801    // -----------------------------------------------------------------
5802
5803    fn enforcement_eval_ctx(role: Role) -> EvalContext {
5804        EvalContext {
5805            principal_tenant: None,
5806            current_tenant: None,
5807            peer_ip: None,
5808            mfa_present: false,
5809            now_ms: 1_700_000_000_000,
5810            principal_is_admin_role: role == Role::Admin,
5811            principal_is_platform_scoped: true,
5812        }
5813    }
5814
5815    /// Helper: install a single unrelated allow policy so the IAM
5816    /// path is the authoritative one — without any installed policy
5817    /// the auth flow short-circuits before we ever consult the mode.
5818    fn install_unrelated_policy(store: &AuthStore) {
5819        store
5820            .put_policy(
5821                Policy::from_json_str(
5822                    r#"{"id":"p-unrelated","version":1,"statements":[{"effect":"allow","actions":["select"],"resources":["table:public.other"]}]}"#,
5823                )
5824                .unwrap(),
5825            )
5826            .unwrap();
5827    }
5828
5829    #[test]
5830    fn enforcement_mode_default_for_new_store_is_legacy_rbac() {
5831        // Pre-bootstrap construction = "existing install" path.
5832        // Defaulting to LegacyRbac preserves pre-#712 behaviour on
5833        // upgrade so an operator that has not yet attached IAM
5834        // policies does not lose access after the upgrade.
5835        let store = AuthStore::new(test_config());
5836        assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::LegacyRbac);
5837    }
5838
5839    #[test]
5840    fn enforcement_mode_set_round_trips_and_returns_previous() {
5841        let store = AuthStore::new(test_config());
5842        let prev = store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5843        assert_eq!(prev, PolicyEnforcementMode::LegacyRbac);
5844        assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::PolicyOnly);
5845        let prev = store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5846        assert_eq!(prev, PolicyEnforcementMode::PolicyOnly);
5847    }
5848
5849    #[test]
5850    fn policy_only_mode_treats_no_matching_policy_as_deny() {
5851        // Acceptance #2: in `policy_only` mode a principal with no
5852        // matching policy is denied even when the action's role
5853        // floor would otherwise have allowed it.
5854        let store = AuthStore::new(test_config());
5855        store.create_user("alice", "p", Role::Admin).unwrap();
5856        let uid = UserId::platform("alice");
5857        install_unrelated_policy(&store);
5858        store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5859
5860        let resource = ResourceRef::new("table", "public.t");
5861        // Even Role::Admin is denied — DefaultDeny is the deciding
5862        // signal in `policy_only`, regardless of role.
5863        assert!(!store.check_policy_authz_with_role(
5864            &uid,
5865            "select",
5866            &resource,
5867            &enforcement_eval_ctx(Role::Admin),
5868            Role::Admin,
5869        ));
5870    }
5871
5872    #[test]
5873    fn legacy_rbac_mode_falls_back_to_role_decision() {
5874        // Acceptance #3: in `legacy_rbac` mode the same principal
5875        // falls through to the role-based decision. `select` requires
5876        // only `Read`, so all three roles satisfy it; a write verb
5877        // requires `Write`, so `Read` does not.
5878        let store = AuthStore::new(test_config());
5879        store.create_user("alice", "p", Role::Read).unwrap();
5880        let uid = UserId::platform("alice");
5881        install_unrelated_policy(&store);
5882        store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5883
5884        let table = ResourceRef::new("table", "public.t");
5885        // Read role + select action: allowed via the legacy fallback.
5886        assert!(store.check_policy_authz_with_role(
5887            &uid,
5888            "select",
5889            &table,
5890            &enforcement_eval_ctx(Role::Read),
5891            Role::Read,
5892        ));
5893        // Read role + insert action: legacy decision says no.
5894        assert!(!store.check_policy_authz_with_role(
5895            &uid,
5896            "insert",
5897            &table,
5898            &enforcement_eval_ctx(Role::Read),
5899            Role::Read,
5900        ));
5901        // Admin role + insert action: legacy decision says yes.
5902        assert!(store.check_policy_authz_with_role(
5903            &uid,
5904            "insert",
5905            &table,
5906            &enforcement_eval_ctx(Role::Admin),
5907            Role::Admin,
5908        ));
5909    }
5910
5911    #[test]
5912    fn explicit_deny_wins_irrespective_of_mode_or_role() {
5913        // Even in `legacy_rbac` with an Admin role, an explicit
5914        // matching Deny statement always denies. The mode only
5915        // influences the `DefaultDeny` arm, never an explicit one.
5916        let store = AuthStore::new(test_config());
5917        store.create_user("alice", "p", Role::Admin).unwrap();
5918        let uid = UserId::platform("alice");
5919        store
5920            .put_policy(
5921                Policy::from_json_str(
5922                    r#"{"id":"p-deny-select","version":1,"statements":[{"effect":"deny","actions":["select"],"resources":["table:public.t"]}]}"#,
5923                )
5924                .unwrap(),
5925            )
5926            .unwrap();
5927        store
5928            .attach_policy(PrincipalRef::User(uid.clone()), "p-deny-select")
5929            .unwrap();
5930
5931        let resource = ResourceRef::new("table", "public.t");
5932        for mode in [
5933            PolicyEnforcementMode::LegacyRbac,
5934            PolicyEnforcementMode::PolicyOnly,
5935        ] {
5936            store.set_enforcement_mode(mode);
5937            assert!(
5938                !store.check_policy_authz_with_role(
5939                    &uid,
5940                    "select",
5941                    &resource,
5942                    &enforcement_eval_ctx(Role::Admin),
5943                    Role::Admin,
5944                ),
5945                "explicit deny must win under mode {mode:?}"
5946            );
5947        }
5948    }
5949
5950    #[test]
5951    fn explicit_allow_wins_irrespective_of_mode_or_role() {
5952        // Symmetric guarantee: an explicit Allow is honoured even in
5953        // `policy_only` mode for a role that the legacy fallback
5954        // would have rejected (Read role + admin-tier action).
5955        let store = AuthStore::new(test_config());
5956        store.create_user("alice", "p", Role::Read).unwrap();
5957        let uid = UserId::platform("alice");
5958        store
5959            .put_policy(
5960                Policy::from_json_str(
5961                    r#"{"id":"p-allow-create","version":1,"statements":[{"effect":"allow","actions":["create"],"resources":["table:public.t"]}]}"#,
5962                )
5963                .unwrap(),
5964            )
5965            .unwrap();
5966        store
5967            .attach_policy(PrincipalRef::User(uid.clone()), "p-allow-create")
5968            .unwrap();
5969        store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5970
5971        let resource = ResourceRef::new("table", "public.t");
5972        assert!(store.check_policy_authz_with_role(
5973            &uid,
5974            "create",
5975            &resource,
5976            &enforcement_eval_ctx(Role::Read),
5977            Role::Read,
5978        ));
5979    }
5980
5981    #[test]
5982    fn take_legacy_rbac_warn_once_is_at_most_once_per_boot() {
5983        let store = AuthStore::new(test_config());
5984        // Default mode = LegacyRbac, so the first call wins the token.
5985        assert!(store.take_legacy_rbac_warn_once());
5986        // Every subsequent call is a no-op for the lifetime of this
5987        // process / AuthStore — that is the "once per boot" guarantee.
5988        for _ in 0..3 {
5989            assert!(!store.take_legacy_rbac_warn_once());
5990        }
5991    }
5992
5993    #[test]
5994    fn take_legacy_rbac_warn_once_is_silent_under_policy_only() {
5995        // Acceptance #5 phrases the warning as "Boot in legacy_rbac
5996        // mode emits exactly one warn entry per boot" — implicitly,
5997        // `policy_only` boots emit zero. The token must not be
5998        // claimable when the mode is strict so the boot path stays
5999        // quiet.
6000        let store = AuthStore::new(test_config());
6001        store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
6002        assert!(!store.take_legacy_rbac_warn_once());
6003        // And, importantly, switching back to LegacyRbac later does
6004        // not "owe" us a warning — the token was never claimed under
6005        // policy_only, so it is still available the first time the
6006        // mode is legacy_rbac.
6007        store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6008        assert!(store.take_legacy_rbac_warn_once());
6009        assert!(!store.take_legacy_rbac_warn_once());
6010    }
6011
6012    #[test]
6013    fn strict_check_policy_authz_ignores_enforcement_mode() {
6014        // Governance APIs (managed_config, registry, managed_policy)
6015        // call the strict `check_policy_authz` — it must return
6016        // `false` on DefaultDeny regardless of mode so the lenient
6017        // posture cannot accidentally elevate an admin-tier mutation.
6018        let store = AuthStore::new(test_config());
6019        store.create_user("alice", "p", Role::Admin).unwrap();
6020        let uid = UserId::platform("alice");
6021        install_unrelated_policy(&store);
6022        store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6023
6024        let resource = ResourceRef::new("table", "public.t");
6025        assert!(!store.check_policy_authz(
6026            &uid,
6027            "select",
6028            &resource,
6029            &enforcement_eval_ctx(Role::Admin),
6030        ));
6031    }
6032
6033    #[test]
6034    fn disabling_a_user_invalidates_its_api_keys_and_sessions() {
6035        let store = AuthStore::new(test_config());
6036        store.create_user("carol", "pw", Role::Write).unwrap();
6037        let key = store
6038            .create_api_key("carol", "ci", Role::Write)
6039            .unwrap()
6040            .key;
6041        let session = store.authenticate("carol", "pw").unwrap();
6042        assert!(store.validate_token(&key).is_some());
6043        assert!(store.validate_token(&session.token).is_some());
6044
6045        let uid = UserId::platform("carol");
6046        store.set_user_enabled(&uid, false).unwrap();
6047
6048        // API keys resolved straight from the index and never consulted the
6049        // user record, so a disabled (or offboarded) account kept a working
6050        // credential forever.
6051        assert!(
6052            store.validate_token(&key).is_none(),
6053            "an API key must stop working the moment its owner is disabled"
6054        );
6055        assert!(
6056            store.validate_token(&session.token).is_none(),
6057            "live sessions must not survive disabling the account"
6058        );
6059        assert!(
6060            store.lookup_scram_verifier(&uid).is_none(),
6061            "SCRAM must not authenticate a disabled account"
6062        );
6063    }
6064
6065    #[test]
6066    fn admin_password_reset_applies_and_revokes_sessions() {
6067        let store = AuthStore::new(test_config());
6068        store.create_user("dave", "old", Role::Write).unwrap();
6069        let session = store.authenticate("dave", "old").unwrap();
6070
6071        store
6072            .set_password_by_admin(&UserId::platform("dave"), "rotated")
6073            .expect("admin reset");
6074
6075        assert!(
6076            store.authenticate("dave", "old").is_err(),
6077            "the old password must stop working after a reset"
6078        );
6079        assert!(
6080            store.authenticate("dave", "rotated").is_ok(),
6081            "the new password must work after a reset"
6082        );
6083        assert!(
6084            store.validate_token(&session.token).is_none(),
6085            "a password rotation must revoke live sessions"
6086        );
6087    }
6088}
6089
6090/// Hand the operator the vault certificate minted by an env bootstrap.
6091///
6092/// It used to be printed to stderr unconditionally, which in a container
6093/// means the log aggregator now holds the one secret that unseals the
6094/// vault. With `REDDB_BOOTSTRAP_CERT_OUT` set it is written there
6095/// (owner-only, thanks to the boot umask) and only the path is logged;
6096/// without it, stderr remains the delivery channel, with a warning that
6097/// says so.
6098fn deliver_bootstrap_certificate(cert: &str) {
6099    if let Some(path) = crate::utils::env_with_file_fallback("REDDB_BOOTSTRAP_CERT_OUT") {
6100        match std::fs::write(&path, format!("{cert}\n")) {
6101            Ok(()) => {
6102                tracing::warn!(
6103                    target: "reddb::security",
6104                    path = %path,
6105                    "vault certificate written — save it: the ONLY way to unseal after a restart"
6106                );
6107                return;
6108            }
6109            Err(err) => {
6110                tracing::error!(
6111                    target: "reddb::security",
6112                    path = %path,
6113                    error = %err,
6114                    "could not write the vault certificate; falling back to stderr"
6115                );
6116            }
6117        }
6118    }
6119    eprintln!("[reddb] CERTIFICATE: {cert}");
6120    tracing::warn!(
6121        target: "reddb::security",
6122        "vault certificate issued on stderr — save it (ONLY way to unseal after restart); \
6123         set REDDB_BOOTSTRAP_CERT_OUT to receive it as a file instead of a log line"
6124    );
6125}