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 api_key_owner(&self, key: &str) -> Option<UserId> {
2455 let key_id = api_key_credential_id(key);
2456 self.api_key_index
2457 .read()
2458 .ok()
2459 .and_then(|index| index.get(&key_id).map(|(id, _)| id.clone()))
2460 }
2461
2462 pub fn revoke_api_key(&self, key: &str) -> Result<(), AuthError> {
2464 if let Some(configured) = self.configured_control_events() {
2465 let ctx = default_user_lifecycle_ctx();
2466 let control = UserLifecycleControl {
2467 ctx: &ctx,
2468 ledger: configured.ledger.as_ref(),
2469 config: configured.config,
2470 };
2471 self.revoke_api_key_controlled(key, &control)
2472 } else {
2473 self.revoke_api_key_unaudited(key)
2474 }
2475 }
2476
2477 pub fn revoke_api_key_with_control_events(
2478 &self,
2479 key: &str,
2480 ctx: &ControlEventCtx<'_>,
2481 ledger: &dyn ControlEventLedger,
2482 config: ControlEventConfig,
2483 ) -> Result<(), AuthError> {
2484 let control = UserLifecycleControl {
2485 ctx,
2486 ledger,
2487 config,
2488 };
2489 self.revoke_api_key_controlled(key, &control)
2490 }
2491
2492 fn revoke_api_key_unaudited(&self, key: &str) -> Result<(), AuthError> {
2493 let key_id = api_key_credential_id(key);
2494 let key = key_id.as_str();
2495 let mut users = self.users.write().map_err(lock_err)?;
2496
2497 let owner_id: UserId = {
2501 if let Ok(idx) = self.api_key_index.read() {
2502 if let Some((id, _)) = idx.get(key) {
2503 id.clone()
2504 } else {
2505 return Err(AuthError::KeyNotFound(key.to_string()));
2506 }
2507 } else {
2508 let owner = users
2509 .iter()
2510 .find(|(_, u)| u.api_keys.iter().any(|k| k.key == key));
2511 match owner {
2512 Some((id, _)) => id.clone(),
2513 None => return Err(AuthError::KeyNotFound(key.to_string())),
2514 }
2515 }
2516 };
2517
2518 let user = users
2519 .get_mut(&owner_id)
2520 .ok_or_else(|| AuthError::KeyNotFound(key.to_string()))?;
2521 user.api_keys.retain(|k| k.key != key);
2522 user.updated_at = now_ms();
2523
2524 if let Ok(mut idx) = self.api_key_index.write() {
2526 idx.remove(key);
2527 }
2528
2529 self.persist_to_vault();
2530 Ok(())
2531 }
2532
2533 fn revoke_api_key_controlled(
2534 &self,
2535 key: &str,
2536 control: &UserLifecycleControl<'_>,
2537 ) -> Result<(), AuthError> {
2538 let key_id = api_key_id(key);
2539 let rollback = self.api_key_rollback_snapshot(key);
2540 let id = rollback
2541 .as_ref()
2542 .map(|r| r.0.clone())
2543 .unwrap_or_else(|| UserId::from_parts(None, ""));
2544 let role = rollback.as_ref().map(|r| r.1.role).unwrap_or(Role::Read);
2545 match self.revoke_api_key_unaudited(key) {
2546 Ok(()) => {
2547 if let Err(err) = self.emit_api_key_allowed(
2548 control,
2549 EventKind::ApiKeyRevoke,
2550 "apikey.revoke",
2551 &key_id,
2552 api_key_control_fields(&id, role, &key_id),
2553 ) {
2554 if let Some((owner, api_key)) = rollback {
2555 self.restore_api_key(&owner, api_key);
2556 }
2557 return Err(err);
2558 }
2559 Ok(())
2560 }
2561 Err(err) => {
2562 self.emit_user_lifecycle_outcome(
2563 control,
2564 if user_error_is_denied(&err) {
2565 Outcome::Denied
2566 } else {
2567 Outcome::Error
2568 },
2569 EventKind::ApiKeyRevoke,
2570 "apikey.revoke",
2571 &id,
2572 Some(err.to_string()),
2573 api_key_control_fields(&id, role, &key_id),
2574 );
2575 Err(err)
2576 }
2577 }
2578 }
2579
2580 pub fn revoke_session(&self, token: &str) {
2586 if let Ok(mut sessions) = self.sessions.write() {
2587 sessions.remove(token);
2588 }
2589 }
2590
2591 pub fn purge_expired_sessions(&self) -> usize {
2593 let now = now_ms();
2594 if let Ok(mut sessions) = self.sessions.write() {
2595 let before = sessions.len();
2596 sessions.retain(|_, s| s.expires_at > now);
2597 return before - sessions.len();
2598 }
2599 0
2600 }
2601
2602 pub fn grant(
2622 &self,
2623 granter: &UserId,
2624 granter_role: Role,
2625 principal: GrantPrincipal,
2626 resource: Resource,
2627 actions: Vec<Action>,
2628 with_grant_option: bool,
2629 tenant: Option<String>,
2630 ) -> Result<(), AuthError> {
2631 if granter_role != Role::Admin {
2632 return Err(AuthError::Forbidden(format!(
2633 "GRANT requires Admin role; granter `{}` has `{:?}`",
2634 granter, granter_role
2635 )));
2636 }
2637
2638 if granter.tenant.is_some() && granter.tenant != tenant {
2642 return Err(AuthError::Forbidden(format!(
2643 "cross-tenant GRANT denied: granter tenant `{:?}` != grant tenant `{:?}`",
2644 granter.tenant, tenant
2645 )));
2646 }
2647
2648 let mut actions_set = std::collections::BTreeSet::new();
2649 for a in actions {
2650 actions_set.insert(a);
2651 }
2652 let g = Grant {
2653 principal: principal.clone(),
2654 resource,
2655 actions: actions_set,
2656 with_grant_option,
2657 granted_by: granter.to_string(),
2658 granted_at: now_ms(),
2659 tenant,
2660 columns: None,
2661 };
2662
2663 match &principal {
2664 GrantPrincipal::User(uid) => {
2665 self.grants
2666 .write()
2667 .unwrap_or_else(|e| e.into_inner())
2668 .entry(uid.clone())
2669 .or_default()
2670 .push(g.clone());
2671 self.invalidate_permission_cache(Some(uid));
2672 }
2673 GrantPrincipal::Public => {
2674 self.public_grants
2675 .write()
2676 .unwrap_or_else(|e| e.into_inner())
2677 .push(g.clone());
2678 self.invalidate_permission_cache(None);
2679 }
2680 GrantPrincipal::Group(_) => {
2681 return Err(AuthError::Forbidden(
2682 "GROUP principals are not yet supported; use a USER or PUBLIC".to_string(),
2683 ));
2684 }
2685 }
2686
2687 self.invalidate_visible_collections_for_tenant(g.tenant.as_deref());
2692
2693 self.persist_acl_to_kv();
2694 Ok(())
2695 }
2696
2697 pub fn revoke(
2700 &self,
2701 granter: &UserId,
2702 granter_role: Role,
2703 principal: &GrantPrincipal,
2704 resource: &Resource,
2705 actions: &[Action],
2706 ) -> Result<usize, AuthError> {
2707 if granter_role != Role::Admin {
2708 return Err(AuthError::Forbidden(format!(
2709 "REVOKE requires Admin role; granter `{}` has `{:?}`",
2710 granter, granter_role
2711 )));
2712 }
2713
2714 if let GrantPrincipal::User(uid) = principal {
2720 if granter.tenant.is_some() && granter.tenant != uid.tenant {
2721 return Err(AuthError::Forbidden(format!(
2722 "cross-tenant REVOKE denied: granter tenant `{:?}` != principal tenant `{:?}`",
2723 granter.tenant, uid.tenant
2724 )));
2725 }
2726 }
2727
2728 let removed = match principal {
2729 GrantPrincipal::User(uid) => {
2730 let mut g = self.grants.write().unwrap_or_else(|e| e.into_inner());
2731 let before = g.get(uid).map(|v| v.len()).unwrap_or(0);
2732 if let Some(list) = g.get_mut(uid) {
2733 list.retain(|gr| {
2734 !(gr.resource == *resource
2735 && (actions.iter().any(|a| gr.actions.contains(a))
2736 || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2737 });
2738 }
2739 let after = g.get(uid).map(|v| v.len()).unwrap_or(0);
2740 drop(g);
2741 self.invalidate_permission_cache(Some(uid));
2742 before - after
2743 }
2744 GrantPrincipal::Public => {
2745 let mut p = self
2746 .public_grants
2747 .write()
2748 .unwrap_or_else(|e| e.into_inner());
2749 let before = p.len();
2750 p.retain(|gr| {
2751 !(gr.resource == *resource
2752 && (actions.iter().any(|a| gr.actions.contains(a))
2753 || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2754 });
2755 let after = p.len();
2756 drop(p);
2757 self.invalidate_permission_cache(None);
2758 before - after
2759 }
2760 GrantPrincipal::Group(_) => 0,
2761 };
2762
2763 if removed > 0 {
2764 match principal {
2770 GrantPrincipal::User(uid) => {
2771 self.invalidate_visible_collections_for_tenant(uid.tenant.as_deref());
2772 }
2773 GrantPrincipal::Public | GrantPrincipal::Group(_) => {
2774 self.invalidate_visible_collections_cache();
2775 }
2776 }
2777 self.persist_acl_to_kv();
2778 }
2779 Ok(removed)
2780 }
2781
2782 pub fn visible_collections_for_scope(
2796 &self,
2797 tenant: Option<&str>,
2798 role: Role,
2799 principal: &str,
2800 all_collections: &[String],
2801 ) -> std::collections::HashSet<String> {
2802 let key = super::scope_cache::ScopeKey::new(tenant, principal, role);
2803 if let Some(hit) = self.visible_collections_cache.get(&key) {
2804 return hit;
2805 }
2806 let ctx = AuthzContext {
2809 principal,
2810 effective_role: role,
2811 tenant,
2812 };
2813 let mut visible = std::collections::HashSet::new();
2814 for collection in all_collections {
2815 let resource = Resource::table_from_name(collection);
2816 if self.check_grant(&ctx, Action::Select, &resource).is_ok() {
2817 visible.insert(collection.clone());
2818 }
2819 }
2820 self.visible_collections_cache.insert(key, visible.clone());
2821 visible
2822 }
2823
2824 pub fn auth_cache_stats(&self) -> super::scope_cache::AuthCacheStats {
2828 self.visible_collections_cache.stats()
2829 }
2830
2831 pub fn invalidate_visible_collections_cache(&self) {
2835 self.visible_collections_cache.invalidate_all();
2836 }
2837
2838 pub fn invalidate_visible_collections_for_tenant(&self, tenant: Option<&str>) {
2841 self.visible_collections_cache.invalidate_tenant(tenant);
2842 }
2843
2844 pub fn effective_grants(&self, uid: &UserId) -> Vec<Grant> {
2847 let mut out = Vec::new();
2848 if let Ok(g) = self.grants.read() {
2849 if let Some(list) = g.get(uid) {
2850 out.extend(list.iter().cloned());
2851 }
2852 }
2853 if let Ok(p) = self.public_grants.read() {
2854 out.extend(p.iter().cloned());
2855 }
2856 out
2857 }
2858
2859 pub fn check_grant(
2862 &self,
2863 ctx: &AuthzContext<'_>,
2864 action: Action,
2865 resource: &Resource,
2866 ) -> Result<(), AuthzError> {
2867 if ctx.effective_role == Role::Admin {
2868 return Ok(());
2869 }
2870
2871 let uid = UserId::from_parts(ctx.tenant, ctx.principal);
2872
2873 if let Ok(cache) = self.permission_cache.read() {
2875 if let Some(pc) = cache.get(&uid) {
2876 if pc.allows(resource, action) {
2877 return Ok(());
2878 }
2879 }
2880 }
2881
2882 let user_grants = self
2884 .grants
2885 .read()
2886 .ok()
2887 .and_then(|g| g.get(&uid).cloned())
2888 .unwrap_or_default();
2889 let any_user_grants = self
2890 .grants
2891 .read()
2892 .ok()
2893 .map(|g| g.values().any(|list| !list.is_empty()))
2894 .unwrap_or(false);
2895 let public_grants = self
2896 .public_grants
2897 .read()
2898 .ok()
2899 .map(|p| p.clone())
2900 .unwrap_or_default();
2901 if user_grants.is_empty() && public_grants.is_empty() && any_user_grants {
2902 return Err(AuthzError::PermissionDenied {
2903 action,
2904 resource: resource.clone(),
2905 principal: ctx.principal.to_string(),
2906 });
2907 }
2908 let view = GrantsView {
2909 user_grants: &user_grants,
2910 public_grants: &public_grants,
2911 };
2912 let result = check_grant(ctx, action, resource, &view);
2913
2914 if result.is_ok() {
2915 let pc = PermissionCache::build(&user_grants, &public_grants);
2916 if let Ok(mut cache) = self.permission_cache.write() {
2917 cache.insert(uid, pc);
2918 }
2919 }
2920 result
2921 }
2922
2923 pub fn set_user_attributes(
2929 &self,
2930 uid: &UserId,
2931 attrs: UserAttributes,
2932 ) -> Result<(), AuthError> {
2933 let users = self.users.read().map_err(lock_err)?;
2934 if !users.contains_key(uid) {
2935 return Err(AuthError::UserNotFound(uid.to_string()));
2936 }
2937 drop(users);
2938
2939 self.user_attributes
2940 .write()
2941 .unwrap_or_else(|e| e.into_inner())
2942 .insert(uid.clone(), attrs);
2943 self.invalidate_iam_cache(Some(uid));
2944 self.persist_acl_to_kv();
2945 Ok(())
2946 }
2947
2948 pub fn user_attributes(&self, uid: &UserId) -> UserAttributes {
2951 self.user_attributes
2952 .read()
2953 .ok()
2954 .and_then(|m| m.get(uid).cloned())
2955 .unwrap_or_default()
2956 }
2957
2958 pub fn add_user_to_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
2959 if group.trim().is_empty() {
2960 return Err(AuthError::Forbidden("group name cannot be empty".into()));
2961 }
2962 let mut attrs = self.user_attributes(uid);
2963 if !attrs.groups.iter().any(|g| g == group) {
2964 attrs.groups.push(group.to_string());
2965 attrs.groups.sort();
2966 }
2967 self.set_user_attributes(uid, attrs)
2968 }
2969
2970 pub fn remove_user_from_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
2971 let mut attrs = self.user_attributes(uid);
2972 attrs.groups.retain(|g| g != group);
2973 self.set_user_attributes(uid, attrs)
2974 }
2975
2976 pub fn set_user_enabled(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
2978 if let Some(configured) = self.configured_control_events() {
2979 let ctx = default_user_lifecycle_ctx();
2980 let control = UserLifecycleControl {
2981 ctx: &ctx,
2982 ledger: configured.ledger.as_ref(),
2983 config: configured.config,
2984 };
2985 self.set_user_enabled_controlled(uid, enabled, &control)
2986 } else {
2987 self.set_user_enabled_unaudited(uid, enabled)
2988 }
2989 }
2990
2991 pub fn disable_user(
2992 &self,
2993 username: &str,
2994 ctx: &ControlEventCtx<'_>,
2995 ledger: &dyn ControlEventLedger,
2996 config: ControlEventConfig,
2997 ) -> Result<(), AuthError> {
2998 self.disable_user_in_tenant(None, username, ctx, ledger, config)
2999 }
3000
3001 pub fn disable_user_in_tenant(
3002 &self,
3003 tenant_id: Option<&str>,
3004 username: &str,
3005 ctx: &ControlEventCtx<'_>,
3006 ledger: &dyn ControlEventLedger,
3007 config: ControlEventConfig,
3008 ) -> Result<(), AuthError> {
3009 let uid = UserId::from_parts(tenant_id, username);
3010 let control = UserLifecycleControl {
3011 ctx,
3012 ledger,
3013 config,
3014 };
3015 self.set_user_enabled_controlled(&uid, false, &control)
3016 }
3017
3018 fn set_user_enabled_unaudited(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
3019 let mut users = self.users.write().map_err(lock_err)?;
3020 let user = users
3021 .get_mut(uid)
3022 .ok_or_else(|| AuthError::UserNotFound(uid.to_string()))?;
3023 user.enabled = enabled;
3024 user.updated_at = now_ms();
3025 drop(users);
3026 if !enabled {
3027 self.purge_sessions_for(uid);
3028 }
3029 self.persist_to_vault();
3030 Ok(())
3031 }
3032
3033 fn purge_sessions_for(&self, uid: &UserId) {
3036 if let Ok(mut sessions) = self.sessions.write() {
3037 sessions.retain(|_, s| !(s.username == uid.username && s.tenant_id == uid.tenant));
3038 }
3039 }
3040
3041 fn set_user_enabled_controlled(
3042 &self,
3043 uid: &UserId,
3044 enabled: bool,
3045 control: &UserLifecycleControl<'_>,
3046 ) -> Result<(), AuthError> {
3047 let previous = self.get_user_cloned(uid);
3048 let kind = if enabled {
3049 EventKind::UserUpdate
3050 } else {
3051 EventKind::UserDisable
3052 };
3053 let action = if enabled {
3054 "user.update"
3055 } else {
3056 "user.disable"
3057 };
3058 match self.set_user_enabled_unaudited(uid, enabled) {
3059 Ok(()) => {
3060 if let Err(err) = self.emit_user_lifecycle_allowed(
3061 control,
3062 kind,
3063 action,
3064 uid,
3065 user_control_fields(
3066 uid,
3067 previous.as_ref().map(|u| u.role),
3068 Some(enabled),
3069 false,
3070 ),
3071 ) {
3072 self.restore_user_snapshot(uid, previous);
3073 return Err(err);
3074 }
3075 Ok(())
3076 }
3077 Err(err) => {
3078 self.emit_user_lifecycle_outcome(
3079 control,
3080 if user_error_is_denied(&err) {
3081 Outcome::Denied
3082 } else {
3083 Outcome::Error
3084 },
3085 kind,
3086 action,
3087 uid,
3088 Some(err.to_string()),
3089 user_control_fields(
3090 uid,
3091 previous.as_ref().map(|u| u.role),
3092 Some(enabled),
3093 false,
3094 ),
3095 );
3096 Err(err)
3097 }
3098 }
3099 }
3100
3101 fn emit_user_lifecycle_allowed(
3102 &self,
3103 control: &UserLifecycleControl<'_>,
3104 kind: EventKind,
3105 action: &'static str,
3106 id: &UserId,
3107 fields: HashMap<String, Sensitivity>,
3108 ) -> Result<(), AuthError> {
3109 self.emit_user_lifecycle_event(control, Outcome::Allowed, kind, action, id, None, fields)
3110 }
3111
3112 fn emit_api_key_allowed(
3113 &self,
3114 control: &UserLifecycleControl<'_>,
3115 kind: EventKind,
3116 action: &'static str,
3117 api_key_id: &str,
3118 fields: HashMap<String, Sensitivity>,
3119 ) -> Result<(), AuthError> {
3120 self.emit_control_event(
3121 control,
3122 Outcome::Allowed,
3123 kind,
3124 action,
3125 Some(api_key_resource(api_key_id)),
3126 None,
3127 fields,
3128 )
3129 }
3130
3131 #[allow(clippy::too_many_arguments)]
3132 fn emit_user_lifecycle_outcome(
3133 &self,
3134 control: &UserLifecycleControl<'_>,
3135 outcome: Outcome,
3136 kind: EventKind,
3137 action: &'static str,
3138 id: &UserId,
3139 reason: Option<String>,
3140 fields: HashMap<String, Sensitivity>,
3141 ) {
3142 let _ = self.emit_user_lifecycle_event(control, outcome, kind, action, id, reason, fields);
3143 }
3144
3145 #[allow(clippy::too_many_arguments)]
3146 fn emit_user_lifecycle_event(
3147 &self,
3148 control: &UserLifecycleControl<'_>,
3149 outcome: Outcome,
3150 kind: EventKind,
3151 action: &'static str,
3152 id: &UserId,
3153 reason: Option<String>,
3154 fields: HashMap<String, Sensitivity>,
3155 ) -> Result<(), AuthError> {
3156 self.emit_control_event(
3157 control,
3158 outcome,
3159 kind,
3160 action,
3161 Some(user_resource(id)),
3162 reason,
3163 fields,
3164 )
3165 }
3166
3167 #[allow(clippy::too_many_arguments)]
3168 fn emit_control_event(
3169 &self,
3170 control: &UserLifecycleControl<'_>,
3171 outcome: Outcome,
3172 kind: EventKind,
3173 action: &'static str,
3174 resource: Option<String>,
3175 reason: Option<String>,
3176 fields: HashMap<String, Sensitivity>,
3177 ) -> Result<(), AuthError> {
3178 let event = ControlEvent {
3179 kind,
3180 outcome,
3181 action: Cow::Borrowed(action),
3182 resource,
3183 reason,
3184 matched_policy_id: None,
3185 fields,
3186 };
3187 match control.ledger.emit(control.ctx, event) {
3188 Ok(_) => Ok(()),
3189 Err(err) if control.config.require_persistence() => {
3190 Err(AuthError::Internal(err.to_string()))
3191 }
3192 Err(_) => Ok(()),
3193 }
3194 }
3195
3196 fn rollback_create_user(&self, id: &UserId) {
3197 let removed = self
3198 .users
3199 .write()
3200 .unwrap_or_else(|e| e.into_inner())
3201 .remove(id);
3202 if let Some(user) = removed {
3203 self.remove_api_key_index_entries(&user);
3204 }
3205 self.persist_to_vault();
3206 }
3207
3208 fn rollback_bootstrap(&self, id: &UserId) {
3209 self.bootstrapped.store(false, Ordering::Release);
3210 self.rollback_create_user(id);
3211 }
3212
3213 fn restore_user_snapshot(&self, id: &UserId, previous: Option<User>) {
3214 match previous {
3215 Some(user) => {
3216 self.users
3217 .write()
3218 .unwrap_or_else(|e| e.into_inner())
3219 .insert(id.clone(), user.clone());
3220 self.index_user_api_keys(id, &user);
3221 }
3222 None => {
3223 self.rollback_create_user(id);
3224 return;
3225 }
3226 }
3227 self.persist_to_vault();
3228 }
3229
3230 fn user_delete_rollback_snapshot(
3231 &self,
3232 id: &UserId,
3233 tenant_id: Option<&str>,
3234 username: &str,
3235 ) -> Option<(User, Vec<(String, Session)>)> {
3236 let user = self.get_user_cloned(id)?;
3237 let sessions = self
3238 .sessions
3239 .read()
3240 .map(|sessions| {
3241 sessions
3242 .iter()
3243 .filter(|(_, session)| {
3244 session.username == username && session.tenant_id.as_deref() == tenant_id
3245 })
3246 .map(|(token, session)| (token.clone(), session.clone()))
3247 .collect()
3248 })
3249 .unwrap_or_default();
3250 Some((user, sessions))
3251 }
3252
3253 fn restore_deleted_user(&self, id: &UserId, user: User, sessions: Vec<(String, Session)>) {
3254 self.users
3255 .write()
3256 .unwrap_or_else(|e| e.into_inner())
3257 .insert(id.clone(), user.clone());
3258 self.index_user_api_keys(id, &user);
3259 if !sessions.is_empty() {
3260 let mut guard = self.sessions.write().unwrap_or_else(|e| e.into_inner());
3261 for (token, session) in sessions {
3262 guard.insert(token, session);
3263 }
3264 }
3265 self.persist_to_vault();
3266 }
3267
3268 fn rollback_create_api_key(&self, id: &UserId, key: &str) {
3269 let key_id = api_key_credential_id(key);
3270 let key = key_id.as_str();
3271 if let Ok(mut users) = self.users.write() {
3272 if let Some(user) = users.get_mut(id) {
3273 user.api_keys.retain(|api_key| api_key.key != key);
3274 user.updated_at = now_ms();
3275 }
3276 }
3277 if let Ok(mut idx) = self.api_key_index.write() {
3278 idx.remove(key);
3279 }
3280 self.persist_to_vault();
3281 }
3282
3283 fn api_key_rollback_snapshot(&self, key: &str) -> Option<(UserId, ApiKey)> {
3284 let key = api_key_credential_id(key);
3285 let users = self.users.read().ok()?;
3286 users.iter().find_map(|(id, user)| {
3287 user.api_keys
3288 .iter()
3289 .find(|api_key| api_key.key == key)
3290 .cloned()
3291 .map(|api_key| (id.clone(), api_key))
3292 })
3293 }
3294
3295 fn restore_api_key(&self, id: &UserId, api_key: ApiKey) {
3296 if let Ok(mut users) = self.users.write() {
3297 if let Some(user) = users.get_mut(id) {
3298 if !user
3299 .api_keys
3300 .iter()
3301 .any(|candidate| candidate.key == api_key.key)
3302 {
3303 user.api_keys.push(api_key.clone());
3304 user.updated_at = now_ms();
3305 }
3306 }
3307 }
3308 if let Ok(mut idx) = self.api_key_index.write() {
3309 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
3310 }
3311 self.persist_to_vault();
3312 }
3313
3314 fn remove_api_key_index_entries(&self, user: &User) {
3315 if let Ok(mut idx) = self.api_key_index.write() {
3316 for api_key in &user.api_keys {
3317 idx.remove(&api_key.key);
3318 }
3319 }
3320 }
3321
3322 fn index_user_api_keys(&self, id: &UserId, user: &User) {
3323 if let Ok(mut idx) = self.api_key_index.write() {
3324 for api_key in &user.api_keys {
3325 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
3326 }
3327 }
3328 }
3329
3330 pub fn authenticate_with_attrs(
3340 &self,
3341 tenant_id: Option<&str>,
3342 username: &str,
3343 password: &str,
3344 ) -> Result<Session, AuthError> {
3345 let uid = UserId::from_parts(tenant_id, username);
3346 let attrs = self.user_attributes(&uid);
3347
3348 if let Some(deadline) = attrs.valid_until {
3349 if now_ms() >= deadline {
3350 return Err(AuthError::Forbidden(format!(
3351 "account `{}` expired (VALID UNTIL exceeded)",
3352 uid
3353 )));
3354 }
3355 }
3356
3357 if let Some(limit) = attrs.connection_limit {
3358 let current = self
3359 .session_count_by_user
3360 .read()
3361 .ok()
3362 .and_then(|m| m.get(&uid).copied())
3363 .unwrap_or(0);
3364 if current >= limit {
3365 return Err(AuthError::Forbidden(format!(
3366 "account `{}` exceeded CONNECTION LIMIT ({})",
3367 uid, limit
3368 )));
3369 }
3370 }
3371
3372 let session = self.authenticate_in_tenant(tenant_id, username, password)?;
3373
3374 if let Ok(mut counts) = self.session_count_by_user.write() {
3375 *counts.entry(uid).or_insert(0) += 1;
3376 }
3377 Ok(session)
3378 }
3379
3380 pub fn decrement_session_count(&self, uid: &UserId) {
3383 if let Ok(mut counts) = self.session_count_by_user.write() {
3384 if let Some(c) = counts.get_mut(uid) {
3385 *c = c.saturating_sub(1);
3386 }
3387 }
3388 }
3389
3390 pub fn rehydrate_acl(&self) {
3397 let kv_snapshot: Vec<(String, String)> = self
3398 .vault_kv
3399 .read()
3400 .map(|kv| {
3401 kv.iter()
3402 .filter(|(k, _)| {
3403 k.starts_with("red.acl.grants.")
3404 || k.starts_with("red.acl.attrs.")
3405 || k == &"red.acl.public_grants"
3406 })
3407 .map(|(k, v)| (k.clone(), v.clone()))
3408 .collect()
3409 })
3410 .unwrap_or_default();
3411
3412 for (k, v) in kv_snapshot {
3413 if k == "red.acl.public_grants" {
3414 if let Some(parsed) = decode_grants_blob(&v) {
3415 *self
3416 .public_grants
3417 .write()
3418 .unwrap_or_else(|e| e.into_inner()) = parsed;
3419 }
3420 } else if let Some(suffix) = k.strip_prefix("red.acl.grants.") {
3421 if let Some(uid) = decode_uid(suffix) {
3422 if let Some(mut parsed) = decode_grants_blob(&v) {
3423 for g in parsed.iter_mut() {
3426 g.principal = GrantPrincipal::User(uid.clone());
3427 }
3428 self.grants
3429 .write()
3430 .unwrap_or_else(|e| e.into_inner())
3431 .insert(uid, parsed);
3432 }
3433 }
3434 } else if let Some(suffix) = k.strip_prefix("red.acl.attrs.") {
3435 if let Some(uid) = decode_uid(suffix) {
3436 if let Some(parsed) = decode_attrs_blob(&v) {
3437 self.user_attributes
3438 .write()
3439 .unwrap_or_else(|e| e.into_inner())
3440 .insert(uid, parsed);
3441 }
3442 }
3443 }
3444 }
3445
3446 self.permission_cache
3447 .write()
3448 .unwrap_or_else(|e| e.into_inner())
3449 .clear();
3450 }
3451
3452 fn persist_acl_to_kv(&self) {
3454 let public = self
3455 .public_grants
3456 .read()
3457 .ok()
3458 .map(|p| encode_grants_blob(&p))
3459 .unwrap_or_default();
3460 self.vault_kv_set("red.acl.public_grants".to_string(), public);
3461
3462 let snapshot: Vec<(UserId, Vec<Grant>)> = self
3463 .grants
3464 .read()
3465 .ok()
3466 .map(|g| g.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3467 .unwrap_or_default();
3468 for (uid, list) in snapshot {
3469 let key = format!("red.acl.grants.{}", encode_uid(&uid));
3470 let val = encode_grants_blob(&list);
3471 self.vault_kv_set(key, val);
3472 }
3473
3474 let attrs_snapshot: Vec<(UserId, UserAttributes)> = self
3475 .user_attributes
3476 .read()
3477 .ok()
3478 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3479 .unwrap_or_default();
3480 for (uid, attrs) in attrs_snapshot {
3481 let key = format!("red.acl.attrs.{}", encode_uid(&uid));
3482 let val = encode_attrs_blob(&attrs);
3483 self.vault_kv_set(key, val);
3484 }
3485 }
3486
3487 fn invalidate_permission_cache(&self, uid: Option<&UserId>) {
3488 if let Ok(mut cache) = self.permission_cache.write() {
3489 match uid {
3490 Some(u) => {
3491 cache.remove(u);
3492 }
3493 None => cache.clear(),
3494 }
3495 }
3496 }
3497
3498 pub fn put_policy_with_control_events(
3507 &self,
3508 p: Policy,
3509 control: &PolicyMutationControl<'_>,
3510 ) -> Result<(), AuthError> {
3511 let policy_id = p.id.clone();
3512 let kind = if self.get_policy(&policy_id).is_some() {
3513 EventKind::PolicyUpdate
3514 } else {
3515 EventKind::PolicyCreate
3516 };
3517
3518 if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
3519 let err = AuthError::Forbidden(format!("policy id `{}` is reserved", p.id));
3520 self.emit_policy_error(
3521 control,
3522 kind,
3523 "policy:put",
3524 &policy_id,
3525 Some(&p),
3526 None,
3527 &err,
3528 );
3529 return Err(err);
3530 }
3531
3532 if let Some(ManagedPolicyDecision::Deny {
3533 entry_id,
3534 matched_action,
3535 matched_resource,
3536 reason,
3537 ..
3538 }) = self.managed_policy_decision(&policy_id, PolicyOp::Put, control)
3539 {
3540 self.emit_policy_denied(
3541 control,
3542 kind,
3543 "policy:put",
3544 &policy_id,
3545 Some(&p),
3546 None,
3547 &reason.to_string(),
3548 Some(entry_id),
3549 Some((matched_action, matched_resource)),
3550 );
3551 return Err(Self::managed_policy_error(&policy_id, &reason.to_string()));
3552 }
3553
3554 let previous = self.get_policy(&policy_id);
3555 let was_enabled = self.iam_authorization_enabled();
3556 match self.put_policy_internal(p.clone()) {
3557 Ok(()) => match self.emit_policy_allowed(
3558 control,
3559 kind,
3560 "policy:put",
3561 &policy_id,
3562 Some(&p),
3563 None,
3564 None,
3565 ) {
3566 Ok(()) => Ok(()),
3567 Err(err) => {
3568 self.restore_policy_put(&policy_id, previous, was_enabled);
3569 Err(err)
3570 }
3571 },
3572 Err(err) => {
3573 self.emit_policy_error(
3574 control,
3575 kind,
3576 "policy:put",
3577 &policy_id,
3578 Some(&p),
3579 None,
3580 &err,
3581 );
3582 Err(err)
3583 }
3584 }
3585 }
3586
3587 pub fn delete_policy_with_control_events(
3588 &self,
3589 id: &str,
3590 control: &PolicyMutationControl<'_>,
3591 ) -> Result<(), AuthError> {
3592 let existing = self.get_policy(id);
3593 if let Some(ManagedPolicyDecision::Deny {
3594 entry_id,
3595 matched_action,
3596 matched_resource,
3597 reason,
3598 ..
3599 }) = self.managed_policy_decision(id, PolicyOp::Drop, control)
3600 {
3601 self.emit_policy_denied(
3602 control,
3603 EventKind::PolicyDelete,
3604 "policy:drop",
3605 id,
3606 existing.as_deref(),
3607 None,
3608 &reason.to_string(),
3609 Some(entry_id),
3610 Some((matched_action, matched_resource)),
3611 );
3612 return Err(Self::managed_policy_error(id, &reason.to_string()));
3613 }
3614
3615 let user_attachments = self
3616 .user_attachments
3617 .read()
3618 .map(|m| m.clone())
3619 .unwrap_or_default();
3620 let group_attachments = self
3621 .group_attachments
3622 .read()
3623 .map(|m| m.clone())
3624 .unwrap_or_default();
3625 let was_enabled = self.iam_authorization_enabled();
3626 match self.delete_policy(id) {
3627 Ok(()) => match self.emit_policy_allowed(
3628 control,
3629 EventKind::PolicyDelete,
3630 "policy:drop",
3631 id,
3632 existing.as_deref(),
3633 None,
3634 None,
3635 ) {
3636 Ok(()) => Ok(()),
3637 Err(err) => {
3638 if let Some(policy) = existing {
3639 self.restore_policy_delete(
3640 id,
3641 policy,
3642 user_attachments,
3643 group_attachments,
3644 was_enabled,
3645 );
3646 }
3647 Err(err)
3648 }
3649 },
3650 Err(err) => {
3651 self.emit_policy_error(
3652 control,
3653 EventKind::PolicyDelete,
3654 "policy:drop",
3655 id,
3656 existing.as_deref(),
3657 None,
3658 &err,
3659 );
3660 Err(err)
3661 }
3662 }
3663 }
3664
3665 pub fn attach_policy_with_control_events(
3666 &self,
3667 principal: PrincipalRef,
3668 policy_id: &str,
3669 control: &PolicyMutationControl<'_>,
3670 ) -> Result<(), AuthError> {
3671 let existing = self.get_policy(policy_id);
3672 if let Some(ManagedPolicyDecision::Deny {
3673 entry_id,
3674 matched_action,
3675 matched_resource,
3676 reason,
3677 ..
3678 }) = self.managed_policy_decision(policy_id, PolicyOp::Attach, control)
3679 {
3680 self.emit_policy_denied(
3681 control,
3682 EventKind::PolicyAttach,
3683 "policy:attach",
3684 policy_id,
3685 existing.as_deref(),
3686 Some(&principal),
3687 &reason.to_string(),
3688 Some(entry_id),
3689 Some((matched_action, matched_resource)),
3690 );
3691 return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3692 }
3693
3694 if let Some(err_msg) = self.check_self_lock_invariant_for_attach(policy_id) {
3695 self.emit_policy_denied(
3696 control,
3697 EventKind::PolicyAttach,
3698 "policy:attach",
3699 policy_id,
3700 existing.as_deref(),
3701 Some(&principal),
3702 &err_msg,
3703 None,
3704 None,
3705 );
3706 return Err(AuthError::Forbidden(err_msg));
3707 }
3708
3709 let user_attachments = self
3710 .user_attachments
3711 .read()
3712 .map(|m| m.clone())
3713 .unwrap_or_default();
3714 let group_attachments = self
3715 .group_attachments
3716 .read()
3717 .map(|m| m.clone())
3718 .unwrap_or_default();
3719 match self.attach_policy(principal.clone(), policy_id) {
3720 Ok(()) => match self.emit_policy_allowed(
3721 control,
3722 EventKind::PolicyAttach,
3723 "policy:attach",
3724 policy_id,
3725 existing.as_deref(),
3726 Some(&principal),
3727 None,
3728 ) {
3729 Ok(()) => Ok(()),
3730 Err(err) => {
3731 self.restore_policy_attachments(user_attachments, group_attachments);
3732 Err(err)
3733 }
3734 },
3735 Err(err) => {
3736 self.emit_policy_error(
3737 control,
3738 EventKind::PolicyAttach,
3739 "policy:attach",
3740 policy_id,
3741 existing.as_deref(),
3742 Some(&principal),
3743 &err,
3744 );
3745 Err(err)
3746 }
3747 }
3748 }
3749
3750 pub fn detach_policy_with_control_events(
3751 &self,
3752 principal: PrincipalRef,
3753 policy_id: &str,
3754 control: &PolicyMutationControl<'_>,
3755 ) -> Result<(), AuthError> {
3756 let existing = self.get_policy(policy_id);
3757 if let Some(ManagedPolicyDecision::Deny {
3758 entry_id,
3759 matched_action,
3760 matched_resource,
3761 reason,
3762 ..
3763 }) = self.managed_policy_decision(policy_id, PolicyOp::Detach, control)
3764 {
3765 self.emit_policy_denied(
3766 control,
3767 EventKind::PolicyDetach,
3768 "policy:detach",
3769 policy_id,
3770 existing.as_deref(),
3771 Some(&principal),
3772 &reason.to_string(),
3773 Some(entry_id),
3774 Some((matched_action, matched_resource)),
3775 );
3776 return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3777 }
3778 let Some(existing_policy) = existing else {
3779 let err = AuthError::Forbidden(format!("policy `{policy_id}` not found"));
3780 self.emit_policy_error(
3781 control,
3782 EventKind::PolicyDetach,
3783 "policy:detach",
3784 policy_id,
3785 None,
3786 Some(&principal),
3787 &err,
3788 );
3789 return Err(err);
3790 };
3791
3792 let user_attachments = self
3793 .user_attachments
3794 .read()
3795 .map(|m| m.clone())
3796 .unwrap_or_default();
3797 let group_attachments = self
3798 .group_attachments
3799 .read()
3800 .map(|m| m.clone())
3801 .unwrap_or_default();
3802 match self.detach_policy(principal.clone(), policy_id) {
3803 Ok(()) => match self.emit_policy_allowed(
3804 control,
3805 EventKind::PolicyDetach,
3806 "policy:detach",
3807 policy_id,
3808 Some(existing_policy.as_ref()),
3809 Some(&principal),
3810 None,
3811 ) {
3812 Ok(()) => Ok(()),
3813 Err(err) => {
3814 self.restore_policy_attachments(user_attachments, group_attachments);
3815 Err(err)
3816 }
3817 },
3818 Err(err) => {
3819 self.emit_policy_error(
3820 control,
3821 EventKind::PolicyDetach,
3822 "policy:detach",
3823 policy_id,
3824 Some(existing_policy.as_ref()),
3825 Some(&principal),
3826 &err,
3827 );
3828 Err(err)
3829 }
3830 }
3831 }
3832
3833 fn managed_policy_decision(
3834 &self,
3835 policy_id: &str,
3836 op: PolicyOp,
3837 control: &PolicyMutationControl<'_>,
3838 ) -> Option<ManagedPolicyDecision> {
3839 control.registry.map(|registry| {
3840 ManagedPolicyGate::new(registry).check_mutation(
3841 self,
3842 control.actor,
3843 control.eval_ctx,
3844 policy_id,
3845 op,
3846 )
3847 })
3848 }
3849
3850 fn check_self_lock_invariant_for_attach(&self, _policy_id: &str) -> Option<String> {
3857 use crate::auth::self_lock_guard::{
3858 check_self_lock_invariant, format_block_error, InvariantOutcome,
3859 };
3860 let policies: Vec<Arc<Policy>> = match self.policies.read() {
3861 Ok(map) => map.values().cloned().collect(),
3862 Err(_) => return None,
3863 };
3864 let outcome = check_self_lock_invariant(&policies);
3865 match &outcome {
3866 InvariantOutcome::Ok => None,
3867 InvariantOutcome::Blocked { .. } => format_block_error(&outcome),
3868 }
3869 }
3870
3871 fn managed_policy_error(policy_id: &str, reason: &str) -> AuthError {
3872 AuthError::Forbidden(format!(
3873 "managed policy mutation blocked for `{policy_id}`: {reason}"
3874 ))
3875 }
3876
3877 fn emit_policy_allowed(
3878 &self,
3879 control: &PolicyMutationControl<'_>,
3880 kind: EventKind,
3881 action: &'static str,
3882 policy_id: &str,
3883 policy: Option<&Policy>,
3884 principal: Option<&PrincipalRef>,
3885 matched_policy_id: Option<String>,
3886 ) -> Result<(), AuthError> {
3887 let event = ControlEvent {
3888 kind,
3889 outcome: Outcome::Allowed,
3890 action: std::borrow::Cow::Borrowed(action),
3891 resource: Some(format!("policy:{policy_id}")),
3892 reason: None,
3893 matched_policy_id,
3894 fields: policy_control_fields(policy_id, policy, principal),
3895 };
3896 match control.ledger.emit(control.ctx, event) {
3897 Ok(_) => Ok(()),
3898 Err(err) if control.config.require_persistence() => {
3899 Err(AuthError::Internal(err.to_string()))
3900 }
3901 Err(_) => Ok(()),
3902 }
3903 }
3904
3905 #[allow(clippy::too_many_arguments)]
3906 fn emit_policy_denied(
3907 &self,
3908 control: &PolicyMutationControl<'_>,
3909 kind: EventKind,
3910 action: &'static str,
3911 policy_id: &str,
3912 policy: Option<&Policy>,
3913 principal: Option<&PrincipalRef>,
3914 reason: &str,
3915 matched_policy_id: Option<String>,
3916 matched: Option<(String, String)>,
3917 ) {
3918 let mut fields = policy_control_fields(policy_id, policy, principal);
3919 if let Some((matched_action, matched_resource)) = matched {
3920 fields.insert(
3921 "matched_action".to_string(),
3922 Sensitivity::raw(matched_action),
3923 );
3924 fields.insert(
3925 "matched_resource".to_string(),
3926 Sensitivity::raw(matched_resource),
3927 );
3928 }
3929 let event = ControlEvent {
3930 kind,
3931 outcome: Outcome::Denied,
3932 action: std::borrow::Cow::Borrowed(action),
3933 resource: Some(format!("policy:{policy_id}")),
3934 reason: Some(reason.to_string()),
3935 matched_policy_id,
3936 fields,
3937 };
3938 let _ = control.ledger.emit(control.ctx, event);
3939 }
3940
3941 fn emit_policy_error(
3942 &self,
3943 control: &PolicyMutationControl<'_>,
3944 kind: EventKind,
3945 action: &'static str,
3946 policy_id: &str,
3947 policy: Option<&Policy>,
3948 principal: Option<&PrincipalRef>,
3949 err: &AuthError,
3950 ) {
3951 let event = ControlEvent {
3952 kind,
3953 outcome: Outcome::Error,
3954 action: std::borrow::Cow::Borrowed(action),
3955 resource: Some(format!("policy:{policy_id}")),
3956 reason: Some(err.to_string()),
3957 matched_policy_id: None,
3958 fields: policy_control_fields(policy_id, policy, principal),
3959 };
3960 let _ = control.ledger.emit(control.ctx, event);
3961 }
3962
3963 fn restore_policy_put(
3964 &self,
3965 policy_id: &str,
3966 previous: Option<Arc<Policy>>,
3967 was_enabled: bool,
3968 ) {
3969 let mut policies = self.policies.write().unwrap_or_else(|e| e.into_inner());
3970 match previous {
3971 Some(policy) => {
3972 policies.insert(policy_id.to_string(), policy);
3973 }
3974 None => {
3975 policies.remove(policy_id);
3976 }
3977 }
3978 drop(policies);
3979 self.iam_authorization_enabled
3980 .store(was_enabled, Ordering::Release);
3981 self.iam_effective_cache
3982 .write()
3983 .unwrap_or_else(|e| e.into_inner())
3984 .clear();
3985 self.invalidate_visible_collections_cache();
3986 self.persist_iam_to_kv();
3987 }
3988
3989 fn restore_policy_delete(
3990 &self,
3991 policy_id: &str,
3992 policy: Arc<Policy>,
3993 user_attachments: HashMap<UserId, Vec<String>>,
3994 group_attachments: HashMap<String, Vec<String>>,
3995 was_enabled: bool,
3996 ) {
3997 self.policies
3998 .write()
3999 .unwrap_or_else(|e| e.into_inner())
4000 .insert(policy_id.to_string(), policy);
4001 self.restore_policy_attachments(user_attachments, group_attachments);
4002 self.iam_authorization_enabled
4003 .store(was_enabled, Ordering::Release);
4004 self.persist_iam_to_kv();
4005 }
4006
4007 fn restore_policy_attachments(
4008 &self,
4009 user_attachments: HashMap<UserId, Vec<String>>,
4010 group_attachments: HashMap<String, Vec<String>>,
4011 ) {
4012 *self
4013 .user_attachments
4014 .write()
4015 .unwrap_or_else(|e| e.into_inner()) = user_attachments;
4016 *self
4017 .group_attachments
4018 .write()
4019 .unwrap_or_else(|e| e.into_inner()) = group_attachments;
4020 self.iam_effective_cache
4021 .write()
4022 .unwrap_or_else(|e| e.into_inner())
4023 .clear();
4024 self.invalidate_visible_collections_cache();
4025 self.persist_iam_to_kv();
4026 }
4027
4028 pub fn put_policy(&self, p: Policy) -> Result<(), AuthError> {
4033 if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
4034 return Err(AuthError::Forbidden(format!(
4035 "policy id `{}` is reserved",
4036 p.id
4037 )));
4038 }
4039 self.put_policy_internal(p)
4040 }
4041
4042 pub fn put_policy_internal(&self, p: Policy) -> Result<(), AuthError> {
4047 p.validate()
4048 .map_err(|e| AuthError::Forbidden(format!("invalid policy `{}`: {e}", p.id)))?;
4049 let id = p.id.clone();
4050 self.policies
4051 .write()
4052 .unwrap_or_else(|e| e.into_inner())
4053 .insert(id, Arc::new(p));
4054 self.iam_authorization_enabled
4055 .store(true, Ordering::Release);
4056 self.iam_effective_cache
4057 .write()
4058 .unwrap_or_else(|e| e.into_inner())
4059 .clear();
4060 self.invalidate_visible_collections_cache();
4064 self.persist_iam_to_kv();
4065 Ok(())
4066 }
4067
4068 pub fn iam_authorization_enabled(&self) -> bool {
4073 self.iam_authorization_enabled.load(Ordering::Acquire)
4074 }
4075
4076 pub fn delete_policy(&self, id: &str) -> Result<(), AuthError> {
4078 let removed = self
4079 .policies
4080 .write()
4081 .unwrap_or_else(|e| e.into_inner())
4082 .remove(id)
4083 .is_some();
4084 if !removed {
4085 return Err(AuthError::Forbidden(format!("policy `{id}` not found")));
4086 }
4087 if let Ok(mut ua) = self.user_attachments.write() {
4089 for ids in ua.values_mut() {
4090 ids.retain(|p| p != id);
4091 }
4092 ua.retain(|_, v| !v.is_empty());
4093 }
4094 if let Ok(mut ga) = self.group_attachments.write() {
4095 for ids in ga.values_mut() {
4096 ids.retain(|p| p != id);
4097 }
4098 ga.retain(|_, v| !v.is_empty());
4099 }
4100 self.iam_effective_cache
4101 .write()
4102 .unwrap_or_else(|e| e.into_inner())
4103 .clear();
4104 self.invalidate_visible_collections_cache();
4107 self.persist_iam_to_kv();
4108 Ok(())
4109 }
4110
4111 pub fn list_policies(&self) -> Vec<Arc<Policy>> {
4113 let map = match self.policies.read() {
4114 Ok(g) => g,
4115 Err(_) => return Vec::new(),
4116 };
4117 let mut out: Vec<Arc<Policy>> = map.values().cloned().collect();
4118 out.sort_by(|a, b| a.id.cmp(&b.id));
4119 out
4120 }
4121
4122 pub fn get_policy(&self, id: &str) -> Option<Arc<Policy>> {
4124 self.policies.read().ok().and_then(|m| m.get(id).cloned())
4125 }
4126
4127 pub fn group_policies(&self, group: &str) -> Vec<Arc<Policy>> {
4129 let policies = self.policies.read();
4130 let attachments = self.group_attachments.read();
4131 let mut out = Vec::new();
4132 if let (Ok(p_map), Ok(ga_map)) = (policies, attachments) {
4133 if let Some(ids) = ga_map.get(group) {
4134 for id in ids {
4135 if let Some(p) = p_map.get(id) {
4136 out.push(p.clone());
4137 }
4138 }
4139 }
4140 }
4141 out.sort_by(|a, b| a.id.cmp(&b.id));
4142 out
4143 }
4144
4145 pub fn delete_synthetic_grant_policies(
4149 &self,
4150 principal: &GrantPrincipal,
4151 resource: &Resource,
4152 actions: &[Action],
4153 ) -> usize {
4154 let attached = match principal {
4155 GrantPrincipal::User(uid) => self
4156 .user_attachments
4157 .read()
4158 .ok()
4159 .and_then(|m| m.get(uid).cloned())
4160 .unwrap_or_default(),
4161 GrantPrincipal::Group(group) => self
4162 .group_attachments
4163 .read()
4164 .ok()
4165 .and_then(|m| m.get(group).cloned())
4166 .unwrap_or_default(),
4167 GrantPrincipal::Public => self
4168 .group_attachments
4169 .read()
4170 .ok()
4171 .and_then(|m| m.get(PUBLIC_IAM_GROUP).cloned())
4172 .unwrap_or_default(),
4173 };
4174 if attached.is_empty() {
4175 return 0;
4176 }
4177
4178 let mut delete_ids = Vec::new();
4179 if let Ok(policies) = self.policies.read() {
4180 for id in attached {
4181 let Some(policy) = policies.get(&id) else {
4182 continue;
4183 };
4184 if !policy.id.starts_with("_grant_") {
4185 continue;
4186 }
4187 if synthetic_grant_matches(policy, resource, actions) {
4188 delete_ids.push(policy.id.clone());
4189 }
4190 }
4191 }
4192
4193 let mut deleted = 0usize;
4194 for id in delete_ids {
4195 if self.delete_policy(&id).is_ok() {
4196 deleted += 1;
4197 }
4198 }
4199 deleted
4200 }
4201
4202 pub fn apply_policy_break_glass(&self, boot_ts_ms: u128) -> Result<(), AuthError> {
4210 use crate::auth::self_lock_guard::{
4211 break_glass_audit_fields, unlock_policy, PLATFORM_OWNER_UNLOCK_POLICY_ID,
4212 PLATFORM_OWNER_USERNAME,
4213 };
4214
4215 let policy = unlock_policy();
4219 self.put_policy_internal(policy)?;
4220
4221 let owner_id = UserId::platform(PLATFORM_OWNER_USERNAME);
4226 {
4227 let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
4228 users.entry(owner_id.clone()).or_insert_with(|| User {
4229 username: PLATFORM_OWNER_USERNAME.to_string(),
4230 tenant_id: None,
4231 password_hash: String::new(),
4236 scram_verifier: None,
4237 role: Role::Admin,
4238 api_keys: Vec::new(),
4239 created_at: boot_ts_ms,
4240 updated_at: boot_ts_ms,
4241 enabled: false,
4242 });
4243 }
4244
4245 self.attach_policy(
4249 PrincipalRef::User(owner_id.clone()),
4250 PLATFORM_OWNER_UNLOCK_POLICY_ID,
4251 )?;
4252
4253 let fields_raw = break_glass_audit_fields(boot_ts_ms);
4258 let mut fields: HashMap<String, Sensitivity> = HashMap::new();
4259 for (k, v) in fields_raw {
4260 fields.insert(k, Sensitivity::raw(v));
4261 }
4262 if let Some(configured) = self.configured_control_events() {
4263 let ctx = ControlEventCtx {
4264 actor: crate::runtime::control_events::ActorRef::System("break_glass"),
4265 scope: None,
4266 request_id: None,
4267 trace_id: None,
4268 };
4269 let event = ControlEvent {
4270 kind: EventKind::PolicyBreakGlass,
4271 outcome: Outcome::Allowed,
4272 action: std::borrow::Cow::Borrowed("policy:break_glass"),
4273 resource: Some(format!("policy:{PLATFORM_OWNER_UNLOCK_POLICY_ID}")),
4274 reason: Some("REDDB_POLICY_BREAK_GLASS=1 at boot".into()),
4275 matched_policy_id: None,
4276 fields,
4277 };
4278 let _ = configured.ledger.emit(&ctx, event);
4279 }
4280 tracing::warn!(
4281 policy_id = PLATFORM_OWNER_UNLOCK_POLICY_ID,
4282 principal = PLATFORM_OWNER_USERNAME,
4283 boot_ts_ms = %boot_ts_ms,
4284 "REDDB_POLICY_BREAK_GLASS=1 — installed/refreshed platform-owner unlock policy and rebound to synthetic principal"
4285 );
4286 Ok(())
4287 }
4288
4289 pub fn attach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
4292 if !self
4293 .policies
4294 .read()
4295 .map(|m| m.contains_key(policy_id))
4296 .unwrap_or(false)
4297 {
4298 return Err(AuthError::Forbidden(format!(
4299 "policy `{policy_id}` not found"
4300 )));
4301 }
4302 match &principal {
4303 PrincipalRef::User(uid) => {
4304 let mut ua = self
4305 .user_attachments
4306 .write()
4307 .unwrap_or_else(|e| e.into_inner());
4308 let list = ua.entry(uid.clone()).or_default();
4309 if !list.iter().any(|p| p == policy_id) {
4310 list.push(policy_id.to_string());
4311 }
4312 drop(ua);
4313 self.invalidate_iam_cache(Some(uid));
4314 }
4315 PrincipalRef::Group(g) => {
4316 let mut ga = self
4317 .group_attachments
4318 .write()
4319 .unwrap_or_else(|e| e.into_inner());
4320 let list = ga.entry(g.clone()).or_default();
4321 if !list.iter().any(|p| p == policy_id) {
4322 list.push(policy_id.to_string());
4323 }
4324 drop(ga);
4325 self.invalidate_iam_cache(None);
4326 }
4327 }
4328 self.persist_iam_to_kv();
4329 Ok(())
4330 }
4331
4332 pub fn detach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
4334 match &principal {
4335 PrincipalRef::User(uid) => {
4336 if let Ok(mut ua) = self.user_attachments.write() {
4337 if let Some(list) = ua.get_mut(uid) {
4338 list.retain(|p| p != policy_id);
4339 if list.is_empty() {
4340 ua.remove(uid);
4341 }
4342 }
4343 }
4344 self.invalidate_iam_cache(Some(uid));
4345 }
4346 PrincipalRef::Group(g) => {
4347 if let Ok(mut ga) = self.group_attachments.write() {
4348 if let Some(list) = ga.get_mut(g) {
4349 list.retain(|p| p != policy_id);
4350 if list.is_empty() {
4351 ga.remove(g);
4352 }
4353 }
4354 }
4355 self.invalidate_iam_cache(None);
4356 }
4357 }
4358 self.persist_iam_to_kv();
4359 Ok(())
4360 }
4361
4362 pub fn effective_policies(&self, user: &UserId) -> Vec<Arc<Policy>> {
4366 if let Ok(cache) = self.iam_effective_cache.read() {
4367 if let Some(hit) = cache.get(user) {
4368 return hit.clone();
4369 }
4370 }
4371 let policies = self.policies.read();
4372 let user_attachments = self.user_attachments.read();
4373 let group_attachments = self.group_attachments.read();
4374 let mut groups = self
4375 .user_attributes
4376 .read()
4377 .ok()
4378 .and_then(|m| m.get(user).map(|attrs| attrs.groups.clone()))
4379 .unwrap_or_default();
4380 groups.insert(0, PUBLIC_IAM_GROUP.to_string());
4381 let mut out: Vec<Arc<Policy>> = Vec::new();
4382 if let (Ok(p_map), Ok(ua_map), Ok(ga_map)) = (policies, user_attachments, group_attachments)
4383 {
4384 for group in groups {
4385 if let Some(ids) = ga_map.get(&group) {
4386 for id in ids {
4387 if let Some(p) = p_map.get(id) {
4388 out.push(p.clone());
4389 }
4390 }
4391 }
4392 }
4393 if let Some(ids) = ua_map.get(user) {
4394 for id in ids {
4395 if let Some(p) = p_map.get(id) {
4396 out.push(p.clone());
4397 }
4398 }
4399 }
4400 }
4401 if let Ok(mut cache) = self.iam_effective_cache.write() {
4402 cache.insert(user.clone(), out.clone());
4403 }
4404 out
4405 }
4406
4407 pub fn simulate(
4410 &self,
4411 principal: &UserId,
4412 action: &str,
4413 resource: &ResourceRef,
4414 ctx_extras: SimCtx,
4415 ) -> SimulationOutcome {
4416 let user_role = self
4417 .users
4418 .read()
4419 .ok()
4420 .and_then(|u| u.get(principal).map(|u| u.role));
4421 let principal_is_admin_role = user_role == Some(Role::Admin);
4422 let now = ctx_extras.now_ms.unwrap_or_else(now_ms);
4423 let ctx = EvalContext {
4424 principal_tenant: principal.tenant.clone(),
4425 current_tenant: ctx_extras.current_tenant,
4426 peer_ip: ctx_extras.peer_ip,
4427 mfa_present: ctx_extras.mfa_present,
4428 now_ms: now,
4429 principal_is_admin_role,
4430 principal_is_platform_scoped: principal.tenant.is_none(),
4431 };
4432 let pols = self.effective_policies(principal);
4433 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4434 iam_policies::simulate(&refs, action, resource, &ctx)
4435 }
4436
4437 pub fn check_policy_authz(
4448 &self,
4449 principal: &UserId,
4450 action: &str,
4451 resource: &ResourceRef,
4452 ctx: &EvalContext,
4453 ) -> bool {
4454 let pols = self.effective_policies(principal);
4455 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4456 let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4457 matches!(
4458 decision,
4459 iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass
4460 )
4461 }
4462
4463 pub fn check_policy_authz_with_role(
4479 &self,
4480 principal: &UserId,
4481 action: &str,
4482 resource: &ResourceRef,
4483 ctx: &EvalContext,
4484 role: Role,
4485 ) -> bool {
4486 let pols = self.effective_policies(principal);
4487 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4488 let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4489 match decision {
4490 iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass => true,
4491 iam_policies::Decision::Deny { .. } => false,
4492 iam_policies::Decision::DefaultDeny => match self.enforcement_mode() {
4493 PolicyEnforcementMode::LegacyRbac => legacy_rbac_decision(role, action),
4494 PolicyEnforcementMode::PolicyOnly => false,
4495 },
4496 }
4497 }
4498
4499 pub fn enforcement_mode(&self) -> PolicyEnforcementMode {
4506 *self
4507 .enforcement_mode
4508 .read()
4509 .unwrap_or_else(|e| e.into_inner())
4510 }
4511
4512 pub fn set_enforcement_mode(&self, mode: PolicyEnforcementMode) -> PolicyEnforcementMode {
4518 let prev = {
4519 let mut guard = self
4520 .enforcement_mode
4521 .write()
4522 .unwrap_or_else(|e| e.into_inner());
4523 let prev = *guard;
4524 *guard = mode;
4525 prev
4526 };
4527 self.vault_kv_set(
4528 "red.config.policy.enforcement_mode".to_string(),
4529 mode.as_str().to_string(),
4530 );
4531 prev
4532 }
4533
4534 pub fn take_legacy_rbac_warn_once(&self) -> bool {
4540 if self.enforcement_mode() != PolicyEnforcementMode::LegacyRbac {
4541 return false;
4542 }
4543 !self
4544 .legacy_rbac_boot_warn_emitted
4545 .swap(true, Ordering::AcqRel)
4546 }
4547
4548 pub fn check_column_projection_authz(
4552 &self,
4553 principal: &UserId,
4554 request: &ColumnAccessRequest,
4555 ctx: &EvalContext,
4556 ) -> ColumnPolicyOutcome {
4557 let pols = self.effective_policies(principal);
4558 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4559 ColumnPolicyGate::new(&refs).evaluate(request, ctx)
4560 }
4561
4562 fn invalidate_iam_cache(&self, uid: Option<&UserId>) {
4563 if let Ok(mut cache) = self.iam_effective_cache.write() {
4564 match uid {
4565 Some(u) => {
4566 cache.remove(u);
4567 }
4568 None => cache.clear(),
4569 }
4570 }
4571 }
4572
4573 pub fn invalidate_all_iam_cache(&self) {
4577 self.invalidate_iam_cache(None);
4578 }
4579
4580 pub fn rehydrate_iam(&self) {
4588 let mut enabled = self
4589 .vault_kv_get("red.iam.enabled")
4590 .map(|v| v == "true")
4591 .unwrap_or(false);
4592 if let Some(blob) = self.vault_kv_get("red.iam.policies") {
4594 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4595 if let Some(obj) = val.as_object() {
4596 let mut map = HashMap::new();
4597 for (id, body) in obj.iter() {
4598 let s = body.to_string_compact();
4599 if let Ok(p) = Policy::from_json_str(&s) {
4600 map.insert(id.clone(), Arc::new(p));
4601 }
4602 }
4603 if !map.is_empty() {
4604 enabled = true;
4605 }
4606 *self.policies.write().unwrap_or_else(|e| e.into_inner()) = map;
4607 }
4608 }
4609 }
4610 if let Some(blob) = self.vault_kv_get("red.iam.attachments.users") {
4612 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4613 if let Some(obj) = val.as_object() {
4614 let mut map: HashMap<UserId, Vec<String>> = HashMap::new();
4615 for (encoded_uid, ids_v) in obj.iter() {
4616 let Some(uid) = decode_uid(encoded_uid) else {
4617 continue;
4618 };
4619 if let Some(arr) = ids_v.as_array() {
4620 let ids: Vec<String> = arr
4621 .iter()
4622 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4623 .collect();
4624 map.insert(uid, ids);
4625 }
4626 }
4627 *self
4628 .user_attachments
4629 .write()
4630 .unwrap_or_else(|e| e.into_inner()) = map;
4631 }
4632 }
4633 }
4634 if let Some(blob) = self.vault_kv_get("red.iam.attachments.groups") {
4636 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4637 if let Some(obj) = val.as_object() {
4638 let mut map: HashMap<String, Vec<String>> = HashMap::new();
4639 for (g, ids_v) in obj.iter() {
4640 if let Some(arr) = ids_v.as_array() {
4641 let ids: Vec<String> = arr
4642 .iter()
4643 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4644 .collect();
4645 map.insert(g.clone(), ids);
4646 }
4647 }
4648 *self
4649 .group_attachments
4650 .write()
4651 .unwrap_or_else(|e| e.into_inner()) = map;
4652 }
4653 }
4654 }
4655 self.iam_authorization_enabled
4656 .store(enabled, Ordering::Release);
4657 self.invalidate_iam_cache(None);
4658
4659 if let Some(stored) = self.vault_kv_get("red.config.policy.enforcement_mode") {
4666 if let Some(mode) = PolicyEnforcementMode::parse(&stored) {
4667 *self
4668 .enforcement_mode
4669 .write()
4670 .unwrap_or_else(|e| e.into_inner()) = mode;
4671 }
4672 }
4673
4674 if self.take_legacy_rbac_warn_once() {
4679 tracing::warn!(
4680 target: "reddb::auth::enforcement_mode",
4681 mode = "legacy_rbac",
4682 hard_version = crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4683 "policy enforcement_mode=legacy_rbac (transitional). Run \
4684 `MIGRATE POLICY MODE TO 'policy_only'` (next slice S5B) \
4685 before {} to flip this install over to the strict posture.",
4686 crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4687 );
4688 }
4689 }
4690
4691 fn persist_iam_to_kv(&self) {
4694 let enabled = if self.iam_authorization_enabled() {
4695 "true"
4696 } else {
4697 "false"
4698 };
4699 self.vault_kv_set("red.iam.enabled".to_string(), enabled.to_string());
4700
4701 let policies_obj = {
4703 let map = self.policies.read().unwrap_or_else(|e| e.into_inner());
4704 let mut obj = crate::serde_json::Map::new();
4705 for (id, p) in map.iter() {
4706 let s = p.to_json_string();
4707 if let Ok(v) = crate::serde_json::from_str::<crate::serde_json::Value>(&s) {
4708 obj.insert(id.clone(), v);
4709 }
4710 }
4711 crate::serde_json::Value::Object(obj).to_string_compact()
4712 };
4713 self.vault_kv_set("red.iam.policies".to_string(), policies_obj);
4714
4715 let users_obj = {
4717 let map = self
4718 .user_attachments
4719 .read()
4720 .unwrap_or_else(|e| e.into_inner());
4721 let mut obj = crate::serde_json::Map::new();
4722 for (uid, ids) in map.iter() {
4723 let arr = crate::serde_json::Value::Array(
4724 ids.iter()
4725 .map(|s| crate::serde_json::Value::String(s.clone()))
4726 .collect(),
4727 );
4728 obj.insert(encode_uid(uid), arr);
4729 }
4730 crate::serde_json::Value::Object(obj).to_string_compact()
4731 };
4732 self.vault_kv_set("red.iam.attachments.users".to_string(), users_obj);
4733
4734 let groups_obj = {
4736 let map = self
4737 .group_attachments
4738 .read()
4739 .unwrap_or_else(|e| e.into_inner());
4740 let mut obj = crate::serde_json::Map::new();
4741 for (g, ids) in map.iter() {
4742 let arr = crate::serde_json::Value::Array(
4743 ids.iter()
4744 .map(|s| crate::serde_json::Value::String(s.clone()))
4745 .collect(),
4746 );
4747 obj.insert(g.clone(), arr);
4748 }
4749 crate::serde_json::Value::Object(obj).to_string_compact()
4750 };
4751 self.vault_kv_set("red.iam.attachments.groups".to_string(), groups_obj);
4752 }
4753}
4754
4755fn synthetic_grant_matches(policy: &Policy, resource: &Resource, actions: &[Action]) -> bool {
4756 policy.statements.iter().any(|st| {
4757 st.effect == crate::auth::policies::Effect::Allow
4758 && st.condition.is_none()
4759 && grant_actions_overlap(&st.actions, actions)
4760 && grant_resource_matches(&st.resources, resource)
4761 })
4762}
4763
4764fn grant_actions_overlap(
4765 patterns: &[crate::auth::policies::ActionPattern],
4766 actions: &[Action],
4767) -> bool {
4768 if actions.contains(&Action::All) {
4769 return true;
4770 }
4771 patterns.iter().any(|pat| match pat {
4772 crate::auth::policies::ActionPattern::Wildcard => true,
4773 crate::auth::policies::ActionPattern::Exact(s) => {
4774 actions.iter().any(|a| s.eq_ignore_ascii_case(a.as_str()))
4775 }
4776 crate::auth::policies::ActionPattern::Prefix(_) => false,
4777 })
4778}
4779
4780fn grant_resource_matches(
4781 patterns: &[crate::auth::policies::ResourcePattern],
4782 resource: &Resource,
4783) -> bool {
4784 let expected = grant_resource_pattern(resource);
4785 patterns.iter().any(|pat| pat == &expected)
4786}
4787
4788fn grant_resource_pattern(resource: &Resource) -> crate::auth::policies::ResourcePattern {
4789 use crate::auth::policies::ResourcePattern;
4790
4791 match resource {
4792 Resource::Database => ResourcePattern::Glob("table:*".to_string()),
4793 Resource::Schema(s) => ResourcePattern::Glob(format!("table:{s}.*")),
4794 Resource::Table { schema, table } => ResourcePattern::Exact {
4795 kind: "table".to_string(),
4796 name: match schema {
4797 Some(s) => format!("{s}.{table}"),
4798 None => table.clone(),
4799 },
4800 },
4801 Resource::Function { schema, name } => ResourcePattern::Exact {
4802 kind: "function".to_string(),
4803 name: match schema {
4804 Some(s) => format!("{s}.{name}"),
4805 None => name.clone(),
4806 },
4807 },
4808 }
4809}
4810
4811fn encode_uid(uid: &UserId) -> String {
4827 match &uid.tenant {
4828 Some(t) => format!("{}/{}", t, uid.username),
4829 None => format!("*/{}", uid.username),
4830 }
4831}
4832
4833fn decode_uid(s: &str) -> Option<UserId> {
4834 let (tenant, username) = s.split_once('/')?;
4835 Some(if tenant == "*" {
4836 UserId::platform(username)
4837 } else {
4838 UserId::scoped(tenant, username)
4839 })
4840}
4841
4842fn encode_resource(r: &Resource) -> String {
4843 match r {
4844 Resource::Database => "db".into(),
4845 Resource::Schema(s) => format!("schema:{}", s),
4846 Resource::Table { schema, table } => {
4847 format!("table:{}:{}", schema.as_deref().unwrap_or("*"), table)
4848 }
4849 Resource::Function { schema, name } => {
4850 format!("func:{}:{}", schema.as_deref().unwrap_or("*"), name)
4851 }
4852 }
4853}
4854
4855fn decode_resource(s: &str) -> Option<Resource> {
4856 if s == "db" {
4857 return Some(Resource::Database);
4858 }
4859 if let Some(rest) = s.strip_prefix("schema:") {
4860 return Some(Resource::Schema(rest.to_string()));
4861 }
4862 if let Some(rest) = s.strip_prefix("table:") {
4863 let (schema, table) = rest.split_once(':')?;
4864 return Some(Resource::Table {
4865 schema: if schema == "*" {
4866 None
4867 } else {
4868 Some(schema.to_string())
4869 },
4870 table: table.to_string(),
4871 });
4872 }
4873 if let Some(rest) = s.strip_prefix("func:") {
4874 let (schema, name) = rest.split_once(':')?;
4875 return Some(Resource::Function {
4876 schema: if schema == "*" {
4877 None
4878 } else {
4879 Some(schema.to_string())
4880 },
4881 name: name.to_string(),
4882 });
4883 }
4884 None
4885}
4886
4887fn encode_grants_blob(grants: &[Grant]) -> String {
4888 let mut out = String::new();
4889 for g in grants {
4890 let actions: Vec<&str> = g.actions.iter().map(|a| a.as_str()).collect();
4891 out.push_str(&format!(
4892 "GRANT|{}|{}|{}|{}|{}|{}\n",
4893 encode_resource(&g.resource),
4894 actions.join(","),
4895 g.with_grant_option,
4896 g.tenant.as_deref().unwrap_or("*"),
4897 g.granted_by,
4898 g.granted_at,
4899 ));
4900 }
4901 out
4902}
4903
4904fn decode_grants_blob(s: &str) -> Option<Vec<Grant>> {
4905 let mut out = Vec::new();
4906 for line in s.lines() {
4907 if line.is_empty() {
4908 continue;
4909 }
4910 let parts: Vec<&str> = line.split('|').collect();
4911 if parts.len() != 7 || parts[0] != "GRANT" {
4912 return None;
4913 }
4914 let resource = decode_resource(parts[1])?;
4915 let mut actions = std::collections::BTreeSet::new();
4916 for token in parts[2].split(',') {
4917 if let Some(a) = Action::from_keyword(token) {
4918 actions.insert(a);
4919 }
4920 }
4921 let with_grant_option = parts[3] == "true";
4922 let tenant = if parts[4] == "*" {
4923 None
4924 } else {
4925 Some(parts[4].to_string())
4926 };
4927 let granted_by = parts[5].to_string();
4928 let granted_at: u128 = parts[6].parse().unwrap_or(0);
4929 out.push(Grant {
4930 principal: GrantPrincipal::Public,
4933 resource,
4934 actions,
4935 with_grant_option,
4936 granted_by,
4937 granted_at,
4938 tenant,
4939 columns: None,
4940 });
4941 }
4942 Some(out)
4943}
4944
4945fn encode_attrs_blob(a: &UserAttributes) -> String {
4946 let valid = a
4947 .valid_until
4948 .map(|t| t.to_string())
4949 .unwrap_or_else(|| "*".into());
4950 let limit = a
4951 .connection_limit
4952 .map(|l| l.to_string())
4953 .unwrap_or_else(|| "*".into());
4954 let path = a.search_path.clone().unwrap_or_else(|| "*".into());
4955 let groups = if a.groups.is_empty() {
4956 "*".to_string()
4957 } else {
4958 a.groups.join(",")
4959 };
4960 format!("ATTR|{}|{}|{}|{}\n", valid, limit, path, groups)
4961}
4962
4963fn decode_attrs_blob(s: &str) -> Option<UserAttributes> {
4964 let line = s.lines().next()?;
4965 let parts: Vec<&str> = line.split('|').collect();
4966 if !(parts.len() == 4 || parts.len() == 5) || parts[0] != "ATTR" {
4967 return None;
4968 }
4969 let groups = if parts.get(4).copied().unwrap_or("*") == "*" {
4970 Vec::new()
4971 } else {
4972 parts[4]
4973 .split(',')
4974 .filter(|g| !g.is_empty())
4975 .map(|g| g.to_string())
4976 .collect()
4977 };
4978 Some(UserAttributes {
4979 valid_until: if parts[1] == "*" {
4980 None
4981 } else {
4982 parts[1].parse().ok()
4983 },
4984 connection_limit: if parts[2] == "*" {
4985 None
4986 } else {
4987 parts[2].parse().ok()
4988 },
4989 search_path: if parts[3] == "*" {
4990 None
4991 } else {
4992 Some(parts[3].to_string())
4993 },
4994 groups,
4995 })
4996}
4997
4998fn make_scram_verifier(password: &str) -> crate::auth::scram::ScramVerifier {
5008 let salt = random_bytes(16);
5009 crate::auth::scram::ScramVerifier::from_password(
5010 password,
5011 salt,
5012 crate::auth::scram::DEFAULT_ITER,
5013 )
5014}
5015
5016const PASSWORD_M_COST_KIB: u32 = 19 * 1024;
5021const PASSWORD_T_COST: u32 = 2;
5022const PASSWORD_P_COST: u32 = 1;
5023const PASSWORD_TAG_LEN: usize = 32;
5024
5025fn password_hasher() -> argon2::Argon2<'static> {
5026 let params = argon2::Params::new(
5027 PASSWORD_M_COST_KIB,
5028 PASSWORD_T_COST,
5029 PASSWORD_P_COST,
5030 Some(PASSWORD_TAG_LEN),
5031 )
5032 .expect("Argon2id parameters are constant and valid");
5033 argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
5034}
5035
5036pub(crate) fn hash_password(password: &str) -> String {
5048 use argon2::PasswordHasher;
5049 password_hasher()
5052 .hash_password(password.as_bytes())
5053 .expect("Argon2id hashing cannot fail for valid parameters and a working CSPRNG")
5054 .to_string()
5055}
5056
5057pub(crate) fn password_hash_needs_upgrade(stored_hash: &str) -> bool {
5060 stored_hash.starts_with("argon2id$")
5061}
5062
5063pub(crate) fn verify_password(password: &str, stored_hash: &str) -> bool {
5068 if let Some(rest) = stored_hash.strip_prefix("argon2id$") {
5069 return verify_legacy_password(password, rest);
5070 }
5071 use argon2::{PasswordHash, PasswordVerifier};
5072 let Ok(parsed) = stored_hash.parse::<PasswordHash>() else {
5073 return false;
5074 };
5075 password_hasher()
5076 .verify_password(password.as_bytes(), &parsed)
5077 .is_ok()
5078}
5079
5080fn verify_legacy_password(password: &str, salt_and_hash: &str) -> bool {
5083 let Some((salt_hex, hash_hex)) = salt_and_hash.split_once('$') else {
5084 return false;
5085 };
5086 let (Ok(salt), Ok(expected)) = (hex::decode(salt_hex), hex::decode(hash_hex)) else {
5087 return false;
5088 };
5089 let computed = derive_key(password.as_bytes(), &salt, &auth_argon2_params());
5090 constant_time_eq(&computed, &expected)
5091}
5092
5093fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
5095 crate::crypto::constant_time_eq(a, b)
5096}
5097
5098fn generate_session_token() -> String {
5103 format!("rs_{}", hex::encode(random_bytes(32)))
5104}
5105
5106fn generate_api_key() -> String {
5107 format!("rk_{}", hex::encode(random_bytes(32)))
5108}
5109
5110fn random_hex(n: usize) -> String {
5112 hex::encode(random_bytes(n))
5113}
5114
5115pub(crate) fn random_bytes(n: usize) -> Vec<u8> {
5118 let mut buf = vec![0u8; n.max(32)];
5119 if let Err(err) = os_random::fill_bytes(&mut buf) {
5123 panic!("OS CSPRNG unavailable; refusing to generate credential material: {err}");
5124 }
5125 let digest = sha256(&buf);
5127 if n <= 32 {
5128 digest[..n].to_vec()
5129 } else {
5130 let mut out = Vec::with_capacity(n);
5132 let mut prev = digest;
5133 while out.len() < n {
5134 out.extend_from_slice(&prev[..std::cmp::min(32, n - out.len())]);
5135 prev = sha256(&prev);
5136 }
5137 out
5138 }
5139}
5140
5141fn lock_err<T>(_: T) -> AuthError {
5146 AuthError::Internal("lock poisoned".to_string())
5147}
5148
5149#[cfg(test)]
5154mod tests {
5155 use super::*;
5156
5157 fn test_config() -> AuthConfig {
5158 AuthConfig {
5159 enabled: true,
5160 session_ttl_secs: 60,
5161 require_auth: true,
5162 auto_encrypt_storage: false,
5163 vault_enabled: false,
5164 cert: Default::default(),
5165 oauth: Default::default(),
5166 }
5167 }
5168
5169 #[test]
5170 fn test_create_and_list_users() {
5171 let store = AuthStore::new(test_config());
5172 store.create_user("alice", "pass1", Role::Admin).unwrap();
5173 store.create_user("bob", "pass2", Role::Read).unwrap();
5174
5175 let users = store.list_users();
5176 assert_eq!(users.len(), 2);
5177 for u in &users {
5179 assert!(u.password_hash.is_empty());
5180 }
5181 }
5182
5183 #[test]
5184 fn test_create_duplicate_user() {
5185 let store = AuthStore::new(test_config());
5186 store.create_user("alice", "pass", Role::Admin).unwrap();
5187 let err = store.create_user("alice", "pass2", Role::Read).unwrap_err();
5188 assert!(matches!(err, AuthError::UserExists(_)));
5189 }
5190
5191 struct RecordingLedger(std::sync::Mutex<Vec<(String, String)>>);
5192
5193 impl crate::runtime::control_events::ControlEventLedger for RecordingLedger {
5194 fn emit(
5195 &self,
5196 _ctx: &ControlEventCtx<'_>,
5197 event: ControlEvent,
5198 ) -> Result<
5199 crate::runtime::control_events::EventId,
5200 crate::runtime::control_events::ControlEventError,
5201 > {
5202 self.0.lock().unwrap().push((
5203 event.kind.as_str().to_string(),
5204 event.outcome.as_str().to_string(),
5205 ));
5206 Ok(crate::runtime::control_events::EventId("evt".to_string()))
5207 }
5208 }
5209
5210 #[test]
5211 fn logins_land_in_the_control_event_ledger() {
5212 let store = AuthStore::new(test_config());
5213 store.create_user("dave", "pw", Role::Read).unwrap();
5214 let ledger = std::sync::Arc::new(RecordingLedger(std::sync::Mutex::new(Vec::new())));
5215 store.configure_control_events(
5216 ledger.clone(),
5217 crate::runtime::control_events::ControlEventConfig::default(),
5218 );
5219 store.authenticate("dave", "pw").expect("login");
5220 store
5221 .authenticate("dave", "nope")
5222 .expect_err("bad password");
5223 store
5224 .authenticate("nobody", "pw")
5225 .expect_err("unknown user");
5226 let seen = ledger.0.lock().unwrap().clone();
5227 let allowed = Outcome::Allowed.as_str().to_string();
5228 let denied = Outcome::Denied.as_str().to_string();
5229 assert!(
5230 seen.contains(&("auth.login".to_string(), allowed)),
5231 "success not recorded: {seen:?}"
5232 );
5233 assert_eq!(
5234 seen.iter()
5235 .filter(|(k, o)| k == "auth.login_failed" && *o == denied)
5236 .count(),
5237 2,
5238 "both refusals must be recorded: {seen:?}"
5239 );
5240 }
5241
5242 #[test]
5243 fn api_keys_are_hashed_at_rest_and_expire() {
5244 let store = AuthStore::new(test_config());
5245 store.create_user("erin", "pw", Role::Write).unwrap();
5246 let key = store
5247 .create_api_key_in_tenant_expiring(None, "erin", "ci", Role::Read, Some(now_ms() + 60))
5248 .unwrap();
5249 assert!(key.key.starts_with("rk_"));
5250
5251 let stored = store
5252 .users
5253 .read()
5254 .unwrap()
5255 .get(&UserId::from_parts(None, "erin"))
5256 .unwrap()
5257 .api_keys[0]
5258 .clone();
5259 assert!(stored.key.starts_with("rkh_"), "{}", stored.key);
5260 assert_ne!(stored.key, key.key, "the secret must not be stored");
5261 assert_eq!(stored.key, api_key_credential_id(&key.key));
5262
5263 assert!(store.validate_token(&key.key).is_some());
5264 std::thread::sleep(std::time::Duration::from_millis(80));
5265 assert!(
5266 store.validate_token(&key.key).is_none(),
5267 "an expired key must not validate"
5268 );
5269
5270 store.revoke_api_key(&stored.key).unwrap();
5272 assert!(store.revoke_api_key(&key.key).is_err());
5273 }
5274
5275 #[test]
5276 fn revoke_is_clamped_to_the_granter_tenant() {
5277 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
5278 let store = AuthStore::new(test_config());
5279 let acme_admin = UserId::from_parts(Some("acme"), "adm");
5280 let globex_user = UserId::from_parts(Some("globex"), "u");
5281 let err = store
5282 .revoke(
5283 &acme_admin,
5284 Role::Admin,
5285 &GrantPrincipal::User(globex_user),
5286 &Resource::table_from_name("orders"),
5287 &[Action::Select],
5288 )
5289 .expect_err("a tenant admin must not touch another tenant's grants");
5290 assert!(err.to_string().contains("cross-tenant"), "{err}");
5291 store
5293 .revoke(
5294 &UserId::from_parts(None, "platform"),
5295 Role::Admin,
5296 &GrantPrincipal::User(UserId::from_parts(Some("globex"), "u")),
5297 &Resource::table_from_name("orders"),
5298 &[Action::Select],
5299 )
5300 .expect("platform admin revokes anywhere");
5301 }
5302
5303 #[test]
5304 fn role_change_revokes_sessions_minted_under_the_old_role() {
5305 let store = AuthStore::new(test_config());
5306 store.create_user("carol", "pw", Role::Write).unwrap();
5307 let session = store.authenticate("carol", "pw").unwrap();
5308 assert!(store.validate_token(&session.token).is_some());
5309 store.change_role("carol", Role::Read).unwrap();
5310 assert!(
5311 store.validate_token(&session.token).is_none(),
5312 "a demotion must not keep riding on the old session"
5313 );
5314 }
5315
5316 #[test]
5317 fn test_authenticate_and_validate() {
5318 let store = AuthStore::new(test_config());
5319 store.create_user("alice", "secret", Role::Write).unwrap();
5320
5321 let session = store.authenticate("alice", "secret").unwrap();
5322 assert!(session.token.starts_with("rs_"));
5323
5324 let (username, role) = store.validate_token(&session.token).unwrap();
5325 assert_eq!(username, "alice");
5326 assert_eq!(role, Role::Write);
5327 }
5328
5329 #[test]
5330 fn test_authenticate_wrong_password() {
5331 let store = AuthStore::new(test_config());
5332 store.create_user("alice", "secret", Role::Read).unwrap();
5333
5334 let err = store.authenticate("alice", "wrong").unwrap_err();
5335 assert!(matches!(err, AuthError::InvalidCredentials));
5336 }
5337
5338 #[test]
5339 fn test_api_key_lifecycle() {
5340 let store = AuthStore::new(test_config());
5341 store.create_user("alice", "pass", Role::Admin).unwrap();
5342
5343 let key = store
5344 .create_api_key("alice", "ci-token", Role::Write)
5345 .unwrap();
5346 assert!(key.key.starts_with("rk_"));
5347
5348 let (username, role) = store.validate_token(&key.key).unwrap();
5349 assert_eq!(username, "alice");
5350 assert_eq!(role, Role::Write);
5351
5352 store.revoke_api_key(&key.key).unwrap();
5353 assert!(store.validate_token(&key.key).is_none());
5354 }
5355
5356 #[test]
5357 fn test_api_key_role_exceeded() {
5358 let store = AuthStore::new(test_config());
5359 store.create_user("bob", "pass", Role::Read).unwrap();
5360
5361 let err = store
5362 .create_api_key("bob", "escalate", Role::Admin)
5363 .unwrap_err();
5364 assert!(matches!(err, AuthError::RoleExceeded { .. }));
5365 }
5366
5367 #[test]
5368 fn test_change_password() {
5369 let store = AuthStore::new(test_config());
5370 store.create_user("alice", "old", Role::Write).unwrap();
5371
5372 store.change_password("alice", "old", "new").unwrap();
5373
5374 assert!(store.authenticate("alice", "old").is_err());
5376 assert!(store.authenticate("alice", "new").is_ok());
5378 }
5379
5380 #[test]
5381 fn test_change_role() {
5382 let store = AuthStore::new(test_config());
5383 store.create_user("alice", "pass", Role::Admin).unwrap();
5384 store.create_api_key("alice", "key1", Role::Admin).unwrap();
5385
5386 store.change_role("alice", Role::Read).unwrap();
5387
5388 let users = store.list_users();
5390 let alice = users.iter().find(|u| u.username == "alice").unwrap();
5391 assert_eq!(alice.role, Role::Read);
5392
5393 assert_eq!(alice.api_keys[0].role, Role::Read);
5395 }
5396
5397 #[test]
5398 fn test_admin_user_mutations_are_policy_controlled_not_flag_guarded() {
5399 let store = AuthStore::new(test_config());
5400 store
5401 .create_admin_user("system", "pass", Role::Admin, None)
5402 .unwrap();
5403
5404 let uid = UserId::platform("system");
5405 store.change_password("system", "pass", "new").unwrap();
5406 store.change_role("system", Role::Read).unwrap();
5407 store.set_user_enabled(&uid, false).unwrap();
5408
5409 let key = store
5410 .create_api_key("system", "rotation", Role::Read)
5411 .unwrap();
5412 assert!(store.validate_token(&key.key).is_none());
5416 store.set_user_enabled(&uid, true).unwrap();
5417 assert!(store.validate_token(&key.key).is_some());
5418 store.revoke_api_key(&key.key).unwrap();
5419 assert!(store.validate_token(&key.key).is_none());
5420 store.delete_user("system").unwrap();
5421 }
5422
5423 #[test]
5424 fn test_user_lifecycle_deny_policy_blocks_cloud_admin_deletion() {
5425 use crate::auth::policies::Policy;
5426
5427 let store = AuthStore::new(test_config());
5428 store
5429 .create_user("cloud-admin", "pass", Role::Admin)
5430 .unwrap();
5431 store
5432 .create_user("customer-admin", "pass", Role::Admin)
5433 .unwrap();
5434
5435 store
5436 .put_policy(
5437 Policy::from_json_str(
5438 r#"{
5439 "id": "customer-admin-allow-all",
5440 "version": 1,
5441 "statements": [{
5442 "effect": "allow",
5443 "actions": ["*"],
5444 "resources": ["*"]
5445 }]
5446 }"#,
5447 )
5448 .unwrap(),
5449 )
5450 .unwrap();
5451 store
5452 .put_policy(
5453 Policy::from_json_str(
5454 r#"{
5455 "id": "cloud-admin-protection",
5456 "version": 1,
5457 "statements": [{
5458 "effect": "deny",
5459 "actions": [
5460 "user:delete",
5461 "user:disable",
5462 "user:password:change",
5463 "user:role:update"
5464 ],
5465 "resources": ["user:cloud-admin"]
5466 }]
5467 }"#,
5468 )
5469 .unwrap(),
5470 )
5471 .unwrap();
5472 let customer = UserId::platform("customer-admin");
5473 store
5474 .attach_policy(
5475 PrincipalRef::User(customer.clone()),
5476 "customer-admin-allow-all",
5477 )
5478 .unwrap();
5479 store
5480 .attach_policy(
5481 PrincipalRef::User(customer.clone()),
5482 "cloud-admin-protection",
5483 )
5484 .unwrap();
5485
5486 let cloud_admin = UserId::platform("cloud-admin");
5487 assert!(!store.check_user_lifecycle_authz(
5488 &customer,
5489 Role::Admin,
5490 "user:delete",
5491 &cloud_admin,
5492 ));
5493 assert!(!store.check_user_lifecycle_authz(
5494 &customer,
5495 Role::Admin,
5496 "user:disable",
5497 &cloud_admin,
5498 ));
5499 assert!(!store.check_user_lifecycle_authz(
5500 &customer,
5501 Role::Admin,
5502 "user:password:change",
5503 &cloud_admin,
5504 ));
5505 assert!(!store.check_user_lifecycle_authz(
5506 &customer,
5507 Role::Admin,
5508 "user:role:update",
5509 &cloud_admin,
5510 ));
5511
5512 let another_user = UserId::platform("someone-else");
5513 assert!(store.check_user_lifecycle_authz(
5514 &customer,
5515 Role::Admin,
5516 "user:delete",
5517 &another_user,
5518 ));
5519 }
5520
5521 #[test]
5522 fn test_regular_user_mutations_still_work() {
5523 let store = AuthStore::new(test_config());
5524 store.create_user("alice", "old", Role::Admin).unwrap();
5525
5526 let uid = UserId::platform("alice");
5527 store.set_user_enabled(&uid, false).unwrap();
5528 assert!(matches!(
5529 store.authenticate("alice", "old"),
5530 Err(AuthError::InvalidCredentials)
5531 ));
5532
5533 store.set_user_enabled(&uid, true).unwrap();
5534 store.change_password("alice", "old", "new").unwrap();
5535 store.change_role("alice", Role::Read).unwrap();
5536 store.delete_user("alice").unwrap();
5537 assert!(matches!(
5538 store.authenticate("alice", "new"),
5539 Err(AuthError::InvalidCredentials)
5540 ));
5541 }
5542
5543 #[test]
5544 fn test_delete_user() {
5545 let store = AuthStore::new(test_config());
5546 store.create_user("alice", "pass", Role::Admin).unwrap();
5547 let key = store.create_api_key("alice", "key1", Role::Read).unwrap();
5548 let session = store.authenticate("alice", "pass").unwrap();
5549
5550 store.delete_user("alice").unwrap();
5551
5552 assert!(store.validate_token(&key.key).is_none());
5553 assert!(store.validate_token(&session.token).is_none());
5554 assert!(store.list_users().is_empty());
5555 }
5556
5557 #[test]
5558 fn test_revoke_session() {
5559 let store = AuthStore::new(test_config());
5560 store.create_user("alice", "pass", Role::Read).unwrap();
5561 let session = store.authenticate("alice", "pass").unwrap();
5562
5563 store.revoke_session(&session.token);
5564 assert!(store.validate_token(&session.token).is_none());
5565 }
5566
5567 #[test]
5568 fn test_password_hash_format() {
5569 let hash = hash_password("test");
5574 assert!(
5575 hash.starts_with("$argon2id$v=19$"),
5576 "expected a PHC-encoded Argon2id hash, got: {hash}"
5577 );
5578 assert!(
5579 hash.contains(&format!(
5580 "m={PASSWORD_M_COST_KIB},t={PASSWORD_T_COST},p={PASSWORD_P_COST}"
5581 )),
5582 "the PHC string must carry the cost parameters: {hash}"
5583 );
5584 assert!(verify_password("test", &hash));
5585 assert!(!verify_password("wrong", &hash));
5586 assert!(
5587 !password_hash_needs_upgrade(&hash),
5588 "a freshly minted hash must not be flagged for upgrade"
5589 );
5590 }
5591
5592 #[test]
5593 fn legacy_password_hashes_still_verify_and_upgrade_on_login() {
5594 let legacy = {
5598 let salt = random_bytes(16);
5599 let hash = derive_key(b"hunter2", &salt, &auth_argon2_params());
5600 format!("argon2id${}${}", hex::encode(&salt), hex::encode(&hash))
5601 };
5602 assert!(verify_password("hunter2", &legacy));
5603 assert!(!verify_password("wrong", &legacy));
5604 assert!(password_hash_needs_upgrade(&legacy));
5605
5606 let store = AuthStore::new(test_config());
5607 store
5608 .create_user("erin", "placeholder", Role::Write)
5609 .unwrap();
5610 {
5611 let mut users = store.users.write().unwrap();
5612 users
5613 .get_mut(&UserId::platform("erin"))
5614 .unwrap()
5615 .password_hash = legacy.clone();
5616 }
5617
5618 store
5619 .authenticate("erin", "hunter2")
5620 .expect("a legacy hash must still authenticate");
5621
5622 let stored = store
5623 .users
5624 .read()
5625 .unwrap()
5626 .get(&UserId::platform("erin"))
5627 .unwrap()
5628 .password_hash
5629 .clone();
5630 assert!(
5631 stored.starts_with("$argon2id$v=19$"),
5632 "a successful login must upgrade the stored hash, got: {stored}"
5633 );
5634 assert!(store.authenticate("erin", "hunter2").is_ok());
5635 }
5636
5637 #[test]
5638 fn test_constant_time_eq() {
5639 assert!(constant_time_eq(b"hello", b"hello"));
5640 assert!(!constant_time_eq(b"hello", b"world"));
5641 assert!(!constant_time_eq(b"short", b"longer"));
5642 }
5643
5644 #[test]
5645 fn test_bootstrap_seals_permanently() {
5646 let store = AuthStore::new(test_config());
5647
5648 assert!(store.needs_bootstrap());
5649 assert!(!store.is_bootstrapped());
5650
5651 let result = store.bootstrap("admin", "secret");
5653 assert!(result.is_ok());
5654 let br = result.unwrap();
5655 assert_eq!(br.user.username, "admin");
5656 assert_eq!(br.user.role, Role::Admin);
5657 assert!(br.api_key.key.starts_with("rk_"));
5658 assert!(br.certificate.is_none());
5660
5661 assert!(!store.needs_bootstrap());
5663 assert!(store.is_bootstrapped());
5664
5665 let result = store.bootstrap("admin2", "secret2");
5667 assert!(result.is_err());
5668 let err = result.unwrap_err();
5669 assert!(err.to_string().contains("sealed permanently"));
5670
5671 assert_eq!(store.list_users().len(), 1);
5673 assert_eq!(store.list_users()[0].username, "admin");
5674 }
5675
5676 #[test]
5677 fn test_bootstrap_after_manual_user_creation() {
5678 let store = AuthStore::new(test_config());
5679
5680 store.create_user("existing", "pass", Role::Read).unwrap();
5682
5683 assert!(!store.needs_bootstrap()); }
5687
5688 #[test]
5693 fn test_same_username_two_tenants_distinct() {
5694 let store = AuthStore::new(test_config());
5695 store
5696 .create_user_in_tenant(Some("acme"), "alice", "pw-acme", Role::Write)
5697 .unwrap();
5698 store
5699 .create_user_in_tenant(Some("globex"), "alice", "pw-globex", Role::Read)
5700 .unwrap();
5701
5702 let users = store.list_users();
5704 assert_eq!(users.len(), 2);
5705
5706 assert!(store
5708 .authenticate_in_tenant(Some("acme"), "alice", "pw-acme")
5709 .is_ok());
5710 assert!(store
5711 .authenticate_in_tenant(Some("globex"), "alice", "pw-globex")
5712 .is_ok());
5713
5714 assert!(store
5716 .authenticate_in_tenant(Some("acme"), "alice", "pw-globex")
5717 .is_err());
5718 assert!(store
5719 .authenticate_in_tenant(Some("globex"), "alice", "pw-acme")
5720 .is_err());
5721 }
5722
5723 #[test]
5724 fn test_session_carries_tenant() {
5725 let store = AuthStore::new(test_config());
5726 store
5727 .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
5728 .unwrap();
5729 let session = store
5730 .authenticate_in_tenant(Some("acme"), "alice", "pw")
5731 .unwrap();
5732 assert_eq!(session.tenant_id.as_deref(), Some("acme"));
5733
5734 let (id, role) = store.validate_token_full(&session.token).unwrap();
5735 assert_eq!(id.tenant.as_deref(), Some("acme"));
5736 assert_eq!(id.username, "alice");
5737 assert_eq!(role, Role::Admin);
5738 }
5739
5740 #[test]
5741 fn test_platform_user_has_no_tenant() {
5742 let store = AuthStore::new(test_config());
5743 store.create_user("admin", "pw", Role::Admin).unwrap();
5744 let session = store.authenticate("admin", "pw").unwrap();
5745 assert!(session.tenant_id.is_none());
5746
5747 let (id, _) = store.validate_token_full(&session.token).unwrap();
5748 assert!(id.tenant.is_none());
5749 }
5750
5751 #[test]
5752 fn test_lookup_scram_verifier_global_resolves_platform() {
5753 let store = AuthStore::new(test_config());
5754 store.create_user("admin", "pw", Role::Admin).unwrap();
5755 store
5756 .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
5757 .unwrap();
5758
5759 let v = store.lookup_scram_verifier_global("admin");
5761 assert!(v.is_some());
5762
5763 let v_acme = store.lookup_scram_verifier(&UserId::scoped("acme", "admin"));
5765 assert!(v_acme.is_some());
5766
5767 assert_ne!(v.unwrap().salt, v_acme.unwrap().salt);
5769 }
5770
5771 #[test]
5772 fn test_delete_in_tenant_does_not_touch_other_tenant() {
5773 let store = AuthStore::new(test_config());
5774 store
5775 .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
5776 .unwrap();
5777 store
5778 .create_user_in_tenant(Some("globex"), "alice", "pw", Role::Admin)
5779 .unwrap();
5780
5781 store.delete_user_in_tenant(Some("acme"), "alice").unwrap();
5782
5783 assert!(store
5785 .authenticate_in_tenant(Some("globex"), "alice", "pw")
5786 .is_ok());
5787 assert!(store
5789 .authenticate_in_tenant(Some("acme"), "alice", "pw")
5790 .is_err());
5791 }
5792
5793 #[test]
5794 fn test_user_id_display() {
5795 assert_eq!(UserId::platform("admin").to_string(), "admin");
5796 assert_eq!(UserId::scoped("acme", "alice").to_string(), "acme/alice");
5797 }
5798
5799 fn enforcement_eval_ctx(role: Role) -> EvalContext {
5804 EvalContext {
5805 principal_tenant: None,
5806 current_tenant: None,
5807 peer_ip: None,
5808 mfa_present: false,
5809 now_ms: 1_700_000_000_000,
5810 principal_is_admin_role: role == Role::Admin,
5811 principal_is_platform_scoped: true,
5812 }
5813 }
5814
5815 fn install_unrelated_policy(store: &AuthStore) {
5819 store
5820 .put_policy(
5821 Policy::from_json_str(
5822 r#"{"id":"p-unrelated","version":1,"statements":[{"effect":"allow","actions":["select"],"resources":["table:public.other"]}]}"#,
5823 )
5824 .unwrap(),
5825 )
5826 .unwrap();
5827 }
5828
5829 #[test]
5830 fn enforcement_mode_default_for_new_store_is_legacy_rbac() {
5831 let store = AuthStore::new(test_config());
5836 assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::LegacyRbac);
5837 }
5838
5839 #[test]
5840 fn enforcement_mode_set_round_trips_and_returns_previous() {
5841 let store = AuthStore::new(test_config());
5842 let prev = store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5843 assert_eq!(prev, PolicyEnforcementMode::LegacyRbac);
5844 assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::PolicyOnly);
5845 let prev = store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5846 assert_eq!(prev, PolicyEnforcementMode::PolicyOnly);
5847 }
5848
5849 #[test]
5850 fn policy_only_mode_treats_no_matching_policy_as_deny() {
5851 let store = AuthStore::new(test_config());
5855 store.create_user("alice", "p", Role::Admin).unwrap();
5856 let uid = UserId::platform("alice");
5857 install_unrelated_policy(&store);
5858 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5859
5860 let resource = ResourceRef::new("table", "public.t");
5861 assert!(!store.check_policy_authz_with_role(
5864 &uid,
5865 "select",
5866 &resource,
5867 &enforcement_eval_ctx(Role::Admin),
5868 Role::Admin,
5869 ));
5870 }
5871
5872 #[test]
5873 fn legacy_rbac_mode_falls_back_to_role_decision() {
5874 let store = AuthStore::new(test_config());
5879 store.create_user("alice", "p", Role::Read).unwrap();
5880 let uid = UserId::platform("alice");
5881 install_unrelated_policy(&store);
5882 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5883
5884 let table = ResourceRef::new("table", "public.t");
5885 assert!(store.check_policy_authz_with_role(
5887 &uid,
5888 "select",
5889 &table,
5890 &enforcement_eval_ctx(Role::Read),
5891 Role::Read,
5892 ));
5893 assert!(!store.check_policy_authz_with_role(
5895 &uid,
5896 "insert",
5897 &table,
5898 &enforcement_eval_ctx(Role::Read),
5899 Role::Read,
5900 ));
5901 assert!(store.check_policy_authz_with_role(
5903 &uid,
5904 "insert",
5905 &table,
5906 &enforcement_eval_ctx(Role::Admin),
5907 Role::Admin,
5908 ));
5909 }
5910
5911 #[test]
5912 fn explicit_deny_wins_irrespective_of_mode_or_role() {
5913 let store = AuthStore::new(test_config());
5917 store.create_user("alice", "p", Role::Admin).unwrap();
5918 let uid = UserId::platform("alice");
5919 store
5920 .put_policy(
5921 Policy::from_json_str(
5922 r#"{"id":"p-deny-select","version":1,"statements":[{"effect":"deny","actions":["select"],"resources":["table:public.t"]}]}"#,
5923 )
5924 .unwrap(),
5925 )
5926 .unwrap();
5927 store
5928 .attach_policy(PrincipalRef::User(uid.clone()), "p-deny-select")
5929 .unwrap();
5930
5931 let resource = ResourceRef::new("table", "public.t");
5932 for mode in [
5933 PolicyEnforcementMode::LegacyRbac,
5934 PolicyEnforcementMode::PolicyOnly,
5935 ] {
5936 store.set_enforcement_mode(mode);
5937 assert!(
5938 !store.check_policy_authz_with_role(
5939 &uid,
5940 "select",
5941 &resource,
5942 &enforcement_eval_ctx(Role::Admin),
5943 Role::Admin,
5944 ),
5945 "explicit deny must win under mode {mode:?}"
5946 );
5947 }
5948 }
5949
5950 #[test]
5951 fn explicit_allow_wins_irrespective_of_mode_or_role() {
5952 let store = AuthStore::new(test_config());
5956 store.create_user("alice", "p", Role::Read).unwrap();
5957 let uid = UserId::platform("alice");
5958 store
5959 .put_policy(
5960 Policy::from_json_str(
5961 r#"{"id":"p-allow-create","version":1,"statements":[{"effect":"allow","actions":["create"],"resources":["table:public.t"]}]}"#,
5962 )
5963 .unwrap(),
5964 )
5965 .unwrap();
5966 store
5967 .attach_policy(PrincipalRef::User(uid.clone()), "p-allow-create")
5968 .unwrap();
5969 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5970
5971 let resource = ResourceRef::new("table", "public.t");
5972 assert!(store.check_policy_authz_with_role(
5973 &uid,
5974 "create",
5975 &resource,
5976 &enforcement_eval_ctx(Role::Read),
5977 Role::Read,
5978 ));
5979 }
5980
5981 #[test]
5982 fn take_legacy_rbac_warn_once_is_at_most_once_per_boot() {
5983 let store = AuthStore::new(test_config());
5984 assert!(store.take_legacy_rbac_warn_once());
5986 for _ in 0..3 {
5989 assert!(!store.take_legacy_rbac_warn_once());
5990 }
5991 }
5992
5993 #[test]
5994 fn take_legacy_rbac_warn_once_is_silent_under_policy_only() {
5995 let store = AuthStore::new(test_config());
6001 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
6002 assert!(!store.take_legacy_rbac_warn_once());
6003 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6008 assert!(store.take_legacy_rbac_warn_once());
6009 assert!(!store.take_legacy_rbac_warn_once());
6010 }
6011
6012 #[test]
6013 fn strict_check_policy_authz_ignores_enforcement_mode() {
6014 let store = AuthStore::new(test_config());
6019 store.create_user("alice", "p", Role::Admin).unwrap();
6020 let uid = UserId::platform("alice");
6021 install_unrelated_policy(&store);
6022 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6023
6024 let resource = ResourceRef::new("table", "public.t");
6025 assert!(!store.check_policy_authz(
6026 &uid,
6027 "select",
6028 &resource,
6029 &enforcement_eval_ctx(Role::Admin),
6030 ));
6031 }
6032
6033 #[test]
6034 fn disabling_a_user_invalidates_its_api_keys_and_sessions() {
6035 let store = AuthStore::new(test_config());
6036 store.create_user("carol", "pw", Role::Write).unwrap();
6037 let key = store
6038 .create_api_key("carol", "ci", Role::Write)
6039 .unwrap()
6040 .key;
6041 let session = store.authenticate("carol", "pw").unwrap();
6042 assert!(store.validate_token(&key).is_some());
6043 assert!(store.validate_token(&session.token).is_some());
6044
6045 let uid = UserId::platform("carol");
6046 store.set_user_enabled(&uid, false).unwrap();
6047
6048 assert!(
6052 store.validate_token(&key).is_none(),
6053 "an API key must stop working the moment its owner is disabled"
6054 );
6055 assert!(
6056 store.validate_token(&session.token).is_none(),
6057 "live sessions must not survive disabling the account"
6058 );
6059 assert!(
6060 store.lookup_scram_verifier(&uid).is_none(),
6061 "SCRAM must not authenticate a disabled account"
6062 );
6063 }
6064
6065 #[test]
6066 fn admin_password_reset_applies_and_revokes_sessions() {
6067 let store = AuthStore::new(test_config());
6068 store.create_user("dave", "old", Role::Write).unwrap();
6069 let session = store.authenticate("dave", "old").unwrap();
6070
6071 store
6072 .set_password_by_admin(&UserId::platform("dave"), "rotated")
6073 .expect("admin reset");
6074
6075 assert!(
6076 store.authenticate("dave", "old").is_err(),
6077 "the old password must stop working after a reset"
6078 );
6079 assert!(
6080 store.authenticate("dave", "rotated").is_ok(),
6081 "the new password must work after a reset"
6082 );
6083 assert!(
6084 store.validate_token(&session.token).is_none(),
6085 "a password rotation must revoke live sessions"
6086 );
6087 }
6088}
6089
6090fn deliver_bootstrap_certificate(cert: &str) {
6099 if let Some(path) = crate::utils::env_with_file_fallback("REDDB_BOOTSTRAP_CERT_OUT") {
6100 match std::fs::write(&path, format!("{cert}\n")) {
6101 Ok(()) => {
6102 tracing::warn!(
6103 target: "reddb::security",
6104 path = %path,
6105 "vault certificate written — save it: the ONLY way to unseal after a restart"
6106 );
6107 return;
6108 }
6109 Err(err) => {
6110 tracing::error!(
6111 target: "reddb::security",
6112 path = %path,
6113 error = %err,
6114 "could not write the vault certificate; falling back to stderr"
6115 );
6116 }
6117 }
6118 }
6119 eprintln!("[reddb] CERTIFICATE: {cert}");
6120 tracing::warn!(
6121 target: "reddb::security",
6122 "vault certificate issued on stderr — save it (ONLY way to unseal after restart); \
6123 set REDDB_BOOTSTRAP_CERT_OUT to receive it as a file instead of a log line"
6124 );
6125}