1#![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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub enum PrincipalRef {
41 User(UserId),
42 Group(String),
43}
44
45pub const PUBLIC_IAM_GROUP: &str = "__public__";
48
49#[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
59pub 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
74pub 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
87pub struct BootstrapResult {
96 pub user: User,
97 pub api_key: ApiKey,
98 pub certificate: Option<String>,
100}
101
102impl std::fmt::Debug for BootstrapResult {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("BootstrapResult")
105 .field("user", &self.user)
106 .field("api_key", &"<redacted>")
107 .field(
108 "certificate",
109 &self.certificate.as_ref().map(|_| "<redacted>"),
110 )
111 .finish()
112 }
113}
114
115pub struct AuthStore {
123 users: RwLock<HashMap<UserId, User>>,
126 sessions: RwLock<HashMap<String, Session>>,
127 api_key_index: RwLock<HashMap<String, (UserId, Role)>>,
129 bootstrapped: AtomicBool,
131 config: AuthConfig,
132 vault: RwLock<Option<Vault>>,
134 pager: Option<Arc<Pager>>,
136 provided_certificate: Option<String>,
138 keypair: RwLock<Option<KeyPair>>,
141 vault_kv: RwLock<HashMap<String, String>>,
144 secret_key_init: std::sync::Mutex<()>,
145 plain_kv: RwLock<HashMap<String, String>>,
150 grants: RwLock<HashMap<UserId, Vec<Grant>>>,
155 public_grants: RwLock<Vec<Grant>>,
157 user_attributes: RwLock<HashMap<UserId, UserAttributes>>,
161 session_count_by_user: RwLock<HashMap<UserId, u32>>,
165 permission_cache: RwLock<HashMap<UserId, PermissionCache>>,
169 policies: RwLock<HashMap<String, Arc<Policy>>>,
173 user_attachments: RwLock<HashMap<UserId, Vec<String>>>,
176 group_attachments: RwLock<HashMap<String, Vec<String>>>,
180 iam_effective_cache: RwLock<HashMap<UserId, Vec<Arc<Policy>>>>,
183 iam_authorization_enabled: AtomicBool,
187 enforcement_mode: RwLock<PolicyEnforcementMode>,
197 legacy_rbac_boot_warn_emitted: AtomicBool,
202 visible_collections_cache: super::scope_cache::AuthCache,
212 control_events: RwLock<Option<AuthStoreControlEvents>>,
213}
214
215fn auth_argon2_params() -> Argon2Params {
218 Argon2Params {
219 m_cost: 4 * 1024, t_cost: 3,
221 p: 1,
222 tag_len: 32,
223 }
224}
225
226fn policy_control_fields(
227 policy_id: &str,
228 policy: Option<&Policy>,
229 principal: Option<&PrincipalRef>,
230) -> HashMap<String, Sensitivity> {
231 let mut fields = HashMap::new();
232 fields.insert("policy_id".to_string(), Sensitivity::raw(policy_id));
233 if let Some(policy) = policy {
234 let effects = policy
235 .statements
236 .iter()
237 .map(|s| match s.effect {
238 iam_policies::Effect::Allow => "allow",
239 iam_policies::Effect::Deny => "deny",
240 })
241 .collect::<Vec<_>>()
242 .join(",");
243 let actions = policy
244 .statements
245 .iter()
246 .flat_map(|s| s.actions.iter())
247 .map(policy_action_pattern)
248 .collect::<Vec<_>>()
249 .join(",");
250 let resources = policy
251 .statements
252 .iter()
253 .flat_map(|s| s.resources.iter())
254 .map(policy_resource_pattern)
255 .collect::<Vec<_>>()
256 .join(",");
257 fields.insert("effect".to_string(), Sensitivity::raw(effects));
258 fields.insert("action".to_string(), Sensitivity::raw(actions));
259 fields.insert("resource".to_string(), Sensitivity::raw(resources));
260 if let Some(attrs) = policy_principal_attrs(policy) {
261 fields.insert("principal_attrs".to_string(), Sensitivity::raw(attrs));
262 }
263 }
264 if let Some(principal) = principal {
265 fields.insert(
266 "principal".to_string(),
267 Sensitivity::raw(policy_principal_label(principal)),
268 );
269 }
270 fields
271}
272
273fn policy_action_pattern(pattern: &iam_policies::ActionPattern) -> String {
274 match pattern {
275 iam_policies::ActionPattern::Exact(s) => s.clone(),
276 iam_policies::ActionPattern::Wildcard => "*".to_string(),
277 iam_policies::ActionPattern::Prefix(s) => format!("{s}:*"),
278 }
279}
280
281fn policy_resource_pattern(pattern: &iam_policies::ResourcePattern) -> String {
282 match pattern {
283 iam_policies::ResourcePattern::Exact { kind, name } => format!("{kind}:{name}"),
284 iam_policies::ResourcePattern::Glob(s) => s.clone(),
285 iam_policies::ResourcePattern::Wildcard => "*".to_string(),
286 }
287}
288
289fn policy_principal_attrs(policy: &Policy) -> Option<String> {
290 let mut attrs = Vec::new();
291 for statement in &policy.statements {
292 let Some(condition) = &statement.condition else {
293 continue;
294 };
295 if let Some(value) = condition.platform_scoped {
296 attrs.push(format!("platform_scoped={value}"));
297 }
298 if let Some(value) = condition.mfa {
299 attrs.push(format!("mfa={value}"));
300 }
301 if let Some(value) = condition.tenant_match {
302 attrs.push(format!("tenant_match={value}"));
303 }
304 }
305 if attrs.is_empty() {
306 None
307 } else {
308 Some(attrs.join(","))
309 }
310}
311
312fn policy_principal_label(principal: &PrincipalRef) -> String {
313 match principal {
314 PrincipalRef::User(uid) => format!("user:{uid}"),
315 PrincipalRef::Group(group) => format!("group:{group}"),
316 }
317}
318
319fn default_user_lifecycle_ctx<'a>() -> ControlEventCtx<'a> {
320 ControlEventCtx {
321 actor: crate::runtime::control_events::ActorRef::Anonymous,
322 scope: None,
323 request_id: None,
324 trace_id: None,
325 }
326}
327
328fn bootstrap_user_lifecycle_ctx<'a>() -> ControlEventCtx<'a> {
329 ControlEventCtx {
330 actor: crate::runtime::control_events::ActorRef::System("bootstrap"),
331 scope: None,
332 request_id: None,
333 trace_id: None,
334 }
335}
336
337fn user_resource(id: &UserId) -> String {
338 format!("user:{id}")
339}
340
341fn api_key_resource(api_key_id: &str) -> String {
342 format!("apikey:{api_key_id}")
343}
344
345fn api_key_id(key: &str) -> String {
349 match key.strip_prefix("rkh_") {
350 Some(hex) => hex.to_string(),
351 None => hex::encode(sha256(key.as_bytes())),
352 }
353}
354
355pub(crate) fn api_key_credential_id(token: &str) -> String {
358 format!("rkh_{}", api_key_id(token))
359}
360
361fn password_evidence() -> Sensitivity {
362 Sensitivity::redacted()
363}
364
365fn user_control_fields(
366 id: &UserId,
367 role: Option<Role>,
368 enabled: Option<bool>,
369 include_password: bool,
370) -> HashMap<String, Sensitivity> {
371 let mut fields = HashMap::new();
372 fields.insert(
373 "username".to_string(),
374 Sensitivity::raw(id.username.clone()),
375 );
376 fields.insert(
377 "tenant_id".to_string(),
378 Sensitivity::raw(id.tenant.clone().unwrap_or_default()),
379 );
380 if let Some(role) = role {
381 fields.insert("role".to_string(), Sensitivity::raw(role.as_str()));
382 }
383 if let Some(enabled) = enabled {
384 fields.insert("enabled".to_string(), Sensitivity::raw(enabled.to_string()));
385 }
386 if include_password {
387 fields.insert("password".to_string(), password_evidence());
388 }
389 fields
390}
391
392fn api_key_control_fields(
393 id: &UserId,
394 role: Role,
395 api_key_id: &str,
396) -> HashMap<String, Sensitivity> {
397 let mut fields = user_control_fields(id, Some(role), None, false);
398 fields.insert("api_key_id".to_string(), Sensitivity::raw(api_key_id));
399 fields.insert("api_key".to_string(), Sensitivity::redacted());
400 fields
401}
402
403fn user_error_is_denied(err: &AuthError) -> bool {
404 !matches!(err, AuthError::Internal(_))
405}
406
407impl AuthStore {
408 pub fn new(config: AuthConfig) -> Self {
413 Self {
414 users: RwLock::new(HashMap::new()),
415 sessions: RwLock::new(HashMap::new()),
416 api_key_index: RwLock::new(HashMap::new()),
417 bootstrapped: AtomicBool::new(false),
418 config,
419 vault: RwLock::new(None),
420 pager: None,
421 provided_certificate: None,
422 keypair: RwLock::new(None),
423 vault_kv: RwLock::new(HashMap::new()),
424 secret_key_init: std::sync::Mutex::new(()),
425 plain_kv: RwLock::new(HashMap::new()),
426 grants: RwLock::new(HashMap::new()),
427 public_grants: RwLock::new(Vec::new()),
428 user_attributes: RwLock::new(HashMap::new()),
429 session_count_by_user: RwLock::new(HashMap::new()),
430 permission_cache: RwLock::new(HashMap::new()),
431 policies: RwLock::new(HashMap::new()),
432 user_attachments: RwLock::new(HashMap::new()),
433 group_attachments: RwLock::new(HashMap::new()),
434 iam_effective_cache: RwLock::new(HashMap::new()),
435 iam_authorization_enabled: AtomicBool::new(false),
436 enforcement_mode: RwLock::new(PolicyEnforcementMode::default_existing_install()),
437 legacy_rbac_boot_warn_emitted: AtomicBool::new(false),
438 visible_collections_cache: super::scope_cache::AuthCache::new(
439 super::scope_cache::DEFAULT_TTL,
440 ),
441 control_events: RwLock::new(None),
442 }
443 }
444
445 pub fn configure_control_events(
446 &self,
447 ledger: Arc<dyn ControlEventLedger>,
448 config: ControlEventConfig,
449 ) {
450 *self
451 .control_events
452 .write()
453 .unwrap_or_else(|e| e.into_inner()) = Some(AuthStoreControlEvents { ledger, config });
454 }
455
456 fn configured_control_events(&self) -> Option<AuthStoreControlEvents> {
457 self.control_events
458 .read()
459 .unwrap_or_else(|e| e.into_inner())
460 .clone()
461 }
462
463 pub fn with_vault(config: AuthConfig, pager: Arc<Pager>) -> Result<Self, AuthError> {
470 let certificate = crate::utils::env_with_file_fallback("REDDB_CERTIFICATE");
471 let passphrase = crate::utils::env_with_file_fallback("REDDB_VAULT_PASSPHRASE");
472 match (certificate, passphrase) {
473 (Some(_), Some(_)) => Err(AuthError::Internal(
474 "set either REDDB_CERTIFICATE or REDDB_VAULT_PASSPHRASE, not both".into(),
475 )),
476 (None, Some(passphrase)) => Self::with_vault_passphrase(config, pager, &passphrase),
477 (certificate, None) => {
478 Self::with_vault_optional_certificate(config, pager, certificate.as_deref())
479 }
480 }
481 }
482
483 pub fn with_vault_certificate(
484 config: AuthConfig,
485 pager: Arc<Pager>,
486 certificate_hex: &str,
487 ) -> Result<Self, AuthError> {
488 Self::with_vault_optional_certificate(config, pager, Some(certificate_hex))
489 }
490
491 pub fn with_vault_passphrase(
492 config: AuthConfig,
493 pager: Arc<Pager>,
494 passphrase: &str,
495 ) -> Result<Self, AuthError> {
496 let mut store = Self::new(config);
497 let vault = Vault::with_passphrase(&pager, passphrase)?;
498 if let Some(state) = vault.load(&pager)? {
499 store.restore_from_vault(state);
500 }
501 *store.vault.write().map_err(lock_err)? = Some(vault);
502 store.pager = Some(pager);
503 Ok(store)
504 }
505
506 fn with_vault_optional_certificate(
507 config: AuthConfig,
508 pager: Arc<Pager>,
509 certificate_hex: Option<&str>,
510 ) -> Result<Self, AuthError> {
511 let mut store = Self::new(config);
512
513 if Vault::has_saved_state(&pager) {
517 let vault = match certificate_hex {
518 Some(certificate) => Vault::with_certificate(&pager, certificate)?,
519 None => Vault::open(&pager)?,
520 };
521 if let Some(state) = vault.load(&pager)? {
522 store.restore_from_vault(state);
523 }
524 *store.vault.write().unwrap_or_else(|e| e.into_inner()) = Some(vault);
525 } else if let Some(certificate) = certificate_hex {
526 let vault = Vault::with_certificate(&pager, certificate)?;
527 *store.vault.write().unwrap_or_else(|e| e.into_inner()) = Some(vault);
528 }
529
530 store.pager = Some(pager);
531 store.provided_certificate =
532 certificate_hex.map(|certificate| certificate.trim().to_ascii_lowercase());
533 Ok(store)
534 }
535
536 pub fn config(&self) -> &AuthConfig {
537 &self.config
538 }
539
540 pub fn is_enabled(&self) -> bool {
541 self.config.enabled
542 }
543
544 pub fn needs_bootstrap(&self) -> bool {
546 !self.bootstrapped.load(Ordering::Acquire)
547 && self.users.read().map(|u| u.is_empty()).unwrap_or(true)
548 }
549
550 fn get_user_cloned(&self, id: &UserId) -> Option<User> {
552 self.users.read().ok().and_then(|m| m.get(id).cloned())
553 }
554
555 pub fn is_bootstrapped(&self) -> bool {
557 self.bootstrapped.load(Ordering::Acquire)
558 }
559
560 pub fn bootstrap(&self, username: &str, password: &str) -> Result<BootstrapResult, AuthError> {
571 if let Some(configured) = self.configured_control_events() {
572 let ctx = bootstrap_user_lifecycle_ctx();
573 let control = UserLifecycleControl {
574 ctx: &ctx,
575 ledger: configured.ledger.as_ref(),
576 config: configured.config,
577 };
578 self.bootstrap_with_control_events_inner(username, password, &control)
579 } else {
580 self.bootstrap_unaudited(username, password)
581 }
582 }
583
584 pub fn bootstrap_with_control_events(
585 &self,
586 username: &str,
587 password: &str,
588 ctx: &ControlEventCtx<'_>,
589 ledger: &dyn ControlEventLedger,
590 config: ControlEventConfig,
591 ) -> Result<BootstrapResult, AuthError> {
592 let system_ctx = ControlEventCtx {
593 actor: crate::runtime::control_events::ActorRef::System("bootstrap"),
594 scope: ctx.scope.clone(),
595 request_id: ctx.request_id.clone(),
596 trace_id: ctx.trace_id.clone(),
597 };
598 let control = UserLifecycleControl {
599 ctx: &system_ctx,
600 ledger,
601 config,
602 };
603 self.bootstrap_with_control_events_inner(username, password, &control)
604 }
605
606 pub fn bootstrap_system_admin(
611 &self,
612 username: &str,
613 password: &str,
614 ) -> Result<BootstrapResult, AuthError> {
615 self.bootstrap(username, password)
616 }
617
618 fn bootstrap_unaudited(
619 &self,
620 username: &str,
621 password: &str,
622 ) -> Result<BootstrapResult, AuthError> {
623 if self
625 .bootstrapped
626 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
627 .is_err()
628 {
629 return Err(AuthError::Forbidden(
630 "bootstrap already completed — sealed permanently".to_string(),
631 ));
632 }
633
634 {
636 let users = self.users.read().map_err(lock_err)?;
637 if !users.is_empty() {
638 return Err(AuthError::Forbidden(
639 "bootstrap already completed — users exist".to_string(),
640 ));
641 }
642 }
643
644 let user = self.create_user_in_tenant_unaudited(None, username, password, Role::Admin)?;
645 let key = self.create_api_key_in_tenant_unaudited(
646 None,
647 username,
648 "bootstrap",
649 Role::Admin,
650 None,
651 )?;
652
653 let credential_result = (|| -> Result<Option<String>, AuthError> {
656 let certificate = if let Some(ref pager) = self.pager {
657 let keypair = KeyPair::generate();
658 let mut vault = self.vault.write().map_err(lock_err)?;
659 let certificate = if vault.is_none() {
660 let certificate = keypair.certificate_hex();
661 *vault = Some(Vault::with_certificate_bytes(pager, &keypair.certificate)?);
662 Some(certificate)
663 } else {
664 self.provided_certificate.clone()
665 };
666 *self.keypair.write().map_err(lock_err)? = Some(keypair);
667 drop(vault);
668 self.ensure_vault_secret_key_result()?;
669 certificate
670 } else {
671 None
672 };
673 *self.enforcement_mode.write().map_err(lock_err)? =
674 PolicyEnforcementMode::default_fresh_bootstrap();
675 self.vault_kv.write().map_err(lock_err)?.insert(
676 "red.config.policy.enforcement_mode".into(),
677 PolicyEnforcementMode::default_fresh_bootstrap()
678 .as_str()
679 .into(),
680 );
681 self.persist_to_vault_result()?;
682 Ok(certificate)
683 })();
684 let certificate = match credential_result {
685 Ok(certificate) => certificate,
686 Err(err) => {
687 self.rollback_bootstrap(&UserId::from_parts(None, username));
688 return Err(err);
689 }
690 };
691
692 Ok(BootstrapResult {
693 user,
694 api_key: key,
695 certificate,
696 })
697 }
698
699 fn bootstrap_with_control_events_inner(
700 &self,
701 username: &str,
702 password: &str,
703 control: &UserLifecycleControl<'_>,
704 ) -> Result<BootstrapResult, AuthError> {
705 let id = UserId::from_parts(None, username);
706 match self.bootstrap_unaudited(username, password) {
707 Ok(result) => {
708 let event_result = self.emit_user_lifecycle_allowed(
709 control,
710 EventKind::UserCreate,
711 "user.create",
712 &id,
713 user_control_fields(&id, Some(Role::Admin), Some(true), true),
714 );
715 if let Err(err) = event_result {
716 self.rollback_bootstrap(&id);
717 return Err(err);
718 }
719 Ok(result)
720 }
721 Err(err) => {
722 self.emit_user_lifecycle_outcome(
723 control,
724 if user_error_is_denied(&err) {
725 Outcome::Denied
726 } else {
727 Outcome::Error
728 },
729 EventKind::UserCreate,
730 "user.create",
731 &id,
732 Some(err.to_string()),
733 user_control_fields(&id, Some(Role::Admin), Some(true), true),
734 );
735 Err(err)
736 }
737 }
738 }
739
740 pub fn bootstrap_from_env(&self) -> Option<BootstrapResult> {
748 if !self.needs_bootstrap() {
749 return None;
750 }
751
752 let username = crate::utils::env_with_file_fallback("REDDB_USERNAME")?;
753 let password = crate::utils::env_with_file_fallback("REDDB_PASSWORD")?;
754
755 if username.is_empty() || password.is_empty() {
756 return None;
757 }
758
759 match self.bootstrap(&username, &password) {
760 Ok(result) => {
761 tracing::info!(
767 username = %reddb_wire::audit_safe_log_field(&username),
768 "bootstrapped admin user from REDDB_USERNAME/REDDB_PASSWORD"
769 );
770 if let Some(ref cert) = result.certificate {
771 deliver_bootstrap_certificate(cert);
772 }
773 Some(result)
774 }
775 Err(e) => {
776 tracing::error!(err = %e, "env bootstrap failed");
777 None
778 }
779 }
780 }
781
782 pub(crate) fn persist_to_vault_result(&self) -> Result<(), AuthError> {
788 let vault_guard = self.vault.read().unwrap_or_else(|e| e.into_inner());
789 if let (Some(ref vault), Some(ref pager)) = (&*vault_guard, &self.pager) {
790 let state = self.snapshot();
791 vault.save(pager, &state)?;
792 }
793 Ok(())
794 }
795
796 fn persist_to_vault(&self) {
802 if let Err(e) = self.persist_to_vault_result() {
803 tracing::error!(err = %e, "vault persist failed");
804 crate::telemetry::operator_event::OperatorEvent::SecretRotationFailed {
810 secret_ref: "auth_vault".to_string(),
811 error: e.to_string(),
812 }
813 .emit_global();
814 }
815 }
816
817 pub fn is_vault_backed(&self) -> bool {
819 self.pager.is_some()
820 && self
821 .vault
822 .read()
823 .map(|guard| guard.is_some())
824 .unwrap_or(false)
825 }
826
827 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
833 self.vault_kv
834 .read()
835 .ok()
836 .and_then(|kv| kv.get(key).cloned())
837 }
838
839 pub fn vault_kv_snapshot(&self) -> HashMap<String, String> {
841 self.vault_kv
842 .read()
843 .map(|kv| kv.clone())
844 .unwrap_or_default()
845 }
846
847 pub fn vault_kv_export_encrypted(&self) -> Result<Option<String>, AuthError> {
850 if !self.is_vault_backed() {
851 return Err(AuthError::Forbidden(
852 "vault KV export requires an enabled, unsealed vault".to_string(),
853 ));
854 }
855 let kv = self.vault_kv_snapshot();
856 if kv.is_empty() {
857 return Ok(None);
858 }
859
860 let vault_guard = self.vault.read().map_err(lock_err)?;
861 let vault = vault_guard.as_ref().ok_or_else(|| {
862 AuthError::Forbidden("vault KV export requires an enabled, unsealed vault".to_string())
863 })?;
864 let state = VaultState {
865 users: Vec::new(),
866 api_keys: Vec::new(),
867 bootstrapped: false,
868 master_secret: None,
869 kv,
870 };
871 Ok(Some(vault.seal_logical_export(&state)?))
872 }
873
874 pub fn vault_kv_try_import(
877 &self,
878 entries: HashMap<String, String>,
879 ) -> Result<usize, AuthError> {
880 if !self.is_vault_backed() {
881 return Err(AuthError::Forbidden(
882 "vault KV import requires an enabled, unsealed vault".to_string(),
883 ));
884 }
885 if entries.is_empty() {
886 return Ok(0);
887 }
888
889 let mut previous = HashMap::new();
890 {
891 let mut kv = self.vault_kv.write().map_err(lock_err)?;
892 for (key, value) in &entries {
893 previous.insert(key.clone(), kv.insert(key.clone(), value.clone()));
894 }
895 }
896
897 if let Err(err) = self.persist_to_vault_result() {
898 if let Ok(mut kv) = self.vault_kv.write() {
899 for (key, old) in previous {
900 match old {
901 Some(value) => {
902 kv.insert(key, value);
903 }
904 None => {
905 kv.remove(&key);
906 }
907 }
908 }
909 }
910 return Err(err);
911 }
912
913 Ok(entries.len())
914 }
915
916 pub fn vault_kv_try_import_placeholders(&self, keys: &[String]) -> Result<usize, AuthError> {
919 let entries = keys
920 .iter()
921 .map(|key| (key.clone(), "false".to_string()))
922 .collect();
923 self.vault_kv_try_import(entries)
924 }
925
926 pub fn vault_kv_set(&self, key: String, value: String) {
928 if let Ok(mut kv) = self.vault_kv.write() {
929 kv.insert(key, value);
930 }
931 self.persist_to_vault();
932 }
933
934 pub fn vault_kv_try_set(&self, key: String, value: String) -> Result<(), AuthError> {
937 if !self.is_vault_backed() {
938 return Err(AuthError::Forbidden(
939 "SET SECRET requires an enabled, unsealed vault".to_string(),
940 ));
941 }
942
943 let previous = {
944 let mut kv = self.vault_kv.write().map_err(lock_err)?;
945 kv.insert(key.clone(), value)
946 };
947
948 if let Err(err) = self.persist_to_vault_result() {
949 if let Ok(mut kv) = self.vault_kv.write() {
950 match previous {
951 Some(value) => {
952 kv.insert(key, value);
953 }
954 None => {
955 kv.remove(&key);
956 }
957 }
958 }
959 return Err(err);
960 }
961
962 Ok(())
963 }
964
965 pub fn vault_kv_delete(&self, key: &str) -> bool {
967 let existed = self
968 .vault_kv
969 .write()
970 .map(|mut kv| kv.remove(key).is_some())
971 .unwrap_or(false);
972 if existed {
973 self.persist_to_vault();
974 }
975 existed
976 }
977
978 pub fn vault_kv_try_delete(&self, key: &str) -> Result<bool, AuthError> {
981 if !self.is_vault_backed() {
982 return Err(AuthError::Forbidden(
983 "DELETE SECRET requires an enabled, unsealed vault".to_string(),
984 ));
985 }
986
987 let removed = {
988 let mut kv = self.vault_kv.write().map_err(lock_err)?;
989 kv.remove(key)
990 };
991
992 if removed.is_none() {
993 return Ok(false);
994 }
995
996 if let Err(err) = self.persist_to_vault_result() {
997 if let Ok(mut kv) = self.vault_kv.write() {
998 if let Some(value) = removed {
999 kv.insert(key.to_string(), value);
1000 }
1001 }
1002 return Err(err);
1003 }
1004
1005 Ok(true)
1006 }
1007
1008 pub fn vault_kv_keys(&self) -> Vec<String> {
1010 self.vault_kv
1011 .read()
1012 .map(|kv| kv.keys().cloned().collect())
1013 .unwrap_or_default()
1014 }
1015
1016 pub fn plain_kv_get(&self, key: &str) -> Option<String> {
1027 self.plain_kv
1028 .read()
1029 .ok()
1030 .and_then(|kv| kv.get(key).cloned())
1031 }
1032
1033 pub fn plain_kv_snapshot(&self) -> HashMap<String, String> {
1035 self.plain_kv
1036 .read()
1037 .map(|kv| kv.clone())
1038 .unwrap_or_default()
1039 }
1040
1041 pub fn plain_kv_set(&self, key: String, value: String) {
1043 if let Ok(mut kv) = self.plain_kv.write() {
1044 kv.insert(key, value);
1045 }
1046 }
1047
1048 pub fn plain_kv_delete(&self, key: &str) -> bool {
1050 self.plain_kv
1051 .write()
1052 .map(|mut kv| kv.remove(key).is_some())
1053 .unwrap_or(false)
1054 }
1055
1056 pub fn plain_kv_keys(&self) -> Vec<String> {
1058 self.plain_kv
1059 .read()
1060 .map(|kv| kv.keys().cloned().collect())
1061 .unwrap_or_default()
1062 }
1063
1064 pub fn vault_secret_key(&self) -> Option<[u8; 32]> {
1067 let hex_str = self.vault_kv_get("red.secret.aes_key")?;
1068 let bytes = hex::decode(hex_str).ok()?;
1069 if bytes.len() == 32 {
1070 let mut key = [0u8; 32];
1071 key.copy_from_slice(&bytes);
1072 Some(key)
1073 } else {
1074 None
1075 }
1076 }
1077
1078 pub fn ensure_vault_secret_key(&self) {
1080 if let Err(err) = self.ensure_vault_secret_key_result() {
1081 tracing::error!(error = %err, "vault secret key initialization failed");
1082 }
1083 }
1084
1085 pub(crate) fn ensure_vault_secret_key_result(&self) -> Result<(), AuthError> {
1086 let _guard = self.secret_key_init.lock().map_err(lock_err)?;
1087 if self.vault_secret_key().is_some() {
1088 return Ok(());
1089 }
1090 if self.vault_kv_get("red.secret.aes_key").is_some() {
1091 return Err(AuthError::Internal(
1092 "stored vault secret key is invalid".into(),
1093 ));
1094 }
1095 let value = hex::encode(random_bytes(32));
1096 if self.is_vault_backed() {
1097 self.vault_kv_try_set("red.secret.aes_key".into(), value)
1098 } else {
1099 self.vault_kv
1100 .write()
1101 .map_err(lock_err)?
1102 .insert("red.secret.aes_key".into(), value);
1103 Ok(())
1104 }
1105 }
1106
1107 fn snapshot(&self) -> VaultState {
1109 let users_guard = self.users.read().unwrap_or_else(|e| e.into_inner());
1110 let users: Vec<User> = users_guard.values().cloned().collect();
1111
1112 let mut api_keys = Vec::new();
1115 for user in &users {
1116 let owner = UserId::from_parts(user.tenant_id.as_deref(), &user.username);
1117 for key in &user.api_keys {
1118 api_keys.push((owner.clone(), key.clone()));
1119 }
1120 }
1121
1122 let master_secret = self
1124 .keypair
1125 .read()
1126 .ok()
1127 .and_then(|guard| guard.as_ref().map(|kp| kp.master_secret.clone()));
1128
1129 let kv = self.vault_kv.read().map(|m| m.clone()).unwrap_or_default();
1130
1131 VaultState {
1132 users,
1133 api_keys,
1134 bootstrapped: self.bootstrapped.load(Ordering::Acquire),
1135 master_secret,
1136 kv,
1137 }
1138 }
1139
1140 fn restore_from_vault(&mut self, state: VaultState) {
1142 if state.bootstrapped {
1144 self.bootstrapped.store(true, Ordering::Release);
1145 }
1146
1147 if let Some(secret) = state.master_secret {
1149 let kp = KeyPair::from_master_secret(secret);
1150 if let Ok(mut guard) = self.keypair.write() {
1151 *guard = Some(kp);
1152 }
1153 }
1154
1155 if let Ok(mut kv) = self.vault_kv.write() {
1157 *kv = state.kv;
1158 }
1159
1160 let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
1162 let mut idx = self
1163 .api_key_index
1164 .write()
1165 .unwrap_or_else(|e| e.into_inner());
1166
1167 for mut user in state.users {
1168 let id = UserId::from_parts(user.tenant_id.as_deref(), &user.username);
1169 for key in &mut user.api_keys {
1173 if !key.key.starts_with("rkh_") {
1174 key.key = api_key_credential_id(&key.key);
1175 }
1176 }
1177 for key in &user.api_keys {
1179 idx.insert(key.key.clone(), (id.clone(), key.role));
1180 }
1181 users.insert(id, user);
1182 }
1183 drop(idx);
1184 drop(users);
1185
1186 self.rehydrate_acl();
1187 self.rehydrate_iam();
1188 }
1189
1190 pub fn create_user(
1198 &self,
1199 username: &str,
1200 password: &str,
1201 role: Role,
1202 ) -> Result<User, AuthError> {
1203 self.create_user_in_tenant(None, username, password, role)
1204 }
1205
1206 pub fn create_user_with_control_events(
1207 &self,
1208 username: &str,
1209 password: &str,
1210 role: Role,
1211 ctx: &ControlEventCtx<'_>,
1212 ledger: &dyn ControlEventLedger,
1213 config: ControlEventConfig,
1214 ) -> Result<User, AuthError> {
1215 self.create_user_in_tenant_with_control_events(
1216 None, username, password, role, ctx, ledger, config,
1217 )
1218 }
1219
1220 pub fn create_user_in_tenant(
1225 &self,
1226 tenant_id: Option<&str>,
1227 username: &str,
1228 password: &str,
1229 role: Role,
1230 ) -> Result<User, AuthError> {
1231 if let Some(configured) = self.configured_control_events() {
1232 let ctx = default_user_lifecycle_ctx();
1233 let control = UserLifecycleControl {
1234 ctx: &ctx,
1235 ledger: configured.ledger.as_ref(),
1236 config: configured.config,
1237 };
1238 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1239 } else {
1240 self.create_user_in_tenant_unaudited(tenant_id, username, password, role)
1241 }
1242 }
1243
1244 #[allow(clippy::too_many_arguments)]
1245 pub fn create_user_in_tenant_with_control_events(
1246 &self,
1247 tenant_id: Option<&str>,
1248 username: &str,
1249 password: &str,
1250 role: Role,
1251 ctx: &ControlEventCtx<'_>,
1252 ledger: &dyn ControlEventLedger,
1253 config: ControlEventConfig,
1254 ) -> Result<User, AuthError> {
1255 let control = UserLifecycleControl {
1256 ctx,
1257 ledger,
1258 config,
1259 };
1260 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1261 }
1262
1263 pub fn create_admin_user(
1264 &self,
1265 username: &str,
1266 password: &str,
1267 role: Role,
1268 tenant_id: Option<&str>,
1269 ) -> Result<User, AuthError> {
1270 if let Some(configured) = self.configured_control_events() {
1271 let ctx = default_user_lifecycle_ctx();
1272 let control = UserLifecycleControl {
1273 ctx: &ctx,
1274 ledger: configured.ledger.as_ref(),
1275 config: configured.config,
1276 };
1277 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1278 } else {
1279 self.create_user_in_tenant_unaudited(tenant_id, username, password, role)
1280 }
1281 }
1282
1283 fn create_user_in_tenant_unaudited(
1284 &self,
1285 tenant_id: Option<&str>,
1286 username: &str,
1287 password: &str,
1288 role: Role,
1289 ) -> Result<User, AuthError> {
1290 let id = UserId::from_parts(tenant_id, username);
1291 let mut users = self.users.write().map_err(lock_err)?;
1292 if users.contains_key(&id) {
1293 return Err(AuthError::UserExists(id.to_string()));
1294 }
1295
1296 let now = now_ms();
1297 let user = User {
1298 username: username.to_string(),
1299 tenant_id: tenant_id.map(|s| s.to_string()),
1300 password_hash: hash_password(password),
1301 scram_verifier: Some(make_scram_verifier(password)),
1302 role,
1303 api_keys: Vec::new(),
1304 created_at: now,
1305 updated_at: now,
1306 enabled: true,
1307 };
1308 users.insert(id, user.clone());
1309 drop(users); self.persist_to_vault();
1311 Ok(user)
1312 }
1313
1314 fn create_user_in_tenant_controlled(
1315 &self,
1316 tenant_id: Option<&str>,
1317 username: &str,
1318 password: &str,
1319 role: Role,
1320 control: &UserLifecycleControl<'_>,
1321 ) -> Result<User, AuthError> {
1322 let id = UserId::from_parts(tenant_id, username);
1323 match self.create_user_in_tenant_unaudited(tenant_id, username, password, role) {
1324 Ok(user) => {
1325 if let Err(err) = self.emit_user_lifecycle_allowed(
1326 control,
1327 EventKind::UserCreate,
1328 "user.create",
1329 &id,
1330 user_control_fields(&id, Some(role), Some(true), true),
1331 ) {
1332 self.rollback_create_user(&id);
1333 return Err(err);
1334 }
1335 Ok(user)
1336 }
1337 Err(err) => {
1338 self.emit_user_lifecycle_outcome(
1339 control,
1340 if user_error_is_denied(&err) {
1341 Outcome::Denied
1342 } else {
1343 Outcome::Error
1344 },
1345 EventKind::UserCreate,
1346 "user.create",
1347 &id,
1348 Some(err.to_string()),
1349 user_control_fields(&id, Some(role), Some(true), true),
1350 );
1351 Err(err)
1352 }
1353 }
1354 }
1355
1356 pub fn lookup_scram_verifier(&self, id: &UserId) -> Option<crate::auth::scram::ScramVerifier> {
1362 if crate::auth::self_lock_guard::is_synthetic_principal(&id.username) {
1364 return None;
1365 }
1366 let users = self.users.read().ok()?;
1367 users
1371 .get(id)
1372 .filter(|u| u.enabled)
1373 .and_then(|u| u.scram_verifier.clone())
1374 }
1375
1376 pub fn lookup_scram_verifier_global(
1381 &self,
1382 username: &str,
1383 ) -> Option<crate::auth::scram::ScramVerifier> {
1384 self.lookup_scram_verifier(&UserId::platform(username))
1385 }
1386
1387 pub fn list_users(&self) -> Vec<User> {
1393 let users = match self.users.read() {
1394 Ok(g) => g,
1395 Err(_) => return Vec::new(),
1396 };
1397 users
1398 .values()
1399 .filter(|u| !crate::auth::self_lock_guard::is_synthetic_principal(&u.username))
1400 .map(|u| User {
1401 password_hash: String::new(), ..u.clone()
1403 })
1404 .collect()
1405 }
1406
1407 pub fn list_users_scoped(&self, tenant_filter: Option<Option<&str>>) -> Vec<User> {
1414 let users = match self.users.read() {
1415 Ok(g) => g,
1416 Err(_) => return Vec::new(),
1417 };
1418 users
1419 .values()
1420 .filter(|u| match tenant_filter {
1421 None => true,
1422 Some(t) => u.tenant_id.as_deref() == t,
1423 })
1424 .filter(|u| !crate::auth::self_lock_guard::is_synthetic_principal(&u.username))
1425 .map(|u| User {
1426 password_hash: String::new(), ..u.clone()
1428 })
1429 .collect()
1430 }
1431
1432 pub fn user_lifecycle_resource(uid: &UserId) -> ResourceRef {
1437 let resource = ResourceRef::new("user", uid.username.clone());
1438 match &uid.tenant {
1439 Some(tenant) => resource.with_tenant(tenant.clone()),
1440 None => resource,
1441 }
1442 }
1443
1444 pub fn eval_context_for_principal(
1445 &self,
1446 principal: &UserId,
1447 role: Role,
1448 current_tenant: Option<String>,
1449 ) -> EvalContext {
1450 EvalContext {
1451 principal_tenant: principal.tenant.clone(),
1452 current_tenant,
1453 peer_ip: None,
1454 mfa_present: false,
1455 now_ms: now_ms(),
1456 principal_is_admin_role: role == Role::Admin,
1457 principal_is_platform_scoped: principal.tenant.is_none(),
1458 }
1459 }
1460
1461 pub fn check_user_lifecycle_authz(
1468 &self,
1469 actor: &UserId,
1470 actor_role: Role,
1471 action: &str,
1472 target: &UserId,
1473 ) -> bool {
1474 let resource = Self::user_lifecycle_resource(target);
1475 let ctx = self.eval_context_for_principal(actor, actor_role, target.tenant.clone());
1476 self.check_policy_authz_with_role(actor, action, &resource, &ctx, actor_role)
1477 }
1478
1479 pub fn has_explicit_user_lifecycle_deny(
1485 &self,
1486 actor: &UserId,
1487 actor_role: Role,
1488 action: &str,
1489 target: &UserId,
1490 ) -> bool {
1491 let resource = Self::user_lifecycle_resource(target);
1492 let ctx = self.eval_context_for_principal(actor, actor_role, target.tenant.clone());
1493 let pols = self.effective_policies(actor);
1494 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
1495 matches!(
1496 iam_policies::evaluate(&refs, action, &resource, &ctx),
1497 iam_policies::Decision::Deny { .. }
1498 )
1499 }
1500
1501 pub fn get_user(&self, tenant_id: Option<&str>, username: &str) -> Option<User> {
1502 let id = UserId::from_parts(tenant_id, username);
1503 self.get_user_cloned(&id).map(|u| User {
1504 password_hash: String::new(),
1505 ..u
1506 })
1507 }
1508
1509 pub fn delete_user(&self, username: &str) -> Result<(), AuthError> {
1514 self.delete_user_in_tenant(None, username)
1515 }
1516
1517 pub fn delete_user_with_control_events(
1518 &self,
1519 username: &str,
1520 ctx: &ControlEventCtx<'_>,
1521 ledger: &dyn ControlEventLedger,
1522 config: ControlEventConfig,
1523 ) -> Result<(), AuthError> {
1524 self.delete_user_in_tenant_with_control_events(None, username, ctx, ledger, config)
1525 }
1526
1527 pub fn delete_user_in_tenant(
1530 &self,
1531 tenant_id: Option<&str>,
1532 username: &str,
1533 ) -> Result<(), AuthError> {
1534 if let Some(configured) = self.configured_control_events() {
1535 let ctx = default_user_lifecycle_ctx();
1536 let control = UserLifecycleControl {
1537 ctx: &ctx,
1538 ledger: configured.ledger.as_ref(),
1539 config: configured.config,
1540 };
1541 self.delete_user_in_tenant_controlled(tenant_id, username, &control)
1542 } else {
1543 self.delete_user_in_tenant_unaudited(tenant_id, username)
1544 }
1545 }
1546
1547 pub fn delete_user_in_tenant_with_control_events(
1548 &self,
1549 tenant_id: Option<&str>,
1550 username: &str,
1551 ctx: &ControlEventCtx<'_>,
1552 ledger: &dyn ControlEventLedger,
1553 config: ControlEventConfig,
1554 ) -> Result<(), AuthError> {
1555 let control = UserLifecycleControl {
1556 ctx,
1557 ledger,
1558 config,
1559 };
1560 self.delete_user_in_tenant_controlled(tenant_id, username, &control)
1561 }
1562
1563 fn delete_user_in_tenant_unaudited(
1564 &self,
1565 tenant_id: Option<&str>,
1566 username: &str,
1567 ) -> Result<(), AuthError> {
1568 let id = UserId::from_parts(tenant_id, username);
1569 let mut users = self.users.write().map_err(lock_err)?;
1570 let user = users
1571 .remove(&id)
1572 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1573
1574 if let Ok(mut idx) = self.api_key_index.write() {
1576 for api_key in &user.api_keys {
1577 idx.remove(&api_key.key);
1578 }
1579 }
1580
1581 if let Ok(mut sessions) = self.sessions.write() {
1584 sessions
1585 .retain(|_, s| !(s.username == username && s.tenant_id.as_deref() == tenant_id));
1586 }
1587
1588 self.persist_to_vault();
1589 Ok(())
1590 }
1591
1592 fn delete_user_in_tenant_controlled(
1593 &self,
1594 tenant_id: Option<&str>,
1595 username: &str,
1596 control: &UserLifecycleControl<'_>,
1597 ) -> Result<(), AuthError> {
1598 let id = UserId::from_parts(tenant_id, username);
1599 let rollback = self.user_delete_rollback_snapshot(&id, tenant_id, username);
1600 match self.delete_user_in_tenant_unaudited(tenant_id, username) {
1601 Ok(()) => {
1602 if let Err(err) = self.emit_user_lifecycle_allowed(
1603 control,
1604 EventKind::UserDelete,
1605 "user.delete",
1606 &id,
1607 user_control_fields(&id, rollback.as_ref().map(|r| r.0.role), None, false),
1608 ) {
1609 if let Some((user, sessions)) = rollback {
1610 self.restore_deleted_user(&id, user, sessions);
1611 }
1612 return Err(err);
1613 }
1614 Ok(())
1615 }
1616 Err(err) => {
1617 self.emit_user_lifecycle_outcome(
1618 control,
1619 if user_error_is_denied(&err) {
1620 Outcome::Denied
1621 } else {
1622 Outcome::Error
1623 },
1624 EventKind::UserDelete,
1625 "user.delete",
1626 &id,
1627 Some(err.to_string()),
1628 user_control_fields(&id, rollback.as_ref().map(|r| r.0.role), None, false),
1629 );
1630 Err(err)
1631 }
1632 }
1633 }
1634
1635 pub fn change_password(
1639 &self,
1640 username: &str,
1641 old_password: &str,
1642 new_password: &str,
1643 ) -> Result<(), AuthError> {
1644 self.change_password_in_tenant(None, username, old_password, new_password)
1645 }
1646
1647 pub fn change_password_with_control_events(
1648 &self,
1649 username: &str,
1650 old_password: &str,
1651 new_password: &str,
1652 ctx: &ControlEventCtx<'_>,
1653 ledger: &dyn ControlEventLedger,
1654 config: ControlEventConfig,
1655 ) -> Result<(), AuthError> {
1656 self.change_password_in_tenant_with_control_events(
1657 None,
1658 username,
1659 old_password,
1660 new_password,
1661 ctx,
1662 ledger,
1663 config,
1664 )
1665 }
1666
1667 #[allow(clippy::too_many_arguments)]
1668 pub fn change_password_in_tenant(
1669 &self,
1670 tenant_id: Option<&str>,
1671 username: &str,
1672 old_password: &str,
1673 new_password: &str,
1674 ) -> Result<(), AuthError> {
1675 if let Some(configured) = self.configured_control_events() {
1676 let ctx = default_user_lifecycle_ctx();
1677 let control = UserLifecycleControl {
1678 ctx: &ctx,
1679 ledger: configured.ledger.as_ref(),
1680 config: configured.config,
1681 };
1682 self.change_password_in_tenant_controlled(
1683 tenant_id,
1684 username,
1685 old_password,
1686 new_password,
1687 &control,
1688 )
1689 } else {
1690 self.change_password_in_tenant_unaudited(
1691 tenant_id,
1692 username,
1693 old_password,
1694 new_password,
1695 )
1696 }
1697 }
1698
1699 #[allow(clippy::too_many_arguments)]
1700 pub fn change_password_in_tenant_with_control_events(
1701 &self,
1702 tenant_id: Option<&str>,
1703 username: &str,
1704 old_password: &str,
1705 new_password: &str,
1706 ctx: &ControlEventCtx<'_>,
1707 ledger: &dyn ControlEventLedger,
1708 config: ControlEventConfig,
1709 ) -> Result<(), AuthError> {
1710 let control = UserLifecycleControl {
1711 ctx,
1712 ledger,
1713 config,
1714 };
1715 self.change_password_in_tenant_controlled(
1716 tenant_id,
1717 username,
1718 old_password,
1719 new_password,
1720 &control,
1721 )
1722 }
1723
1724 fn change_password_in_tenant_unaudited(
1725 &self,
1726 tenant_id: Option<&str>,
1727 username: &str,
1728 old_password: &str,
1729 new_password: &str,
1730 ) -> Result<(), AuthError> {
1731 let id = UserId::from_parts(tenant_id, username);
1732 let mut users = self.users.write().map_err(lock_err)?;
1733 let user = users
1734 .get_mut(&id)
1735 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1736
1737 if !verify_password(old_password, &user.password_hash) {
1738 return Err(AuthError::InvalidCredentials);
1739 }
1740
1741 user.password_hash = hash_password(new_password);
1742 user.scram_verifier = Some(make_scram_verifier(new_password));
1743 user.updated_at = now_ms();
1744 drop(users); self.persist_to_vault();
1746 Ok(())
1747 }
1748
1749 pub fn set_password_by_admin(&self, id: &UserId, new_password: &str) -> Result<(), AuthError> {
1754 if new_password.is_empty() {
1755 return Err(AuthError::Forbidden("password cannot be empty".to_string()));
1756 }
1757 {
1758 let mut users = self.users.write().map_err(lock_err)?;
1759 let user = users
1760 .get_mut(id)
1761 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1762 user.password_hash = hash_password(new_password);
1763 user.scram_verifier = Some(make_scram_verifier(new_password));
1764 user.updated_at = now_ms();
1765 }
1766 self.purge_sessions_for(id);
1767 self.persist_to_vault();
1768 Ok(())
1769 }
1770
1771 fn change_password_in_tenant_controlled(
1772 &self,
1773 tenant_id: Option<&str>,
1774 username: &str,
1775 old_password: &str,
1776 new_password: &str,
1777 control: &UserLifecycleControl<'_>,
1778 ) -> Result<(), AuthError> {
1779 let id = UserId::from_parts(tenant_id, username);
1780 let previous = self.get_user_cloned(&id);
1781 match self.change_password_in_tenant_unaudited(
1782 tenant_id,
1783 username,
1784 old_password,
1785 new_password,
1786 ) {
1787 Ok(()) => {
1788 if let Err(err) = self.emit_user_lifecycle_allowed(
1789 control,
1790 EventKind::UserUpdate,
1791 "user.update",
1792 &id,
1793 user_control_fields(
1794 &id,
1795 previous.as_ref().map(|u| u.role),
1796 previous.as_ref().map(|u| u.enabled),
1797 true,
1798 ),
1799 ) {
1800 self.restore_user_snapshot(&id, previous);
1801 return Err(err);
1802 }
1803 Ok(())
1804 }
1805 Err(err) => {
1806 self.emit_user_lifecycle_outcome(
1807 control,
1808 if user_error_is_denied(&err) {
1809 Outcome::Denied
1810 } else {
1811 Outcome::Error
1812 },
1813 EventKind::UserUpdate,
1814 "user.update",
1815 &id,
1816 Some(err.to_string()),
1817 user_control_fields(
1818 &id,
1819 previous.as_ref().map(|u| u.role),
1820 previous.as_ref().map(|u| u.enabled),
1821 true,
1822 ),
1823 );
1824 Err(err)
1825 }
1826 }
1827 }
1828
1829 pub fn change_role(&self, username: &str, new_role: Role) -> Result<(), AuthError> {
1832 self.change_role_in_tenant(None, username, new_role)
1833 }
1834
1835 pub fn change_role_in_tenant(
1836 &self,
1837 tenant_id: Option<&str>,
1838 username: &str,
1839 new_role: Role,
1840 ) -> Result<(), AuthError> {
1841 if let Some(configured) = self.configured_control_events() {
1842 let ctx = default_user_lifecycle_ctx();
1843 let control = UserLifecycleControl {
1844 ctx: &ctx,
1845 ledger: configured.ledger.as_ref(),
1846 config: configured.config,
1847 };
1848 self.change_role_in_tenant_controlled(tenant_id, username, new_role, &control)
1849 } else {
1850 self.change_role_in_tenant_unaudited(tenant_id, username, new_role)
1851 }
1852 }
1853
1854 pub fn change_role_in_tenant_with_control_events(
1855 &self,
1856 tenant_id: Option<&str>,
1857 username: &str,
1858 new_role: Role,
1859 ctx: &ControlEventCtx<'_>,
1860 ledger: &dyn ControlEventLedger,
1861 config: ControlEventConfig,
1862 ) -> Result<(), AuthError> {
1863 let control = UserLifecycleControl {
1864 ctx,
1865 ledger,
1866 config,
1867 };
1868 self.change_role_in_tenant_controlled(tenant_id, username, new_role, &control)
1869 }
1870
1871 fn change_role_in_tenant_unaudited(
1872 &self,
1873 tenant_id: Option<&str>,
1874 username: &str,
1875 new_role: Role,
1876 ) -> Result<(), AuthError> {
1877 let id = UserId::from_parts(tenant_id, username);
1878 let mut users = self.users.write().map_err(lock_err)?;
1879 let user = users
1880 .get_mut(&id)
1881 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1882
1883 let prior_role = user.role;
1884 user.role = new_role;
1885 user.updated_at = now_ms();
1886
1887 if new_role == Role::Admin && prior_role != Role::Admin {
1892 crate::telemetry::operator_event::OperatorEvent::AdminCapabilityGranted {
1893 granted_to: id.to_string(),
1894 capability: "Role::Admin".to_string(),
1895 granted_by: "auth_store::change_role".to_string(),
1896 }
1897 .emit_global();
1898 }
1899
1900 for key in &mut user.api_keys {
1902 if key.role > new_role {
1903 key.role = new_role;
1904 }
1905 }
1906
1907 if let Ok(mut idx) = self.api_key_index.write() {
1909 for key in &user.api_keys {
1910 if let Some(entry) = idx.get_mut(&key.key) {
1911 entry.1 = key.role;
1912 }
1913 }
1914 }
1915
1916 if prior_role != new_role {
1920 self.purge_sessions_for(&id);
1921 }
1922
1923 self.persist_to_vault();
1924 Ok(())
1925 }
1926
1927 fn change_role_in_tenant_controlled(
1928 &self,
1929 tenant_id: Option<&str>,
1930 username: &str,
1931 new_role: Role,
1932 control: &UserLifecycleControl<'_>,
1933 ) -> Result<(), AuthError> {
1934 let id = UserId::from_parts(tenant_id, username);
1935 let previous = self.get_user_cloned(&id);
1936 match self.change_role_in_tenant_unaudited(tenant_id, username, new_role) {
1937 Ok(()) => {
1938 if let Err(err) = self.emit_user_lifecycle_allowed(
1939 control,
1940 EventKind::UserUpdate,
1941 "user.update",
1942 &id,
1943 user_control_fields(
1944 &id,
1945 Some(new_role),
1946 previous.as_ref().map(|u| u.enabled),
1947 false,
1948 ),
1949 ) {
1950 self.restore_user_snapshot(&id, previous);
1951 return Err(err);
1952 }
1953 Ok(())
1954 }
1955 Err(err) => {
1956 self.emit_user_lifecycle_outcome(
1957 control,
1958 if user_error_is_denied(&err) {
1959 Outcome::Denied
1960 } else {
1961 Outcome::Error
1962 },
1963 EventKind::UserUpdate,
1964 "user.update",
1965 &id,
1966 Some(err.to_string()),
1967 user_control_fields(
1968 &id,
1969 Some(new_role),
1970 previous.as_ref().map(|u| u.enabled),
1971 false,
1972 ),
1973 );
1974 Err(err)
1975 }
1976 }
1977 }
1978
1979 pub fn authenticate(&self, username: &str, password: &str) -> Result<Session, AuthError> {
1991 self.authenticate_in_tenant(None, username, password)
1992 }
1993
1994 pub fn authenticate_in_tenant(
2004 &self,
2005 tenant_id: Option<&str>,
2006 username: &str,
2007 password: &str,
2008 ) -> Result<Session, AuthError> {
2009 let id = UserId::from_parts(tenant_id, username);
2010 match self.authenticate_in_tenant_unaudited(tenant_id, username, password) {
2011 Ok(session) => {
2012 if let Err(err) = self.emit_auth_event(
2013 EventKind::AuthLogin,
2014 Outcome::Allowed,
2015 &id,
2016 Some(session.role),
2017 None,
2018 ) {
2019 if let Ok(mut sessions) = self.sessions.write() {
2020 sessions.remove(&session.token);
2021 }
2022 return Err(err);
2023 }
2024 Ok(session)
2025 }
2026 Err(err) => {
2027 let outcome = if matches!(err, AuthError::InvalidCredentials) {
2028 Outcome::Denied
2029 } else {
2030 Outcome::Error
2031 };
2032 let _ = self.emit_auth_event(
2034 EventKind::AuthLoginFailed,
2035 outcome,
2036 &id,
2037 None,
2038 Some(&err.to_string()),
2039 );
2040 Err(err)
2041 }
2042 }
2043 }
2044
2045 fn emit_auth_event(
2048 &self,
2049 kind: EventKind,
2050 outcome: Outcome,
2051 id: &UserId,
2052 role: Option<Role>,
2053 reason: Option<&str>,
2054 ) -> Result<(), AuthError> {
2055 let Some(configured) = self.configured_control_events() else {
2056 return Ok(());
2057 };
2058 let ctx = ControlEventCtx {
2059 actor: crate::runtime::control_events::ActorRef::User(id),
2060 scope: id.tenant.as_deref().map(Cow::Borrowed),
2061 request_id: None,
2062 trace_id: None,
2063 };
2064 let mut fields: HashMap<String, Sensitivity> = HashMap::new();
2065 fields.insert("principal".to_string(), Sensitivity::raw(id.to_string()));
2066 if let Some(role) = role {
2067 fields.insert("role".to_string(), Sensitivity::raw(role.as_str()));
2068 }
2069 let event = ControlEvent {
2070 kind,
2071 outcome,
2072 action: Cow::Borrowed(kind.as_str()),
2073 resource: Some(format!("principal:{id}")),
2074 reason: reason.map(str::to_string),
2075 matched_policy_id: None,
2076 fields,
2077 };
2078 match configured.ledger.emit(&ctx, event) {
2079 Ok(_) => Ok(()),
2080 Err(err) if configured.config.require_persistence() => Err(AuthError::Internal(
2081 format!("control-event ledger refused {}: {err}", kind.as_str()),
2082 )),
2083 Err(err) => {
2084 tracing::warn!(
2085 target: "reddb::security",
2086 kind = kind.as_str(),
2087 error = %err,
2088 "control-event ledger refused an auth event"
2089 );
2090 Ok(())
2091 }
2092 }
2093 }
2094
2095 fn authenticate_in_tenant_unaudited(
2096 &self,
2097 tenant_id: Option<&str>,
2098 username: &str,
2099 password: &str,
2100 ) -> Result<Session, AuthError> {
2101 if crate::auth::self_lock_guard::is_synthetic_principal(username) {
2105 return Err(AuthError::InvalidCredentials);
2106 }
2107 let id = UserId::from_parts(tenant_id, username);
2108 let users = self.users.read().map_err(lock_err)?;
2109 let user = users.get(&id).ok_or(AuthError::InvalidCredentials)?;
2110
2111 if !user.enabled {
2112 return Err(AuthError::InvalidCredentials);
2113 }
2114
2115 let stored_hash = user.password_hash.clone();
2116 if !verify_password(password, &stored_hash) {
2117 return Err(AuthError::InvalidCredentials);
2118 }
2119
2120 let token = match self.keypair.read().ok().and_then(|g| {
2122 g.as_ref().map(|kp| {
2123 let token_id = random_hex(16);
2124 let sig = kp.sign(format!("session:{}", token_id).as_bytes());
2125 format!("rs_{}{}", token_id, hex::encode(&sig[..16]))
2127 })
2128 }) {
2129 Some(signed_token) => signed_token,
2130 None => generate_session_token(),
2131 };
2132
2133 let now = now_ms();
2134 let session = Session {
2135 token,
2136 username: username.to_string(),
2137 tenant_id: user.tenant_id.clone(),
2138 role: user.role,
2139 created_at: now,
2140 expires_at: now + (self.config.session_ttl_secs as u128 * 1000),
2141 };
2142
2143 drop(users); let mut sessions = self.sessions.write().map_err(lock_err)?;
2146 sessions.insert(session.token.clone(), session.clone());
2147 drop(sessions);
2148
2149 if password_hash_needs_upgrade(&stored_hash) {
2152 let upgraded = hash_password(password);
2153 let mut rehashed = false;
2154 if let Ok(mut users) = self.users.write() {
2155 if let Some(user) = users.get_mut(&id) {
2156 if user.password_hash == stored_hash {
2158 user.password_hash = upgraded;
2159 rehashed = true;
2160 }
2161 }
2162 }
2163 if rehashed {
2164 self.persist_to_vault();
2165 }
2166 }
2167
2168 Ok(session)
2169 }
2170
2171 pub fn validate_token(&self, token: &str) -> Option<(String, Role)> {
2183 self.validate_token_full(token)
2184 .map(|(id, role)| (id.username, role))
2185 }
2186
2187 pub fn validate_token_full(&self, token: &str) -> Option<(UserId, Role)> {
2190 let resolved = self.resolve_token_unchecked(token)?;
2191 if !self.principal_is_active(&resolved.0) {
2195 return None;
2196 }
2197 Some(resolved)
2198 }
2199
2200 fn resolve_token_unchecked(&self, token: &str) -> Option<(UserId, Role)> {
2202 if token.starts_with("rs_") {
2204 if let Ok(sessions) = self.sessions.read() {
2205 if let Some(session) = sessions.get(token) {
2206 let now = now_ms();
2207 if now < session.expires_at {
2208 return Some((
2209 UserId::from_parts(session.tenant_id.as_deref(), &session.username),
2210 session.role,
2211 ));
2212 }
2213 }
2214 }
2215 return None;
2216 }
2217
2218 if token.starts_with("rk_") {
2221 let id = api_key_credential_id(token);
2222 let (owner, role) = self
2223 .api_key_index
2224 .read()
2225 .ok()
2226 .and_then(|idx| idx.get(&id).cloned())?;
2227 if self.api_key_expired(&owner, &id) {
2228 return None;
2229 }
2230 return Some((owner, role));
2231 }
2232
2233 None
2234 }
2235
2236 fn api_key_expired(&self, owner: &UserId, id: &str) -> bool {
2239 let Ok(users) = self.users.read() else {
2240 return true;
2241 };
2242 users
2243 .get(owner)
2244 .and_then(|user| user.api_keys.iter().find(|key| key.key == id))
2245 .map(|key| key.expires_at.is_some_and(|deadline| now_ms() >= deadline))
2246 .unwrap_or(true)
2247 }
2248
2249 pub fn active_user_role(&self, id: &UserId) -> Option<Role> {
2255 if !self.principal_is_active(id) {
2256 return None;
2257 }
2258 self.users.read().ok()?.get(id).map(|user| user.role)
2259 }
2260
2261 fn principal_is_active(&self, id: &UserId) -> bool {
2264 let enabled = match self.users.read() {
2265 Ok(users) => users.get(id).map(|u| u.enabled).unwrap_or(false),
2266 Err(_) => false,
2267 };
2268 if !enabled {
2269 return false;
2270 }
2271 match self.user_attributes(id).valid_until {
2272 Some(deadline) => now_ms() < deadline,
2273 None => true,
2274 }
2275 }
2276
2277 pub fn create_api_key(
2285 &self,
2286 username: &str,
2287 name: &str,
2288 role: Role,
2289 ) -> Result<ApiKey, AuthError> {
2290 self.create_api_key_in_tenant(None, username, name, role)
2291 }
2292
2293 pub fn create_api_key_with_control_events(
2294 &self,
2295 username: &str,
2296 name: &str,
2297 role: Role,
2298 ctx: &ControlEventCtx<'_>,
2299 ledger: &dyn ControlEventLedger,
2300 config: ControlEventConfig,
2301 ) -> Result<ApiKey, AuthError> {
2302 self.create_api_key_in_tenant_with_control_events(
2303 None, username, name, role, ctx, ledger, config,
2304 )
2305 }
2306
2307 pub fn create_api_key_in_tenant(
2308 &self,
2309 tenant_id: Option<&str>,
2310 username: &str,
2311 name: &str,
2312 role: Role,
2313 ) -> Result<ApiKey, AuthError> {
2314 self.create_api_key_in_tenant_expiring(tenant_id, username, name, role, None)
2315 }
2316
2317 pub fn create_api_key_in_tenant_expiring(
2320 &self,
2321 tenant_id: Option<&str>,
2322 username: &str,
2323 name: &str,
2324 role: Role,
2325 expires_at: Option<u128>,
2326 ) -> Result<ApiKey, AuthError> {
2327 if let Some(configured) = self.configured_control_events() {
2328 let ctx = default_user_lifecycle_ctx();
2329 let control = UserLifecycleControl {
2330 ctx: &ctx,
2331 ledger: configured.ledger.as_ref(),
2332 config: configured.config,
2333 };
2334 self.create_api_key_in_tenant_controlled(
2335 tenant_id, username, name, role, expires_at, &control,
2336 )
2337 } else {
2338 self.create_api_key_in_tenant_unaudited(tenant_id, username, name, role, expires_at)
2339 }
2340 }
2341
2342 #[allow(clippy::too_many_arguments)]
2343 pub fn create_api_key_in_tenant_with_control_events(
2344 &self,
2345 tenant_id: Option<&str>,
2346 username: &str,
2347 name: &str,
2348 role: Role,
2349 ctx: &ControlEventCtx<'_>,
2350 ledger: &dyn ControlEventLedger,
2351 config: ControlEventConfig,
2352 ) -> Result<ApiKey, AuthError> {
2353 let control = UserLifecycleControl {
2354 ctx,
2355 ledger,
2356 config,
2357 };
2358 self.create_api_key_in_tenant_controlled(tenant_id, username, name, role, None, &control)
2359 }
2360
2361 fn create_api_key_in_tenant_unaudited(
2362 &self,
2363 tenant_id: Option<&str>,
2364 username: &str,
2365 name: &str,
2366 role: Role,
2367 expires_at: Option<u128>,
2368 ) -> Result<ApiKey, AuthError> {
2369 let id = UserId::from_parts(tenant_id, username);
2370 let mut users = self.users.write().map_err(lock_err)?;
2371 let user = users
2372 .get_mut(&id)
2373 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
2374
2375 if role > user.role {
2377 return Err(AuthError::RoleExceeded {
2378 requested: role,
2379 ceiling: user.role,
2380 });
2381 }
2382
2383 let secret = generate_api_key();
2384 let stored = ApiKey {
2385 key: api_key_credential_id(&secret),
2386 name: name.to_string(),
2387 role,
2388 created_at: now_ms(),
2389 expires_at,
2390 };
2391
2392 user.api_keys.push(stored.clone());
2393 user.updated_at = now_ms();
2394
2395 if let Ok(mut idx) = self.api_key_index.write() {
2397 idx.insert(stored.key.clone(), (id.clone(), stored.role));
2398 }
2399
2400 drop(users); self.persist_to_vault();
2402 Ok(ApiKey {
2404 key: secret,
2405 ..stored
2406 })
2407 }
2408
2409 fn create_api_key_in_tenant_controlled(
2410 &self,
2411 tenant_id: Option<&str>,
2412 username: &str,
2413 name: &str,
2414 role: Role,
2415 expires_at: Option<u128>,
2416 control: &UserLifecycleControl<'_>,
2417 ) -> Result<ApiKey, AuthError> {
2418 let id = UserId::from_parts(tenant_id, username);
2419 match self.create_api_key_in_tenant_unaudited(tenant_id, username, name, role, expires_at) {
2420 Ok(api_key) => {
2421 let key_id = api_key_id(&api_key.key);
2422 if let Err(err) = self.emit_api_key_allowed(
2423 control,
2424 EventKind::ApiKeyCreate,
2425 "apikey.create",
2426 &key_id,
2427 api_key_control_fields(&id, role, &key_id),
2428 ) {
2429 self.rollback_create_api_key(&id, &api_key.key);
2430 return Err(err);
2431 }
2432 Ok(api_key)
2433 }
2434 Err(err) => {
2435 self.emit_user_lifecycle_outcome(
2436 control,
2437 if user_error_is_denied(&err) {
2438 Outcome::Denied
2439 } else {
2440 Outcome::Error
2441 },
2442 EventKind::ApiKeyCreate,
2443 "apikey.create",
2444 &id,
2445 Some(err.to_string()),
2446 user_control_fields(&id, Some(role), None, false),
2447 );
2448 Err(err)
2449 }
2450 }
2451 }
2452
2453 pub fn revoke_api_key(&self, key: &str) -> Result<(), AuthError> {
2455 if let Some(configured) = self.configured_control_events() {
2456 let ctx = default_user_lifecycle_ctx();
2457 let control = UserLifecycleControl {
2458 ctx: &ctx,
2459 ledger: configured.ledger.as_ref(),
2460 config: configured.config,
2461 };
2462 self.revoke_api_key_controlled(key, &control)
2463 } else {
2464 self.revoke_api_key_unaudited(key)
2465 }
2466 }
2467
2468 pub fn revoke_api_key_with_control_events(
2469 &self,
2470 key: &str,
2471 ctx: &ControlEventCtx<'_>,
2472 ledger: &dyn ControlEventLedger,
2473 config: ControlEventConfig,
2474 ) -> Result<(), AuthError> {
2475 let control = UserLifecycleControl {
2476 ctx,
2477 ledger,
2478 config,
2479 };
2480 self.revoke_api_key_controlled(key, &control)
2481 }
2482
2483 fn revoke_api_key_unaudited(&self, key: &str) -> Result<(), AuthError> {
2484 let key_id = api_key_credential_id(key);
2485 let key = key_id.as_str();
2486 let mut users = self.users.write().map_err(lock_err)?;
2487
2488 let owner_id: UserId = {
2492 if let Ok(idx) = self.api_key_index.read() {
2493 if let Some((id, _)) = idx.get(key) {
2494 id.clone()
2495 } else {
2496 return Err(AuthError::KeyNotFound(key.to_string()));
2497 }
2498 } else {
2499 let owner = users
2500 .iter()
2501 .find(|(_, u)| u.api_keys.iter().any(|k| k.key == key));
2502 match owner {
2503 Some((id, _)) => id.clone(),
2504 None => return Err(AuthError::KeyNotFound(key.to_string())),
2505 }
2506 }
2507 };
2508
2509 let user = users
2510 .get_mut(&owner_id)
2511 .ok_or_else(|| AuthError::KeyNotFound(key.to_string()))?;
2512 user.api_keys.retain(|k| k.key != key);
2513 user.updated_at = now_ms();
2514
2515 if let Ok(mut idx) = self.api_key_index.write() {
2517 idx.remove(key);
2518 }
2519
2520 self.persist_to_vault();
2521 Ok(())
2522 }
2523
2524 fn revoke_api_key_controlled(
2525 &self,
2526 key: &str,
2527 control: &UserLifecycleControl<'_>,
2528 ) -> Result<(), AuthError> {
2529 let key_id = api_key_id(key);
2530 let rollback = self.api_key_rollback_snapshot(key);
2531 let id = rollback
2532 .as_ref()
2533 .map(|r| r.0.clone())
2534 .unwrap_or_else(|| UserId::from_parts(None, ""));
2535 let role = rollback.as_ref().map(|r| r.1.role).unwrap_or(Role::Read);
2536 match self.revoke_api_key_unaudited(key) {
2537 Ok(()) => {
2538 if let Err(err) = self.emit_api_key_allowed(
2539 control,
2540 EventKind::ApiKeyRevoke,
2541 "apikey.revoke",
2542 &key_id,
2543 api_key_control_fields(&id, role, &key_id),
2544 ) {
2545 if let Some((owner, api_key)) = rollback {
2546 self.restore_api_key(&owner, api_key);
2547 }
2548 return Err(err);
2549 }
2550 Ok(())
2551 }
2552 Err(err) => {
2553 self.emit_user_lifecycle_outcome(
2554 control,
2555 if user_error_is_denied(&err) {
2556 Outcome::Denied
2557 } else {
2558 Outcome::Error
2559 },
2560 EventKind::ApiKeyRevoke,
2561 "apikey.revoke",
2562 &id,
2563 Some(err.to_string()),
2564 api_key_control_fields(&id, role, &key_id),
2565 );
2566 Err(err)
2567 }
2568 }
2569 }
2570
2571 pub fn revoke_session(&self, token: &str) {
2577 if let Ok(mut sessions) = self.sessions.write() {
2578 sessions.remove(token);
2579 }
2580 }
2581
2582 pub fn purge_expired_sessions(&self) -> usize {
2584 let now = now_ms();
2585 if let Ok(mut sessions) = self.sessions.write() {
2586 let before = sessions.len();
2587 sessions.retain(|_, s| s.expires_at > now);
2588 return before - sessions.len();
2589 }
2590 0
2591 }
2592
2593 pub fn grant(
2613 &self,
2614 granter: &UserId,
2615 granter_role: Role,
2616 principal: GrantPrincipal,
2617 resource: Resource,
2618 actions: Vec<Action>,
2619 with_grant_option: bool,
2620 tenant: Option<String>,
2621 ) -> Result<(), AuthError> {
2622 if granter_role != Role::Admin {
2623 return Err(AuthError::Forbidden(format!(
2624 "GRANT requires Admin role; granter `{}` has `{:?}`",
2625 granter, granter_role
2626 )));
2627 }
2628
2629 if granter.tenant.is_some() && granter.tenant != tenant {
2633 return Err(AuthError::Forbidden(format!(
2634 "cross-tenant GRANT denied: granter tenant `{:?}` != grant tenant `{:?}`",
2635 granter.tenant, tenant
2636 )));
2637 }
2638
2639 let mut actions_set = std::collections::BTreeSet::new();
2640 for a in actions {
2641 actions_set.insert(a);
2642 }
2643 let g = Grant {
2644 principal: principal.clone(),
2645 resource,
2646 actions: actions_set,
2647 with_grant_option,
2648 granted_by: granter.to_string(),
2649 granted_at: now_ms(),
2650 tenant,
2651 columns: None,
2652 };
2653
2654 match &principal {
2655 GrantPrincipal::User(uid) => {
2656 self.grants
2657 .write()
2658 .unwrap_or_else(|e| e.into_inner())
2659 .entry(uid.clone())
2660 .or_default()
2661 .push(g.clone());
2662 self.invalidate_permission_cache(Some(uid));
2663 }
2664 GrantPrincipal::Public => {
2665 self.public_grants
2666 .write()
2667 .unwrap_or_else(|e| e.into_inner())
2668 .push(g.clone());
2669 self.invalidate_permission_cache(None);
2670 }
2671 GrantPrincipal::Group(_) => {
2672 return Err(AuthError::Forbidden(
2673 "GROUP principals are not yet supported; use a USER or PUBLIC".to_string(),
2674 ));
2675 }
2676 }
2677
2678 self.invalidate_visible_collections_for_tenant(g.tenant.as_deref());
2683
2684 self.persist_acl_to_kv();
2685 Ok(())
2686 }
2687
2688 pub fn revoke(
2691 &self,
2692 granter: &UserId,
2693 granter_role: Role,
2694 principal: &GrantPrincipal,
2695 resource: &Resource,
2696 actions: &[Action],
2697 ) -> Result<usize, AuthError> {
2698 if granter_role != Role::Admin {
2699 return Err(AuthError::Forbidden(format!(
2700 "REVOKE requires Admin role; granter `{}` has `{:?}`",
2701 granter, granter_role
2702 )));
2703 }
2704
2705 if let GrantPrincipal::User(uid) = principal {
2711 if granter.tenant.is_some() && granter.tenant != uid.tenant {
2712 return Err(AuthError::Forbidden(format!(
2713 "cross-tenant REVOKE denied: granter tenant `{:?}` != principal tenant `{:?}`",
2714 granter.tenant, uid.tenant
2715 )));
2716 }
2717 }
2718
2719 let removed = match principal {
2720 GrantPrincipal::User(uid) => {
2721 let mut g = self.grants.write().unwrap_or_else(|e| e.into_inner());
2722 let before = g.get(uid).map(|v| v.len()).unwrap_or(0);
2723 if let Some(list) = g.get_mut(uid) {
2724 list.retain(|gr| {
2725 !(gr.resource == *resource
2726 && (actions.iter().any(|a| gr.actions.contains(a))
2727 || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2728 });
2729 }
2730 let after = g.get(uid).map(|v| v.len()).unwrap_or(0);
2731 drop(g);
2732 self.invalidate_permission_cache(Some(uid));
2733 before - after
2734 }
2735 GrantPrincipal::Public => {
2736 let mut p = self
2737 .public_grants
2738 .write()
2739 .unwrap_or_else(|e| e.into_inner());
2740 let before = p.len();
2741 p.retain(|gr| {
2742 !(gr.resource == *resource
2743 && (actions.iter().any(|a| gr.actions.contains(a))
2744 || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2745 });
2746 let after = p.len();
2747 drop(p);
2748 self.invalidate_permission_cache(None);
2749 before - after
2750 }
2751 GrantPrincipal::Group(_) => 0,
2752 };
2753
2754 if removed > 0 {
2755 match principal {
2761 GrantPrincipal::User(uid) => {
2762 self.invalidate_visible_collections_for_tenant(uid.tenant.as_deref());
2763 }
2764 GrantPrincipal::Public | GrantPrincipal::Group(_) => {
2765 self.invalidate_visible_collections_cache();
2766 }
2767 }
2768 self.persist_acl_to_kv();
2769 }
2770 Ok(removed)
2771 }
2772
2773 pub fn visible_collections_for_scope(
2787 &self,
2788 tenant: Option<&str>,
2789 role: Role,
2790 principal: &str,
2791 all_collections: &[String],
2792 ) -> std::collections::HashSet<String> {
2793 let key = super::scope_cache::ScopeKey::new(tenant, principal, role);
2794 if let Some(hit) = self.visible_collections_cache.get(&key) {
2795 return hit;
2796 }
2797 let ctx = AuthzContext {
2800 principal,
2801 effective_role: role,
2802 tenant,
2803 };
2804 let mut visible = std::collections::HashSet::new();
2805 for collection in all_collections {
2806 let resource = Resource::table_from_name(collection);
2807 if self.check_grant(&ctx, Action::Select, &resource).is_ok() {
2808 visible.insert(collection.clone());
2809 }
2810 }
2811 self.visible_collections_cache.insert(key, visible.clone());
2812 visible
2813 }
2814
2815 pub fn auth_cache_stats(&self) -> super::scope_cache::AuthCacheStats {
2819 self.visible_collections_cache.stats()
2820 }
2821
2822 pub fn invalidate_visible_collections_cache(&self) {
2826 self.visible_collections_cache.invalidate_all();
2827 }
2828
2829 pub fn invalidate_visible_collections_for_tenant(&self, tenant: Option<&str>) {
2832 self.visible_collections_cache.invalidate_tenant(tenant);
2833 }
2834
2835 pub fn effective_grants(&self, uid: &UserId) -> Vec<Grant> {
2838 let mut out = Vec::new();
2839 if let Ok(g) = self.grants.read() {
2840 if let Some(list) = g.get(uid) {
2841 out.extend(list.iter().cloned());
2842 }
2843 }
2844 if let Ok(p) = self.public_grants.read() {
2845 out.extend(p.iter().cloned());
2846 }
2847 out
2848 }
2849
2850 pub fn check_grant(
2853 &self,
2854 ctx: &AuthzContext<'_>,
2855 action: Action,
2856 resource: &Resource,
2857 ) -> Result<(), AuthzError> {
2858 if ctx.effective_role == Role::Admin {
2859 return Ok(());
2860 }
2861
2862 let uid = UserId::from_parts(ctx.tenant, ctx.principal);
2863
2864 if let Ok(cache) = self.permission_cache.read() {
2866 if let Some(pc) = cache.get(&uid) {
2867 if pc.allows(resource, action) {
2868 return Ok(());
2869 }
2870 }
2871 }
2872
2873 let user_grants = self
2875 .grants
2876 .read()
2877 .ok()
2878 .and_then(|g| g.get(&uid).cloned())
2879 .unwrap_or_default();
2880 let any_user_grants = self
2881 .grants
2882 .read()
2883 .ok()
2884 .map(|g| g.values().any(|list| !list.is_empty()))
2885 .unwrap_or(false);
2886 let public_grants = self
2887 .public_grants
2888 .read()
2889 .ok()
2890 .map(|p| p.clone())
2891 .unwrap_or_default();
2892 if user_grants.is_empty() && public_grants.is_empty() && any_user_grants {
2893 return Err(AuthzError::PermissionDenied {
2894 action,
2895 resource: resource.clone(),
2896 principal: ctx.principal.to_string(),
2897 });
2898 }
2899 let view = GrantsView {
2900 user_grants: &user_grants,
2901 public_grants: &public_grants,
2902 };
2903 let result = check_grant(ctx, action, resource, &view);
2904
2905 if result.is_ok() {
2906 let pc = PermissionCache::build(&user_grants, &public_grants);
2907 if let Ok(mut cache) = self.permission_cache.write() {
2908 cache.insert(uid, pc);
2909 }
2910 }
2911 result
2912 }
2913
2914 pub fn set_user_attributes(
2920 &self,
2921 uid: &UserId,
2922 attrs: UserAttributes,
2923 ) -> Result<(), AuthError> {
2924 let users = self.users.read().map_err(lock_err)?;
2925 if !users.contains_key(uid) {
2926 return Err(AuthError::UserNotFound(uid.to_string()));
2927 }
2928 drop(users);
2929
2930 self.user_attributes
2931 .write()
2932 .unwrap_or_else(|e| e.into_inner())
2933 .insert(uid.clone(), attrs);
2934 self.invalidate_iam_cache(Some(uid));
2935 self.persist_acl_to_kv();
2936 Ok(())
2937 }
2938
2939 pub fn user_attributes(&self, uid: &UserId) -> UserAttributes {
2942 self.user_attributes
2943 .read()
2944 .ok()
2945 .and_then(|m| m.get(uid).cloned())
2946 .unwrap_or_default()
2947 }
2948
2949 pub fn add_user_to_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
2950 if group.trim().is_empty() {
2951 return Err(AuthError::Forbidden("group name cannot be empty".into()));
2952 }
2953 let mut attrs = self.user_attributes(uid);
2954 if !attrs.groups.iter().any(|g| g == group) {
2955 attrs.groups.push(group.to_string());
2956 attrs.groups.sort();
2957 }
2958 self.set_user_attributes(uid, attrs)
2959 }
2960
2961 pub fn remove_user_from_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
2962 let mut attrs = self.user_attributes(uid);
2963 attrs.groups.retain(|g| g != group);
2964 self.set_user_attributes(uid, attrs)
2965 }
2966
2967 pub fn set_user_enabled(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
2969 if let Some(configured) = self.configured_control_events() {
2970 let ctx = default_user_lifecycle_ctx();
2971 let control = UserLifecycleControl {
2972 ctx: &ctx,
2973 ledger: configured.ledger.as_ref(),
2974 config: configured.config,
2975 };
2976 self.set_user_enabled_controlled(uid, enabled, &control)
2977 } else {
2978 self.set_user_enabled_unaudited(uid, enabled)
2979 }
2980 }
2981
2982 pub fn disable_user(
2983 &self,
2984 username: &str,
2985 ctx: &ControlEventCtx<'_>,
2986 ledger: &dyn ControlEventLedger,
2987 config: ControlEventConfig,
2988 ) -> Result<(), AuthError> {
2989 self.disable_user_in_tenant(None, username, ctx, ledger, config)
2990 }
2991
2992 pub fn disable_user_in_tenant(
2993 &self,
2994 tenant_id: Option<&str>,
2995 username: &str,
2996 ctx: &ControlEventCtx<'_>,
2997 ledger: &dyn ControlEventLedger,
2998 config: ControlEventConfig,
2999 ) -> Result<(), AuthError> {
3000 let uid = UserId::from_parts(tenant_id, username);
3001 let control = UserLifecycleControl {
3002 ctx,
3003 ledger,
3004 config,
3005 };
3006 self.set_user_enabled_controlled(&uid, false, &control)
3007 }
3008
3009 fn set_user_enabled_unaudited(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
3010 let mut users = self.users.write().map_err(lock_err)?;
3011 let user = users
3012 .get_mut(uid)
3013 .ok_or_else(|| AuthError::UserNotFound(uid.to_string()))?;
3014 user.enabled = enabled;
3015 user.updated_at = now_ms();
3016 drop(users);
3017 if !enabled {
3018 self.purge_sessions_for(uid);
3019 }
3020 self.persist_to_vault();
3021 Ok(())
3022 }
3023
3024 fn purge_sessions_for(&self, uid: &UserId) {
3027 if let Ok(mut sessions) = self.sessions.write() {
3028 sessions.retain(|_, s| !(s.username == uid.username && s.tenant_id == uid.tenant));
3029 }
3030 }
3031
3032 fn set_user_enabled_controlled(
3033 &self,
3034 uid: &UserId,
3035 enabled: bool,
3036 control: &UserLifecycleControl<'_>,
3037 ) -> Result<(), AuthError> {
3038 let previous = self.get_user_cloned(uid);
3039 let kind = if enabled {
3040 EventKind::UserUpdate
3041 } else {
3042 EventKind::UserDisable
3043 };
3044 let action = if enabled {
3045 "user.update"
3046 } else {
3047 "user.disable"
3048 };
3049 match self.set_user_enabled_unaudited(uid, enabled) {
3050 Ok(()) => {
3051 if let Err(err) = self.emit_user_lifecycle_allowed(
3052 control,
3053 kind,
3054 action,
3055 uid,
3056 user_control_fields(
3057 uid,
3058 previous.as_ref().map(|u| u.role),
3059 Some(enabled),
3060 false,
3061 ),
3062 ) {
3063 self.restore_user_snapshot(uid, previous);
3064 return Err(err);
3065 }
3066 Ok(())
3067 }
3068 Err(err) => {
3069 self.emit_user_lifecycle_outcome(
3070 control,
3071 if user_error_is_denied(&err) {
3072 Outcome::Denied
3073 } else {
3074 Outcome::Error
3075 },
3076 kind,
3077 action,
3078 uid,
3079 Some(err.to_string()),
3080 user_control_fields(
3081 uid,
3082 previous.as_ref().map(|u| u.role),
3083 Some(enabled),
3084 false,
3085 ),
3086 );
3087 Err(err)
3088 }
3089 }
3090 }
3091
3092 fn emit_user_lifecycle_allowed(
3093 &self,
3094 control: &UserLifecycleControl<'_>,
3095 kind: EventKind,
3096 action: &'static str,
3097 id: &UserId,
3098 fields: HashMap<String, Sensitivity>,
3099 ) -> Result<(), AuthError> {
3100 self.emit_user_lifecycle_event(control, Outcome::Allowed, kind, action, id, None, fields)
3101 }
3102
3103 fn emit_api_key_allowed(
3104 &self,
3105 control: &UserLifecycleControl<'_>,
3106 kind: EventKind,
3107 action: &'static str,
3108 api_key_id: &str,
3109 fields: HashMap<String, Sensitivity>,
3110 ) -> Result<(), AuthError> {
3111 self.emit_control_event(
3112 control,
3113 Outcome::Allowed,
3114 kind,
3115 action,
3116 Some(api_key_resource(api_key_id)),
3117 None,
3118 fields,
3119 )
3120 }
3121
3122 #[allow(clippy::too_many_arguments)]
3123 fn emit_user_lifecycle_outcome(
3124 &self,
3125 control: &UserLifecycleControl<'_>,
3126 outcome: Outcome,
3127 kind: EventKind,
3128 action: &'static str,
3129 id: &UserId,
3130 reason: Option<String>,
3131 fields: HashMap<String, Sensitivity>,
3132 ) {
3133 let _ = self.emit_user_lifecycle_event(control, outcome, kind, action, id, reason, fields);
3134 }
3135
3136 #[allow(clippy::too_many_arguments)]
3137 fn emit_user_lifecycle_event(
3138 &self,
3139 control: &UserLifecycleControl<'_>,
3140 outcome: Outcome,
3141 kind: EventKind,
3142 action: &'static str,
3143 id: &UserId,
3144 reason: Option<String>,
3145 fields: HashMap<String, Sensitivity>,
3146 ) -> Result<(), AuthError> {
3147 self.emit_control_event(
3148 control,
3149 outcome,
3150 kind,
3151 action,
3152 Some(user_resource(id)),
3153 reason,
3154 fields,
3155 )
3156 }
3157
3158 #[allow(clippy::too_many_arguments)]
3159 fn emit_control_event(
3160 &self,
3161 control: &UserLifecycleControl<'_>,
3162 outcome: Outcome,
3163 kind: EventKind,
3164 action: &'static str,
3165 resource: Option<String>,
3166 reason: Option<String>,
3167 fields: HashMap<String, Sensitivity>,
3168 ) -> Result<(), AuthError> {
3169 let event = ControlEvent {
3170 kind,
3171 outcome,
3172 action: Cow::Borrowed(action),
3173 resource,
3174 reason,
3175 matched_policy_id: None,
3176 fields,
3177 };
3178 match control.ledger.emit(control.ctx, event) {
3179 Ok(_) => Ok(()),
3180 Err(err) if control.config.require_persistence() => {
3181 Err(AuthError::Internal(err.to_string()))
3182 }
3183 Err(_) => Ok(()),
3184 }
3185 }
3186
3187 fn rollback_create_user(&self, id: &UserId) {
3188 let removed = self
3189 .users
3190 .write()
3191 .unwrap_or_else(|e| e.into_inner())
3192 .remove(id);
3193 if let Some(user) = removed {
3194 self.remove_api_key_index_entries(&user);
3195 }
3196 self.persist_to_vault();
3197 }
3198
3199 fn rollback_bootstrap(&self, id: &UserId) {
3200 self.bootstrapped.store(false, Ordering::Release);
3201 self.rollback_create_user(id);
3202 }
3203
3204 fn restore_user_snapshot(&self, id: &UserId, previous: Option<User>) {
3205 match previous {
3206 Some(user) => {
3207 self.users
3208 .write()
3209 .unwrap_or_else(|e| e.into_inner())
3210 .insert(id.clone(), user.clone());
3211 self.index_user_api_keys(id, &user);
3212 }
3213 None => {
3214 self.rollback_create_user(id);
3215 return;
3216 }
3217 }
3218 self.persist_to_vault();
3219 }
3220
3221 fn user_delete_rollback_snapshot(
3222 &self,
3223 id: &UserId,
3224 tenant_id: Option<&str>,
3225 username: &str,
3226 ) -> Option<(User, Vec<(String, Session)>)> {
3227 let user = self.get_user_cloned(id)?;
3228 let sessions = self
3229 .sessions
3230 .read()
3231 .map(|sessions| {
3232 sessions
3233 .iter()
3234 .filter(|(_, session)| {
3235 session.username == username && session.tenant_id.as_deref() == tenant_id
3236 })
3237 .map(|(token, session)| (token.clone(), session.clone()))
3238 .collect()
3239 })
3240 .unwrap_or_default();
3241 Some((user, sessions))
3242 }
3243
3244 fn restore_deleted_user(&self, id: &UserId, user: User, sessions: Vec<(String, Session)>) {
3245 self.users
3246 .write()
3247 .unwrap_or_else(|e| e.into_inner())
3248 .insert(id.clone(), user.clone());
3249 self.index_user_api_keys(id, &user);
3250 if !sessions.is_empty() {
3251 let mut guard = self.sessions.write().unwrap_or_else(|e| e.into_inner());
3252 for (token, session) in sessions {
3253 guard.insert(token, session);
3254 }
3255 }
3256 self.persist_to_vault();
3257 }
3258
3259 fn rollback_create_api_key(&self, id: &UserId, key: &str) {
3260 let key_id = api_key_credential_id(key);
3261 let key = key_id.as_str();
3262 if let Ok(mut users) = self.users.write() {
3263 if let Some(user) = users.get_mut(id) {
3264 user.api_keys.retain(|api_key| api_key.key != key);
3265 user.updated_at = now_ms();
3266 }
3267 }
3268 if let Ok(mut idx) = self.api_key_index.write() {
3269 idx.remove(key);
3270 }
3271 self.persist_to_vault();
3272 }
3273
3274 fn api_key_rollback_snapshot(&self, key: &str) -> Option<(UserId, ApiKey)> {
3275 let key = api_key_credential_id(key);
3276 let users = self.users.read().ok()?;
3277 users.iter().find_map(|(id, user)| {
3278 user.api_keys
3279 .iter()
3280 .find(|api_key| api_key.key == key)
3281 .cloned()
3282 .map(|api_key| (id.clone(), api_key))
3283 })
3284 }
3285
3286 fn restore_api_key(&self, id: &UserId, api_key: ApiKey) {
3287 if let Ok(mut users) = self.users.write() {
3288 if let Some(user) = users.get_mut(id) {
3289 if !user
3290 .api_keys
3291 .iter()
3292 .any(|candidate| candidate.key == api_key.key)
3293 {
3294 user.api_keys.push(api_key.clone());
3295 user.updated_at = now_ms();
3296 }
3297 }
3298 }
3299 if let Ok(mut idx) = self.api_key_index.write() {
3300 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
3301 }
3302 self.persist_to_vault();
3303 }
3304
3305 fn remove_api_key_index_entries(&self, user: &User) {
3306 if let Ok(mut idx) = self.api_key_index.write() {
3307 for api_key in &user.api_keys {
3308 idx.remove(&api_key.key);
3309 }
3310 }
3311 }
3312
3313 fn index_user_api_keys(&self, id: &UserId, user: &User) {
3314 if let Ok(mut idx) = self.api_key_index.write() {
3315 for api_key in &user.api_keys {
3316 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
3317 }
3318 }
3319 }
3320
3321 pub fn authenticate_with_attrs(
3331 &self,
3332 tenant_id: Option<&str>,
3333 username: &str,
3334 password: &str,
3335 ) -> Result<Session, AuthError> {
3336 let uid = UserId::from_parts(tenant_id, username);
3337 let attrs = self.user_attributes(&uid);
3338
3339 if let Some(deadline) = attrs.valid_until {
3340 if now_ms() >= deadline {
3341 return Err(AuthError::Forbidden(format!(
3342 "account `{}` expired (VALID UNTIL exceeded)",
3343 uid
3344 )));
3345 }
3346 }
3347
3348 if let Some(limit) = attrs.connection_limit {
3349 let current = self
3350 .session_count_by_user
3351 .read()
3352 .ok()
3353 .and_then(|m| m.get(&uid).copied())
3354 .unwrap_or(0);
3355 if current >= limit {
3356 return Err(AuthError::Forbidden(format!(
3357 "account `{}` exceeded CONNECTION LIMIT ({})",
3358 uid, limit
3359 )));
3360 }
3361 }
3362
3363 let session = self.authenticate_in_tenant(tenant_id, username, password)?;
3364
3365 if let Ok(mut counts) = self.session_count_by_user.write() {
3366 *counts.entry(uid).or_insert(0) += 1;
3367 }
3368 Ok(session)
3369 }
3370
3371 pub fn decrement_session_count(&self, uid: &UserId) {
3374 if let Ok(mut counts) = self.session_count_by_user.write() {
3375 if let Some(c) = counts.get_mut(uid) {
3376 *c = c.saturating_sub(1);
3377 }
3378 }
3379 }
3380
3381 pub fn rehydrate_acl(&self) {
3388 let kv_snapshot: Vec<(String, String)> = self
3389 .vault_kv
3390 .read()
3391 .map(|kv| {
3392 kv.iter()
3393 .filter(|(k, _)| {
3394 k.starts_with("red.acl.grants.")
3395 || k.starts_with("red.acl.attrs.")
3396 || k == &"red.acl.public_grants"
3397 })
3398 .map(|(k, v)| (k.clone(), v.clone()))
3399 .collect()
3400 })
3401 .unwrap_or_default();
3402
3403 for (k, v) in kv_snapshot {
3404 if k == "red.acl.public_grants" {
3405 if let Some(parsed) = decode_grants_blob(&v) {
3406 *self
3407 .public_grants
3408 .write()
3409 .unwrap_or_else(|e| e.into_inner()) = parsed;
3410 }
3411 } else if let Some(suffix) = k.strip_prefix("red.acl.grants.") {
3412 if let Some(uid) = decode_uid(suffix) {
3413 if let Some(mut parsed) = decode_grants_blob(&v) {
3414 for g in parsed.iter_mut() {
3417 g.principal = GrantPrincipal::User(uid.clone());
3418 }
3419 self.grants
3420 .write()
3421 .unwrap_or_else(|e| e.into_inner())
3422 .insert(uid, parsed);
3423 }
3424 }
3425 } else if let Some(suffix) = k.strip_prefix("red.acl.attrs.") {
3426 if let Some(uid) = decode_uid(suffix) {
3427 if let Some(parsed) = decode_attrs_blob(&v) {
3428 self.user_attributes
3429 .write()
3430 .unwrap_or_else(|e| e.into_inner())
3431 .insert(uid, parsed);
3432 }
3433 }
3434 }
3435 }
3436
3437 self.permission_cache
3438 .write()
3439 .unwrap_or_else(|e| e.into_inner())
3440 .clear();
3441 }
3442
3443 fn persist_acl_to_kv(&self) {
3445 let public = self
3446 .public_grants
3447 .read()
3448 .ok()
3449 .map(|p| encode_grants_blob(&p))
3450 .unwrap_or_default();
3451 self.vault_kv_set("red.acl.public_grants".to_string(), public);
3452
3453 let snapshot: Vec<(UserId, Vec<Grant>)> = self
3454 .grants
3455 .read()
3456 .ok()
3457 .map(|g| g.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3458 .unwrap_or_default();
3459 for (uid, list) in snapshot {
3460 let key = format!("red.acl.grants.{}", encode_uid(&uid));
3461 let val = encode_grants_blob(&list);
3462 self.vault_kv_set(key, val);
3463 }
3464
3465 let attrs_snapshot: Vec<(UserId, UserAttributes)> = self
3466 .user_attributes
3467 .read()
3468 .ok()
3469 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3470 .unwrap_or_default();
3471 for (uid, attrs) in attrs_snapshot {
3472 let key = format!("red.acl.attrs.{}", encode_uid(&uid));
3473 let val = encode_attrs_blob(&attrs);
3474 self.vault_kv_set(key, val);
3475 }
3476 }
3477
3478 fn invalidate_permission_cache(&self, uid: Option<&UserId>) {
3479 if let Ok(mut cache) = self.permission_cache.write() {
3480 match uid {
3481 Some(u) => {
3482 cache.remove(u);
3483 }
3484 None => cache.clear(),
3485 }
3486 }
3487 }
3488
3489 pub fn put_policy_with_control_events(
3498 &self,
3499 p: Policy,
3500 control: &PolicyMutationControl<'_>,
3501 ) -> Result<(), AuthError> {
3502 let policy_id = p.id.clone();
3503 let kind = if self.get_policy(&policy_id).is_some() {
3504 EventKind::PolicyUpdate
3505 } else {
3506 EventKind::PolicyCreate
3507 };
3508
3509 if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
3510 let err = AuthError::Forbidden(format!("policy id `{}` is reserved", p.id));
3511 self.emit_policy_error(
3512 control,
3513 kind,
3514 "policy:put",
3515 &policy_id,
3516 Some(&p),
3517 None,
3518 &err,
3519 );
3520 return Err(err);
3521 }
3522
3523 if let Some(ManagedPolicyDecision::Deny {
3524 entry_id,
3525 matched_action,
3526 matched_resource,
3527 reason,
3528 ..
3529 }) = self.managed_policy_decision(&policy_id, PolicyOp::Put, control)
3530 {
3531 self.emit_policy_denied(
3532 control,
3533 kind,
3534 "policy:put",
3535 &policy_id,
3536 Some(&p),
3537 None,
3538 &reason.to_string(),
3539 Some(entry_id),
3540 Some((matched_action, matched_resource)),
3541 );
3542 return Err(Self::managed_policy_error(&policy_id, &reason.to_string()));
3543 }
3544
3545 let previous = self.get_policy(&policy_id);
3546 let was_enabled = self.iam_authorization_enabled();
3547 match self.put_policy_internal(p.clone()) {
3548 Ok(()) => match self.emit_policy_allowed(
3549 control,
3550 kind,
3551 "policy:put",
3552 &policy_id,
3553 Some(&p),
3554 None,
3555 None,
3556 ) {
3557 Ok(()) => Ok(()),
3558 Err(err) => {
3559 self.restore_policy_put(&policy_id, previous, was_enabled);
3560 Err(err)
3561 }
3562 },
3563 Err(err) => {
3564 self.emit_policy_error(
3565 control,
3566 kind,
3567 "policy:put",
3568 &policy_id,
3569 Some(&p),
3570 None,
3571 &err,
3572 );
3573 Err(err)
3574 }
3575 }
3576 }
3577
3578 pub fn delete_policy_with_control_events(
3579 &self,
3580 id: &str,
3581 control: &PolicyMutationControl<'_>,
3582 ) -> Result<(), AuthError> {
3583 let existing = self.get_policy(id);
3584 if let Some(ManagedPolicyDecision::Deny {
3585 entry_id,
3586 matched_action,
3587 matched_resource,
3588 reason,
3589 ..
3590 }) = self.managed_policy_decision(id, PolicyOp::Drop, control)
3591 {
3592 self.emit_policy_denied(
3593 control,
3594 EventKind::PolicyDelete,
3595 "policy:drop",
3596 id,
3597 existing.as_deref(),
3598 None,
3599 &reason.to_string(),
3600 Some(entry_id),
3601 Some((matched_action, matched_resource)),
3602 );
3603 return Err(Self::managed_policy_error(id, &reason.to_string()));
3604 }
3605
3606 let user_attachments = self
3607 .user_attachments
3608 .read()
3609 .map(|m| m.clone())
3610 .unwrap_or_default();
3611 let group_attachments = self
3612 .group_attachments
3613 .read()
3614 .map(|m| m.clone())
3615 .unwrap_or_default();
3616 let was_enabled = self.iam_authorization_enabled();
3617 match self.delete_policy(id) {
3618 Ok(()) => match self.emit_policy_allowed(
3619 control,
3620 EventKind::PolicyDelete,
3621 "policy:drop",
3622 id,
3623 existing.as_deref(),
3624 None,
3625 None,
3626 ) {
3627 Ok(()) => Ok(()),
3628 Err(err) => {
3629 if let Some(policy) = existing {
3630 self.restore_policy_delete(
3631 id,
3632 policy,
3633 user_attachments,
3634 group_attachments,
3635 was_enabled,
3636 );
3637 }
3638 Err(err)
3639 }
3640 },
3641 Err(err) => {
3642 self.emit_policy_error(
3643 control,
3644 EventKind::PolicyDelete,
3645 "policy:drop",
3646 id,
3647 existing.as_deref(),
3648 None,
3649 &err,
3650 );
3651 Err(err)
3652 }
3653 }
3654 }
3655
3656 pub fn attach_policy_with_control_events(
3657 &self,
3658 principal: PrincipalRef,
3659 policy_id: &str,
3660 control: &PolicyMutationControl<'_>,
3661 ) -> Result<(), AuthError> {
3662 let existing = self.get_policy(policy_id);
3663 if let Some(ManagedPolicyDecision::Deny {
3664 entry_id,
3665 matched_action,
3666 matched_resource,
3667 reason,
3668 ..
3669 }) = self.managed_policy_decision(policy_id, PolicyOp::Attach, control)
3670 {
3671 self.emit_policy_denied(
3672 control,
3673 EventKind::PolicyAttach,
3674 "policy:attach",
3675 policy_id,
3676 existing.as_deref(),
3677 Some(&principal),
3678 &reason.to_string(),
3679 Some(entry_id),
3680 Some((matched_action, matched_resource)),
3681 );
3682 return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3683 }
3684
3685 if let Some(err_msg) = self.check_self_lock_invariant_for_attach(policy_id) {
3686 self.emit_policy_denied(
3687 control,
3688 EventKind::PolicyAttach,
3689 "policy:attach",
3690 policy_id,
3691 existing.as_deref(),
3692 Some(&principal),
3693 &err_msg,
3694 None,
3695 None,
3696 );
3697 return Err(AuthError::Forbidden(err_msg));
3698 }
3699
3700 let user_attachments = self
3701 .user_attachments
3702 .read()
3703 .map(|m| m.clone())
3704 .unwrap_or_default();
3705 let group_attachments = self
3706 .group_attachments
3707 .read()
3708 .map(|m| m.clone())
3709 .unwrap_or_default();
3710 match self.attach_policy(principal.clone(), policy_id) {
3711 Ok(()) => match self.emit_policy_allowed(
3712 control,
3713 EventKind::PolicyAttach,
3714 "policy:attach",
3715 policy_id,
3716 existing.as_deref(),
3717 Some(&principal),
3718 None,
3719 ) {
3720 Ok(()) => Ok(()),
3721 Err(err) => {
3722 self.restore_policy_attachments(user_attachments, group_attachments);
3723 Err(err)
3724 }
3725 },
3726 Err(err) => {
3727 self.emit_policy_error(
3728 control,
3729 EventKind::PolicyAttach,
3730 "policy:attach",
3731 policy_id,
3732 existing.as_deref(),
3733 Some(&principal),
3734 &err,
3735 );
3736 Err(err)
3737 }
3738 }
3739 }
3740
3741 pub fn detach_policy_with_control_events(
3742 &self,
3743 principal: PrincipalRef,
3744 policy_id: &str,
3745 control: &PolicyMutationControl<'_>,
3746 ) -> Result<(), AuthError> {
3747 let existing = self.get_policy(policy_id);
3748 if let Some(ManagedPolicyDecision::Deny {
3749 entry_id,
3750 matched_action,
3751 matched_resource,
3752 reason,
3753 ..
3754 }) = self.managed_policy_decision(policy_id, PolicyOp::Detach, control)
3755 {
3756 self.emit_policy_denied(
3757 control,
3758 EventKind::PolicyDetach,
3759 "policy:detach",
3760 policy_id,
3761 existing.as_deref(),
3762 Some(&principal),
3763 &reason.to_string(),
3764 Some(entry_id),
3765 Some((matched_action, matched_resource)),
3766 );
3767 return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3768 }
3769 let Some(existing_policy) = existing else {
3770 let err = AuthError::Forbidden(format!("policy `{policy_id}` not found"));
3771 self.emit_policy_error(
3772 control,
3773 EventKind::PolicyDetach,
3774 "policy:detach",
3775 policy_id,
3776 None,
3777 Some(&principal),
3778 &err,
3779 );
3780 return Err(err);
3781 };
3782
3783 let user_attachments = self
3784 .user_attachments
3785 .read()
3786 .map(|m| m.clone())
3787 .unwrap_or_default();
3788 let group_attachments = self
3789 .group_attachments
3790 .read()
3791 .map(|m| m.clone())
3792 .unwrap_or_default();
3793 match self.detach_policy(principal.clone(), policy_id) {
3794 Ok(()) => match self.emit_policy_allowed(
3795 control,
3796 EventKind::PolicyDetach,
3797 "policy:detach",
3798 policy_id,
3799 Some(existing_policy.as_ref()),
3800 Some(&principal),
3801 None,
3802 ) {
3803 Ok(()) => Ok(()),
3804 Err(err) => {
3805 self.restore_policy_attachments(user_attachments, group_attachments);
3806 Err(err)
3807 }
3808 },
3809 Err(err) => {
3810 self.emit_policy_error(
3811 control,
3812 EventKind::PolicyDetach,
3813 "policy:detach",
3814 policy_id,
3815 Some(existing_policy.as_ref()),
3816 Some(&principal),
3817 &err,
3818 );
3819 Err(err)
3820 }
3821 }
3822 }
3823
3824 fn managed_policy_decision(
3825 &self,
3826 policy_id: &str,
3827 op: PolicyOp,
3828 control: &PolicyMutationControl<'_>,
3829 ) -> Option<ManagedPolicyDecision> {
3830 control.registry.map(|registry| {
3831 ManagedPolicyGate::new(registry).check_mutation(
3832 self,
3833 control.actor,
3834 control.eval_ctx,
3835 policy_id,
3836 op,
3837 )
3838 })
3839 }
3840
3841 fn check_self_lock_invariant_for_attach(&self, _policy_id: &str) -> Option<String> {
3848 use crate::auth::self_lock_guard::{
3849 check_self_lock_invariant, format_block_error, InvariantOutcome,
3850 };
3851 let policies: Vec<Arc<Policy>> = match self.policies.read() {
3852 Ok(map) => map.values().cloned().collect(),
3853 Err(_) => return None,
3854 };
3855 let outcome = check_self_lock_invariant(&policies);
3856 match &outcome {
3857 InvariantOutcome::Ok => None,
3858 InvariantOutcome::Blocked { .. } => format_block_error(&outcome),
3859 }
3860 }
3861
3862 fn managed_policy_error(policy_id: &str, reason: &str) -> AuthError {
3863 AuthError::Forbidden(format!(
3864 "managed policy mutation blocked for `{policy_id}`: {reason}"
3865 ))
3866 }
3867
3868 fn emit_policy_allowed(
3869 &self,
3870 control: &PolicyMutationControl<'_>,
3871 kind: EventKind,
3872 action: &'static str,
3873 policy_id: &str,
3874 policy: Option<&Policy>,
3875 principal: Option<&PrincipalRef>,
3876 matched_policy_id: Option<String>,
3877 ) -> Result<(), AuthError> {
3878 let event = ControlEvent {
3879 kind,
3880 outcome: Outcome::Allowed,
3881 action: std::borrow::Cow::Borrowed(action),
3882 resource: Some(format!("policy:{policy_id}")),
3883 reason: None,
3884 matched_policy_id,
3885 fields: policy_control_fields(policy_id, policy, principal),
3886 };
3887 match control.ledger.emit(control.ctx, event) {
3888 Ok(_) => Ok(()),
3889 Err(err) if control.config.require_persistence() => {
3890 Err(AuthError::Internal(err.to_string()))
3891 }
3892 Err(_) => Ok(()),
3893 }
3894 }
3895
3896 #[allow(clippy::too_many_arguments)]
3897 fn emit_policy_denied(
3898 &self,
3899 control: &PolicyMutationControl<'_>,
3900 kind: EventKind,
3901 action: &'static str,
3902 policy_id: &str,
3903 policy: Option<&Policy>,
3904 principal: Option<&PrincipalRef>,
3905 reason: &str,
3906 matched_policy_id: Option<String>,
3907 matched: Option<(String, String)>,
3908 ) {
3909 let mut fields = policy_control_fields(policy_id, policy, principal);
3910 if let Some((matched_action, matched_resource)) = matched {
3911 fields.insert(
3912 "matched_action".to_string(),
3913 Sensitivity::raw(matched_action),
3914 );
3915 fields.insert(
3916 "matched_resource".to_string(),
3917 Sensitivity::raw(matched_resource),
3918 );
3919 }
3920 let event = ControlEvent {
3921 kind,
3922 outcome: Outcome::Denied,
3923 action: std::borrow::Cow::Borrowed(action),
3924 resource: Some(format!("policy:{policy_id}")),
3925 reason: Some(reason.to_string()),
3926 matched_policy_id,
3927 fields,
3928 };
3929 let _ = control.ledger.emit(control.ctx, event);
3930 }
3931
3932 fn emit_policy_error(
3933 &self,
3934 control: &PolicyMutationControl<'_>,
3935 kind: EventKind,
3936 action: &'static str,
3937 policy_id: &str,
3938 policy: Option<&Policy>,
3939 principal: Option<&PrincipalRef>,
3940 err: &AuthError,
3941 ) {
3942 let event = ControlEvent {
3943 kind,
3944 outcome: Outcome::Error,
3945 action: std::borrow::Cow::Borrowed(action),
3946 resource: Some(format!("policy:{policy_id}")),
3947 reason: Some(err.to_string()),
3948 matched_policy_id: None,
3949 fields: policy_control_fields(policy_id, policy, principal),
3950 };
3951 let _ = control.ledger.emit(control.ctx, event);
3952 }
3953
3954 fn restore_policy_put(
3955 &self,
3956 policy_id: &str,
3957 previous: Option<Arc<Policy>>,
3958 was_enabled: bool,
3959 ) {
3960 let mut policies = self.policies.write().unwrap_or_else(|e| e.into_inner());
3961 match previous {
3962 Some(policy) => {
3963 policies.insert(policy_id.to_string(), policy);
3964 }
3965 None => {
3966 policies.remove(policy_id);
3967 }
3968 }
3969 drop(policies);
3970 self.iam_authorization_enabled
3971 .store(was_enabled, Ordering::Release);
3972 self.iam_effective_cache
3973 .write()
3974 .unwrap_or_else(|e| e.into_inner())
3975 .clear();
3976 self.invalidate_visible_collections_cache();
3977 self.persist_iam_to_kv();
3978 }
3979
3980 fn restore_policy_delete(
3981 &self,
3982 policy_id: &str,
3983 policy: Arc<Policy>,
3984 user_attachments: HashMap<UserId, Vec<String>>,
3985 group_attachments: HashMap<String, Vec<String>>,
3986 was_enabled: bool,
3987 ) {
3988 self.policies
3989 .write()
3990 .unwrap_or_else(|e| e.into_inner())
3991 .insert(policy_id.to_string(), policy);
3992 self.restore_policy_attachments(user_attachments, group_attachments);
3993 self.iam_authorization_enabled
3994 .store(was_enabled, Ordering::Release);
3995 self.persist_iam_to_kv();
3996 }
3997
3998 fn restore_policy_attachments(
3999 &self,
4000 user_attachments: HashMap<UserId, Vec<String>>,
4001 group_attachments: HashMap<String, Vec<String>>,
4002 ) {
4003 *self
4004 .user_attachments
4005 .write()
4006 .unwrap_or_else(|e| e.into_inner()) = user_attachments;
4007 *self
4008 .group_attachments
4009 .write()
4010 .unwrap_or_else(|e| e.into_inner()) = group_attachments;
4011 self.iam_effective_cache
4012 .write()
4013 .unwrap_or_else(|e| e.into_inner())
4014 .clear();
4015 self.invalidate_visible_collections_cache();
4016 self.persist_iam_to_kv();
4017 }
4018
4019 pub fn put_policy(&self, p: Policy) -> Result<(), AuthError> {
4024 if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
4025 return Err(AuthError::Forbidden(format!(
4026 "policy id `{}` is reserved",
4027 p.id
4028 )));
4029 }
4030 self.put_policy_internal(p)
4031 }
4032
4033 pub fn put_policy_internal(&self, p: Policy) -> Result<(), AuthError> {
4038 p.validate()
4039 .map_err(|e| AuthError::Forbidden(format!("invalid policy `{}`: {e}", p.id)))?;
4040 let id = p.id.clone();
4041 self.policies
4042 .write()
4043 .unwrap_or_else(|e| e.into_inner())
4044 .insert(id, Arc::new(p));
4045 self.iam_authorization_enabled
4046 .store(true, Ordering::Release);
4047 self.iam_effective_cache
4048 .write()
4049 .unwrap_or_else(|e| e.into_inner())
4050 .clear();
4051 self.invalidate_visible_collections_cache();
4055 self.persist_iam_to_kv();
4056 Ok(())
4057 }
4058
4059 pub fn iam_authorization_enabled(&self) -> bool {
4064 self.iam_authorization_enabled.load(Ordering::Acquire)
4065 }
4066
4067 pub fn delete_policy(&self, id: &str) -> Result<(), AuthError> {
4069 let removed = self
4070 .policies
4071 .write()
4072 .unwrap_or_else(|e| e.into_inner())
4073 .remove(id)
4074 .is_some();
4075 if !removed {
4076 return Err(AuthError::Forbidden(format!("policy `{id}` not found")));
4077 }
4078 if let Ok(mut ua) = self.user_attachments.write() {
4080 for ids in ua.values_mut() {
4081 ids.retain(|p| p != id);
4082 }
4083 ua.retain(|_, v| !v.is_empty());
4084 }
4085 if let Ok(mut ga) = self.group_attachments.write() {
4086 for ids in ga.values_mut() {
4087 ids.retain(|p| p != id);
4088 }
4089 ga.retain(|_, v| !v.is_empty());
4090 }
4091 self.iam_effective_cache
4092 .write()
4093 .unwrap_or_else(|e| e.into_inner())
4094 .clear();
4095 self.invalidate_visible_collections_cache();
4098 self.persist_iam_to_kv();
4099 Ok(())
4100 }
4101
4102 pub fn list_policies(&self) -> Vec<Arc<Policy>> {
4104 let map = match self.policies.read() {
4105 Ok(g) => g,
4106 Err(_) => return Vec::new(),
4107 };
4108 let mut out: Vec<Arc<Policy>> = map.values().cloned().collect();
4109 out.sort_by(|a, b| a.id.cmp(&b.id));
4110 out
4111 }
4112
4113 pub fn get_policy(&self, id: &str) -> Option<Arc<Policy>> {
4115 self.policies.read().ok().and_then(|m| m.get(id).cloned())
4116 }
4117
4118 pub fn group_policies(&self, group: &str) -> Vec<Arc<Policy>> {
4120 let policies = self.policies.read();
4121 let attachments = self.group_attachments.read();
4122 let mut out = Vec::new();
4123 if let (Ok(p_map), Ok(ga_map)) = (policies, attachments) {
4124 if let Some(ids) = ga_map.get(group) {
4125 for id in ids {
4126 if let Some(p) = p_map.get(id) {
4127 out.push(p.clone());
4128 }
4129 }
4130 }
4131 }
4132 out.sort_by(|a, b| a.id.cmp(&b.id));
4133 out
4134 }
4135
4136 pub fn delete_synthetic_grant_policies(
4140 &self,
4141 principal: &GrantPrincipal,
4142 resource: &Resource,
4143 actions: &[Action],
4144 ) -> usize {
4145 let attached = match principal {
4146 GrantPrincipal::User(uid) => self
4147 .user_attachments
4148 .read()
4149 .ok()
4150 .and_then(|m| m.get(uid).cloned())
4151 .unwrap_or_default(),
4152 GrantPrincipal::Group(group) => self
4153 .group_attachments
4154 .read()
4155 .ok()
4156 .and_then(|m| m.get(group).cloned())
4157 .unwrap_or_default(),
4158 GrantPrincipal::Public => self
4159 .group_attachments
4160 .read()
4161 .ok()
4162 .and_then(|m| m.get(PUBLIC_IAM_GROUP).cloned())
4163 .unwrap_or_default(),
4164 };
4165 if attached.is_empty() {
4166 return 0;
4167 }
4168
4169 let mut delete_ids = Vec::new();
4170 if let Ok(policies) = self.policies.read() {
4171 for id in attached {
4172 let Some(policy) = policies.get(&id) else {
4173 continue;
4174 };
4175 if !policy.id.starts_with("_grant_") {
4176 continue;
4177 }
4178 if synthetic_grant_matches(policy, resource, actions) {
4179 delete_ids.push(policy.id.clone());
4180 }
4181 }
4182 }
4183
4184 let mut deleted = 0usize;
4185 for id in delete_ids {
4186 if self.delete_policy(&id).is_ok() {
4187 deleted += 1;
4188 }
4189 }
4190 deleted
4191 }
4192
4193 pub fn apply_policy_break_glass(&self, boot_ts_ms: u128) -> Result<(), AuthError> {
4201 use crate::auth::self_lock_guard::{
4202 break_glass_audit_fields, unlock_policy, PLATFORM_OWNER_UNLOCK_POLICY_ID,
4203 PLATFORM_OWNER_USERNAME,
4204 };
4205
4206 let policy = unlock_policy();
4210 self.put_policy_internal(policy)?;
4211
4212 let owner_id = UserId::platform(PLATFORM_OWNER_USERNAME);
4217 {
4218 let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
4219 users.entry(owner_id.clone()).or_insert_with(|| User {
4220 username: PLATFORM_OWNER_USERNAME.to_string(),
4221 tenant_id: None,
4222 password_hash: String::new(),
4227 scram_verifier: None,
4228 role: Role::Admin,
4229 api_keys: Vec::new(),
4230 created_at: boot_ts_ms,
4231 updated_at: boot_ts_ms,
4232 enabled: false,
4233 });
4234 }
4235
4236 self.attach_policy(
4240 PrincipalRef::User(owner_id.clone()),
4241 PLATFORM_OWNER_UNLOCK_POLICY_ID,
4242 )?;
4243
4244 let fields_raw = break_glass_audit_fields(boot_ts_ms);
4249 let mut fields: HashMap<String, Sensitivity> = HashMap::new();
4250 for (k, v) in fields_raw {
4251 fields.insert(k, Sensitivity::raw(v));
4252 }
4253 if let Some(configured) = self.configured_control_events() {
4254 let ctx = ControlEventCtx {
4255 actor: crate::runtime::control_events::ActorRef::System("break_glass"),
4256 scope: None,
4257 request_id: None,
4258 trace_id: None,
4259 };
4260 let event = ControlEvent {
4261 kind: EventKind::PolicyBreakGlass,
4262 outcome: Outcome::Allowed,
4263 action: std::borrow::Cow::Borrowed("policy:break_glass"),
4264 resource: Some(format!("policy:{PLATFORM_OWNER_UNLOCK_POLICY_ID}")),
4265 reason: Some("REDDB_POLICY_BREAK_GLASS=1 at boot".into()),
4266 matched_policy_id: None,
4267 fields,
4268 };
4269 let _ = configured.ledger.emit(&ctx, event);
4270 }
4271 tracing::warn!(
4272 policy_id = PLATFORM_OWNER_UNLOCK_POLICY_ID,
4273 principal = PLATFORM_OWNER_USERNAME,
4274 boot_ts_ms = %boot_ts_ms,
4275 "REDDB_POLICY_BREAK_GLASS=1 — installed/refreshed platform-owner unlock policy and rebound to synthetic principal"
4276 );
4277 Ok(())
4278 }
4279
4280 pub fn attach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
4283 if !self
4284 .policies
4285 .read()
4286 .map(|m| m.contains_key(policy_id))
4287 .unwrap_or(false)
4288 {
4289 return Err(AuthError::Forbidden(format!(
4290 "policy `{policy_id}` not found"
4291 )));
4292 }
4293 match &principal {
4294 PrincipalRef::User(uid) => {
4295 let mut ua = self
4296 .user_attachments
4297 .write()
4298 .unwrap_or_else(|e| e.into_inner());
4299 let list = ua.entry(uid.clone()).or_default();
4300 if !list.iter().any(|p| p == policy_id) {
4301 list.push(policy_id.to_string());
4302 }
4303 drop(ua);
4304 self.invalidate_iam_cache(Some(uid));
4305 }
4306 PrincipalRef::Group(g) => {
4307 let mut ga = self
4308 .group_attachments
4309 .write()
4310 .unwrap_or_else(|e| e.into_inner());
4311 let list = ga.entry(g.clone()).or_default();
4312 if !list.iter().any(|p| p == policy_id) {
4313 list.push(policy_id.to_string());
4314 }
4315 drop(ga);
4316 self.invalidate_iam_cache(None);
4317 }
4318 }
4319 self.persist_iam_to_kv();
4320 Ok(())
4321 }
4322
4323 pub fn detach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
4325 match &principal {
4326 PrincipalRef::User(uid) => {
4327 if let Ok(mut ua) = self.user_attachments.write() {
4328 if let Some(list) = ua.get_mut(uid) {
4329 list.retain(|p| p != policy_id);
4330 if list.is_empty() {
4331 ua.remove(uid);
4332 }
4333 }
4334 }
4335 self.invalidate_iam_cache(Some(uid));
4336 }
4337 PrincipalRef::Group(g) => {
4338 if let Ok(mut ga) = self.group_attachments.write() {
4339 if let Some(list) = ga.get_mut(g) {
4340 list.retain(|p| p != policy_id);
4341 if list.is_empty() {
4342 ga.remove(g);
4343 }
4344 }
4345 }
4346 self.invalidate_iam_cache(None);
4347 }
4348 }
4349 self.persist_iam_to_kv();
4350 Ok(())
4351 }
4352
4353 pub fn effective_policies(&self, user: &UserId) -> Vec<Arc<Policy>> {
4357 if let Ok(cache) = self.iam_effective_cache.read() {
4358 if let Some(hit) = cache.get(user) {
4359 return hit.clone();
4360 }
4361 }
4362 let policies = self.policies.read();
4363 let user_attachments = self.user_attachments.read();
4364 let group_attachments = self.group_attachments.read();
4365 let mut groups = self
4366 .user_attributes
4367 .read()
4368 .ok()
4369 .and_then(|m| m.get(user).map(|attrs| attrs.groups.clone()))
4370 .unwrap_or_default();
4371 groups.insert(0, PUBLIC_IAM_GROUP.to_string());
4372 let mut out: Vec<Arc<Policy>> = Vec::new();
4373 if let (Ok(p_map), Ok(ua_map), Ok(ga_map)) = (policies, user_attachments, group_attachments)
4374 {
4375 for group in groups {
4376 if let Some(ids) = ga_map.get(&group) {
4377 for id in ids {
4378 if let Some(p) = p_map.get(id) {
4379 out.push(p.clone());
4380 }
4381 }
4382 }
4383 }
4384 if let Some(ids) = ua_map.get(user) {
4385 for id in ids {
4386 if let Some(p) = p_map.get(id) {
4387 out.push(p.clone());
4388 }
4389 }
4390 }
4391 }
4392 if let Ok(mut cache) = self.iam_effective_cache.write() {
4393 cache.insert(user.clone(), out.clone());
4394 }
4395 out
4396 }
4397
4398 pub fn simulate(
4401 &self,
4402 principal: &UserId,
4403 action: &str,
4404 resource: &ResourceRef,
4405 ctx_extras: SimCtx,
4406 ) -> SimulationOutcome {
4407 let user_role = self
4408 .users
4409 .read()
4410 .ok()
4411 .and_then(|u| u.get(principal).map(|u| u.role));
4412 let principal_is_admin_role = user_role == Some(Role::Admin);
4413 let now = ctx_extras.now_ms.unwrap_or_else(now_ms);
4414 let ctx = EvalContext {
4415 principal_tenant: principal.tenant.clone(),
4416 current_tenant: ctx_extras.current_tenant,
4417 peer_ip: ctx_extras.peer_ip,
4418 mfa_present: ctx_extras.mfa_present,
4419 now_ms: now,
4420 principal_is_admin_role,
4421 principal_is_platform_scoped: principal.tenant.is_none(),
4422 };
4423 let pols = self.effective_policies(principal);
4424 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4425 iam_policies::simulate(&refs, action, resource, &ctx)
4426 }
4427
4428 pub fn check_policy_authz(
4439 &self,
4440 principal: &UserId,
4441 action: &str,
4442 resource: &ResourceRef,
4443 ctx: &EvalContext,
4444 ) -> bool {
4445 let pols = self.effective_policies(principal);
4446 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4447 let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4448 matches!(
4449 decision,
4450 iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass
4451 )
4452 }
4453
4454 pub fn check_policy_authz_with_role(
4470 &self,
4471 principal: &UserId,
4472 action: &str,
4473 resource: &ResourceRef,
4474 ctx: &EvalContext,
4475 role: Role,
4476 ) -> bool {
4477 let pols = self.effective_policies(principal);
4478 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4479 let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4480 match decision {
4481 iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass => true,
4482 iam_policies::Decision::Deny { .. } => false,
4483 iam_policies::Decision::DefaultDeny => match self.enforcement_mode() {
4484 PolicyEnforcementMode::LegacyRbac => legacy_rbac_decision(role, action),
4485 PolicyEnforcementMode::PolicyOnly => false,
4486 },
4487 }
4488 }
4489
4490 pub fn enforcement_mode(&self) -> PolicyEnforcementMode {
4497 *self
4498 .enforcement_mode
4499 .read()
4500 .unwrap_or_else(|e| e.into_inner())
4501 }
4502
4503 pub fn set_enforcement_mode(&self, mode: PolicyEnforcementMode) -> PolicyEnforcementMode {
4509 let prev = {
4510 let mut guard = self
4511 .enforcement_mode
4512 .write()
4513 .unwrap_or_else(|e| e.into_inner());
4514 let prev = *guard;
4515 *guard = mode;
4516 prev
4517 };
4518 self.vault_kv_set(
4519 "red.config.policy.enforcement_mode".to_string(),
4520 mode.as_str().to_string(),
4521 );
4522 prev
4523 }
4524
4525 pub fn take_legacy_rbac_warn_once(&self) -> bool {
4531 if self.enforcement_mode() != PolicyEnforcementMode::LegacyRbac {
4532 return false;
4533 }
4534 !self
4535 .legacy_rbac_boot_warn_emitted
4536 .swap(true, Ordering::AcqRel)
4537 }
4538
4539 pub fn check_column_projection_authz(
4543 &self,
4544 principal: &UserId,
4545 request: &ColumnAccessRequest,
4546 ctx: &EvalContext,
4547 ) -> ColumnPolicyOutcome {
4548 let pols = self.effective_policies(principal);
4549 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4550 ColumnPolicyGate::new(&refs).evaluate(request, ctx)
4551 }
4552
4553 fn invalidate_iam_cache(&self, uid: Option<&UserId>) {
4554 if let Ok(mut cache) = self.iam_effective_cache.write() {
4555 match uid {
4556 Some(u) => {
4557 cache.remove(u);
4558 }
4559 None => cache.clear(),
4560 }
4561 }
4562 }
4563
4564 pub fn invalidate_all_iam_cache(&self) {
4568 self.invalidate_iam_cache(None);
4569 }
4570
4571 pub fn rehydrate_iam(&self) {
4579 let mut enabled = self
4580 .vault_kv_get("red.iam.enabled")
4581 .map(|v| v == "true")
4582 .unwrap_or(false);
4583 if let Some(blob) = self.vault_kv_get("red.iam.policies") {
4585 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4586 if let Some(obj) = val.as_object() {
4587 let mut map = HashMap::new();
4588 for (id, body) in obj.iter() {
4589 let s = body.to_string_compact();
4590 if let Ok(p) = Policy::from_json_str(&s) {
4591 map.insert(id.clone(), Arc::new(p));
4592 }
4593 }
4594 if !map.is_empty() {
4595 enabled = true;
4596 }
4597 *self.policies.write().unwrap_or_else(|e| e.into_inner()) = map;
4598 }
4599 }
4600 }
4601 if let Some(blob) = self.vault_kv_get("red.iam.attachments.users") {
4603 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4604 if let Some(obj) = val.as_object() {
4605 let mut map: HashMap<UserId, Vec<String>> = HashMap::new();
4606 for (encoded_uid, ids_v) in obj.iter() {
4607 let Some(uid) = decode_uid(encoded_uid) else {
4608 continue;
4609 };
4610 if let Some(arr) = ids_v.as_array() {
4611 let ids: Vec<String> = arr
4612 .iter()
4613 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4614 .collect();
4615 map.insert(uid, ids);
4616 }
4617 }
4618 *self
4619 .user_attachments
4620 .write()
4621 .unwrap_or_else(|e| e.into_inner()) = map;
4622 }
4623 }
4624 }
4625 if let Some(blob) = self.vault_kv_get("red.iam.attachments.groups") {
4627 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4628 if let Some(obj) = val.as_object() {
4629 let mut map: HashMap<String, Vec<String>> = HashMap::new();
4630 for (g, ids_v) in obj.iter() {
4631 if let Some(arr) = ids_v.as_array() {
4632 let ids: Vec<String> = arr
4633 .iter()
4634 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4635 .collect();
4636 map.insert(g.clone(), ids);
4637 }
4638 }
4639 *self
4640 .group_attachments
4641 .write()
4642 .unwrap_or_else(|e| e.into_inner()) = map;
4643 }
4644 }
4645 }
4646 self.iam_authorization_enabled
4647 .store(enabled, Ordering::Release);
4648 self.invalidate_iam_cache(None);
4649
4650 if let Some(stored) = self.vault_kv_get("red.config.policy.enforcement_mode") {
4657 if let Some(mode) = PolicyEnforcementMode::parse(&stored) {
4658 *self
4659 .enforcement_mode
4660 .write()
4661 .unwrap_or_else(|e| e.into_inner()) = mode;
4662 }
4663 }
4664
4665 if self.take_legacy_rbac_warn_once() {
4670 tracing::warn!(
4671 target: "reddb::auth::enforcement_mode",
4672 mode = "legacy_rbac",
4673 hard_version = crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4674 "policy enforcement_mode=legacy_rbac (transitional). Run \
4675 `MIGRATE POLICY MODE TO 'policy_only'` (next slice S5B) \
4676 before {} to flip this install over to the strict posture.",
4677 crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4678 );
4679 }
4680 }
4681
4682 fn persist_iam_to_kv(&self) {
4685 let enabled = if self.iam_authorization_enabled() {
4686 "true"
4687 } else {
4688 "false"
4689 };
4690 self.vault_kv_set("red.iam.enabled".to_string(), enabled.to_string());
4691
4692 let policies_obj = {
4694 let map = self.policies.read().unwrap_or_else(|e| e.into_inner());
4695 let mut obj = crate::serde_json::Map::new();
4696 for (id, p) in map.iter() {
4697 let s = p.to_json_string();
4698 if let Ok(v) = crate::serde_json::from_str::<crate::serde_json::Value>(&s) {
4699 obj.insert(id.clone(), v);
4700 }
4701 }
4702 crate::serde_json::Value::Object(obj).to_string_compact()
4703 };
4704 self.vault_kv_set("red.iam.policies".to_string(), policies_obj);
4705
4706 let users_obj = {
4708 let map = self
4709 .user_attachments
4710 .read()
4711 .unwrap_or_else(|e| e.into_inner());
4712 let mut obj = crate::serde_json::Map::new();
4713 for (uid, ids) in map.iter() {
4714 let arr = crate::serde_json::Value::Array(
4715 ids.iter()
4716 .map(|s| crate::serde_json::Value::String(s.clone()))
4717 .collect(),
4718 );
4719 obj.insert(encode_uid(uid), arr);
4720 }
4721 crate::serde_json::Value::Object(obj).to_string_compact()
4722 };
4723 self.vault_kv_set("red.iam.attachments.users".to_string(), users_obj);
4724
4725 let groups_obj = {
4727 let map = self
4728 .group_attachments
4729 .read()
4730 .unwrap_or_else(|e| e.into_inner());
4731 let mut obj = crate::serde_json::Map::new();
4732 for (g, ids) in map.iter() {
4733 let arr = crate::serde_json::Value::Array(
4734 ids.iter()
4735 .map(|s| crate::serde_json::Value::String(s.clone()))
4736 .collect(),
4737 );
4738 obj.insert(g.clone(), arr);
4739 }
4740 crate::serde_json::Value::Object(obj).to_string_compact()
4741 };
4742 self.vault_kv_set("red.iam.attachments.groups".to_string(), groups_obj);
4743 }
4744}
4745
4746fn synthetic_grant_matches(policy: &Policy, resource: &Resource, actions: &[Action]) -> bool {
4747 policy.statements.iter().any(|st| {
4748 st.effect == crate::auth::policies::Effect::Allow
4749 && st.condition.is_none()
4750 && grant_actions_overlap(&st.actions, actions)
4751 && grant_resource_matches(&st.resources, resource)
4752 })
4753}
4754
4755fn grant_actions_overlap(
4756 patterns: &[crate::auth::policies::ActionPattern],
4757 actions: &[Action],
4758) -> bool {
4759 if actions.contains(&Action::All) {
4760 return true;
4761 }
4762 patterns.iter().any(|pat| match pat {
4763 crate::auth::policies::ActionPattern::Wildcard => true,
4764 crate::auth::policies::ActionPattern::Exact(s) => {
4765 actions.iter().any(|a| s.eq_ignore_ascii_case(a.as_str()))
4766 }
4767 crate::auth::policies::ActionPattern::Prefix(_) => false,
4768 })
4769}
4770
4771fn grant_resource_matches(
4772 patterns: &[crate::auth::policies::ResourcePattern],
4773 resource: &Resource,
4774) -> bool {
4775 let expected = grant_resource_pattern(resource);
4776 patterns.iter().any(|pat| pat == &expected)
4777}
4778
4779fn grant_resource_pattern(resource: &Resource) -> crate::auth::policies::ResourcePattern {
4780 use crate::auth::policies::ResourcePattern;
4781
4782 match resource {
4783 Resource::Database => ResourcePattern::Glob("table:*".to_string()),
4784 Resource::Schema(s) => ResourcePattern::Glob(format!("table:{s}.*")),
4785 Resource::Table { schema, table } => ResourcePattern::Exact {
4786 kind: "table".to_string(),
4787 name: match schema {
4788 Some(s) => format!("{s}.{table}"),
4789 None => table.clone(),
4790 },
4791 },
4792 Resource::Function { schema, name } => ResourcePattern::Exact {
4793 kind: "function".to_string(),
4794 name: match schema {
4795 Some(s) => format!("{s}.{name}"),
4796 None => name.clone(),
4797 },
4798 },
4799 }
4800}
4801
4802fn encode_uid(uid: &UserId) -> String {
4818 match &uid.tenant {
4819 Some(t) => format!("{}/{}", t, uid.username),
4820 None => format!("*/{}", uid.username),
4821 }
4822}
4823
4824fn decode_uid(s: &str) -> Option<UserId> {
4825 let (tenant, username) = s.split_once('/')?;
4826 Some(if tenant == "*" {
4827 UserId::platform(username)
4828 } else {
4829 UserId::scoped(tenant, username)
4830 })
4831}
4832
4833fn encode_resource(r: &Resource) -> String {
4834 match r {
4835 Resource::Database => "db".into(),
4836 Resource::Schema(s) => format!("schema:{}", s),
4837 Resource::Table { schema, table } => {
4838 format!("table:{}:{}", schema.as_deref().unwrap_or("*"), table)
4839 }
4840 Resource::Function { schema, name } => {
4841 format!("func:{}:{}", schema.as_deref().unwrap_or("*"), name)
4842 }
4843 }
4844}
4845
4846fn decode_resource(s: &str) -> Option<Resource> {
4847 if s == "db" {
4848 return Some(Resource::Database);
4849 }
4850 if let Some(rest) = s.strip_prefix("schema:") {
4851 return Some(Resource::Schema(rest.to_string()));
4852 }
4853 if let Some(rest) = s.strip_prefix("table:") {
4854 let (schema, table) = rest.split_once(':')?;
4855 return Some(Resource::Table {
4856 schema: if schema == "*" {
4857 None
4858 } else {
4859 Some(schema.to_string())
4860 },
4861 table: table.to_string(),
4862 });
4863 }
4864 if let Some(rest) = s.strip_prefix("func:") {
4865 let (schema, name) = rest.split_once(':')?;
4866 return Some(Resource::Function {
4867 schema: if schema == "*" {
4868 None
4869 } else {
4870 Some(schema.to_string())
4871 },
4872 name: name.to_string(),
4873 });
4874 }
4875 None
4876}
4877
4878fn encode_grants_blob(grants: &[Grant]) -> String {
4879 let mut out = String::new();
4880 for g in grants {
4881 let actions: Vec<&str> = g.actions.iter().map(|a| a.as_str()).collect();
4882 out.push_str(&format!(
4883 "GRANT|{}|{}|{}|{}|{}|{}\n",
4884 encode_resource(&g.resource),
4885 actions.join(","),
4886 g.with_grant_option,
4887 g.tenant.as_deref().unwrap_or("*"),
4888 g.granted_by,
4889 g.granted_at,
4890 ));
4891 }
4892 out
4893}
4894
4895fn decode_grants_blob(s: &str) -> Option<Vec<Grant>> {
4896 let mut out = Vec::new();
4897 for line in s.lines() {
4898 if line.is_empty() {
4899 continue;
4900 }
4901 let parts: Vec<&str> = line.split('|').collect();
4902 if parts.len() != 7 || parts[0] != "GRANT" {
4903 return None;
4904 }
4905 let resource = decode_resource(parts[1])?;
4906 let mut actions = std::collections::BTreeSet::new();
4907 for token in parts[2].split(',') {
4908 if let Some(a) = Action::from_keyword(token) {
4909 actions.insert(a);
4910 }
4911 }
4912 let with_grant_option = parts[3] == "true";
4913 let tenant = if parts[4] == "*" {
4914 None
4915 } else {
4916 Some(parts[4].to_string())
4917 };
4918 let granted_by = parts[5].to_string();
4919 let granted_at: u128 = parts[6].parse().unwrap_or(0);
4920 out.push(Grant {
4921 principal: GrantPrincipal::Public,
4924 resource,
4925 actions,
4926 with_grant_option,
4927 granted_by,
4928 granted_at,
4929 tenant,
4930 columns: None,
4931 });
4932 }
4933 Some(out)
4934}
4935
4936fn encode_attrs_blob(a: &UserAttributes) -> String {
4937 let valid = a
4938 .valid_until
4939 .map(|t| t.to_string())
4940 .unwrap_or_else(|| "*".into());
4941 let limit = a
4942 .connection_limit
4943 .map(|l| l.to_string())
4944 .unwrap_or_else(|| "*".into());
4945 let path = a.search_path.clone().unwrap_or_else(|| "*".into());
4946 let groups = if a.groups.is_empty() {
4947 "*".to_string()
4948 } else {
4949 a.groups.join(",")
4950 };
4951 format!("ATTR|{}|{}|{}|{}\n", valid, limit, path, groups)
4952}
4953
4954fn decode_attrs_blob(s: &str) -> Option<UserAttributes> {
4955 let line = s.lines().next()?;
4956 let parts: Vec<&str> = line.split('|').collect();
4957 if !(parts.len() == 4 || parts.len() == 5) || parts[0] != "ATTR" {
4958 return None;
4959 }
4960 let groups = if parts.get(4).copied().unwrap_or("*") == "*" {
4961 Vec::new()
4962 } else {
4963 parts[4]
4964 .split(',')
4965 .filter(|g| !g.is_empty())
4966 .map(|g| g.to_string())
4967 .collect()
4968 };
4969 Some(UserAttributes {
4970 valid_until: if parts[1] == "*" {
4971 None
4972 } else {
4973 parts[1].parse().ok()
4974 },
4975 connection_limit: if parts[2] == "*" {
4976 None
4977 } else {
4978 parts[2].parse().ok()
4979 },
4980 search_path: if parts[3] == "*" {
4981 None
4982 } else {
4983 Some(parts[3].to_string())
4984 },
4985 groups,
4986 })
4987}
4988
4989fn make_scram_verifier(password: &str) -> crate::auth::scram::ScramVerifier {
4999 let salt = random_bytes(16);
5000 crate::auth::scram::ScramVerifier::from_password(
5001 password,
5002 salt,
5003 crate::auth::scram::DEFAULT_ITER,
5004 )
5005}
5006
5007const PASSWORD_M_COST_KIB: u32 = 19 * 1024;
5012const PASSWORD_T_COST: u32 = 2;
5013const PASSWORD_P_COST: u32 = 1;
5014const PASSWORD_TAG_LEN: usize = 32;
5015
5016fn password_hasher() -> argon2::Argon2<'static> {
5017 let params = argon2::Params::new(
5018 PASSWORD_M_COST_KIB,
5019 PASSWORD_T_COST,
5020 PASSWORD_P_COST,
5021 Some(PASSWORD_TAG_LEN),
5022 )
5023 .expect("Argon2id parameters are constant and valid");
5024 argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
5025}
5026
5027pub(crate) fn hash_password(password: &str) -> String {
5039 use argon2::PasswordHasher;
5040 password_hasher()
5043 .hash_password(password.as_bytes())
5044 .expect("Argon2id hashing cannot fail for valid parameters and a working CSPRNG")
5045 .to_string()
5046}
5047
5048pub(crate) fn password_hash_needs_upgrade(stored_hash: &str) -> bool {
5051 stored_hash.starts_with("argon2id$")
5052}
5053
5054pub(crate) fn verify_password(password: &str, stored_hash: &str) -> bool {
5059 if let Some(rest) = stored_hash.strip_prefix("argon2id$") {
5060 return verify_legacy_password(password, rest);
5061 }
5062 use argon2::{PasswordHash, PasswordVerifier};
5063 let Ok(parsed) = stored_hash.parse::<PasswordHash>() else {
5064 return false;
5065 };
5066 password_hasher()
5067 .verify_password(password.as_bytes(), &parsed)
5068 .is_ok()
5069}
5070
5071fn verify_legacy_password(password: &str, salt_and_hash: &str) -> bool {
5074 let Some((salt_hex, hash_hex)) = salt_and_hash.split_once('$') else {
5075 return false;
5076 };
5077 let (Ok(salt), Ok(expected)) = (hex::decode(salt_hex), hex::decode(hash_hex)) else {
5078 return false;
5079 };
5080 let computed = derive_key(password.as_bytes(), &salt, &auth_argon2_params());
5081 constant_time_eq(&computed, &expected)
5082}
5083
5084fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
5086 crate::crypto::constant_time_eq(a, b)
5087}
5088
5089fn generate_session_token() -> String {
5094 format!("rs_{}", hex::encode(random_bytes(32)))
5095}
5096
5097fn generate_api_key() -> String {
5098 format!("rk_{}", hex::encode(random_bytes(32)))
5099}
5100
5101fn random_hex(n: usize) -> String {
5103 hex::encode(random_bytes(n))
5104}
5105
5106pub(crate) fn random_bytes(n: usize) -> Vec<u8> {
5109 let mut buf = vec![0u8; n.max(32)];
5110 if let Err(err) = os_random::fill_bytes(&mut buf) {
5114 panic!("OS CSPRNG unavailable; refusing to generate credential material: {err}");
5115 }
5116 let digest = sha256(&buf);
5118 if n <= 32 {
5119 digest[..n].to_vec()
5120 } else {
5121 let mut out = Vec::with_capacity(n);
5123 let mut prev = digest;
5124 while out.len() < n {
5125 out.extend_from_slice(&prev[..std::cmp::min(32, n - out.len())]);
5126 prev = sha256(&prev);
5127 }
5128 out
5129 }
5130}
5131
5132fn lock_err<T>(_: T) -> AuthError {
5137 AuthError::Internal("lock poisoned".to_string())
5138}
5139
5140#[cfg(test)]
5145mod tests {
5146 use super::*;
5147
5148 fn test_config() -> AuthConfig {
5149 AuthConfig {
5150 enabled: true,
5151 session_ttl_secs: 60,
5152 require_auth: true,
5153 auto_encrypt_storage: false,
5154 vault_enabled: false,
5155 cert: Default::default(),
5156 oauth: Default::default(),
5157 }
5158 }
5159
5160 #[test]
5161 fn test_create_and_list_users() {
5162 let store = AuthStore::new(test_config());
5163 store.create_user("alice", "pass1", Role::Admin).unwrap();
5164 store.create_user("bob", "pass2", Role::Read).unwrap();
5165
5166 let users = store.list_users();
5167 assert_eq!(users.len(), 2);
5168 for u in &users {
5170 assert!(u.password_hash.is_empty());
5171 }
5172 }
5173
5174 #[test]
5175 fn test_create_duplicate_user() {
5176 let store = AuthStore::new(test_config());
5177 store.create_user("alice", "pass", Role::Admin).unwrap();
5178 let err = store.create_user("alice", "pass2", Role::Read).unwrap_err();
5179 assert!(matches!(err, AuthError::UserExists(_)));
5180 }
5181
5182 struct RecordingLedger(std::sync::Mutex<Vec<(String, String)>>);
5183
5184 impl crate::runtime::control_events::ControlEventLedger for RecordingLedger {
5185 fn emit(
5186 &self,
5187 _ctx: &ControlEventCtx<'_>,
5188 event: ControlEvent,
5189 ) -> Result<
5190 crate::runtime::control_events::EventId,
5191 crate::runtime::control_events::ControlEventError,
5192 > {
5193 self.0.lock().unwrap().push((
5194 event.kind.as_str().to_string(),
5195 event.outcome.as_str().to_string(),
5196 ));
5197 Ok(crate::runtime::control_events::EventId("evt".to_string()))
5198 }
5199 }
5200
5201 #[test]
5202 fn logins_land_in_the_control_event_ledger() {
5203 let store = AuthStore::new(test_config());
5204 store.create_user("dave", "pw", Role::Read).unwrap();
5205 let ledger = std::sync::Arc::new(RecordingLedger(std::sync::Mutex::new(Vec::new())));
5206 store.configure_control_events(
5207 ledger.clone(),
5208 crate::runtime::control_events::ControlEventConfig::default(),
5209 );
5210 store.authenticate("dave", "pw").expect("login");
5211 store
5212 .authenticate("dave", "nope")
5213 .expect_err("bad password");
5214 store
5215 .authenticate("nobody", "pw")
5216 .expect_err("unknown user");
5217 let seen = ledger.0.lock().unwrap().clone();
5218 let allowed = Outcome::Allowed.as_str().to_string();
5219 let denied = Outcome::Denied.as_str().to_string();
5220 assert!(
5221 seen.contains(&("auth.login".to_string(), allowed)),
5222 "success not recorded: {seen:?}"
5223 );
5224 assert_eq!(
5225 seen.iter()
5226 .filter(|(k, o)| k == "auth.login_failed" && *o == denied)
5227 .count(),
5228 2,
5229 "both refusals must be recorded: {seen:?}"
5230 );
5231 }
5232
5233 #[test]
5234 fn api_keys_are_hashed_at_rest_and_expire() {
5235 let store = AuthStore::new(test_config());
5236 store.create_user("erin", "pw", Role::Write).unwrap();
5237 let key = store
5238 .create_api_key_in_tenant_expiring(None, "erin", "ci", Role::Read, Some(now_ms() + 60))
5239 .unwrap();
5240 assert!(key.key.starts_with("rk_"));
5241
5242 let stored = store
5243 .users
5244 .read()
5245 .unwrap()
5246 .get(&UserId::from_parts(None, "erin"))
5247 .unwrap()
5248 .api_keys[0]
5249 .clone();
5250 assert!(stored.key.starts_with("rkh_"), "{}", stored.key);
5251 assert_ne!(stored.key, key.key, "the secret must not be stored");
5252 assert_eq!(stored.key, api_key_credential_id(&key.key));
5253
5254 assert!(store.validate_token(&key.key).is_some());
5255 std::thread::sleep(std::time::Duration::from_millis(80));
5256 assert!(
5257 store.validate_token(&key.key).is_none(),
5258 "an expired key must not validate"
5259 );
5260
5261 store.revoke_api_key(&stored.key).unwrap();
5263 assert!(store.revoke_api_key(&key.key).is_err());
5264 }
5265
5266 #[test]
5267 fn revoke_is_clamped_to_the_granter_tenant() {
5268 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
5269 let store = AuthStore::new(test_config());
5270 let acme_admin = UserId::from_parts(Some("acme"), "adm");
5271 let globex_user = UserId::from_parts(Some("globex"), "u");
5272 let err = store
5273 .revoke(
5274 &acme_admin,
5275 Role::Admin,
5276 &GrantPrincipal::User(globex_user),
5277 &Resource::table_from_name("orders"),
5278 &[Action::Select],
5279 )
5280 .expect_err("a tenant admin must not touch another tenant's grants");
5281 assert!(err.to_string().contains("cross-tenant"), "{err}");
5282 store
5284 .revoke(
5285 &UserId::from_parts(None, "platform"),
5286 Role::Admin,
5287 &GrantPrincipal::User(UserId::from_parts(Some("globex"), "u")),
5288 &Resource::table_from_name("orders"),
5289 &[Action::Select],
5290 )
5291 .expect("platform admin revokes anywhere");
5292 }
5293
5294 #[test]
5295 fn role_change_revokes_sessions_minted_under_the_old_role() {
5296 let store = AuthStore::new(test_config());
5297 store.create_user("carol", "pw", Role::Write).unwrap();
5298 let session = store.authenticate("carol", "pw").unwrap();
5299 assert!(store.validate_token(&session.token).is_some());
5300 store.change_role("carol", Role::Read).unwrap();
5301 assert!(
5302 store.validate_token(&session.token).is_none(),
5303 "a demotion must not keep riding on the old session"
5304 );
5305 }
5306
5307 #[test]
5308 fn test_authenticate_and_validate() {
5309 let store = AuthStore::new(test_config());
5310 store.create_user("alice", "secret", Role::Write).unwrap();
5311
5312 let session = store.authenticate("alice", "secret").unwrap();
5313 assert!(session.token.starts_with("rs_"));
5314
5315 let (username, role) = store.validate_token(&session.token).unwrap();
5316 assert_eq!(username, "alice");
5317 assert_eq!(role, Role::Write);
5318 }
5319
5320 #[test]
5321 fn test_authenticate_wrong_password() {
5322 let store = AuthStore::new(test_config());
5323 store.create_user("alice", "secret", Role::Read).unwrap();
5324
5325 let err = store.authenticate("alice", "wrong").unwrap_err();
5326 assert!(matches!(err, AuthError::InvalidCredentials));
5327 }
5328
5329 #[test]
5330 fn test_api_key_lifecycle() {
5331 let store = AuthStore::new(test_config());
5332 store.create_user("alice", "pass", Role::Admin).unwrap();
5333
5334 let key = store
5335 .create_api_key("alice", "ci-token", Role::Write)
5336 .unwrap();
5337 assert!(key.key.starts_with("rk_"));
5338
5339 let (username, role) = store.validate_token(&key.key).unwrap();
5340 assert_eq!(username, "alice");
5341 assert_eq!(role, Role::Write);
5342
5343 store.revoke_api_key(&key.key).unwrap();
5344 assert!(store.validate_token(&key.key).is_none());
5345 }
5346
5347 #[test]
5348 fn test_api_key_role_exceeded() {
5349 let store = AuthStore::new(test_config());
5350 store.create_user("bob", "pass", Role::Read).unwrap();
5351
5352 let err = store
5353 .create_api_key("bob", "escalate", Role::Admin)
5354 .unwrap_err();
5355 assert!(matches!(err, AuthError::RoleExceeded { .. }));
5356 }
5357
5358 #[test]
5359 fn test_change_password() {
5360 let store = AuthStore::new(test_config());
5361 store.create_user("alice", "old", Role::Write).unwrap();
5362
5363 store.change_password("alice", "old", "new").unwrap();
5364
5365 assert!(store.authenticate("alice", "old").is_err());
5367 assert!(store.authenticate("alice", "new").is_ok());
5369 }
5370
5371 #[test]
5372 fn test_change_role() {
5373 let store = AuthStore::new(test_config());
5374 store.create_user("alice", "pass", Role::Admin).unwrap();
5375 store.create_api_key("alice", "key1", Role::Admin).unwrap();
5376
5377 store.change_role("alice", Role::Read).unwrap();
5378
5379 let users = store.list_users();
5381 let alice = users.iter().find(|u| u.username == "alice").unwrap();
5382 assert_eq!(alice.role, Role::Read);
5383
5384 assert_eq!(alice.api_keys[0].role, Role::Read);
5386 }
5387
5388 #[test]
5389 fn test_admin_user_mutations_are_policy_controlled_not_flag_guarded() {
5390 let store = AuthStore::new(test_config());
5391 store
5392 .create_admin_user("system", "pass", Role::Admin, None)
5393 .unwrap();
5394
5395 let uid = UserId::platform("system");
5396 store.change_password("system", "pass", "new").unwrap();
5397 store.change_role("system", Role::Read).unwrap();
5398 store.set_user_enabled(&uid, false).unwrap();
5399
5400 let key = store
5401 .create_api_key("system", "rotation", Role::Read)
5402 .unwrap();
5403 assert!(store.validate_token(&key.key).is_none());
5407 store.set_user_enabled(&uid, true).unwrap();
5408 assert!(store.validate_token(&key.key).is_some());
5409 store.revoke_api_key(&key.key).unwrap();
5410 assert!(store.validate_token(&key.key).is_none());
5411 store.delete_user("system").unwrap();
5412 }
5413
5414 #[test]
5415 fn test_user_lifecycle_deny_policy_blocks_cloud_admin_deletion() {
5416 use crate::auth::policies::Policy;
5417
5418 let store = AuthStore::new(test_config());
5419 store
5420 .create_user("cloud-admin", "pass", Role::Admin)
5421 .unwrap();
5422 store
5423 .create_user("customer-admin", "pass", Role::Admin)
5424 .unwrap();
5425
5426 store
5427 .put_policy(
5428 Policy::from_json_str(
5429 r#"{
5430 "id": "customer-admin-allow-all",
5431 "version": 1,
5432 "statements": [{
5433 "effect": "allow",
5434 "actions": ["*"],
5435 "resources": ["*"]
5436 }]
5437 }"#,
5438 )
5439 .unwrap(),
5440 )
5441 .unwrap();
5442 store
5443 .put_policy(
5444 Policy::from_json_str(
5445 r#"{
5446 "id": "cloud-admin-protection",
5447 "version": 1,
5448 "statements": [{
5449 "effect": "deny",
5450 "actions": [
5451 "user:delete",
5452 "user:disable",
5453 "user:password:change",
5454 "user:role:update"
5455 ],
5456 "resources": ["user:cloud-admin"]
5457 }]
5458 }"#,
5459 )
5460 .unwrap(),
5461 )
5462 .unwrap();
5463 let customer = UserId::platform("customer-admin");
5464 store
5465 .attach_policy(
5466 PrincipalRef::User(customer.clone()),
5467 "customer-admin-allow-all",
5468 )
5469 .unwrap();
5470 store
5471 .attach_policy(
5472 PrincipalRef::User(customer.clone()),
5473 "cloud-admin-protection",
5474 )
5475 .unwrap();
5476
5477 let cloud_admin = UserId::platform("cloud-admin");
5478 assert!(!store.check_user_lifecycle_authz(
5479 &customer,
5480 Role::Admin,
5481 "user:delete",
5482 &cloud_admin,
5483 ));
5484 assert!(!store.check_user_lifecycle_authz(
5485 &customer,
5486 Role::Admin,
5487 "user:disable",
5488 &cloud_admin,
5489 ));
5490 assert!(!store.check_user_lifecycle_authz(
5491 &customer,
5492 Role::Admin,
5493 "user:password:change",
5494 &cloud_admin,
5495 ));
5496 assert!(!store.check_user_lifecycle_authz(
5497 &customer,
5498 Role::Admin,
5499 "user:role:update",
5500 &cloud_admin,
5501 ));
5502
5503 let another_user = UserId::platform("someone-else");
5504 assert!(store.check_user_lifecycle_authz(
5505 &customer,
5506 Role::Admin,
5507 "user:delete",
5508 &another_user,
5509 ));
5510 }
5511
5512 #[test]
5513 fn test_regular_user_mutations_still_work() {
5514 let store = AuthStore::new(test_config());
5515 store.create_user("alice", "old", Role::Admin).unwrap();
5516
5517 let uid = UserId::platform("alice");
5518 store.set_user_enabled(&uid, false).unwrap();
5519 assert!(matches!(
5520 store.authenticate("alice", "old"),
5521 Err(AuthError::InvalidCredentials)
5522 ));
5523
5524 store.set_user_enabled(&uid, true).unwrap();
5525 store.change_password("alice", "old", "new").unwrap();
5526 store.change_role("alice", Role::Read).unwrap();
5527 store.delete_user("alice").unwrap();
5528 assert!(matches!(
5529 store.authenticate("alice", "new"),
5530 Err(AuthError::InvalidCredentials)
5531 ));
5532 }
5533
5534 #[test]
5535 fn test_delete_user() {
5536 let store = AuthStore::new(test_config());
5537 store.create_user("alice", "pass", Role::Admin).unwrap();
5538 let key = store.create_api_key("alice", "key1", Role::Read).unwrap();
5539 let session = store.authenticate("alice", "pass").unwrap();
5540
5541 store.delete_user("alice").unwrap();
5542
5543 assert!(store.validate_token(&key.key).is_none());
5544 assert!(store.validate_token(&session.token).is_none());
5545 assert!(store.list_users().is_empty());
5546 }
5547
5548 #[test]
5549 fn test_revoke_session() {
5550 let store = AuthStore::new(test_config());
5551 store.create_user("alice", "pass", Role::Read).unwrap();
5552 let session = store.authenticate("alice", "pass").unwrap();
5553
5554 store.revoke_session(&session.token);
5555 assert!(store.validate_token(&session.token).is_none());
5556 }
5557
5558 #[test]
5559 fn test_password_hash_format() {
5560 let hash = hash_password("test");
5565 assert!(
5566 hash.starts_with("$argon2id$v=19$"),
5567 "expected a PHC-encoded Argon2id hash, got: {hash}"
5568 );
5569 assert!(
5570 hash.contains(&format!(
5571 "m={PASSWORD_M_COST_KIB},t={PASSWORD_T_COST},p={PASSWORD_P_COST}"
5572 )),
5573 "the PHC string must carry the cost parameters: {hash}"
5574 );
5575 assert!(verify_password("test", &hash));
5576 assert!(!verify_password("wrong", &hash));
5577 assert!(
5578 !password_hash_needs_upgrade(&hash),
5579 "a freshly minted hash must not be flagged for upgrade"
5580 );
5581 }
5582
5583 #[test]
5584 fn legacy_password_hashes_still_verify_and_upgrade_on_login() {
5585 let legacy = {
5589 let salt = random_bytes(16);
5590 let hash = derive_key(b"hunter2", &salt, &auth_argon2_params());
5591 format!("argon2id${}${}", hex::encode(&salt), hex::encode(&hash))
5592 };
5593 assert!(verify_password("hunter2", &legacy));
5594 assert!(!verify_password("wrong", &legacy));
5595 assert!(password_hash_needs_upgrade(&legacy));
5596
5597 let store = AuthStore::new(test_config());
5598 store
5599 .create_user("erin", "placeholder", Role::Write)
5600 .unwrap();
5601 {
5602 let mut users = store.users.write().unwrap();
5603 users
5604 .get_mut(&UserId::platform("erin"))
5605 .unwrap()
5606 .password_hash = legacy.clone();
5607 }
5608
5609 store
5610 .authenticate("erin", "hunter2")
5611 .expect("a legacy hash must still authenticate");
5612
5613 let stored = store
5614 .users
5615 .read()
5616 .unwrap()
5617 .get(&UserId::platform("erin"))
5618 .unwrap()
5619 .password_hash
5620 .clone();
5621 assert!(
5622 stored.starts_with("$argon2id$v=19$"),
5623 "a successful login must upgrade the stored hash, got: {stored}"
5624 );
5625 assert!(store.authenticate("erin", "hunter2").is_ok());
5626 }
5627
5628 #[test]
5629 fn test_constant_time_eq() {
5630 assert!(constant_time_eq(b"hello", b"hello"));
5631 assert!(!constant_time_eq(b"hello", b"world"));
5632 assert!(!constant_time_eq(b"short", b"longer"));
5633 }
5634
5635 #[test]
5636 fn test_bootstrap_seals_permanently() {
5637 let store = AuthStore::new(test_config());
5638
5639 assert!(store.needs_bootstrap());
5640 assert!(!store.is_bootstrapped());
5641
5642 let result = store.bootstrap("admin", "secret");
5644 assert!(result.is_ok());
5645 let br = result.unwrap();
5646 assert_eq!(br.user.username, "admin");
5647 assert_eq!(br.user.role, Role::Admin);
5648 assert!(br.api_key.key.starts_with("rk_"));
5649 assert!(br.certificate.is_none());
5651
5652 assert!(!store.needs_bootstrap());
5654 assert!(store.is_bootstrapped());
5655
5656 let result = store.bootstrap("admin2", "secret2");
5658 assert!(result.is_err());
5659 let err = result.unwrap_err();
5660 assert!(err.to_string().contains("sealed permanently"));
5661
5662 assert_eq!(store.list_users().len(), 1);
5664 assert_eq!(store.list_users()[0].username, "admin");
5665 }
5666
5667 #[test]
5668 fn test_bootstrap_after_manual_user_creation() {
5669 let store = AuthStore::new(test_config());
5670
5671 store.create_user("existing", "pass", Role::Read).unwrap();
5673
5674 assert!(!store.needs_bootstrap()); }
5678
5679 #[test]
5684 fn test_same_username_two_tenants_distinct() {
5685 let store = AuthStore::new(test_config());
5686 store
5687 .create_user_in_tenant(Some("acme"), "alice", "pw-acme", Role::Write)
5688 .unwrap();
5689 store
5690 .create_user_in_tenant(Some("globex"), "alice", "pw-globex", Role::Read)
5691 .unwrap();
5692
5693 let users = store.list_users();
5695 assert_eq!(users.len(), 2);
5696
5697 assert!(store
5699 .authenticate_in_tenant(Some("acme"), "alice", "pw-acme")
5700 .is_ok());
5701 assert!(store
5702 .authenticate_in_tenant(Some("globex"), "alice", "pw-globex")
5703 .is_ok());
5704
5705 assert!(store
5707 .authenticate_in_tenant(Some("acme"), "alice", "pw-globex")
5708 .is_err());
5709 assert!(store
5710 .authenticate_in_tenant(Some("globex"), "alice", "pw-acme")
5711 .is_err());
5712 }
5713
5714 #[test]
5715 fn test_session_carries_tenant() {
5716 let store = AuthStore::new(test_config());
5717 store
5718 .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
5719 .unwrap();
5720 let session = store
5721 .authenticate_in_tenant(Some("acme"), "alice", "pw")
5722 .unwrap();
5723 assert_eq!(session.tenant_id.as_deref(), Some("acme"));
5724
5725 let (id, role) = store.validate_token_full(&session.token).unwrap();
5726 assert_eq!(id.tenant.as_deref(), Some("acme"));
5727 assert_eq!(id.username, "alice");
5728 assert_eq!(role, Role::Admin);
5729 }
5730
5731 #[test]
5732 fn test_platform_user_has_no_tenant() {
5733 let store = AuthStore::new(test_config());
5734 store.create_user("admin", "pw", Role::Admin).unwrap();
5735 let session = store.authenticate("admin", "pw").unwrap();
5736 assert!(session.tenant_id.is_none());
5737
5738 let (id, _) = store.validate_token_full(&session.token).unwrap();
5739 assert!(id.tenant.is_none());
5740 }
5741
5742 #[test]
5743 fn test_lookup_scram_verifier_global_resolves_platform() {
5744 let store = AuthStore::new(test_config());
5745 store.create_user("admin", "pw", Role::Admin).unwrap();
5746 store
5747 .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
5748 .unwrap();
5749
5750 let v = store.lookup_scram_verifier_global("admin");
5752 assert!(v.is_some());
5753
5754 let v_acme = store.lookup_scram_verifier(&UserId::scoped("acme", "admin"));
5756 assert!(v_acme.is_some());
5757
5758 assert_ne!(v.unwrap().salt, v_acme.unwrap().salt);
5760 }
5761
5762 #[test]
5763 fn test_delete_in_tenant_does_not_touch_other_tenant() {
5764 let store = AuthStore::new(test_config());
5765 store
5766 .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
5767 .unwrap();
5768 store
5769 .create_user_in_tenant(Some("globex"), "alice", "pw", Role::Admin)
5770 .unwrap();
5771
5772 store.delete_user_in_tenant(Some("acme"), "alice").unwrap();
5773
5774 assert!(store
5776 .authenticate_in_tenant(Some("globex"), "alice", "pw")
5777 .is_ok());
5778 assert!(store
5780 .authenticate_in_tenant(Some("acme"), "alice", "pw")
5781 .is_err());
5782 }
5783
5784 #[test]
5785 fn test_user_id_display() {
5786 assert_eq!(UserId::platform("admin").to_string(), "admin");
5787 assert_eq!(UserId::scoped("acme", "alice").to_string(), "acme/alice");
5788 }
5789
5790 fn enforcement_eval_ctx(role: Role) -> EvalContext {
5795 EvalContext {
5796 principal_tenant: None,
5797 current_tenant: None,
5798 peer_ip: None,
5799 mfa_present: false,
5800 now_ms: 1_700_000_000_000,
5801 principal_is_admin_role: role == Role::Admin,
5802 principal_is_platform_scoped: true,
5803 }
5804 }
5805
5806 fn install_unrelated_policy(store: &AuthStore) {
5810 store
5811 .put_policy(
5812 Policy::from_json_str(
5813 r#"{"id":"p-unrelated","version":1,"statements":[{"effect":"allow","actions":["select"],"resources":["table:public.other"]}]}"#,
5814 )
5815 .unwrap(),
5816 )
5817 .unwrap();
5818 }
5819
5820 #[test]
5821 fn enforcement_mode_default_for_new_store_is_legacy_rbac() {
5822 let store = AuthStore::new(test_config());
5827 assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::LegacyRbac);
5828 }
5829
5830 #[test]
5831 fn enforcement_mode_set_round_trips_and_returns_previous() {
5832 let store = AuthStore::new(test_config());
5833 let prev = store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5834 assert_eq!(prev, PolicyEnforcementMode::LegacyRbac);
5835 assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::PolicyOnly);
5836 let prev = store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5837 assert_eq!(prev, PolicyEnforcementMode::PolicyOnly);
5838 }
5839
5840 #[test]
5841 fn policy_only_mode_treats_no_matching_policy_as_deny() {
5842 let store = AuthStore::new(test_config());
5846 store.create_user("alice", "p", Role::Admin).unwrap();
5847 let uid = UserId::platform("alice");
5848 install_unrelated_policy(&store);
5849 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5850
5851 let resource = ResourceRef::new("table", "public.t");
5852 assert!(!store.check_policy_authz_with_role(
5855 &uid,
5856 "select",
5857 &resource,
5858 &enforcement_eval_ctx(Role::Admin),
5859 Role::Admin,
5860 ));
5861 }
5862
5863 #[test]
5864 fn legacy_rbac_mode_falls_back_to_role_decision() {
5865 let store = AuthStore::new(test_config());
5870 store.create_user("alice", "p", Role::Read).unwrap();
5871 let uid = UserId::platform("alice");
5872 install_unrelated_policy(&store);
5873 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5874
5875 let table = ResourceRef::new("table", "public.t");
5876 assert!(store.check_policy_authz_with_role(
5878 &uid,
5879 "select",
5880 &table,
5881 &enforcement_eval_ctx(Role::Read),
5882 Role::Read,
5883 ));
5884 assert!(!store.check_policy_authz_with_role(
5886 &uid,
5887 "insert",
5888 &table,
5889 &enforcement_eval_ctx(Role::Read),
5890 Role::Read,
5891 ));
5892 assert!(store.check_policy_authz_with_role(
5894 &uid,
5895 "insert",
5896 &table,
5897 &enforcement_eval_ctx(Role::Admin),
5898 Role::Admin,
5899 ));
5900 }
5901
5902 #[test]
5903 fn explicit_deny_wins_irrespective_of_mode_or_role() {
5904 let store = AuthStore::new(test_config());
5908 store.create_user("alice", "p", Role::Admin).unwrap();
5909 let uid = UserId::platform("alice");
5910 store
5911 .put_policy(
5912 Policy::from_json_str(
5913 r#"{"id":"p-deny-select","version":1,"statements":[{"effect":"deny","actions":["select"],"resources":["table:public.t"]}]}"#,
5914 )
5915 .unwrap(),
5916 )
5917 .unwrap();
5918 store
5919 .attach_policy(PrincipalRef::User(uid.clone()), "p-deny-select")
5920 .unwrap();
5921
5922 let resource = ResourceRef::new("table", "public.t");
5923 for mode in [
5924 PolicyEnforcementMode::LegacyRbac,
5925 PolicyEnforcementMode::PolicyOnly,
5926 ] {
5927 store.set_enforcement_mode(mode);
5928 assert!(
5929 !store.check_policy_authz_with_role(
5930 &uid,
5931 "select",
5932 &resource,
5933 &enforcement_eval_ctx(Role::Admin),
5934 Role::Admin,
5935 ),
5936 "explicit deny must win under mode {mode:?}"
5937 );
5938 }
5939 }
5940
5941 #[test]
5942 fn explicit_allow_wins_irrespective_of_mode_or_role() {
5943 let store = AuthStore::new(test_config());
5947 store.create_user("alice", "p", Role::Read).unwrap();
5948 let uid = UserId::platform("alice");
5949 store
5950 .put_policy(
5951 Policy::from_json_str(
5952 r#"{"id":"p-allow-create","version":1,"statements":[{"effect":"allow","actions":["create"],"resources":["table:public.t"]}]}"#,
5953 )
5954 .unwrap(),
5955 )
5956 .unwrap();
5957 store
5958 .attach_policy(PrincipalRef::User(uid.clone()), "p-allow-create")
5959 .unwrap();
5960 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5961
5962 let resource = ResourceRef::new("table", "public.t");
5963 assert!(store.check_policy_authz_with_role(
5964 &uid,
5965 "create",
5966 &resource,
5967 &enforcement_eval_ctx(Role::Read),
5968 Role::Read,
5969 ));
5970 }
5971
5972 #[test]
5973 fn take_legacy_rbac_warn_once_is_at_most_once_per_boot() {
5974 let store = AuthStore::new(test_config());
5975 assert!(store.take_legacy_rbac_warn_once());
5977 for _ in 0..3 {
5980 assert!(!store.take_legacy_rbac_warn_once());
5981 }
5982 }
5983
5984 #[test]
5985 fn take_legacy_rbac_warn_once_is_silent_under_policy_only() {
5986 let store = AuthStore::new(test_config());
5992 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5993 assert!(!store.take_legacy_rbac_warn_once());
5994 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5999 assert!(store.take_legacy_rbac_warn_once());
6000 assert!(!store.take_legacy_rbac_warn_once());
6001 }
6002
6003 #[test]
6004 fn strict_check_policy_authz_ignores_enforcement_mode() {
6005 let store = AuthStore::new(test_config());
6010 store.create_user("alice", "p", Role::Admin).unwrap();
6011 let uid = UserId::platform("alice");
6012 install_unrelated_policy(&store);
6013 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6014
6015 let resource = ResourceRef::new("table", "public.t");
6016 assert!(!store.check_policy_authz(
6017 &uid,
6018 "select",
6019 &resource,
6020 &enforcement_eval_ctx(Role::Admin),
6021 ));
6022 }
6023
6024 #[test]
6025 fn disabling_a_user_invalidates_its_api_keys_and_sessions() {
6026 let store = AuthStore::new(test_config());
6027 store.create_user("carol", "pw", Role::Write).unwrap();
6028 let key = store
6029 .create_api_key("carol", "ci", Role::Write)
6030 .unwrap()
6031 .key;
6032 let session = store.authenticate("carol", "pw").unwrap();
6033 assert!(store.validate_token(&key).is_some());
6034 assert!(store.validate_token(&session.token).is_some());
6035
6036 let uid = UserId::platform("carol");
6037 store.set_user_enabled(&uid, false).unwrap();
6038
6039 assert!(
6043 store.validate_token(&key).is_none(),
6044 "an API key must stop working the moment its owner is disabled"
6045 );
6046 assert!(
6047 store.validate_token(&session.token).is_none(),
6048 "live sessions must not survive disabling the account"
6049 );
6050 assert!(
6051 store.lookup_scram_verifier(&uid).is_none(),
6052 "SCRAM must not authenticate a disabled account"
6053 );
6054 }
6055
6056 #[test]
6057 fn admin_password_reset_applies_and_revokes_sessions() {
6058 let store = AuthStore::new(test_config());
6059 store.create_user("dave", "old", Role::Write).unwrap();
6060 let session = store.authenticate("dave", "old").unwrap();
6061
6062 store
6063 .set_password_by_admin(&UserId::platform("dave"), "rotated")
6064 .expect("admin reset");
6065
6066 assert!(
6067 store.authenticate("dave", "old").is_err(),
6068 "the old password must stop working after a reset"
6069 );
6070 assert!(
6071 store.authenticate("dave", "rotated").is_ok(),
6072 "the new password must work after a reset"
6073 );
6074 assert!(
6075 store.validate_token(&session.token).is_none(),
6076 "a password rotation must revoke live sessions"
6077 );
6078 }
6079}
6080
6081fn deliver_bootstrap_certificate(cert: &str) {
6090 if let Some(path) = crate::utils::env_with_file_fallback("REDDB_BOOTSTRAP_CERT_OUT") {
6091 match std::fs::write(&path, format!("{cert}\n")) {
6092 Ok(()) => {
6093 tracing::warn!(
6094 target: "reddb::security",
6095 path = %path,
6096 "vault certificate written — save it: the ONLY way to unseal after a restart"
6097 );
6098 return;
6099 }
6100 Err(err) => {
6101 tracing::error!(
6102 target: "reddb::security",
6103 path = %path,
6104 error = %err,
6105 "could not write the vault certificate; falling back to stderr"
6106 );
6107 }
6108 }
6109 }
6110 eprintln!("[reddb] CERTIFICATE: {cert}");
6111 tracing::warn!(
6112 target: "reddb::security",
6113 "vault certificate issued on stderr — save it (ONLY way to unseal after restart); \
6114 set REDDB_BOOTSTRAP_CERT_OUT to receive it as a file instead of a log line"
6115 );
6116}