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