Skip to main content

reddb_server/auth/
store.rs

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