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