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