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
87#[derive(Debug)]
96pub struct BootstrapResult {
97 pub user: User,
98 pub api_key: ApiKey,
99 pub certificate: Option<String>,
101}
102
103pub struct AuthStore {
111 users: RwLock<HashMap<UserId, User>>,
114 sessions: RwLock<HashMap<String, Session>>,
115 api_key_index: RwLock<HashMap<String, (UserId, Role)>>,
117 bootstrapped: AtomicBool,
119 config: AuthConfig,
120 vault: RwLock<Option<Vault>>,
122 pager: Option<Arc<Pager>>,
124 keypair: RwLock<Option<KeyPair>>,
127 vault_kv: RwLock<HashMap<String, String>>,
130 plain_kv: RwLock<HashMap<String, String>>,
135 grants: RwLock<HashMap<UserId, Vec<Grant>>>,
140 public_grants: RwLock<Vec<Grant>>,
142 user_attributes: RwLock<HashMap<UserId, UserAttributes>>,
146 session_count_by_user: RwLock<HashMap<UserId, u32>>,
150 permission_cache: RwLock<HashMap<UserId, PermissionCache>>,
154 policies: RwLock<HashMap<String, Arc<Policy>>>,
158 user_attachments: RwLock<HashMap<UserId, Vec<String>>>,
161 group_attachments: RwLock<HashMap<String, Vec<String>>>,
165 iam_effective_cache: RwLock<HashMap<UserId, Vec<Arc<Policy>>>>,
168 iam_authorization_enabled: AtomicBool,
172 enforcement_mode: RwLock<PolicyEnforcementMode>,
182 legacy_rbac_boot_warn_emitted: AtomicBool,
187 visible_collections_cache: super::scope_cache::AuthCache,
197 control_events: RwLock<Option<AuthStoreControlEvents>>,
198}
199
200fn auth_argon2_params() -> Argon2Params {
203 Argon2Params {
204 m_cost: 4 * 1024, t_cost: 3,
206 p: 1,
207 tag_len: 32,
208 }
209}
210
211fn policy_control_fields(
212 policy_id: &str,
213 policy: Option<&Policy>,
214 principal: Option<&PrincipalRef>,
215) -> HashMap<String, Sensitivity> {
216 let mut fields = HashMap::new();
217 fields.insert("policy_id".to_string(), Sensitivity::raw(policy_id));
218 if let Some(policy) = policy {
219 let effects = policy
220 .statements
221 .iter()
222 .map(|s| match s.effect {
223 iam_policies::Effect::Allow => "allow",
224 iam_policies::Effect::Deny => "deny",
225 })
226 .collect::<Vec<_>>()
227 .join(",");
228 let actions = policy
229 .statements
230 .iter()
231 .flat_map(|s| s.actions.iter())
232 .map(policy_action_pattern)
233 .collect::<Vec<_>>()
234 .join(",");
235 let resources = policy
236 .statements
237 .iter()
238 .flat_map(|s| s.resources.iter())
239 .map(policy_resource_pattern)
240 .collect::<Vec<_>>()
241 .join(",");
242 fields.insert("effect".to_string(), Sensitivity::raw(effects));
243 fields.insert("action".to_string(), Sensitivity::raw(actions));
244 fields.insert("resource".to_string(), Sensitivity::raw(resources));
245 if let Some(attrs) = policy_principal_attrs(policy) {
246 fields.insert("principal_attrs".to_string(), Sensitivity::raw(attrs));
247 }
248 }
249 if let Some(principal) = principal {
250 fields.insert(
251 "principal".to_string(),
252 Sensitivity::raw(policy_principal_label(principal)),
253 );
254 }
255 fields
256}
257
258fn policy_action_pattern(pattern: &iam_policies::ActionPattern) -> String {
259 match pattern {
260 iam_policies::ActionPattern::Exact(s) => s.clone(),
261 iam_policies::ActionPattern::Wildcard => "*".to_string(),
262 iam_policies::ActionPattern::Prefix(s) => format!("{s}:*"),
263 }
264}
265
266fn policy_resource_pattern(pattern: &iam_policies::ResourcePattern) -> String {
267 match pattern {
268 iam_policies::ResourcePattern::Exact { kind, name } => format!("{kind}:{name}"),
269 iam_policies::ResourcePattern::Glob(s) => s.clone(),
270 iam_policies::ResourcePattern::Wildcard => "*".to_string(),
271 }
272}
273
274fn policy_principal_attrs(policy: &Policy) -> Option<String> {
275 let mut attrs = Vec::new();
276 for statement in &policy.statements {
277 let Some(condition) = &statement.condition else {
278 continue;
279 };
280 if let Some(value) = condition.platform_scoped {
281 attrs.push(format!("platform_scoped={value}"));
282 }
283 if let Some(value) = condition.mfa {
284 attrs.push(format!("mfa={value}"));
285 }
286 if let Some(value) = condition.tenant_match {
287 attrs.push(format!("tenant_match={value}"));
288 }
289 }
290 if attrs.is_empty() {
291 None
292 } else {
293 Some(attrs.join(","))
294 }
295}
296
297fn policy_principal_label(principal: &PrincipalRef) -> String {
298 match principal {
299 PrincipalRef::User(uid) => format!("user:{uid}"),
300 PrincipalRef::Group(group) => format!("group:{group}"),
301 }
302}
303
304fn default_user_lifecycle_ctx<'a>() -> ControlEventCtx<'a> {
305 ControlEventCtx {
306 actor: crate::runtime::control_events::ActorRef::Anonymous,
307 scope: None,
308 request_id: None,
309 trace_id: None,
310 }
311}
312
313fn bootstrap_user_lifecycle_ctx<'a>() -> ControlEventCtx<'a> {
314 ControlEventCtx {
315 actor: crate::runtime::control_events::ActorRef::System("bootstrap"),
316 scope: None,
317 request_id: None,
318 trace_id: None,
319 }
320}
321
322fn user_resource(id: &UserId) -> String {
323 format!("user:{id}")
324}
325
326fn api_key_resource(api_key_id: &str) -> String {
327 format!("apikey:{api_key_id}")
328}
329
330fn api_key_id(key: &str) -> String {
331 hex::encode(sha256(key.as_bytes()))
332}
333
334fn password_evidence() -> Sensitivity {
335 Sensitivity::redacted()
336}
337
338fn user_control_fields(
339 id: &UserId,
340 role: Option<Role>,
341 enabled: Option<bool>,
342 include_password: bool,
343) -> HashMap<String, Sensitivity> {
344 let mut fields = HashMap::new();
345 fields.insert(
346 "username".to_string(),
347 Sensitivity::raw(id.username.clone()),
348 );
349 fields.insert(
350 "tenant_id".to_string(),
351 Sensitivity::raw(id.tenant.clone().unwrap_or_default()),
352 );
353 if let Some(role) = role {
354 fields.insert("role".to_string(), Sensitivity::raw(role.as_str()));
355 }
356 if let Some(enabled) = enabled {
357 fields.insert("enabled".to_string(), Sensitivity::raw(enabled.to_string()));
358 }
359 if include_password {
360 fields.insert("password".to_string(), password_evidence());
361 }
362 fields
363}
364
365fn api_key_control_fields(
366 id: &UserId,
367 role: Role,
368 api_key_id: &str,
369) -> HashMap<String, Sensitivity> {
370 let mut fields = user_control_fields(id, Some(role), None, false);
371 fields.insert("api_key_id".to_string(), Sensitivity::raw(api_key_id));
372 fields.insert("api_key".to_string(), Sensitivity::redacted());
373 fields
374}
375
376fn user_error_is_denied(err: &AuthError) -> bool {
377 !matches!(err, AuthError::Internal(_))
378}
379
380impl AuthStore {
381 pub fn new(config: AuthConfig) -> Self {
386 Self {
387 users: RwLock::new(HashMap::new()),
388 sessions: RwLock::new(HashMap::new()),
389 api_key_index: RwLock::new(HashMap::new()),
390 bootstrapped: AtomicBool::new(false),
391 config,
392 vault: RwLock::new(None),
393 pager: None,
394 keypair: RwLock::new(None),
395 vault_kv: RwLock::new(HashMap::new()),
396 plain_kv: RwLock::new(HashMap::new()),
397 grants: RwLock::new(HashMap::new()),
398 public_grants: RwLock::new(Vec::new()),
399 user_attributes: RwLock::new(HashMap::new()),
400 session_count_by_user: RwLock::new(HashMap::new()),
401 permission_cache: RwLock::new(HashMap::new()),
402 policies: RwLock::new(HashMap::new()),
403 user_attachments: RwLock::new(HashMap::new()),
404 group_attachments: RwLock::new(HashMap::new()),
405 iam_effective_cache: RwLock::new(HashMap::new()),
406 iam_authorization_enabled: AtomicBool::new(false),
407 enforcement_mode: RwLock::new(PolicyEnforcementMode::default_existing_install()),
408 legacy_rbac_boot_warn_emitted: AtomicBool::new(false),
409 visible_collections_cache: super::scope_cache::AuthCache::new(
410 super::scope_cache::DEFAULT_TTL,
411 ),
412 control_events: RwLock::new(None),
413 }
414 }
415
416 pub fn configure_control_events(
417 &self,
418 ledger: Arc<dyn ControlEventLedger>,
419 config: ControlEventConfig,
420 ) {
421 *self
422 .control_events
423 .write()
424 .unwrap_or_else(|e| e.into_inner()) = Some(AuthStoreControlEvents { ledger, config });
425 }
426
427 fn configured_control_events(&self) -> Option<AuthStoreControlEvents> {
428 self.control_events
429 .read()
430 .unwrap_or_else(|e| e.into_inner())
431 .clone()
432 }
433
434 pub fn with_vault(config: AuthConfig, pager: Arc<Pager>) -> Result<Self, AuthError> {
441 let certificate = crate::utils::env_with_file_fallback("REDDB_CERTIFICATE");
442 Self::with_vault_optional_certificate(config, pager, certificate.as_deref())
443 }
444
445 pub fn with_vault_certificate(
446 config: AuthConfig,
447 pager: Arc<Pager>,
448 certificate_hex: &str,
449 ) -> Result<Self, AuthError> {
450 Self::with_vault_optional_certificate(config, pager, Some(certificate_hex))
451 }
452
453 fn with_vault_optional_certificate(
454 config: AuthConfig,
455 pager: Arc<Pager>,
456 certificate_hex: Option<&str>,
457 ) -> Result<Self, AuthError> {
458 let mut store = Self::new(config);
459
460 if Vault::has_saved_state(&pager) {
464 let vault = match certificate_hex {
465 Some(certificate) => Vault::with_certificate(&pager, certificate)?,
466 None => Vault::open(&pager)?,
467 };
468 if let Some(state) = vault.load(&pager)? {
469 store.restore_from_vault(state);
470 }
471 *store.vault.write().unwrap_or_else(|e| e.into_inner()) = Some(vault);
472 } else if let Some(certificate) = certificate_hex {
473 let vault = Vault::with_certificate(&pager, certificate)?;
474 *store.vault.write().unwrap_or_else(|e| e.into_inner()) = Some(vault);
475 }
476
477 store.pager = Some(pager);
478 Ok(store)
479 }
480
481 pub fn config(&self) -> &AuthConfig {
482 &self.config
483 }
484
485 pub fn is_enabled(&self) -> bool {
486 self.config.enabled
487 }
488
489 pub fn needs_bootstrap(&self) -> bool {
491 !self.bootstrapped.load(Ordering::Acquire)
492 && self.users.read().map(|u| u.is_empty()).unwrap_or(true)
493 }
494
495 fn get_user_cloned(&self, id: &UserId) -> Option<User> {
497 self.users.read().ok().and_then(|m| m.get(id).cloned())
498 }
499
500 pub fn is_bootstrapped(&self) -> bool {
502 self.bootstrapped.load(Ordering::Acquire)
503 }
504
505 pub fn bootstrap(&self, username: &str, password: &str) -> Result<BootstrapResult, AuthError> {
516 if let Some(configured) = self.configured_control_events() {
517 let ctx = bootstrap_user_lifecycle_ctx();
518 let control = UserLifecycleControl {
519 ctx: &ctx,
520 ledger: configured.ledger.as_ref(),
521 config: configured.config,
522 };
523 self.bootstrap_with_control_events_inner(username, password, &control)
524 } else {
525 self.bootstrap_unaudited(username, password)
526 }
527 }
528
529 pub fn bootstrap_with_control_events(
530 &self,
531 username: &str,
532 password: &str,
533 ctx: &ControlEventCtx<'_>,
534 ledger: &dyn ControlEventLedger,
535 config: ControlEventConfig,
536 ) -> Result<BootstrapResult, AuthError> {
537 let system_ctx = ControlEventCtx {
538 actor: crate::runtime::control_events::ActorRef::System("bootstrap"),
539 scope: ctx.scope.clone(),
540 request_id: ctx.request_id.clone(),
541 trace_id: ctx.trace_id.clone(),
542 };
543 let control = UserLifecycleControl {
544 ctx: &system_ctx,
545 ledger,
546 config,
547 };
548 self.bootstrap_with_control_events_inner(username, password, &control)
549 }
550
551 pub fn bootstrap_system_admin(
556 &self,
557 username: &str,
558 password: &str,
559 ) -> Result<BootstrapResult, AuthError> {
560 self.bootstrap(username, password)
561 }
562
563 fn bootstrap_unaudited(
564 &self,
565 username: &str,
566 password: &str,
567 ) -> Result<BootstrapResult, AuthError> {
568 if self
570 .bootstrapped
571 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
572 .is_err()
573 {
574 return Err(AuthError::Forbidden(
575 "bootstrap already completed — sealed permanently".to_string(),
576 ));
577 }
578
579 {
581 let users = self.users.read().map_err(lock_err)?;
582 if !users.is_empty() {
583 return Err(AuthError::Forbidden(
584 "bootstrap already completed — users exist".to_string(),
585 ));
586 }
587 }
588
589 let user = self.create_user_in_tenant_unaudited(None, username, password, Role::Admin)?;
590 let key =
591 self.create_api_key_in_tenant_unaudited(None, username, "bootstrap", Role::Admin)?;
592
593 let certificate = if let Some(ref pager) = self.pager {
595 let kp = KeyPair::generate();
596 let cert_hex = kp.certificate_hex();
597
598 let new_vault = Vault::with_certificate_bytes(pager, &kp.certificate)
600 .map_err(|e| AuthError::Internal(format!("vault re-seal failed: {e}")))?;
601
602 if let Ok(mut kp_guard) = self.keypair.write() {
604 *kp_guard = Some(kp);
605 }
606
607 if let Ok(mut vault_guard) = self.vault.write() {
609 *vault_guard = Some(new_vault);
610 }
611 self.ensure_vault_secret_key();
613 self.persist_to_vault();
614
615 Some(cert_hex)
616 } else {
617 None
618 };
619
620 self.set_enforcement_mode(PolicyEnforcementMode::default_fresh_bootstrap());
627
628 Ok(BootstrapResult {
629 user,
630 api_key: key,
631 certificate,
632 })
633 }
634
635 fn bootstrap_with_control_events_inner(
636 &self,
637 username: &str,
638 password: &str,
639 control: &UserLifecycleControl<'_>,
640 ) -> Result<BootstrapResult, AuthError> {
641 let id = UserId::from_parts(None, username);
642 match self.bootstrap_unaudited(username, password) {
643 Ok(result) => {
644 let event_result = self.emit_user_lifecycle_allowed(
645 control,
646 EventKind::UserCreate,
647 "user.create",
648 &id,
649 user_control_fields(&id, Some(Role::Admin), Some(true), true),
650 );
651 if let Err(err) = event_result {
652 self.rollback_bootstrap(&id);
653 return Err(err);
654 }
655 Ok(result)
656 }
657 Err(err) => {
658 self.emit_user_lifecycle_outcome(
659 control,
660 if user_error_is_denied(&err) {
661 Outcome::Denied
662 } else {
663 Outcome::Error
664 },
665 EventKind::UserCreate,
666 "user.create",
667 &id,
668 Some(err.to_string()),
669 user_control_fields(&id, Some(Role::Admin), Some(true), true),
670 );
671 Err(err)
672 }
673 }
674 }
675
676 pub fn bootstrap_from_env(&self) -> Option<BootstrapResult> {
684 if !self.needs_bootstrap() {
685 return None;
686 }
687
688 let username = crate::utils::env_with_file_fallback("REDDB_USERNAME")?;
689 let password = crate::utils::env_with_file_fallback("REDDB_PASSWORD")?;
690
691 if username.is_empty() || password.is_empty() {
692 return None;
693 }
694
695 match self.bootstrap(&username, &password) {
696 Ok(result) => {
697 tracing::info!(
703 username = %reddb_wire::audit_safe_log_field(&username),
704 "bootstrapped admin user from REDDB_USERNAME/REDDB_PASSWORD"
705 );
706 if let Some(ref cert) = result.certificate {
707 eprintln!("[reddb] CERTIFICATE: {}", cert);
711 tracing::warn!(
712 "vault certificate issued — save it: ONLY way to unseal after restart"
713 );
714 }
715 Some(result)
716 }
717 Err(e) => {
718 tracing::error!(err = %e, "env bootstrap failed");
719 None
720 }
721 }
722 }
723
724 fn persist_to_vault_result(&self) -> Result<(), AuthError> {
730 let vault_guard = self.vault.read().unwrap_or_else(|e| e.into_inner());
731 if let (Some(ref vault), Some(ref pager)) = (&*vault_guard, &self.pager) {
732 let state = self.snapshot();
733 vault.save(pager, &state)?;
734 }
735 Ok(())
736 }
737
738 fn persist_to_vault(&self) {
744 if let Err(e) = self.persist_to_vault_result() {
745 tracing::error!(err = %e, "vault persist failed");
746 crate::telemetry::operator_event::OperatorEvent::SecretRotationFailed {
752 secret_ref: "auth_vault".to_string(),
753 error: e.to_string(),
754 }
755 .emit_global();
756 }
757 }
758
759 pub fn is_vault_backed(&self) -> bool {
761 self.pager.is_some()
762 && self
763 .vault
764 .read()
765 .map(|guard| guard.is_some())
766 .unwrap_or(false)
767 }
768
769 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
775 self.vault_kv
776 .read()
777 .ok()
778 .and_then(|kv| kv.get(key).cloned())
779 }
780
781 pub fn vault_kv_snapshot(&self) -> HashMap<String, String> {
783 self.vault_kv
784 .read()
785 .map(|kv| kv.clone())
786 .unwrap_or_default()
787 }
788
789 pub fn vault_kv_export_encrypted(&self) -> Result<Option<String>, AuthError> {
792 if !self.is_vault_backed() {
793 return Err(AuthError::Forbidden(
794 "vault KV export requires an enabled, unsealed vault".to_string(),
795 ));
796 }
797 let kv = self.vault_kv_snapshot();
798 if kv.is_empty() {
799 return Ok(None);
800 }
801
802 let vault_guard = self.vault.read().map_err(lock_err)?;
803 let vault = vault_guard.as_ref().ok_or_else(|| {
804 AuthError::Forbidden("vault KV export requires an enabled, unsealed vault".to_string())
805 })?;
806 let state = VaultState {
807 users: Vec::new(),
808 api_keys: Vec::new(),
809 bootstrapped: false,
810 master_secret: None,
811 kv,
812 };
813 Ok(Some(vault.seal_logical_export(&state)?))
814 }
815
816 pub fn vault_kv_try_import(
819 &self,
820 entries: HashMap<String, String>,
821 ) -> Result<usize, AuthError> {
822 if !self.is_vault_backed() {
823 return Err(AuthError::Forbidden(
824 "vault KV import requires an enabled, unsealed vault".to_string(),
825 ));
826 }
827 if entries.is_empty() {
828 return Ok(0);
829 }
830
831 let mut previous = HashMap::new();
832 {
833 let mut kv = self.vault_kv.write().map_err(lock_err)?;
834 for (key, value) in &entries {
835 previous.insert(key.clone(), kv.insert(key.clone(), value.clone()));
836 }
837 }
838
839 if let Err(err) = self.persist_to_vault_result() {
840 if let Ok(mut kv) = self.vault_kv.write() {
841 for (key, old) in previous {
842 match old {
843 Some(value) => {
844 kv.insert(key, value);
845 }
846 None => {
847 kv.remove(&key);
848 }
849 }
850 }
851 }
852 return Err(err);
853 }
854
855 Ok(entries.len())
856 }
857
858 pub fn vault_kv_try_import_placeholders(&self, keys: &[String]) -> Result<usize, AuthError> {
861 let entries = keys
862 .iter()
863 .map(|key| (key.clone(), "false".to_string()))
864 .collect();
865 self.vault_kv_try_import(entries)
866 }
867
868 pub fn vault_kv_set(&self, key: String, value: String) {
870 if let Ok(mut kv) = self.vault_kv.write() {
871 kv.insert(key, value);
872 }
873 self.persist_to_vault();
874 }
875
876 pub fn vault_kv_try_set(&self, key: String, value: String) -> Result<(), AuthError> {
879 if !self.is_vault_backed() {
880 return Err(AuthError::Forbidden(
881 "SET SECRET requires an enabled, unsealed vault".to_string(),
882 ));
883 }
884
885 let previous = {
886 let mut kv = self.vault_kv.write().map_err(lock_err)?;
887 kv.insert(key.clone(), value)
888 };
889
890 if let Err(err) = self.persist_to_vault_result() {
891 if let Ok(mut kv) = self.vault_kv.write() {
892 match previous {
893 Some(value) => {
894 kv.insert(key, value);
895 }
896 None => {
897 kv.remove(&key);
898 }
899 }
900 }
901 return Err(err);
902 }
903
904 Ok(())
905 }
906
907 pub fn vault_kv_delete(&self, key: &str) -> bool {
909 let existed = self
910 .vault_kv
911 .write()
912 .map(|mut kv| kv.remove(key).is_some())
913 .unwrap_or(false);
914 if existed {
915 self.persist_to_vault();
916 }
917 existed
918 }
919
920 pub fn vault_kv_try_delete(&self, key: &str) -> Result<bool, AuthError> {
923 if !self.is_vault_backed() {
924 return Err(AuthError::Forbidden(
925 "DELETE SECRET requires an enabled, unsealed vault".to_string(),
926 ));
927 }
928
929 let removed = {
930 let mut kv = self.vault_kv.write().map_err(lock_err)?;
931 kv.remove(key)
932 };
933
934 if removed.is_none() {
935 return Ok(false);
936 }
937
938 if let Err(err) = self.persist_to_vault_result() {
939 if let Ok(mut kv) = self.vault_kv.write() {
940 if let Some(value) = removed {
941 kv.insert(key.to_string(), value);
942 }
943 }
944 return Err(err);
945 }
946
947 Ok(true)
948 }
949
950 pub fn vault_kv_keys(&self) -> Vec<String> {
952 self.vault_kv
953 .read()
954 .map(|kv| kv.keys().cloned().collect())
955 .unwrap_or_default()
956 }
957
958 pub fn plain_kv_get(&self, key: &str) -> Option<String> {
969 self.plain_kv
970 .read()
971 .ok()
972 .and_then(|kv| kv.get(key).cloned())
973 }
974
975 pub fn plain_kv_snapshot(&self) -> HashMap<String, String> {
977 self.plain_kv
978 .read()
979 .map(|kv| kv.clone())
980 .unwrap_or_default()
981 }
982
983 pub fn plain_kv_set(&self, key: String, value: String) {
985 if let Ok(mut kv) = self.plain_kv.write() {
986 kv.insert(key, value);
987 }
988 }
989
990 pub fn plain_kv_delete(&self, key: &str) -> bool {
992 self.plain_kv
993 .write()
994 .map(|mut kv| kv.remove(key).is_some())
995 .unwrap_or(false)
996 }
997
998 pub fn plain_kv_keys(&self) -> Vec<String> {
1000 self.plain_kv
1001 .read()
1002 .map(|kv| kv.keys().cloned().collect())
1003 .unwrap_or_default()
1004 }
1005
1006 pub fn vault_secret_key(&self) -> Option<[u8; 32]> {
1009 let hex_str = self.vault_kv_get("red.secret.aes_key")?;
1010 let bytes = hex::decode(hex_str).ok()?;
1011 if bytes.len() == 32 {
1012 let mut key = [0u8; 32];
1013 key.copy_from_slice(&bytes);
1014 Some(key)
1015 } else {
1016 None
1017 }
1018 }
1019
1020 pub fn ensure_vault_secret_key(&self) {
1022 if self.vault_kv_get("red.secret.aes_key").is_none() {
1023 let key = random_bytes(32);
1024 self.vault_kv_set("red.secret.aes_key".to_string(), hex::encode(key));
1025 }
1026 }
1027
1028 fn snapshot(&self) -> VaultState {
1030 let users_guard = self.users.read().unwrap_or_else(|e| e.into_inner());
1031 let users: Vec<User> = users_guard.values().cloned().collect();
1032
1033 let mut api_keys = Vec::new();
1036 for user in &users {
1037 let owner = UserId::from_parts(user.tenant_id.as_deref(), &user.username);
1038 for key in &user.api_keys {
1039 api_keys.push((owner.clone(), key.clone()));
1040 }
1041 }
1042
1043 let master_secret = self
1045 .keypair
1046 .read()
1047 .ok()
1048 .and_then(|guard| guard.as_ref().map(|kp| kp.master_secret.clone()));
1049
1050 let kv = self.vault_kv.read().map(|m| m.clone()).unwrap_or_default();
1051
1052 VaultState {
1053 users,
1054 api_keys,
1055 bootstrapped: self.bootstrapped.load(Ordering::Acquire),
1056 master_secret,
1057 kv,
1058 }
1059 }
1060
1061 fn restore_from_vault(&mut self, state: VaultState) {
1063 if state.bootstrapped {
1065 self.bootstrapped.store(true, Ordering::Release);
1066 }
1067
1068 if let Some(secret) = state.master_secret {
1070 let kp = KeyPair::from_master_secret(secret);
1071 if let Ok(mut guard) = self.keypair.write() {
1072 *guard = Some(kp);
1073 }
1074 }
1075
1076 if let Ok(mut kv) = self.vault_kv.write() {
1078 *kv = state.kv;
1079 }
1080
1081 let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
1083 let mut idx = self
1084 .api_key_index
1085 .write()
1086 .unwrap_or_else(|e| e.into_inner());
1087
1088 for user in state.users {
1089 let id = UserId::from_parts(user.tenant_id.as_deref(), &user.username);
1090 for key in &user.api_keys {
1092 idx.insert(key.key.clone(), (id.clone(), key.role));
1093 }
1094 users.insert(id, user);
1095 }
1096 drop(idx);
1097 drop(users);
1098
1099 self.rehydrate_acl();
1100 self.rehydrate_iam();
1101 }
1102
1103 pub fn create_user(
1111 &self,
1112 username: &str,
1113 password: &str,
1114 role: Role,
1115 ) -> Result<User, AuthError> {
1116 self.create_user_in_tenant(None, username, password, role)
1117 }
1118
1119 pub fn create_user_with_control_events(
1120 &self,
1121 username: &str,
1122 password: &str,
1123 role: Role,
1124 ctx: &ControlEventCtx<'_>,
1125 ledger: &dyn ControlEventLedger,
1126 config: ControlEventConfig,
1127 ) -> Result<User, AuthError> {
1128 self.create_user_in_tenant_with_control_events(
1129 None, username, password, role, ctx, ledger, config,
1130 )
1131 }
1132
1133 pub fn create_user_in_tenant(
1138 &self,
1139 tenant_id: Option<&str>,
1140 username: &str,
1141 password: &str,
1142 role: Role,
1143 ) -> Result<User, AuthError> {
1144 if let Some(configured) = self.configured_control_events() {
1145 let ctx = default_user_lifecycle_ctx();
1146 let control = UserLifecycleControl {
1147 ctx: &ctx,
1148 ledger: configured.ledger.as_ref(),
1149 config: configured.config,
1150 };
1151 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1152 } else {
1153 self.create_user_in_tenant_unaudited(tenant_id, username, password, role)
1154 }
1155 }
1156
1157 #[allow(clippy::too_many_arguments)]
1158 pub fn create_user_in_tenant_with_control_events(
1159 &self,
1160 tenant_id: Option<&str>,
1161 username: &str,
1162 password: &str,
1163 role: Role,
1164 ctx: &ControlEventCtx<'_>,
1165 ledger: &dyn ControlEventLedger,
1166 config: ControlEventConfig,
1167 ) -> Result<User, AuthError> {
1168 let control = UserLifecycleControl {
1169 ctx,
1170 ledger,
1171 config,
1172 };
1173 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1174 }
1175
1176 pub fn create_admin_user(
1177 &self,
1178 username: &str,
1179 password: &str,
1180 role: Role,
1181 tenant_id: Option<&str>,
1182 ) -> Result<User, AuthError> {
1183 if let Some(configured) = self.configured_control_events() {
1184 let ctx = default_user_lifecycle_ctx();
1185 let control = UserLifecycleControl {
1186 ctx: &ctx,
1187 ledger: configured.ledger.as_ref(),
1188 config: configured.config,
1189 };
1190 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1191 } else {
1192 self.create_user_in_tenant_unaudited(tenant_id, username, password, role)
1193 }
1194 }
1195
1196 fn create_user_in_tenant_unaudited(
1197 &self,
1198 tenant_id: Option<&str>,
1199 username: &str,
1200 password: &str,
1201 role: Role,
1202 ) -> Result<User, AuthError> {
1203 let id = UserId::from_parts(tenant_id, username);
1204 let mut users = self.users.write().map_err(lock_err)?;
1205 if users.contains_key(&id) {
1206 return Err(AuthError::UserExists(id.to_string()));
1207 }
1208
1209 let now = now_ms();
1210 let user = User {
1211 username: username.to_string(),
1212 tenant_id: tenant_id.map(|s| s.to_string()),
1213 password_hash: hash_password(password),
1214 scram_verifier: Some(make_scram_verifier(password)),
1215 role,
1216 api_keys: Vec::new(),
1217 created_at: now,
1218 updated_at: now,
1219 enabled: true,
1220 };
1221 users.insert(id, user.clone());
1222 drop(users); self.persist_to_vault();
1224 Ok(user)
1225 }
1226
1227 fn create_user_in_tenant_controlled(
1228 &self,
1229 tenant_id: Option<&str>,
1230 username: &str,
1231 password: &str,
1232 role: Role,
1233 control: &UserLifecycleControl<'_>,
1234 ) -> Result<User, AuthError> {
1235 let id = UserId::from_parts(tenant_id, username);
1236 match self.create_user_in_tenant_unaudited(tenant_id, username, password, role) {
1237 Ok(user) => {
1238 if let Err(err) = self.emit_user_lifecycle_allowed(
1239 control,
1240 EventKind::UserCreate,
1241 "user.create",
1242 &id,
1243 user_control_fields(&id, Some(role), Some(true), true),
1244 ) {
1245 self.rollback_create_user(&id);
1246 return Err(err);
1247 }
1248 Ok(user)
1249 }
1250 Err(err) => {
1251 self.emit_user_lifecycle_outcome(
1252 control,
1253 if user_error_is_denied(&err) {
1254 Outcome::Denied
1255 } else {
1256 Outcome::Error
1257 },
1258 EventKind::UserCreate,
1259 "user.create",
1260 &id,
1261 Some(err.to_string()),
1262 user_control_fields(&id, Some(role), Some(true), true),
1263 );
1264 Err(err)
1265 }
1266 }
1267 }
1268
1269 pub fn lookup_scram_verifier(&self, id: &UserId) -> Option<crate::auth::scram::ScramVerifier> {
1275 if crate::auth::self_lock_guard::is_synthetic_principal(&id.username) {
1277 return None;
1278 }
1279 let users = self.users.read().ok()?;
1280 users.get(id).and_then(|u| u.scram_verifier.clone())
1281 }
1282
1283 pub fn lookup_scram_verifier_global(
1288 &self,
1289 username: &str,
1290 ) -> Option<crate::auth::scram::ScramVerifier> {
1291 self.lookup_scram_verifier(&UserId::platform(username))
1292 }
1293
1294 pub fn list_users(&self) -> Vec<User> {
1300 let users = match self.users.read() {
1301 Ok(g) => g,
1302 Err(_) => return Vec::new(),
1303 };
1304 users
1305 .values()
1306 .filter(|u| !crate::auth::self_lock_guard::is_synthetic_principal(&u.username))
1307 .map(|u| User {
1308 password_hash: String::new(), ..u.clone()
1310 })
1311 .collect()
1312 }
1313
1314 pub fn list_users_scoped(&self, tenant_filter: Option<Option<&str>>) -> Vec<User> {
1321 let users = match self.users.read() {
1322 Ok(g) => g,
1323 Err(_) => return Vec::new(),
1324 };
1325 users
1326 .values()
1327 .filter(|u| match tenant_filter {
1328 None => true,
1329 Some(t) => u.tenant_id.as_deref() == t,
1330 })
1331 .filter(|u| !crate::auth::self_lock_guard::is_synthetic_principal(&u.username))
1332 .map(|u| User {
1333 password_hash: String::new(), ..u.clone()
1335 })
1336 .collect()
1337 }
1338
1339 pub fn user_lifecycle_resource(uid: &UserId) -> ResourceRef {
1344 let resource = ResourceRef::new("user", uid.username.clone());
1345 match &uid.tenant {
1346 Some(tenant) => resource.with_tenant(tenant.clone()),
1347 None => resource,
1348 }
1349 }
1350
1351 pub fn eval_context_for_principal(
1352 &self,
1353 principal: &UserId,
1354 role: Role,
1355 current_tenant: Option<String>,
1356 ) -> EvalContext {
1357 EvalContext {
1358 principal_tenant: principal.tenant.clone(),
1359 current_tenant,
1360 peer_ip: None,
1361 mfa_present: false,
1362 now_ms: now_ms(),
1363 principal_is_admin_role: role == Role::Admin,
1364 principal_is_platform_scoped: principal.tenant.is_none(),
1365 }
1366 }
1367
1368 pub fn check_user_lifecycle_authz(
1375 &self,
1376 actor: &UserId,
1377 actor_role: Role,
1378 action: &str,
1379 target: &UserId,
1380 ) -> bool {
1381 let resource = Self::user_lifecycle_resource(target);
1382 let ctx = self.eval_context_for_principal(actor, actor_role, target.tenant.clone());
1383 self.check_policy_authz_with_role(actor, action, &resource, &ctx, actor_role)
1384 }
1385
1386 pub fn has_explicit_user_lifecycle_deny(
1392 &self,
1393 actor: &UserId,
1394 actor_role: Role,
1395 action: &str,
1396 target: &UserId,
1397 ) -> bool {
1398 let resource = Self::user_lifecycle_resource(target);
1399 let ctx = self.eval_context_for_principal(actor, actor_role, target.tenant.clone());
1400 let pols = self.effective_policies(actor);
1401 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
1402 matches!(
1403 iam_policies::evaluate(&refs, action, &resource, &ctx),
1404 iam_policies::Decision::Deny { .. }
1405 )
1406 }
1407
1408 pub fn get_user(&self, tenant_id: Option<&str>, username: &str) -> Option<User> {
1409 let id = UserId::from_parts(tenant_id, username);
1410 self.get_user_cloned(&id).map(|u| User {
1411 password_hash: String::new(),
1412 ..u
1413 })
1414 }
1415
1416 pub fn delete_user(&self, username: &str) -> Result<(), AuthError> {
1421 self.delete_user_in_tenant(None, username)
1422 }
1423
1424 pub fn delete_user_with_control_events(
1425 &self,
1426 username: &str,
1427 ctx: &ControlEventCtx<'_>,
1428 ledger: &dyn ControlEventLedger,
1429 config: ControlEventConfig,
1430 ) -> Result<(), AuthError> {
1431 self.delete_user_in_tenant_with_control_events(None, username, ctx, ledger, config)
1432 }
1433
1434 pub fn delete_user_in_tenant(
1437 &self,
1438 tenant_id: Option<&str>,
1439 username: &str,
1440 ) -> Result<(), AuthError> {
1441 if let Some(configured) = self.configured_control_events() {
1442 let ctx = default_user_lifecycle_ctx();
1443 let control = UserLifecycleControl {
1444 ctx: &ctx,
1445 ledger: configured.ledger.as_ref(),
1446 config: configured.config,
1447 };
1448 self.delete_user_in_tenant_controlled(tenant_id, username, &control)
1449 } else {
1450 self.delete_user_in_tenant_unaudited(tenant_id, username)
1451 }
1452 }
1453
1454 pub fn delete_user_in_tenant_with_control_events(
1455 &self,
1456 tenant_id: Option<&str>,
1457 username: &str,
1458 ctx: &ControlEventCtx<'_>,
1459 ledger: &dyn ControlEventLedger,
1460 config: ControlEventConfig,
1461 ) -> Result<(), AuthError> {
1462 let control = UserLifecycleControl {
1463 ctx,
1464 ledger,
1465 config,
1466 };
1467 self.delete_user_in_tenant_controlled(tenant_id, username, &control)
1468 }
1469
1470 fn delete_user_in_tenant_unaudited(
1471 &self,
1472 tenant_id: Option<&str>,
1473 username: &str,
1474 ) -> Result<(), AuthError> {
1475 let id = UserId::from_parts(tenant_id, username);
1476 let mut users = self.users.write().map_err(lock_err)?;
1477 let user = users
1478 .remove(&id)
1479 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1480
1481 if let Ok(mut idx) = self.api_key_index.write() {
1483 for api_key in &user.api_keys {
1484 idx.remove(&api_key.key);
1485 }
1486 }
1487
1488 if let Ok(mut sessions) = self.sessions.write() {
1491 sessions
1492 .retain(|_, s| !(s.username == username && s.tenant_id.as_deref() == tenant_id));
1493 }
1494
1495 self.persist_to_vault();
1496 Ok(())
1497 }
1498
1499 fn delete_user_in_tenant_controlled(
1500 &self,
1501 tenant_id: Option<&str>,
1502 username: &str,
1503 control: &UserLifecycleControl<'_>,
1504 ) -> Result<(), AuthError> {
1505 let id = UserId::from_parts(tenant_id, username);
1506 let rollback = self.user_delete_rollback_snapshot(&id, tenant_id, username);
1507 match self.delete_user_in_tenant_unaudited(tenant_id, username) {
1508 Ok(()) => {
1509 if let Err(err) = self.emit_user_lifecycle_allowed(
1510 control,
1511 EventKind::UserDelete,
1512 "user.delete",
1513 &id,
1514 user_control_fields(&id, rollback.as_ref().map(|r| r.0.role), None, false),
1515 ) {
1516 if let Some((user, sessions)) = rollback {
1517 self.restore_deleted_user(&id, user, sessions);
1518 }
1519 return Err(err);
1520 }
1521 Ok(())
1522 }
1523 Err(err) => {
1524 self.emit_user_lifecycle_outcome(
1525 control,
1526 if user_error_is_denied(&err) {
1527 Outcome::Denied
1528 } else {
1529 Outcome::Error
1530 },
1531 EventKind::UserDelete,
1532 "user.delete",
1533 &id,
1534 Some(err.to_string()),
1535 user_control_fields(&id, rollback.as_ref().map(|r| r.0.role), None, false),
1536 );
1537 Err(err)
1538 }
1539 }
1540 }
1541
1542 pub fn change_password(
1546 &self,
1547 username: &str,
1548 old_password: &str,
1549 new_password: &str,
1550 ) -> Result<(), AuthError> {
1551 self.change_password_in_tenant(None, username, old_password, new_password)
1552 }
1553
1554 pub fn change_password_with_control_events(
1555 &self,
1556 username: &str,
1557 old_password: &str,
1558 new_password: &str,
1559 ctx: &ControlEventCtx<'_>,
1560 ledger: &dyn ControlEventLedger,
1561 config: ControlEventConfig,
1562 ) -> Result<(), AuthError> {
1563 self.change_password_in_tenant_with_control_events(
1564 None,
1565 username,
1566 old_password,
1567 new_password,
1568 ctx,
1569 ledger,
1570 config,
1571 )
1572 }
1573
1574 #[allow(clippy::too_many_arguments)]
1575 pub fn change_password_in_tenant(
1576 &self,
1577 tenant_id: Option<&str>,
1578 username: &str,
1579 old_password: &str,
1580 new_password: &str,
1581 ) -> Result<(), AuthError> {
1582 if let Some(configured) = self.configured_control_events() {
1583 let ctx = default_user_lifecycle_ctx();
1584 let control = UserLifecycleControl {
1585 ctx: &ctx,
1586 ledger: configured.ledger.as_ref(),
1587 config: configured.config,
1588 };
1589 self.change_password_in_tenant_controlled(
1590 tenant_id,
1591 username,
1592 old_password,
1593 new_password,
1594 &control,
1595 )
1596 } else {
1597 self.change_password_in_tenant_unaudited(
1598 tenant_id,
1599 username,
1600 old_password,
1601 new_password,
1602 )
1603 }
1604 }
1605
1606 #[allow(clippy::too_many_arguments)]
1607 pub fn change_password_in_tenant_with_control_events(
1608 &self,
1609 tenant_id: Option<&str>,
1610 username: &str,
1611 old_password: &str,
1612 new_password: &str,
1613 ctx: &ControlEventCtx<'_>,
1614 ledger: &dyn ControlEventLedger,
1615 config: ControlEventConfig,
1616 ) -> Result<(), AuthError> {
1617 let control = UserLifecycleControl {
1618 ctx,
1619 ledger,
1620 config,
1621 };
1622 self.change_password_in_tenant_controlled(
1623 tenant_id,
1624 username,
1625 old_password,
1626 new_password,
1627 &control,
1628 )
1629 }
1630
1631 fn change_password_in_tenant_unaudited(
1632 &self,
1633 tenant_id: Option<&str>,
1634 username: &str,
1635 old_password: &str,
1636 new_password: &str,
1637 ) -> Result<(), AuthError> {
1638 let id = UserId::from_parts(tenant_id, username);
1639 let mut users = self.users.write().map_err(lock_err)?;
1640 let user = users
1641 .get_mut(&id)
1642 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1643
1644 if !verify_password(old_password, &user.password_hash) {
1645 return Err(AuthError::InvalidCredentials);
1646 }
1647
1648 user.password_hash = hash_password(new_password);
1649 user.scram_verifier = Some(make_scram_verifier(new_password));
1650 user.updated_at = now_ms();
1651 drop(users); self.persist_to_vault();
1653 Ok(())
1654 }
1655
1656 fn change_password_in_tenant_controlled(
1657 &self,
1658 tenant_id: Option<&str>,
1659 username: &str,
1660 old_password: &str,
1661 new_password: &str,
1662 control: &UserLifecycleControl<'_>,
1663 ) -> Result<(), AuthError> {
1664 let id = UserId::from_parts(tenant_id, username);
1665 let previous = self.get_user_cloned(&id);
1666 match self.change_password_in_tenant_unaudited(
1667 tenant_id,
1668 username,
1669 old_password,
1670 new_password,
1671 ) {
1672 Ok(()) => {
1673 if let Err(err) = self.emit_user_lifecycle_allowed(
1674 control,
1675 EventKind::UserUpdate,
1676 "user.update",
1677 &id,
1678 user_control_fields(
1679 &id,
1680 previous.as_ref().map(|u| u.role),
1681 previous.as_ref().map(|u| u.enabled),
1682 true,
1683 ),
1684 ) {
1685 self.restore_user_snapshot(&id, previous);
1686 return Err(err);
1687 }
1688 Ok(())
1689 }
1690 Err(err) => {
1691 self.emit_user_lifecycle_outcome(
1692 control,
1693 if user_error_is_denied(&err) {
1694 Outcome::Denied
1695 } else {
1696 Outcome::Error
1697 },
1698 EventKind::UserUpdate,
1699 "user.update",
1700 &id,
1701 Some(err.to_string()),
1702 user_control_fields(
1703 &id,
1704 previous.as_ref().map(|u| u.role),
1705 previous.as_ref().map(|u| u.enabled),
1706 true,
1707 ),
1708 );
1709 Err(err)
1710 }
1711 }
1712 }
1713
1714 pub fn change_role(&self, username: &str, new_role: Role) -> Result<(), AuthError> {
1717 self.change_role_in_tenant(None, username, new_role)
1718 }
1719
1720 pub fn change_role_in_tenant(
1721 &self,
1722 tenant_id: Option<&str>,
1723 username: &str,
1724 new_role: Role,
1725 ) -> Result<(), AuthError> {
1726 if let Some(configured) = self.configured_control_events() {
1727 let ctx = default_user_lifecycle_ctx();
1728 let control = UserLifecycleControl {
1729 ctx: &ctx,
1730 ledger: configured.ledger.as_ref(),
1731 config: configured.config,
1732 };
1733 self.change_role_in_tenant_controlled(tenant_id, username, new_role, &control)
1734 } else {
1735 self.change_role_in_tenant_unaudited(tenant_id, username, new_role)
1736 }
1737 }
1738
1739 pub fn change_role_in_tenant_with_control_events(
1740 &self,
1741 tenant_id: Option<&str>,
1742 username: &str,
1743 new_role: Role,
1744 ctx: &ControlEventCtx<'_>,
1745 ledger: &dyn ControlEventLedger,
1746 config: ControlEventConfig,
1747 ) -> Result<(), AuthError> {
1748 let control = UserLifecycleControl {
1749 ctx,
1750 ledger,
1751 config,
1752 };
1753 self.change_role_in_tenant_controlled(tenant_id, username, new_role, &control)
1754 }
1755
1756 fn change_role_in_tenant_unaudited(
1757 &self,
1758 tenant_id: Option<&str>,
1759 username: &str,
1760 new_role: Role,
1761 ) -> Result<(), AuthError> {
1762 let id = UserId::from_parts(tenant_id, username);
1763 let mut users = self.users.write().map_err(lock_err)?;
1764 let user = users
1765 .get_mut(&id)
1766 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1767
1768 let prior_role = user.role;
1769 user.role = new_role;
1770 user.updated_at = now_ms();
1771
1772 if new_role == Role::Admin && prior_role != Role::Admin {
1777 crate::telemetry::operator_event::OperatorEvent::AdminCapabilityGranted {
1778 granted_to: id.to_string(),
1779 capability: "Role::Admin".to_string(),
1780 granted_by: "auth_store::change_role".to_string(),
1781 }
1782 .emit_global();
1783 }
1784
1785 for key in &mut user.api_keys {
1787 if key.role > new_role {
1788 key.role = new_role;
1789 }
1790 }
1791
1792 if let Ok(mut idx) = self.api_key_index.write() {
1794 for key in &user.api_keys {
1795 if let Some(entry) = idx.get_mut(&key.key) {
1796 entry.1 = key.role;
1797 }
1798 }
1799 }
1800
1801 self.persist_to_vault();
1802 Ok(())
1803 }
1804
1805 fn change_role_in_tenant_controlled(
1806 &self,
1807 tenant_id: Option<&str>,
1808 username: &str,
1809 new_role: Role,
1810 control: &UserLifecycleControl<'_>,
1811 ) -> Result<(), AuthError> {
1812 let id = UserId::from_parts(tenant_id, username);
1813 let previous = self.get_user_cloned(&id);
1814 match self.change_role_in_tenant_unaudited(tenant_id, username, new_role) {
1815 Ok(()) => {
1816 if let Err(err) = self.emit_user_lifecycle_allowed(
1817 control,
1818 EventKind::UserUpdate,
1819 "user.update",
1820 &id,
1821 user_control_fields(
1822 &id,
1823 Some(new_role),
1824 previous.as_ref().map(|u| u.enabled),
1825 false,
1826 ),
1827 ) {
1828 self.restore_user_snapshot(&id, previous);
1829 return Err(err);
1830 }
1831 Ok(())
1832 }
1833 Err(err) => {
1834 self.emit_user_lifecycle_outcome(
1835 control,
1836 if user_error_is_denied(&err) {
1837 Outcome::Denied
1838 } else {
1839 Outcome::Error
1840 },
1841 EventKind::UserUpdate,
1842 "user.update",
1843 &id,
1844 Some(err.to_string()),
1845 user_control_fields(
1846 &id,
1847 Some(new_role),
1848 previous.as_ref().map(|u| u.enabled),
1849 false,
1850 ),
1851 );
1852 Err(err)
1853 }
1854 }
1855 }
1856
1857 pub fn authenticate(&self, username: &str, password: &str) -> Result<Session, AuthError> {
1869 self.authenticate_in_tenant(None, username, password)
1870 }
1871
1872 pub fn authenticate_in_tenant(
1876 &self,
1877 tenant_id: Option<&str>,
1878 username: &str,
1879 password: &str,
1880 ) -> Result<Session, AuthError> {
1881 if crate::auth::self_lock_guard::is_synthetic_principal(username) {
1885 return Err(AuthError::InvalidCredentials);
1886 }
1887 let id = UserId::from_parts(tenant_id, username);
1888 let users = self.users.read().map_err(lock_err)?;
1889 let user = users.get(&id).ok_or(AuthError::InvalidCredentials)?;
1890
1891 if !user.enabled {
1892 return Err(AuthError::InvalidCredentials);
1893 }
1894
1895 if !verify_password(password, &user.password_hash) {
1896 return Err(AuthError::InvalidCredentials);
1897 }
1898
1899 let token = match self.keypair.read().ok().and_then(|g| {
1901 g.as_ref().map(|kp| {
1902 let token_id = random_hex(16);
1903 let sig = kp.sign(format!("session:{}", token_id).as_bytes());
1904 format!("rs_{}{}", token_id, hex::encode(&sig[..16]))
1906 })
1907 }) {
1908 Some(signed_token) => signed_token,
1909 None => generate_session_token(),
1910 };
1911
1912 let now = now_ms();
1913 let session = Session {
1914 token,
1915 username: username.to_string(),
1916 tenant_id: user.tenant_id.clone(),
1917 role: user.role,
1918 created_at: now,
1919 expires_at: now + (self.config.session_ttl_secs as u128 * 1000),
1920 };
1921
1922 drop(users); let mut sessions = self.sessions.write().map_err(lock_err)?;
1925 sessions.insert(session.token.clone(), session.clone());
1926 Ok(session)
1927 }
1928
1929 pub fn validate_token(&self, token: &str) -> Option<(String, Role)> {
1941 self.validate_token_full(token)
1942 .map(|(id, role)| (id.username, role))
1943 }
1944
1945 pub fn validate_token_full(&self, token: &str) -> Option<(UserId, Role)> {
1948 if token.starts_with("rs_") {
1950 if let Ok(sessions) = self.sessions.read() {
1951 if let Some(session) = sessions.get(token) {
1952 let now = now_ms();
1953 if now < session.expires_at {
1954 return Some((
1955 UserId::from_parts(session.tenant_id.as_deref(), &session.username),
1956 session.role,
1957 ));
1958 }
1959 }
1960 }
1961 return None;
1962 }
1963
1964 if token.starts_with("rk_") {
1966 if let Ok(idx) = self.api_key_index.read() {
1967 return idx.get(token).cloned();
1968 }
1969 return None;
1970 }
1971
1972 None
1973 }
1974
1975 pub fn create_api_key(
1983 &self,
1984 username: &str,
1985 name: &str,
1986 role: Role,
1987 ) -> Result<ApiKey, AuthError> {
1988 self.create_api_key_in_tenant(None, username, name, role)
1989 }
1990
1991 pub fn create_api_key_with_control_events(
1992 &self,
1993 username: &str,
1994 name: &str,
1995 role: Role,
1996 ctx: &ControlEventCtx<'_>,
1997 ledger: &dyn ControlEventLedger,
1998 config: ControlEventConfig,
1999 ) -> Result<ApiKey, AuthError> {
2000 self.create_api_key_in_tenant_with_control_events(
2001 None, username, name, role, ctx, ledger, config,
2002 )
2003 }
2004
2005 pub fn create_api_key_in_tenant(
2006 &self,
2007 tenant_id: Option<&str>,
2008 username: &str,
2009 name: &str,
2010 role: Role,
2011 ) -> Result<ApiKey, AuthError> {
2012 if let Some(configured) = self.configured_control_events() {
2013 let ctx = default_user_lifecycle_ctx();
2014 let control = UserLifecycleControl {
2015 ctx: &ctx,
2016 ledger: configured.ledger.as_ref(),
2017 config: configured.config,
2018 };
2019 self.create_api_key_in_tenant_controlled(tenant_id, username, name, role, &control)
2020 } else {
2021 self.create_api_key_in_tenant_unaudited(tenant_id, username, name, role)
2022 }
2023 }
2024
2025 #[allow(clippy::too_many_arguments)]
2026 pub fn create_api_key_in_tenant_with_control_events(
2027 &self,
2028 tenant_id: Option<&str>,
2029 username: &str,
2030 name: &str,
2031 role: Role,
2032 ctx: &ControlEventCtx<'_>,
2033 ledger: &dyn ControlEventLedger,
2034 config: ControlEventConfig,
2035 ) -> Result<ApiKey, AuthError> {
2036 let control = UserLifecycleControl {
2037 ctx,
2038 ledger,
2039 config,
2040 };
2041 self.create_api_key_in_tenant_controlled(tenant_id, username, name, role, &control)
2042 }
2043
2044 fn create_api_key_in_tenant_unaudited(
2045 &self,
2046 tenant_id: Option<&str>,
2047 username: &str,
2048 name: &str,
2049 role: Role,
2050 ) -> Result<ApiKey, AuthError> {
2051 let id = UserId::from_parts(tenant_id, username);
2052 let mut users = self.users.write().map_err(lock_err)?;
2053 let user = users
2054 .get_mut(&id)
2055 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
2056
2057 if role > user.role {
2059 return Err(AuthError::RoleExceeded {
2060 requested: role,
2061 ceiling: user.role,
2062 });
2063 }
2064
2065 let api_key = ApiKey {
2066 key: generate_api_key(),
2067 name: name.to_string(),
2068 role,
2069 created_at: now_ms(),
2070 };
2071
2072 user.api_keys.push(api_key.clone());
2073 user.updated_at = now_ms();
2074
2075 if let Ok(mut idx) = self.api_key_index.write() {
2077 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
2078 }
2079
2080 drop(users); self.persist_to_vault();
2082 Ok(api_key)
2083 }
2084
2085 fn create_api_key_in_tenant_controlled(
2086 &self,
2087 tenant_id: Option<&str>,
2088 username: &str,
2089 name: &str,
2090 role: Role,
2091 control: &UserLifecycleControl<'_>,
2092 ) -> Result<ApiKey, AuthError> {
2093 let id = UserId::from_parts(tenant_id, username);
2094 match self.create_api_key_in_tenant_unaudited(tenant_id, username, name, role) {
2095 Ok(api_key) => {
2096 let key_id = api_key_id(&api_key.key);
2097 if let Err(err) = self.emit_api_key_allowed(
2098 control,
2099 EventKind::ApiKeyCreate,
2100 "apikey.create",
2101 &key_id,
2102 api_key_control_fields(&id, role, &key_id),
2103 ) {
2104 self.rollback_create_api_key(&id, &api_key.key);
2105 return Err(err);
2106 }
2107 Ok(api_key)
2108 }
2109 Err(err) => {
2110 self.emit_user_lifecycle_outcome(
2111 control,
2112 if user_error_is_denied(&err) {
2113 Outcome::Denied
2114 } else {
2115 Outcome::Error
2116 },
2117 EventKind::ApiKeyCreate,
2118 "apikey.create",
2119 &id,
2120 Some(err.to_string()),
2121 user_control_fields(&id, Some(role), None, false),
2122 );
2123 Err(err)
2124 }
2125 }
2126 }
2127
2128 pub fn revoke_api_key(&self, key: &str) -> Result<(), AuthError> {
2130 if let Some(configured) = self.configured_control_events() {
2131 let ctx = default_user_lifecycle_ctx();
2132 let control = UserLifecycleControl {
2133 ctx: &ctx,
2134 ledger: configured.ledger.as_ref(),
2135 config: configured.config,
2136 };
2137 self.revoke_api_key_controlled(key, &control)
2138 } else {
2139 self.revoke_api_key_unaudited(key)
2140 }
2141 }
2142
2143 pub fn revoke_api_key_with_control_events(
2144 &self,
2145 key: &str,
2146 ctx: &ControlEventCtx<'_>,
2147 ledger: &dyn ControlEventLedger,
2148 config: ControlEventConfig,
2149 ) -> Result<(), AuthError> {
2150 let control = UserLifecycleControl {
2151 ctx,
2152 ledger,
2153 config,
2154 };
2155 self.revoke_api_key_controlled(key, &control)
2156 }
2157
2158 fn revoke_api_key_unaudited(&self, key: &str) -> Result<(), AuthError> {
2159 let mut users = self.users.write().map_err(lock_err)?;
2160
2161 let owner_id: UserId = {
2165 if let Ok(idx) = self.api_key_index.read() {
2166 if let Some((id, _)) = idx.get(key) {
2167 id.clone()
2168 } else {
2169 return Err(AuthError::KeyNotFound(key.to_string()));
2170 }
2171 } else {
2172 let owner = users
2173 .iter()
2174 .find(|(_, u)| u.api_keys.iter().any(|k| k.key == key));
2175 match owner {
2176 Some((id, _)) => id.clone(),
2177 None => return Err(AuthError::KeyNotFound(key.to_string())),
2178 }
2179 }
2180 };
2181
2182 let user = users
2183 .get_mut(&owner_id)
2184 .ok_or_else(|| AuthError::KeyNotFound(key.to_string()))?;
2185 user.api_keys.retain(|k| k.key != key);
2186 user.updated_at = now_ms();
2187
2188 if let Ok(mut idx) = self.api_key_index.write() {
2190 idx.remove(key);
2191 }
2192
2193 self.persist_to_vault();
2194 Ok(())
2195 }
2196
2197 fn revoke_api_key_controlled(
2198 &self,
2199 key: &str,
2200 control: &UserLifecycleControl<'_>,
2201 ) -> Result<(), AuthError> {
2202 let key_id = api_key_id(key);
2203 let rollback = self.api_key_rollback_snapshot(key);
2204 let id = rollback
2205 .as_ref()
2206 .map(|r| r.0.clone())
2207 .unwrap_or_else(|| UserId::from_parts(None, ""));
2208 let role = rollback.as_ref().map(|r| r.1.role).unwrap_or(Role::Read);
2209 match self.revoke_api_key_unaudited(key) {
2210 Ok(()) => {
2211 if let Err(err) = self.emit_api_key_allowed(
2212 control,
2213 EventKind::ApiKeyRevoke,
2214 "apikey.revoke",
2215 &key_id,
2216 api_key_control_fields(&id, role, &key_id),
2217 ) {
2218 if let Some((owner, api_key)) = rollback {
2219 self.restore_api_key(&owner, api_key);
2220 }
2221 return Err(err);
2222 }
2223 Ok(())
2224 }
2225 Err(err) => {
2226 self.emit_user_lifecycle_outcome(
2227 control,
2228 if user_error_is_denied(&err) {
2229 Outcome::Denied
2230 } else {
2231 Outcome::Error
2232 },
2233 EventKind::ApiKeyRevoke,
2234 "apikey.revoke",
2235 &id,
2236 Some(err.to_string()),
2237 api_key_control_fields(&id, role, &key_id),
2238 );
2239 Err(err)
2240 }
2241 }
2242 }
2243
2244 pub fn revoke_session(&self, token: &str) {
2250 if let Ok(mut sessions) = self.sessions.write() {
2251 sessions.remove(token);
2252 }
2253 }
2254
2255 pub fn purge_expired_sessions(&self) -> usize {
2257 let now = now_ms();
2258 if let Ok(mut sessions) = self.sessions.write() {
2259 let before = sessions.len();
2260 sessions.retain(|_, s| s.expires_at > now);
2261 return before - sessions.len();
2262 }
2263 0
2264 }
2265
2266 pub fn grant(
2286 &self,
2287 granter: &UserId,
2288 granter_role: Role,
2289 principal: GrantPrincipal,
2290 resource: Resource,
2291 actions: Vec<Action>,
2292 with_grant_option: bool,
2293 tenant: Option<String>,
2294 ) -> Result<(), AuthError> {
2295 if granter_role != Role::Admin {
2296 return Err(AuthError::Forbidden(format!(
2297 "GRANT requires Admin role; granter `{}` has `{:?}`",
2298 granter, granter_role
2299 )));
2300 }
2301
2302 if granter.tenant.is_some() && granter.tenant != tenant {
2306 return Err(AuthError::Forbidden(format!(
2307 "cross-tenant GRANT denied: granter tenant `{:?}` != grant tenant `{:?}`",
2308 granter.tenant, tenant
2309 )));
2310 }
2311
2312 let mut actions_set = std::collections::BTreeSet::new();
2313 for a in actions {
2314 actions_set.insert(a);
2315 }
2316 let g = Grant {
2317 principal: principal.clone(),
2318 resource,
2319 actions: actions_set,
2320 with_grant_option,
2321 granted_by: granter.to_string(),
2322 granted_at: now_ms(),
2323 tenant,
2324 columns: None,
2325 };
2326
2327 match &principal {
2328 GrantPrincipal::User(uid) => {
2329 self.grants
2330 .write()
2331 .unwrap_or_else(|e| e.into_inner())
2332 .entry(uid.clone())
2333 .or_default()
2334 .push(g.clone());
2335 self.invalidate_permission_cache(Some(uid));
2336 }
2337 GrantPrincipal::Public => {
2338 self.public_grants
2339 .write()
2340 .unwrap_or_else(|e| e.into_inner())
2341 .push(g.clone());
2342 self.invalidate_permission_cache(None);
2343 }
2344 GrantPrincipal::Group(_) => {
2345 return Err(AuthError::Forbidden(
2346 "GROUP principals are not yet supported; use a USER or PUBLIC".to_string(),
2347 ));
2348 }
2349 }
2350
2351 self.invalidate_visible_collections_for_tenant(g.tenant.as_deref());
2356
2357 self.persist_acl_to_kv();
2358 Ok(())
2359 }
2360
2361 pub fn revoke(
2364 &self,
2365 granter_role: Role,
2366 principal: &GrantPrincipal,
2367 resource: &Resource,
2368 actions: &[Action],
2369 ) -> Result<usize, AuthError> {
2370 if granter_role != Role::Admin {
2371 return Err(AuthError::Forbidden(format!(
2372 "REVOKE requires Admin role; granter has `{:?}`",
2373 granter_role
2374 )));
2375 }
2376
2377 let removed = match principal {
2378 GrantPrincipal::User(uid) => {
2379 let mut g = self.grants.write().unwrap_or_else(|e| e.into_inner());
2380 let before = g.get(uid).map(|v| v.len()).unwrap_or(0);
2381 if let Some(list) = g.get_mut(uid) {
2382 list.retain(|gr| {
2383 !(gr.resource == *resource
2384 && (actions.iter().any(|a| gr.actions.contains(a))
2385 || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2386 });
2387 }
2388 let after = g.get(uid).map(|v| v.len()).unwrap_or(0);
2389 drop(g);
2390 self.invalidate_permission_cache(Some(uid));
2391 before - after
2392 }
2393 GrantPrincipal::Public => {
2394 let mut p = self
2395 .public_grants
2396 .write()
2397 .unwrap_or_else(|e| e.into_inner());
2398 let before = p.len();
2399 p.retain(|gr| {
2400 !(gr.resource == *resource
2401 && (actions.iter().any(|a| gr.actions.contains(a))
2402 || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2403 });
2404 let after = p.len();
2405 drop(p);
2406 self.invalidate_permission_cache(None);
2407 before - after
2408 }
2409 GrantPrincipal::Group(_) => 0,
2410 };
2411
2412 if removed > 0 {
2413 match principal {
2419 GrantPrincipal::User(uid) => {
2420 self.invalidate_visible_collections_for_tenant(uid.tenant.as_deref());
2421 }
2422 GrantPrincipal::Public | GrantPrincipal::Group(_) => {
2423 self.invalidate_visible_collections_cache();
2424 }
2425 }
2426 self.persist_acl_to_kv();
2427 }
2428 Ok(removed)
2429 }
2430
2431 pub fn visible_collections_for_scope(
2445 &self,
2446 tenant: Option<&str>,
2447 role: Role,
2448 principal: &str,
2449 all_collections: &[String],
2450 ) -> std::collections::HashSet<String> {
2451 let key = super::scope_cache::ScopeKey::new(tenant, principal, role);
2452 if let Some(hit) = self.visible_collections_cache.get(&key) {
2453 return hit;
2454 }
2455 let ctx = AuthzContext {
2458 principal,
2459 effective_role: role,
2460 tenant,
2461 };
2462 let mut visible = std::collections::HashSet::new();
2463 for collection in all_collections {
2464 let resource = Resource::table_from_name(collection);
2465 if self.check_grant(&ctx, Action::Select, &resource).is_ok() {
2466 visible.insert(collection.clone());
2467 }
2468 }
2469 self.visible_collections_cache.insert(key, visible.clone());
2470 visible
2471 }
2472
2473 pub fn auth_cache_stats(&self) -> super::scope_cache::AuthCacheStats {
2477 self.visible_collections_cache.stats()
2478 }
2479
2480 pub fn invalidate_visible_collections_cache(&self) {
2484 self.visible_collections_cache.invalidate_all();
2485 }
2486
2487 pub fn invalidate_visible_collections_for_tenant(&self, tenant: Option<&str>) {
2490 self.visible_collections_cache.invalidate_tenant(tenant);
2491 }
2492
2493 pub fn effective_grants(&self, uid: &UserId) -> Vec<Grant> {
2496 let mut out = Vec::new();
2497 if let Ok(g) = self.grants.read() {
2498 if let Some(list) = g.get(uid) {
2499 out.extend(list.iter().cloned());
2500 }
2501 }
2502 if let Ok(p) = self.public_grants.read() {
2503 out.extend(p.iter().cloned());
2504 }
2505 out
2506 }
2507
2508 pub fn check_grant(
2511 &self,
2512 ctx: &AuthzContext<'_>,
2513 action: Action,
2514 resource: &Resource,
2515 ) -> Result<(), AuthzError> {
2516 if ctx.effective_role == Role::Admin {
2517 return Ok(());
2518 }
2519
2520 let uid = UserId::from_parts(ctx.tenant, ctx.principal);
2521
2522 if let Ok(cache) = self.permission_cache.read() {
2524 if let Some(pc) = cache.get(&uid) {
2525 if pc.allows(resource, action) {
2526 return Ok(());
2527 }
2528 }
2529 }
2530
2531 let user_grants = self
2533 .grants
2534 .read()
2535 .ok()
2536 .and_then(|g| g.get(&uid).cloned())
2537 .unwrap_or_default();
2538 let any_user_grants = self
2539 .grants
2540 .read()
2541 .ok()
2542 .map(|g| g.values().any(|list| !list.is_empty()))
2543 .unwrap_or(false);
2544 let public_grants = self
2545 .public_grants
2546 .read()
2547 .ok()
2548 .map(|p| p.clone())
2549 .unwrap_or_default();
2550 if user_grants.is_empty() && public_grants.is_empty() && any_user_grants {
2551 return Err(AuthzError::PermissionDenied {
2552 action,
2553 resource: resource.clone(),
2554 principal: ctx.principal.to_string(),
2555 });
2556 }
2557 let view = GrantsView {
2558 user_grants: &user_grants,
2559 public_grants: &public_grants,
2560 };
2561 let result = check_grant(ctx, action, resource, &view);
2562
2563 if result.is_ok() {
2564 let pc = PermissionCache::build(&user_grants, &public_grants);
2565 if let Ok(mut cache) = self.permission_cache.write() {
2566 cache.insert(uid, pc);
2567 }
2568 }
2569 result
2570 }
2571
2572 pub fn set_user_attributes(
2578 &self,
2579 uid: &UserId,
2580 attrs: UserAttributes,
2581 ) -> Result<(), AuthError> {
2582 let users = self.users.read().map_err(lock_err)?;
2583 if !users.contains_key(uid) {
2584 return Err(AuthError::UserNotFound(uid.to_string()));
2585 }
2586 drop(users);
2587
2588 self.user_attributes
2589 .write()
2590 .unwrap_or_else(|e| e.into_inner())
2591 .insert(uid.clone(), attrs);
2592 self.invalidate_iam_cache(Some(uid));
2593 self.persist_acl_to_kv();
2594 Ok(())
2595 }
2596
2597 pub fn user_attributes(&self, uid: &UserId) -> UserAttributes {
2600 self.user_attributes
2601 .read()
2602 .ok()
2603 .and_then(|m| m.get(uid).cloned())
2604 .unwrap_or_default()
2605 }
2606
2607 pub fn add_user_to_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
2608 if group.trim().is_empty() {
2609 return Err(AuthError::Forbidden("group name cannot be empty".into()));
2610 }
2611 let mut attrs = self.user_attributes(uid);
2612 if !attrs.groups.iter().any(|g| g == group) {
2613 attrs.groups.push(group.to_string());
2614 attrs.groups.sort();
2615 }
2616 self.set_user_attributes(uid, attrs)
2617 }
2618
2619 pub fn remove_user_from_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
2620 let mut attrs = self.user_attributes(uid);
2621 attrs.groups.retain(|g| g != group);
2622 self.set_user_attributes(uid, attrs)
2623 }
2624
2625 pub fn set_user_enabled(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
2627 if let Some(configured) = self.configured_control_events() {
2628 let ctx = default_user_lifecycle_ctx();
2629 let control = UserLifecycleControl {
2630 ctx: &ctx,
2631 ledger: configured.ledger.as_ref(),
2632 config: configured.config,
2633 };
2634 self.set_user_enabled_controlled(uid, enabled, &control)
2635 } else {
2636 self.set_user_enabled_unaudited(uid, enabled)
2637 }
2638 }
2639
2640 pub fn disable_user(
2641 &self,
2642 username: &str,
2643 ctx: &ControlEventCtx<'_>,
2644 ledger: &dyn ControlEventLedger,
2645 config: ControlEventConfig,
2646 ) -> Result<(), AuthError> {
2647 self.disable_user_in_tenant(None, username, ctx, ledger, config)
2648 }
2649
2650 pub fn disable_user_in_tenant(
2651 &self,
2652 tenant_id: Option<&str>,
2653 username: &str,
2654 ctx: &ControlEventCtx<'_>,
2655 ledger: &dyn ControlEventLedger,
2656 config: ControlEventConfig,
2657 ) -> Result<(), AuthError> {
2658 let uid = UserId::from_parts(tenant_id, username);
2659 let control = UserLifecycleControl {
2660 ctx,
2661 ledger,
2662 config,
2663 };
2664 self.set_user_enabled_controlled(&uid, false, &control)
2665 }
2666
2667 fn set_user_enabled_unaudited(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
2668 let mut users = self.users.write().map_err(lock_err)?;
2669 let user = users
2670 .get_mut(uid)
2671 .ok_or_else(|| AuthError::UserNotFound(uid.to_string()))?;
2672 user.enabled = enabled;
2673 user.updated_at = now_ms();
2674 drop(users);
2675 self.persist_to_vault();
2676 Ok(())
2677 }
2678
2679 fn set_user_enabled_controlled(
2680 &self,
2681 uid: &UserId,
2682 enabled: bool,
2683 control: &UserLifecycleControl<'_>,
2684 ) -> Result<(), AuthError> {
2685 let previous = self.get_user_cloned(uid);
2686 let kind = if enabled {
2687 EventKind::UserUpdate
2688 } else {
2689 EventKind::UserDisable
2690 };
2691 let action = if enabled {
2692 "user.update"
2693 } else {
2694 "user.disable"
2695 };
2696 match self.set_user_enabled_unaudited(uid, enabled) {
2697 Ok(()) => {
2698 if let Err(err) = self.emit_user_lifecycle_allowed(
2699 control,
2700 kind,
2701 action,
2702 uid,
2703 user_control_fields(
2704 uid,
2705 previous.as_ref().map(|u| u.role),
2706 Some(enabled),
2707 false,
2708 ),
2709 ) {
2710 self.restore_user_snapshot(uid, previous);
2711 return Err(err);
2712 }
2713 Ok(())
2714 }
2715 Err(err) => {
2716 self.emit_user_lifecycle_outcome(
2717 control,
2718 if user_error_is_denied(&err) {
2719 Outcome::Denied
2720 } else {
2721 Outcome::Error
2722 },
2723 kind,
2724 action,
2725 uid,
2726 Some(err.to_string()),
2727 user_control_fields(
2728 uid,
2729 previous.as_ref().map(|u| u.role),
2730 Some(enabled),
2731 false,
2732 ),
2733 );
2734 Err(err)
2735 }
2736 }
2737 }
2738
2739 fn emit_user_lifecycle_allowed(
2740 &self,
2741 control: &UserLifecycleControl<'_>,
2742 kind: EventKind,
2743 action: &'static str,
2744 id: &UserId,
2745 fields: HashMap<String, Sensitivity>,
2746 ) -> Result<(), AuthError> {
2747 self.emit_user_lifecycle_event(control, Outcome::Allowed, kind, action, id, None, fields)
2748 }
2749
2750 fn emit_api_key_allowed(
2751 &self,
2752 control: &UserLifecycleControl<'_>,
2753 kind: EventKind,
2754 action: &'static str,
2755 api_key_id: &str,
2756 fields: HashMap<String, Sensitivity>,
2757 ) -> Result<(), AuthError> {
2758 self.emit_control_event(
2759 control,
2760 Outcome::Allowed,
2761 kind,
2762 action,
2763 Some(api_key_resource(api_key_id)),
2764 None,
2765 fields,
2766 )
2767 }
2768
2769 #[allow(clippy::too_many_arguments)]
2770 fn emit_user_lifecycle_outcome(
2771 &self,
2772 control: &UserLifecycleControl<'_>,
2773 outcome: Outcome,
2774 kind: EventKind,
2775 action: &'static str,
2776 id: &UserId,
2777 reason: Option<String>,
2778 fields: HashMap<String, Sensitivity>,
2779 ) {
2780 let _ = self.emit_user_lifecycle_event(control, outcome, kind, action, id, reason, fields);
2781 }
2782
2783 #[allow(clippy::too_many_arguments)]
2784 fn emit_user_lifecycle_event(
2785 &self,
2786 control: &UserLifecycleControl<'_>,
2787 outcome: Outcome,
2788 kind: EventKind,
2789 action: &'static str,
2790 id: &UserId,
2791 reason: Option<String>,
2792 fields: HashMap<String, Sensitivity>,
2793 ) -> Result<(), AuthError> {
2794 self.emit_control_event(
2795 control,
2796 outcome,
2797 kind,
2798 action,
2799 Some(user_resource(id)),
2800 reason,
2801 fields,
2802 )
2803 }
2804
2805 #[allow(clippy::too_many_arguments)]
2806 fn emit_control_event(
2807 &self,
2808 control: &UserLifecycleControl<'_>,
2809 outcome: Outcome,
2810 kind: EventKind,
2811 action: &'static str,
2812 resource: Option<String>,
2813 reason: Option<String>,
2814 fields: HashMap<String, Sensitivity>,
2815 ) -> Result<(), AuthError> {
2816 let event = ControlEvent {
2817 kind,
2818 outcome,
2819 action: Cow::Borrowed(action),
2820 resource,
2821 reason,
2822 matched_policy_id: None,
2823 fields,
2824 };
2825 match control.ledger.emit(control.ctx, event) {
2826 Ok(_) => Ok(()),
2827 Err(err) if control.config.require_persistence() => {
2828 Err(AuthError::Internal(err.to_string()))
2829 }
2830 Err(_) => Ok(()),
2831 }
2832 }
2833
2834 fn rollback_create_user(&self, id: &UserId) {
2835 let removed = self
2836 .users
2837 .write()
2838 .unwrap_or_else(|e| e.into_inner())
2839 .remove(id);
2840 if let Some(user) = removed {
2841 self.remove_api_key_index_entries(&user);
2842 }
2843 self.persist_to_vault();
2844 }
2845
2846 fn rollback_bootstrap(&self, id: &UserId) {
2847 self.bootstrapped.store(false, Ordering::Release);
2848 self.rollback_create_user(id);
2849 }
2850
2851 fn restore_user_snapshot(&self, id: &UserId, previous: Option<User>) {
2852 match previous {
2853 Some(user) => {
2854 self.users
2855 .write()
2856 .unwrap_or_else(|e| e.into_inner())
2857 .insert(id.clone(), user.clone());
2858 self.index_user_api_keys(id, &user);
2859 }
2860 None => {
2861 self.rollback_create_user(id);
2862 return;
2863 }
2864 }
2865 self.persist_to_vault();
2866 }
2867
2868 fn user_delete_rollback_snapshot(
2869 &self,
2870 id: &UserId,
2871 tenant_id: Option<&str>,
2872 username: &str,
2873 ) -> Option<(User, Vec<(String, Session)>)> {
2874 let user = self.get_user_cloned(id)?;
2875 let sessions = self
2876 .sessions
2877 .read()
2878 .map(|sessions| {
2879 sessions
2880 .iter()
2881 .filter(|(_, session)| {
2882 session.username == username && session.tenant_id.as_deref() == tenant_id
2883 })
2884 .map(|(token, session)| (token.clone(), session.clone()))
2885 .collect()
2886 })
2887 .unwrap_or_default();
2888 Some((user, sessions))
2889 }
2890
2891 fn restore_deleted_user(&self, id: &UserId, user: User, sessions: Vec<(String, Session)>) {
2892 self.users
2893 .write()
2894 .unwrap_or_else(|e| e.into_inner())
2895 .insert(id.clone(), user.clone());
2896 self.index_user_api_keys(id, &user);
2897 if !sessions.is_empty() {
2898 let mut guard = self.sessions.write().unwrap_or_else(|e| e.into_inner());
2899 for (token, session) in sessions {
2900 guard.insert(token, session);
2901 }
2902 }
2903 self.persist_to_vault();
2904 }
2905
2906 fn rollback_create_api_key(&self, id: &UserId, key: &str) {
2907 if let Ok(mut users) = self.users.write() {
2908 if let Some(user) = users.get_mut(id) {
2909 user.api_keys.retain(|api_key| api_key.key != key);
2910 user.updated_at = now_ms();
2911 }
2912 }
2913 if let Ok(mut idx) = self.api_key_index.write() {
2914 idx.remove(key);
2915 }
2916 self.persist_to_vault();
2917 }
2918
2919 fn api_key_rollback_snapshot(&self, key: &str) -> Option<(UserId, ApiKey)> {
2920 let users = self.users.read().ok()?;
2921 users.iter().find_map(|(id, user)| {
2922 user.api_keys
2923 .iter()
2924 .find(|api_key| api_key.key == key)
2925 .cloned()
2926 .map(|api_key| (id.clone(), api_key))
2927 })
2928 }
2929
2930 fn restore_api_key(&self, id: &UserId, api_key: ApiKey) {
2931 if let Ok(mut users) = self.users.write() {
2932 if let Some(user) = users.get_mut(id) {
2933 if !user
2934 .api_keys
2935 .iter()
2936 .any(|candidate| candidate.key == api_key.key)
2937 {
2938 user.api_keys.push(api_key.clone());
2939 user.updated_at = now_ms();
2940 }
2941 }
2942 }
2943 if let Ok(mut idx) = self.api_key_index.write() {
2944 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
2945 }
2946 self.persist_to_vault();
2947 }
2948
2949 fn remove_api_key_index_entries(&self, user: &User) {
2950 if let Ok(mut idx) = self.api_key_index.write() {
2951 for api_key in &user.api_keys {
2952 idx.remove(&api_key.key);
2953 }
2954 }
2955 }
2956
2957 fn index_user_api_keys(&self, id: &UserId, user: &User) {
2958 if let Ok(mut idx) = self.api_key_index.write() {
2959 for api_key in &user.api_keys {
2960 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
2961 }
2962 }
2963 }
2964
2965 pub fn authenticate_with_attrs(
2975 &self,
2976 tenant_id: Option<&str>,
2977 username: &str,
2978 password: &str,
2979 ) -> Result<Session, AuthError> {
2980 let uid = UserId::from_parts(tenant_id, username);
2981 let attrs = self.user_attributes(&uid);
2982
2983 if let Some(deadline) = attrs.valid_until {
2984 if now_ms() >= deadline {
2985 return Err(AuthError::Forbidden(format!(
2986 "account `{}` expired (VALID UNTIL exceeded)",
2987 uid
2988 )));
2989 }
2990 }
2991
2992 if let Some(limit) = attrs.connection_limit {
2993 let current = self
2994 .session_count_by_user
2995 .read()
2996 .ok()
2997 .and_then(|m| m.get(&uid).copied())
2998 .unwrap_or(0);
2999 if current >= limit {
3000 return Err(AuthError::Forbidden(format!(
3001 "account `{}` exceeded CONNECTION LIMIT ({})",
3002 uid, limit
3003 )));
3004 }
3005 }
3006
3007 let session = self.authenticate_in_tenant(tenant_id, username, password)?;
3008
3009 if let Ok(mut counts) = self.session_count_by_user.write() {
3010 *counts.entry(uid).or_insert(0) += 1;
3011 }
3012 Ok(session)
3013 }
3014
3015 pub fn decrement_session_count(&self, uid: &UserId) {
3018 if let Ok(mut counts) = self.session_count_by_user.write() {
3019 if let Some(c) = counts.get_mut(uid) {
3020 *c = c.saturating_sub(1);
3021 }
3022 }
3023 }
3024
3025 pub fn rehydrate_acl(&self) {
3032 let kv_snapshot: Vec<(String, String)> = self
3033 .vault_kv
3034 .read()
3035 .map(|kv| {
3036 kv.iter()
3037 .filter(|(k, _)| {
3038 k.starts_with("red.acl.grants.")
3039 || k.starts_with("red.acl.attrs.")
3040 || k == &"red.acl.public_grants"
3041 })
3042 .map(|(k, v)| (k.clone(), v.clone()))
3043 .collect()
3044 })
3045 .unwrap_or_default();
3046
3047 for (k, v) in kv_snapshot {
3048 if k == "red.acl.public_grants" {
3049 if let Some(parsed) = decode_grants_blob(&v) {
3050 *self
3051 .public_grants
3052 .write()
3053 .unwrap_or_else(|e| e.into_inner()) = parsed;
3054 }
3055 } else if let Some(suffix) = k.strip_prefix("red.acl.grants.") {
3056 if let Some(uid) = decode_uid(suffix) {
3057 if let Some(mut parsed) = decode_grants_blob(&v) {
3058 for g in parsed.iter_mut() {
3061 g.principal = GrantPrincipal::User(uid.clone());
3062 }
3063 self.grants
3064 .write()
3065 .unwrap_or_else(|e| e.into_inner())
3066 .insert(uid, parsed);
3067 }
3068 }
3069 } else if let Some(suffix) = k.strip_prefix("red.acl.attrs.") {
3070 if let Some(uid) = decode_uid(suffix) {
3071 if let Some(parsed) = decode_attrs_blob(&v) {
3072 self.user_attributes
3073 .write()
3074 .unwrap_or_else(|e| e.into_inner())
3075 .insert(uid, parsed);
3076 }
3077 }
3078 }
3079 }
3080
3081 self.permission_cache
3082 .write()
3083 .unwrap_or_else(|e| e.into_inner())
3084 .clear();
3085 }
3086
3087 fn persist_acl_to_kv(&self) {
3089 let public = self
3090 .public_grants
3091 .read()
3092 .ok()
3093 .map(|p| encode_grants_blob(&p))
3094 .unwrap_or_default();
3095 self.vault_kv_set("red.acl.public_grants".to_string(), public);
3096
3097 let snapshot: Vec<(UserId, Vec<Grant>)> = self
3098 .grants
3099 .read()
3100 .ok()
3101 .map(|g| g.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3102 .unwrap_or_default();
3103 for (uid, list) in snapshot {
3104 let key = format!("red.acl.grants.{}", encode_uid(&uid));
3105 let val = encode_grants_blob(&list);
3106 self.vault_kv_set(key, val);
3107 }
3108
3109 let attrs_snapshot: Vec<(UserId, UserAttributes)> = self
3110 .user_attributes
3111 .read()
3112 .ok()
3113 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3114 .unwrap_or_default();
3115 for (uid, attrs) in attrs_snapshot {
3116 let key = format!("red.acl.attrs.{}", encode_uid(&uid));
3117 let val = encode_attrs_blob(&attrs);
3118 self.vault_kv_set(key, val);
3119 }
3120 }
3121
3122 fn invalidate_permission_cache(&self, uid: Option<&UserId>) {
3123 if let Ok(mut cache) = self.permission_cache.write() {
3124 match uid {
3125 Some(u) => {
3126 cache.remove(u);
3127 }
3128 None => cache.clear(),
3129 }
3130 }
3131 }
3132
3133 pub fn put_policy_with_control_events(
3142 &self,
3143 p: Policy,
3144 control: &PolicyMutationControl<'_>,
3145 ) -> Result<(), AuthError> {
3146 let policy_id = p.id.clone();
3147 let kind = if self.get_policy(&policy_id).is_some() {
3148 EventKind::PolicyUpdate
3149 } else {
3150 EventKind::PolicyCreate
3151 };
3152
3153 if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
3154 let err = AuthError::Forbidden(format!("policy id `{}` is reserved", p.id));
3155 self.emit_policy_error(
3156 control,
3157 kind,
3158 "policy:put",
3159 &policy_id,
3160 Some(&p),
3161 None,
3162 &err,
3163 );
3164 return Err(err);
3165 }
3166
3167 if let Some(ManagedPolicyDecision::Deny {
3168 entry_id,
3169 matched_action,
3170 matched_resource,
3171 reason,
3172 ..
3173 }) = self.managed_policy_decision(&policy_id, PolicyOp::Put, control)
3174 {
3175 self.emit_policy_denied(
3176 control,
3177 kind,
3178 "policy:put",
3179 &policy_id,
3180 Some(&p),
3181 None,
3182 &reason.to_string(),
3183 Some(entry_id),
3184 Some((matched_action, matched_resource)),
3185 );
3186 return Err(Self::managed_policy_error(&policy_id, &reason.to_string()));
3187 }
3188
3189 let previous = self.get_policy(&policy_id);
3190 let was_enabled = self.iam_authorization_enabled();
3191 match self.put_policy_internal(p.clone()) {
3192 Ok(()) => match self.emit_policy_allowed(
3193 control,
3194 kind,
3195 "policy:put",
3196 &policy_id,
3197 Some(&p),
3198 None,
3199 None,
3200 ) {
3201 Ok(()) => Ok(()),
3202 Err(err) => {
3203 self.restore_policy_put(&policy_id, previous, was_enabled);
3204 Err(err)
3205 }
3206 },
3207 Err(err) => {
3208 self.emit_policy_error(
3209 control,
3210 kind,
3211 "policy:put",
3212 &policy_id,
3213 Some(&p),
3214 None,
3215 &err,
3216 );
3217 Err(err)
3218 }
3219 }
3220 }
3221
3222 pub fn delete_policy_with_control_events(
3223 &self,
3224 id: &str,
3225 control: &PolicyMutationControl<'_>,
3226 ) -> Result<(), AuthError> {
3227 let existing = self.get_policy(id);
3228 if let Some(ManagedPolicyDecision::Deny {
3229 entry_id,
3230 matched_action,
3231 matched_resource,
3232 reason,
3233 ..
3234 }) = self.managed_policy_decision(id, PolicyOp::Drop, control)
3235 {
3236 self.emit_policy_denied(
3237 control,
3238 EventKind::PolicyDelete,
3239 "policy:drop",
3240 id,
3241 existing.as_deref(),
3242 None,
3243 &reason.to_string(),
3244 Some(entry_id),
3245 Some((matched_action, matched_resource)),
3246 );
3247 return Err(Self::managed_policy_error(id, &reason.to_string()));
3248 }
3249
3250 let user_attachments = self
3251 .user_attachments
3252 .read()
3253 .map(|m| m.clone())
3254 .unwrap_or_default();
3255 let group_attachments = self
3256 .group_attachments
3257 .read()
3258 .map(|m| m.clone())
3259 .unwrap_or_default();
3260 let was_enabled = self.iam_authorization_enabled();
3261 match self.delete_policy(id) {
3262 Ok(()) => match self.emit_policy_allowed(
3263 control,
3264 EventKind::PolicyDelete,
3265 "policy:drop",
3266 id,
3267 existing.as_deref(),
3268 None,
3269 None,
3270 ) {
3271 Ok(()) => Ok(()),
3272 Err(err) => {
3273 if let Some(policy) = existing {
3274 self.restore_policy_delete(
3275 id,
3276 policy,
3277 user_attachments,
3278 group_attachments,
3279 was_enabled,
3280 );
3281 }
3282 Err(err)
3283 }
3284 },
3285 Err(err) => {
3286 self.emit_policy_error(
3287 control,
3288 EventKind::PolicyDelete,
3289 "policy:drop",
3290 id,
3291 existing.as_deref(),
3292 None,
3293 &err,
3294 );
3295 Err(err)
3296 }
3297 }
3298 }
3299
3300 pub fn attach_policy_with_control_events(
3301 &self,
3302 principal: PrincipalRef,
3303 policy_id: &str,
3304 control: &PolicyMutationControl<'_>,
3305 ) -> Result<(), AuthError> {
3306 let existing = self.get_policy(policy_id);
3307 if let Some(ManagedPolicyDecision::Deny {
3308 entry_id,
3309 matched_action,
3310 matched_resource,
3311 reason,
3312 ..
3313 }) = self.managed_policy_decision(policy_id, PolicyOp::Attach, control)
3314 {
3315 self.emit_policy_denied(
3316 control,
3317 EventKind::PolicyAttach,
3318 "policy:attach",
3319 policy_id,
3320 existing.as_deref(),
3321 Some(&principal),
3322 &reason.to_string(),
3323 Some(entry_id),
3324 Some((matched_action, matched_resource)),
3325 );
3326 return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3327 }
3328
3329 if let Some(err_msg) = self.check_self_lock_invariant_for_attach(policy_id) {
3330 self.emit_policy_denied(
3331 control,
3332 EventKind::PolicyAttach,
3333 "policy:attach",
3334 policy_id,
3335 existing.as_deref(),
3336 Some(&principal),
3337 &err_msg,
3338 None,
3339 None,
3340 );
3341 return Err(AuthError::Forbidden(err_msg));
3342 }
3343
3344 let user_attachments = self
3345 .user_attachments
3346 .read()
3347 .map(|m| m.clone())
3348 .unwrap_or_default();
3349 let group_attachments = self
3350 .group_attachments
3351 .read()
3352 .map(|m| m.clone())
3353 .unwrap_or_default();
3354 match self.attach_policy(principal.clone(), policy_id) {
3355 Ok(()) => match self.emit_policy_allowed(
3356 control,
3357 EventKind::PolicyAttach,
3358 "policy:attach",
3359 policy_id,
3360 existing.as_deref(),
3361 Some(&principal),
3362 None,
3363 ) {
3364 Ok(()) => Ok(()),
3365 Err(err) => {
3366 self.restore_policy_attachments(user_attachments, group_attachments);
3367 Err(err)
3368 }
3369 },
3370 Err(err) => {
3371 self.emit_policy_error(
3372 control,
3373 EventKind::PolicyAttach,
3374 "policy:attach",
3375 policy_id,
3376 existing.as_deref(),
3377 Some(&principal),
3378 &err,
3379 );
3380 Err(err)
3381 }
3382 }
3383 }
3384
3385 pub fn detach_policy_with_control_events(
3386 &self,
3387 principal: PrincipalRef,
3388 policy_id: &str,
3389 control: &PolicyMutationControl<'_>,
3390 ) -> Result<(), AuthError> {
3391 let existing = self.get_policy(policy_id);
3392 if let Some(ManagedPolicyDecision::Deny {
3393 entry_id,
3394 matched_action,
3395 matched_resource,
3396 reason,
3397 ..
3398 }) = self.managed_policy_decision(policy_id, PolicyOp::Detach, control)
3399 {
3400 self.emit_policy_denied(
3401 control,
3402 EventKind::PolicyDetach,
3403 "policy:detach",
3404 policy_id,
3405 existing.as_deref(),
3406 Some(&principal),
3407 &reason.to_string(),
3408 Some(entry_id),
3409 Some((matched_action, matched_resource)),
3410 );
3411 return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3412 }
3413 let Some(existing_policy) = existing else {
3414 let err = AuthError::Forbidden(format!("policy `{policy_id}` not found"));
3415 self.emit_policy_error(
3416 control,
3417 EventKind::PolicyDetach,
3418 "policy:detach",
3419 policy_id,
3420 None,
3421 Some(&principal),
3422 &err,
3423 );
3424 return Err(err);
3425 };
3426
3427 let user_attachments = self
3428 .user_attachments
3429 .read()
3430 .map(|m| m.clone())
3431 .unwrap_or_default();
3432 let group_attachments = self
3433 .group_attachments
3434 .read()
3435 .map(|m| m.clone())
3436 .unwrap_or_default();
3437 match self.detach_policy(principal.clone(), policy_id) {
3438 Ok(()) => match self.emit_policy_allowed(
3439 control,
3440 EventKind::PolicyDetach,
3441 "policy:detach",
3442 policy_id,
3443 Some(existing_policy.as_ref()),
3444 Some(&principal),
3445 None,
3446 ) {
3447 Ok(()) => Ok(()),
3448 Err(err) => {
3449 self.restore_policy_attachments(user_attachments, group_attachments);
3450 Err(err)
3451 }
3452 },
3453 Err(err) => {
3454 self.emit_policy_error(
3455 control,
3456 EventKind::PolicyDetach,
3457 "policy:detach",
3458 policy_id,
3459 Some(existing_policy.as_ref()),
3460 Some(&principal),
3461 &err,
3462 );
3463 Err(err)
3464 }
3465 }
3466 }
3467
3468 fn managed_policy_decision(
3469 &self,
3470 policy_id: &str,
3471 op: PolicyOp,
3472 control: &PolicyMutationControl<'_>,
3473 ) -> Option<ManagedPolicyDecision> {
3474 control.registry.map(|registry| {
3475 ManagedPolicyGate::new(registry).check_mutation(
3476 self,
3477 control.actor,
3478 control.eval_ctx,
3479 policy_id,
3480 op,
3481 )
3482 })
3483 }
3484
3485 fn check_self_lock_invariant_for_attach(&self, _policy_id: &str) -> Option<String> {
3492 use crate::auth::self_lock_guard::{
3493 check_self_lock_invariant, format_block_error, InvariantOutcome,
3494 };
3495 let policies: Vec<Arc<Policy>> = match self.policies.read() {
3496 Ok(map) => map.values().cloned().collect(),
3497 Err(_) => return None,
3498 };
3499 let outcome = check_self_lock_invariant(&policies);
3500 match &outcome {
3501 InvariantOutcome::Ok => None,
3502 InvariantOutcome::Blocked { .. } => format_block_error(&outcome),
3503 }
3504 }
3505
3506 fn managed_policy_error(policy_id: &str, reason: &str) -> AuthError {
3507 AuthError::Forbidden(format!(
3508 "managed policy mutation blocked for `{policy_id}`: {reason}"
3509 ))
3510 }
3511
3512 fn emit_policy_allowed(
3513 &self,
3514 control: &PolicyMutationControl<'_>,
3515 kind: EventKind,
3516 action: &'static str,
3517 policy_id: &str,
3518 policy: Option<&Policy>,
3519 principal: Option<&PrincipalRef>,
3520 matched_policy_id: Option<String>,
3521 ) -> Result<(), AuthError> {
3522 let event = ControlEvent {
3523 kind,
3524 outcome: Outcome::Allowed,
3525 action: std::borrow::Cow::Borrowed(action),
3526 resource: Some(format!("policy:{policy_id}")),
3527 reason: None,
3528 matched_policy_id,
3529 fields: policy_control_fields(policy_id, policy, principal),
3530 };
3531 match control.ledger.emit(control.ctx, event) {
3532 Ok(_) => Ok(()),
3533 Err(err) if control.config.require_persistence() => {
3534 Err(AuthError::Internal(err.to_string()))
3535 }
3536 Err(_) => Ok(()),
3537 }
3538 }
3539
3540 #[allow(clippy::too_many_arguments)]
3541 fn emit_policy_denied(
3542 &self,
3543 control: &PolicyMutationControl<'_>,
3544 kind: EventKind,
3545 action: &'static str,
3546 policy_id: &str,
3547 policy: Option<&Policy>,
3548 principal: Option<&PrincipalRef>,
3549 reason: &str,
3550 matched_policy_id: Option<String>,
3551 matched: Option<(String, String)>,
3552 ) {
3553 let mut fields = policy_control_fields(policy_id, policy, principal);
3554 if let Some((matched_action, matched_resource)) = matched {
3555 fields.insert(
3556 "matched_action".to_string(),
3557 Sensitivity::raw(matched_action),
3558 );
3559 fields.insert(
3560 "matched_resource".to_string(),
3561 Sensitivity::raw(matched_resource),
3562 );
3563 }
3564 let event = ControlEvent {
3565 kind,
3566 outcome: Outcome::Denied,
3567 action: std::borrow::Cow::Borrowed(action),
3568 resource: Some(format!("policy:{policy_id}")),
3569 reason: Some(reason.to_string()),
3570 matched_policy_id,
3571 fields,
3572 };
3573 let _ = control.ledger.emit(control.ctx, event);
3574 }
3575
3576 fn emit_policy_error(
3577 &self,
3578 control: &PolicyMutationControl<'_>,
3579 kind: EventKind,
3580 action: &'static str,
3581 policy_id: &str,
3582 policy: Option<&Policy>,
3583 principal: Option<&PrincipalRef>,
3584 err: &AuthError,
3585 ) {
3586 let event = ControlEvent {
3587 kind,
3588 outcome: Outcome::Error,
3589 action: std::borrow::Cow::Borrowed(action),
3590 resource: Some(format!("policy:{policy_id}")),
3591 reason: Some(err.to_string()),
3592 matched_policy_id: None,
3593 fields: policy_control_fields(policy_id, policy, principal),
3594 };
3595 let _ = control.ledger.emit(control.ctx, event);
3596 }
3597
3598 fn restore_policy_put(
3599 &self,
3600 policy_id: &str,
3601 previous: Option<Arc<Policy>>,
3602 was_enabled: bool,
3603 ) {
3604 let mut policies = self.policies.write().unwrap_or_else(|e| e.into_inner());
3605 match previous {
3606 Some(policy) => {
3607 policies.insert(policy_id.to_string(), policy);
3608 }
3609 None => {
3610 policies.remove(policy_id);
3611 }
3612 }
3613 drop(policies);
3614 self.iam_authorization_enabled
3615 .store(was_enabled, Ordering::Release);
3616 self.iam_effective_cache
3617 .write()
3618 .unwrap_or_else(|e| e.into_inner())
3619 .clear();
3620 self.invalidate_visible_collections_cache();
3621 self.persist_iam_to_kv();
3622 }
3623
3624 fn restore_policy_delete(
3625 &self,
3626 policy_id: &str,
3627 policy: Arc<Policy>,
3628 user_attachments: HashMap<UserId, Vec<String>>,
3629 group_attachments: HashMap<String, Vec<String>>,
3630 was_enabled: bool,
3631 ) {
3632 self.policies
3633 .write()
3634 .unwrap_or_else(|e| e.into_inner())
3635 .insert(policy_id.to_string(), policy);
3636 self.restore_policy_attachments(user_attachments, group_attachments);
3637 self.iam_authorization_enabled
3638 .store(was_enabled, Ordering::Release);
3639 self.persist_iam_to_kv();
3640 }
3641
3642 fn restore_policy_attachments(
3643 &self,
3644 user_attachments: HashMap<UserId, Vec<String>>,
3645 group_attachments: HashMap<String, Vec<String>>,
3646 ) {
3647 *self
3648 .user_attachments
3649 .write()
3650 .unwrap_or_else(|e| e.into_inner()) = user_attachments;
3651 *self
3652 .group_attachments
3653 .write()
3654 .unwrap_or_else(|e| e.into_inner()) = group_attachments;
3655 self.iam_effective_cache
3656 .write()
3657 .unwrap_or_else(|e| e.into_inner())
3658 .clear();
3659 self.invalidate_visible_collections_cache();
3660 self.persist_iam_to_kv();
3661 }
3662
3663 pub fn put_policy(&self, p: Policy) -> Result<(), AuthError> {
3668 if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
3669 return Err(AuthError::Forbidden(format!(
3670 "policy id `{}` is reserved",
3671 p.id
3672 )));
3673 }
3674 self.put_policy_internal(p)
3675 }
3676
3677 pub fn put_policy_internal(&self, p: Policy) -> Result<(), AuthError> {
3682 p.validate()
3683 .map_err(|e| AuthError::Forbidden(format!("invalid policy `{}`: {e}", p.id)))?;
3684 let id = p.id.clone();
3685 self.policies
3686 .write()
3687 .unwrap_or_else(|e| e.into_inner())
3688 .insert(id, Arc::new(p));
3689 self.iam_authorization_enabled
3690 .store(true, Ordering::Release);
3691 self.iam_effective_cache
3692 .write()
3693 .unwrap_or_else(|e| e.into_inner())
3694 .clear();
3695 self.invalidate_visible_collections_cache();
3699 self.persist_iam_to_kv();
3700 Ok(())
3701 }
3702
3703 pub fn iam_authorization_enabled(&self) -> bool {
3708 self.iam_authorization_enabled.load(Ordering::Acquire)
3709 }
3710
3711 pub fn delete_policy(&self, id: &str) -> Result<(), AuthError> {
3713 let removed = self
3714 .policies
3715 .write()
3716 .unwrap_or_else(|e| e.into_inner())
3717 .remove(id)
3718 .is_some();
3719 if !removed {
3720 return Err(AuthError::Forbidden(format!("policy `{id}` not found")));
3721 }
3722 if let Ok(mut ua) = self.user_attachments.write() {
3724 for ids in ua.values_mut() {
3725 ids.retain(|p| p != id);
3726 }
3727 ua.retain(|_, v| !v.is_empty());
3728 }
3729 if let Ok(mut ga) = self.group_attachments.write() {
3730 for ids in ga.values_mut() {
3731 ids.retain(|p| p != id);
3732 }
3733 ga.retain(|_, v| !v.is_empty());
3734 }
3735 self.iam_effective_cache
3736 .write()
3737 .unwrap_or_else(|e| e.into_inner())
3738 .clear();
3739 self.invalidate_visible_collections_cache();
3742 self.persist_iam_to_kv();
3743 Ok(())
3744 }
3745
3746 pub fn list_policies(&self) -> Vec<Arc<Policy>> {
3748 let map = match self.policies.read() {
3749 Ok(g) => g,
3750 Err(_) => return Vec::new(),
3751 };
3752 let mut out: Vec<Arc<Policy>> = map.values().cloned().collect();
3753 out.sort_by(|a, b| a.id.cmp(&b.id));
3754 out
3755 }
3756
3757 pub fn get_policy(&self, id: &str) -> Option<Arc<Policy>> {
3759 self.policies.read().ok().and_then(|m| m.get(id).cloned())
3760 }
3761
3762 pub fn group_policies(&self, group: &str) -> Vec<Arc<Policy>> {
3764 let policies = self.policies.read();
3765 let attachments = self.group_attachments.read();
3766 let mut out = Vec::new();
3767 if let (Ok(p_map), Ok(ga_map)) = (policies, attachments) {
3768 if let Some(ids) = ga_map.get(group) {
3769 for id in ids {
3770 if let Some(p) = p_map.get(id) {
3771 out.push(p.clone());
3772 }
3773 }
3774 }
3775 }
3776 out.sort_by(|a, b| a.id.cmp(&b.id));
3777 out
3778 }
3779
3780 pub fn delete_synthetic_grant_policies(
3784 &self,
3785 principal: &GrantPrincipal,
3786 resource: &Resource,
3787 actions: &[Action],
3788 ) -> usize {
3789 let attached = match principal {
3790 GrantPrincipal::User(uid) => self
3791 .user_attachments
3792 .read()
3793 .ok()
3794 .and_then(|m| m.get(uid).cloned())
3795 .unwrap_or_default(),
3796 GrantPrincipal::Group(group) => self
3797 .group_attachments
3798 .read()
3799 .ok()
3800 .and_then(|m| m.get(group).cloned())
3801 .unwrap_or_default(),
3802 GrantPrincipal::Public => self
3803 .group_attachments
3804 .read()
3805 .ok()
3806 .and_then(|m| m.get(PUBLIC_IAM_GROUP).cloned())
3807 .unwrap_or_default(),
3808 };
3809 if attached.is_empty() {
3810 return 0;
3811 }
3812
3813 let mut delete_ids = Vec::new();
3814 if let Ok(policies) = self.policies.read() {
3815 for id in attached {
3816 let Some(policy) = policies.get(&id) else {
3817 continue;
3818 };
3819 if !policy.id.starts_with("_grant_") {
3820 continue;
3821 }
3822 if synthetic_grant_matches(policy, resource, actions) {
3823 delete_ids.push(policy.id.clone());
3824 }
3825 }
3826 }
3827
3828 let mut deleted = 0usize;
3829 for id in delete_ids {
3830 if self.delete_policy(&id).is_ok() {
3831 deleted += 1;
3832 }
3833 }
3834 deleted
3835 }
3836
3837 pub fn apply_policy_break_glass(&self, boot_ts_ms: u128) -> Result<(), AuthError> {
3845 use crate::auth::self_lock_guard::{
3846 break_glass_audit_fields, unlock_policy, PLATFORM_OWNER_UNLOCK_POLICY_ID,
3847 PLATFORM_OWNER_USERNAME,
3848 };
3849
3850 let policy = unlock_policy();
3854 self.put_policy_internal(policy)?;
3855
3856 let owner_id = UserId::platform(PLATFORM_OWNER_USERNAME);
3861 {
3862 let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
3863 users.entry(owner_id.clone()).or_insert_with(|| User {
3864 username: PLATFORM_OWNER_USERNAME.to_string(),
3865 tenant_id: None,
3866 password_hash: String::new(),
3871 scram_verifier: None,
3872 role: Role::Admin,
3873 api_keys: Vec::new(),
3874 created_at: boot_ts_ms,
3875 updated_at: boot_ts_ms,
3876 enabled: false,
3877 });
3878 }
3879
3880 self.attach_policy(
3884 PrincipalRef::User(owner_id.clone()),
3885 PLATFORM_OWNER_UNLOCK_POLICY_ID,
3886 )?;
3887
3888 let fields_raw = break_glass_audit_fields(boot_ts_ms);
3893 let mut fields: HashMap<String, Sensitivity> = HashMap::new();
3894 for (k, v) in fields_raw {
3895 fields.insert(k, Sensitivity::raw(v));
3896 }
3897 if let Some(configured) = self.configured_control_events() {
3898 let ctx = ControlEventCtx {
3899 actor: crate::runtime::control_events::ActorRef::System("break_glass"),
3900 scope: None,
3901 request_id: None,
3902 trace_id: None,
3903 };
3904 let event = ControlEvent {
3905 kind: EventKind::PolicyBreakGlass,
3906 outcome: Outcome::Allowed,
3907 action: std::borrow::Cow::Borrowed("policy:break_glass"),
3908 resource: Some(format!("policy:{PLATFORM_OWNER_UNLOCK_POLICY_ID}")),
3909 reason: Some("REDDB_POLICY_BREAK_GLASS=1 at boot".into()),
3910 matched_policy_id: None,
3911 fields,
3912 };
3913 let _ = configured.ledger.emit(&ctx, event);
3914 }
3915 tracing::warn!(
3916 policy_id = PLATFORM_OWNER_UNLOCK_POLICY_ID,
3917 principal = PLATFORM_OWNER_USERNAME,
3918 boot_ts_ms = %boot_ts_ms,
3919 "REDDB_POLICY_BREAK_GLASS=1 — installed/refreshed platform-owner unlock policy and rebound to synthetic principal"
3920 );
3921 Ok(())
3922 }
3923
3924 pub fn attach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
3927 if !self
3928 .policies
3929 .read()
3930 .map(|m| m.contains_key(policy_id))
3931 .unwrap_or(false)
3932 {
3933 return Err(AuthError::Forbidden(format!(
3934 "policy `{policy_id}` not found"
3935 )));
3936 }
3937 match &principal {
3938 PrincipalRef::User(uid) => {
3939 let mut ua = self
3940 .user_attachments
3941 .write()
3942 .unwrap_or_else(|e| e.into_inner());
3943 let list = ua.entry(uid.clone()).or_default();
3944 if !list.iter().any(|p| p == policy_id) {
3945 list.push(policy_id.to_string());
3946 }
3947 drop(ua);
3948 self.invalidate_iam_cache(Some(uid));
3949 }
3950 PrincipalRef::Group(g) => {
3951 let mut ga = self
3952 .group_attachments
3953 .write()
3954 .unwrap_or_else(|e| e.into_inner());
3955 let list = ga.entry(g.clone()).or_default();
3956 if !list.iter().any(|p| p == policy_id) {
3957 list.push(policy_id.to_string());
3958 }
3959 drop(ga);
3960 self.invalidate_iam_cache(None);
3961 }
3962 }
3963 self.persist_iam_to_kv();
3964 Ok(())
3965 }
3966
3967 pub fn detach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
3969 match &principal {
3970 PrincipalRef::User(uid) => {
3971 if let Ok(mut ua) = self.user_attachments.write() {
3972 if let Some(list) = ua.get_mut(uid) {
3973 list.retain(|p| p != policy_id);
3974 if list.is_empty() {
3975 ua.remove(uid);
3976 }
3977 }
3978 }
3979 self.invalidate_iam_cache(Some(uid));
3980 }
3981 PrincipalRef::Group(g) => {
3982 if let Ok(mut ga) = self.group_attachments.write() {
3983 if let Some(list) = ga.get_mut(g) {
3984 list.retain(|p| p != policy_id);
3985 if list.is_empty() {
3986 ga.remove(g);
3987 }
3988 }
3989 }
3990 self.invalidate_iam_cache(None);
3991 }
3992 }
3993 self.persist_iam_to_kv();
3994 Ok(())
3995 }
3996
3997 pub fn effective_policies(&self, user: &UserId) -> Vec<Arc<Policy>> {
4001 if let Ok(cache) = self.iam_effective_cache.read() {
4002 if let Some(hit) = cache.get(user) {
4003 return hit.clone();
4004 }
4005 }
4006 let policies = self.policies.read();
4007 let user_attachments = self.user_attachments.read();
4008 let group_attachments = self.group_attachments.read();
4009 let mut groups = self
4010 .user_attributes
4011 .read()
4012 .ok()
4013 .and_then(|m| m.get(user).map(|attrs| attrs.groups.clone()))
4014 .unwrap_or_default();
4015 groups.insert(0, PUBLIC_IAM_GROUP.to_string());
4016 let mut out: Vec<Arc<Policy>> = Vec::new();
4017 if let (Ok(p_map), Ok(ua_map), Ok(ga_map)) = (policies, user_attachments, group_attachments)
4018 {
4019 for group in groups {
4020 if let Some(ids) = ga_map.get(&group) {
4021 for id in ids {
4022 if let Some(p) = p_map.get(id) {
4023 out.push(p.clone());
4024 }
4025 }
4026 }
4027 }
4028 if let Some(ids) = ua_map.get(user) {
4029 for id in ids {
4030 if let Some(p) = p_map.get(id) {
4031 out.push(p.clone());
4032 }
4033 }
4034 }
4035 }
4036 if let Ok(mut cache) = self.iam_effective_cache.write() {
4037 cache.insert(user.clone(), out.clone());
4038 }
4039 out
4040 }
4041
4042 pub fn simulate(
4045 &self,
4046 principal: &UserId,
4047 action: &str,
4048 resource: &ResourceRef,
4049 ctx_extras: SimCtx,
4050 ) -> SimulationOutcome {
4051 let user_role = self
4052 .users
4053 .read()
4054 .ok()
4055 .and_then(|u| u.get(principal).map(|u| u.role));
4056 let principal_is_admin_role = user_role == Some(Role::Admin);
4057 let now = ctx_extras.now_ms.unwrap_or_else(now_ms);
4058 let ctx = EvalContext {
4059 principal_tenant: principal.tenant.clone(),
4060 current_tenant: ctx_extras.current_tenant,
4061 peer_ip: ctx_extras.peer_ip,
4062 mfa_present: ctx_extras.mfa_present,
4063 now_ms: now,
4064 principal_is_admin_role,
4065 principal_is_platform_scoped: principal.tenant.is_none(),
4066 };
4067 let pols = self.effective_policies(principal);
4068 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4069 iam_policies::simulate(&refs, action, resource, &ctx)
4070 }
4071
4072 pub fn check_policy_authz(
4083 &self,
4084 principal: &UserId,
4085 action: &str,
4086 resource: &ResourceRef,
4087 ctx: &EvalContext,
4088 ) -> bool {
4089 let pols = self.effective_policies(principal);
4090 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4091 let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4092 matches!(
4093 decision,
4094 iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass
4095 )
4096 }
4097
4098 pub fn check_policy_authz_with_role(
4114 &self,
4115 principal: &UserId,
4116 action: &str,
4117 resource: &ResourceRef,
4118 ctx: &EvalContext,
4119 role: Role,
4120 ) -> bool {
4121 let pols = self.effective_policies(principal);
4122 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4123 let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4124 match decision {
4125 iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass => true,
4126 iam_policies::Decision::Deny { .. } => false,
4127 iam_policies::Decision::DefaultDeny => match self.enforcement_mode() {
4128 PolicyEnforcementMode::LegacyRbac => legacy_rbac_decision(role, action),
4129 PolicyEnforcementMode::PolicyOnly => false,
4130 },
4131 }
4132 }
4133
4134 pub fn enforcement_mode(&self) -> PolicyEnforcementMode {
4141 *self
4142 .enforcement_mode
4143 .read()
4144 .unwrap_or_else(|e| e.into_inner())
4145 }
4146
4147 pub fn set_enforcement_mode(&self, mode: PolicyEnforcementMode) -> PolicyEnforcementMode {
4153 let prev = {
4154 let mut guard = self
4155 .enforcement_mode
4156 .write()
4157 .unwrap_or_else(|e| e.into_inner());
4158 let prev = *guard;
4159 *guard = mode;
4160 prev
4161 };
4162 self.vault_kv_set(
4163 "red.config.policy.enforcement_mode".to_string(),
4164 mode.as_str().to_string(),
4165 );
4166 prev
4167 }
4168
4169 pub fn take_legacy_rbac_warn_once(&self) -> bool {
4175 if self.enforcement_mode() != PolicyEnforcementMode::LegacyRbac {
4176 return false;
4177 }
4178 !self
4179 .legacy_rbac_boot_warn_emitted
4180 .swap(true, Ordering::AcqRel)
4181 }
4182
4183 pub fn check_column_projection_authz(
4187 &self,
4188 principal: &UserId,
4189 request: &ColumnAccessRequest,
4190 ctx: &EvalContext,
4191 ) -> ColumnPolicyOutcome {
4192 let pols = self.effective_policies(principal);
4193 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4194 ColumnPolicyGate::new(&refs).evaluate(request, ctx)
4195 }
4196
4197 fn invalidate_iam_cache(&self, uid: Option<&UserId>) {
4198 if let Ok(mut cache) = self.iam_effective_cache.write() {
4199 match uid {
4200 Some(u) => {
4201 cache.remove(u);
4202 }
4203 None => cache.clear(),
4204 }
4205 }
4206 }
4207
4208 pub fn invalidate_all_iam_cache(&self) {
4212 self.invalidate_iam_cache(None);
4213 }
4214
4215 pub fn rehydrate_iam(&self) {
4223 let mut enabled = self
4224 .vault_kv_get("red.iam.enabled")
4225 .map(|v| v == "true")
4226 .unwrap_or(false);
4227 if let Some(blob) = self.vault_kv_get("red.iam.policies") {
4229 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4230 if let Some(obj) = val.as_object() {
4231 let mut map = HashMap::new();
4232 for (id, body) in obj.iter() {
4233 let s = body.to_string_compact();
4234 if let Ok(p) = Policy::from_json_str(&s) {
4235 map.insert(id.clone(), Arc::new(p));
4236 }
4237 }
4238 if !map.is_empty() {
4239 enabled = true;
4240 }
4241 *self.policies.write().unwrap_or_else(|e| e.into_inner()) = map;
4242 }
4243 }
4244 }
4245 if let Some(blob) = self.vault_kv_get("red.iam.attachments.users") {
4247 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4248 if let Some(obj) = val.as_object() {
4249 let mut map: HashMap<UserId, Vec<String>> = HashMap::new();
4250 for (encoded_uid, ids_v) in obj.iter() {
4251 let Some(uid) = decode_uid(encoded_uid) else {
4252 continue;
4253 };
4254 if let Some(arr) = ids_v.as_array() {
4255 let ids: Vec<String> = arr
4256 .iter()
4257 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4258 .collect();
4259 map.insert(uid, ids);
4260 }
4261 }
4262 *self
4263 .user_attachments
4264 .write()
4265 .unwrap_or_else(|e| e.into_inner()) = map;
4266 }
4267 }
4268 }
4269 if let Some(blob) = self.vault_kv_get("red.iam.attachments.groups") {
4271 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4272 if let Some(obj) = val.as_object() {
4273 let mut map: HashMap<String, Vec<String>> = HashMap::new();
4274 for (g, ids_v) in obj.iter() {
4275 if let Some(arr) = ids_v.as_array() {
4276 let ids: Vec<String> = arr
4277 .iter()
4278 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4279 .collect();
4280 map.insert(g.clone(), ids);
4281 }
4282 }
4283 *self
4284 .group_attachments
4285 .write()
4286 .unwrap_or_else(|e| e.into_inner()) = map;
4287 }
4288 }
4289 }
4290 self.iam_authorization_enabled
4291 .store(enabled, Ordering::Release);
4292 self.invalidate_iam_cache(None);
4293
4294 if let Some(stored) = self.vault_kv_get("red.config.policy.enforcement_mode") {
4301 if let Some(mode) = PolicyEnforcementMode::parse(&stored) {
4302 *self
4303 .enforcement_mode
4304 .write()
4305 .unwrap_or_else(|e| e.into_inner()) = mode;
4306 }
4307 }
4308
4309 if self.take_legacy_rbac_warn_once() {
4314 tracing::warn!(
4315 target: "reddb::auth::enforcement_mode",
4316 mode = "legacy_rbac",
4317 hard_version = crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4318 "policy enforcement_mode=legacy_rbac (transitional). Run \
4319 `MIGRATE POLICY MODE TO 'policy_only'` (next slice S5B) \
4320 before {} to flip this install over to the strict posture.",
4321 crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4322 );
4323 }
4324 }
4325
4326 fn persist_iam_to_kv(&self) {
4329 let enabled = if self.iam_authorization_enabled() {
4330 "true"
4331 } else {
4332 "false"
4333 };
4334 self.vault_kv_set("red.iam.enabled".to_string(), enabled.to_string());
4335
4336 let policies_obj = {
4338 let map = self.policies.read().unwrap_or_else(|e| e.into_inner());
4339 let mut obj = crate::serde_json::Map::new();
4340 for (id, p) in map.iter() {
4341 let s = p.to_json_string();
4342 if let Ok(v) = crate::serde_json::from_str::<crate::serde_json::Value>(&s) {
4343 obj.insert(id.clone(), v);
4344 }
4345 }
4346 crate::serde_json::Value::Object(obj).to_string_compact()
4347 };
4348 self.vault_kv_set("red.iam.policies".to_string(), policies_obj);
4349
4350 let users_obj = {
4352 let map = self
4353 .user_attachments
4354 .read()
4355 .unwrap_or_else(|e| e.into_inner());
4356 let mut obj = crate::serde_json::Map::new();
4357 for (uid, ids) in map.iter() {
4358 let arr = crate::serde_json::Value::Array(
4359 ids.iter()
4360 .map(|s| crate::serde_json::Value::String(s.clone()))
4361 .collect(),
4362 );
4363 obj.insert(encode_uid(uid), arr);
4364 }
4365 crate::serde_json::Value::Object(obj).to_string_compact()
4366 };
4367 self.vault_kv_set("red.iam.attachments.users".to_string(), users_obj);
4368
4369 let groups_obj = {
4371 let map = self
4372 .group_attachments
4373 .read()
4374 .unwrap_or_else(|e| e.into_inner());
4375 let mut obj = crate::serde_json::Map::new();
4376 for (g, ids) in map.iter() {
4377 let arr = crate::serde_json::Value::Array(
4378 ids.iter()
4379 .map(|s| crate::serde_json::Value::String(s.clone()))
4380 .collect(),
4381 );
4382 obj.insert(g.clone(), arr);
4383 }
4384 crate::serde_json::Value::Object(obj).to_string_compact()
4385 };
4386 self.vault_kv_set("red.iam.attachments.groups".to_string(), groups_obj);
4387 }
4388}
4389
4390fn synthetic_grant_matches(policy: &Policy, resource: &Resource, actions: &[Action]) -> bool {
4391 policy.statements.iter().any(|st| {
4392 st.effect == crate::auth::policies::Effect::Allow
4393 && st.condition.is_none()
4394 && grant_actions_overlap(&st.actions, actions)
4395 && grant_resource_matches(&st.resources, resource)
4396 })
4397}
4398
4399fn grant_actions_overlap(
4400 patterns: &[crate::auth::policies::ActionPattern],
4401 actions: &[Action],
4402) -> bool {
4403 if actions.contains(&Action::All) {
4404 return true;
4405 }
4406 patterns.iter().any(|pat| match pat {
4407 crate::auth::policies::ActionPattern::Wildcard => true,
4408 crate::auth::policies::ActionPattern::Exact(s) => {
4409 actions.iter().any(|a| s.eq_ignore_ascii_case(a.as_str()))
4410 }
4411 crate::auth::policies::ActionPattern::Prefix(_) => false,
4412 })
4413}
4414
4415fn grant_resource_matches(
4416 patterns: &[crate::auth::policies::ResourcePattern],
4417 resource: &Resource,
4418) -> bool {
4419 let expected = grant_resource_pattern(resource);
4420 patterns.iter().any(|pat| pat == &expected)
4421}
4422
4423fn grant_resource_pattern(resource: &Resource) -> crate::auth::policies::ResourcePattern {
4424 use crate::auth::policies::ResourcePattern;
4425
4426 match resource {
4427 Resource::Database => ResourcePattern::Glob("table:*".to_string()),
4428 Resource::Schema(s) => ResourcePattern::Glob(format!("table:{s}.*")),
4429 Resource::Table { schema, table } => ResourcePattern::Exact {
4430 kind: "table".to_string(),
4431 name: match schema {
4432 Some(s) => format!("{s}.{table}"),
4433 None => table.clone(),
4434 },
4435 },
4436 Resource::Function { schema, name } => ResourcePattern::Exact {
4437 kind: "function".to_string(),
4438 name: match schema {
4439 Some(s) => format!("{s}.{name}"),
4440 None => name.clone(),
4441 },
4442 },
4443 }
4444}
4445
4446fn encode_uid(uid: &UserId) -> String {
4462 match &uid.tenant {
4463 Some(t) => format!("{}/{}", t, uid.username),
4464 None => format!("*/{}", uid.username),
4465 }
4466}
4467
4468fn decode_uid(s: &str) -> Option<UserId> {
4469 let (tenant, username) = s.split_once('/')?;
4470 Some(if tenant == "*" {
4471 UserId::platform(username)
4472 } else {
4473 UserId::scoped(tenant, username)
4474 })
4475}
4476
4477fn encode_resource(r: &Resource) -> String {
4478 match r {
4479 Resource::Database => "db".into(),
4480 Resource::Schema(s) => format!("schema:{}", s),
4481 Resource::Table { schema, table } => {
4482 format!("table:{}:{}", schema.as_deref().unwrap_or("*"), table)
4483 }
4484 Resource::Function { schema, name } => {
4485 format!("func:{}:{}", schema.as_deref().unwrap_or("*"), name)
4486 }
4487 }
4488}
4489
4490fn decode_resource(s: &str) -> Option<Resource> {
4491 if s == "db" {
4492 return Some(Resource::Database);
4493 }
4494 if let Some(rest) = s.strip_prefix("schema:") {
4495 return Some(Resource::Schema(rest.to_string()));
4496 }
4497 if let Some(rest) = s.strip_prefix("table:") {
4498 let (schema, table) = rest.split_once(':')?;
4499 return Some(Resource::Table {
4500 schema: if schema == "*" {
4501 None
4502 } else {
4503 Some(schema.to_string())
4504 },
4505 table: table.to_string(),
4506 });
4507 }
4508 if let Some(rest) = s.strip_prefix("func:") {
4509 let (schema, name) = rest.split_once(':')?;
4510 return Some(Resource::Function {
4511 schema: if schema == "*" {
4512 None
4513 } else {
4514 Some(schema.to_string())
4515 },
4516 name: name.to_string(),
4517 });
4518 }
4519 None
4520}
4521
4522fn encode_grants_blob(grants: &[Grant]) -> String {
4523 let mut out = String::new();
4524 for g in grants {
4525 let actions: Vec<&str> = g.actions.iter().map(|a| a.as_str()).collect();
4526 out.push_str(&format!(
4527 "GRANT|{}|{}|{}|{}|{}|{}\n",
4528 encode_resource(&g.resource),
4529 actions.join(","),
4530 g.with_grant_option,
4531 g.tenant.as_deref().unwrap_or("*"),
4532 g.granted_by,
4533 g.granted_at,
4534 ));
4535 }
4536 out
4537}
4538
4539fn decode_grants_blob(s: &str) -> Option<Vec<Grant>> {
4540 let mut out = Vec::new();
4541 for line in s.lines() {
4542 if line.is_empty() {
4543 continue;
4544 }
4545 let parts: Vec<&str> = line.split('|').collect();
4546 if parts.len() != 7 || parts[0] != "GRANT" {
4547 return None;
4548 }
4549 let resource = decode_resource(parts[1])?;
4550 let mut actions = std::collections::BTreeSet::new();
4551 for token in parts[2].split(',') {
4552 if let Some(a) = Action::from_keyword(token) {
4553 actions.insert(a);
4554 }
4555 }
4556 let with_grant_option = parts[3] == "true";
4557 let tenant = if parts[4] == "*" {
4558 None
4559 } else {
4560 Some(parts[4].to_string())
4561 };
4562 let granted_by = parts[5].to_string();
4563 let granted_at: u128 = parts[6].parse().unwrap_or(0);
4564 out.push(Grant {
4565 principal: GrantPrincipal::Public,
4568 resource,
4569 actions,
4570 with_grant_option,
4571 granted_by,
4572 granted_at,
4573 tenant,
4574 columns: None,
4575 });
4576 }
4577 Some(out)
4578}
4579
4580fn encode_attrs_blob(a: &UserAttributes) -> String {
4581 let valid = a
4582 .valid_until
4583 .map(|t| t.to_string())
4584 .unwrap_or_else(|| "*".into());
4585 let limit = a
4586 .connection_limit
4587 .map(|l| l.to_string())
4588 .unwrap_or_else(|| "*".into());
4589 let path = a.search_path.clone().unwrap_or_else(|| "*".into());
4590 let groups = if a.groups.is_empty() {
4591 "*".to_string()
4592 } else {
4593 a.groups.join(",")
4594 };
4595 format!("ATTR|{}|{}|{}|{}\n", valid, limit, path, groups)
4596}
4597
4598fn decode_attrs_blob(s: &str) -> Option<UserAttributes> {
4599 let line = s.lines().next()?;
4600 let parts: Vec<&str> = line.split('|').collect();
4601 if !(parts.len() == 4 || parts.len() == 5) || parts[0] != "ATTR" {
4602 return None;
4603 }
4604 let groups = if parts.get(4).copied().unwrap_or("*") == "*" {
4605 Vec::new()
4606 } else {
4607 parts[4]
4608 .split(',')
4609 .filter(|g| !g.is_empty())
4610 .map(|g| g.to_string())
4611 .collect()
4612 };
4613 Some(UserAttributes {
4614 valid_until: if parts[1] == "*" {
4615 None
4616 } else {
4617 parts[1].parse().ok()
4618 },
4619 connection_limit: if parts[2] == "*" {
4620 None
4621 } else {
4622 parts[2].parse().ok()
4623 },
4624 search_path: if parts[3] == "*" {
4625 None
4626 } else {
4627 Some(parts[3].to_string())
4628 },
4629 groups,
4630 })
4631}
4632
4633fn make_scram_verifier(password: &str) -> crate::auth::scram::ScramVerifier {
4643 let salt = random_bytes(16);
4644 crate::auth::scram::ScramVerifier::from_password(
4645 password,
4646 salt,
4647 crate::auth::scram::DEFAULT_ITER,
4648 )
4649}
4650
4651pub(crate) fn hash_password(password: &str) -> String {
4655 let salt = random_bytes(16);
4656 let params = auth_argon2_params();
4657 let hash = derive_key(password.as_bytes(), &salt, ¶ms);
4658 format!("argon2id${}${}", hex::encode(&salt), hex::encode(&hash))
4659}
4660
4661pub(crate) fn verify_password(password: &str, stored_hash: &str) -> bool {
4663 let parts: Vec<&str> = stored_hash.splitn(3, '$').collect();
4664 if parts.len() != 3 || parts[0] != "argon2id" {
4665 return false;
4666 }
4667
4668 let salt = match hex::decode(parts[1]) {
4669 Ok(s) => s,
4670 Err(_) => return false,
4671 };
4672
4673 let expected_hash = match hex::decode(parts[2]) {
4674 Ok(h) => h,
4675 Err(_) => return false,
4676 };
4677
4678 let params = auth_argon2_params();
4679 let computed = derive_key(password.as_bytes(), &salt, ¶ms);
4680 constant_time_eq(&computed, &expected_hash)
4681}
4682
4683fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
4685 if a.len() != b.len() {
4686 return false;
4687 }
4688 let mut diff: u8 = 0;
4689 for (x, y) in a.iter().zip(b.iter()) {
4690 diff |= x ^ y;
4691 }
4692 diff == 0
4693}
4694
4695fn generate_session_token() -> String {
4700 format!("rs_{}", hex::encode(random_bytes(32)))
4701}
4702
4703fn generate_api_key() -> String {
4704 format!("rk_{}", hex::encode(random_bytes(32)))
4705}
4706
4707fn random_hex(n: usize) -> String {
4709 hex::encode(random_bytes(n))
4710}
4711
4712pub(crate) fn random_bytes(n: usize) -> Vec<u8> {
4715 let mut buf = vec![0u8; n.max(32)];
4716 if os_random::fill_bytes(&mut buf).is_err() {
4717 let seed = now_ms().to_le_bytes();
4719 for (i, byte) in buf.iter_mut().enumerate() {
4720 *byte = seed[i % seed.len()] ^ (i as u8);
4721 }
4722 }
4723 let digest = sha256(&buf);
4725 if n <= 32 {
4726 digest[..n].to_vec()
4727 } else {
4728 let mut out = Vec::with_capacity(n);
4730 let mut prev = digest;
4731 while out.len() < n {
4732 out.extend_from_slice(&prev[..std::cmp::min(32, n - out.len())]);
4733 prev = sha256(&prev);
4734 }
4735 out
4736 }
4737}
4738
4739fn lock_err<T>(_: T) -> AuthError {
4744 AuthError::Internal("lock poisoned".to_string())
4745}
4746
4747#[cfg(test)]
4752mod tests {
4753 use super::*;
4754
4755 fn test_config() -> AuthConfig {
4756 AuthConfig {
4757 enabled: true,
4758 session_ttl_secs: 60,
4759 require_auth: true,
4760 auto_encrypt_storage: false,
4761 vault_enabled: false,
4762 cert: Default::default(),
4763 oauth: Default::default(),
4764 }
4765 }
4766
4767 #[test]
4768 fn test_create_and_list_users() {
4769 let store = AuthStore::new(test_config());
4770 store.create_user("alice", "pass1", Role::Admin).unwrap();
4771 store.create_user("bob", "pass2", Role::Read).unwrap();
4772
4773 let users = store.list_users();
4774 assert_eq!(users.len(), 2);
4775 for u in &users {
4777 assert!(u.password_hash.is_empty());
4778 }
4779 }
4780
4781 #[test]
4782 fn test_create_duplicate_user() {
4783 let store = AuthStore::new(test_config());
4784 store.create_user("alice", "pass", Role::Admin).unwrap();
4785 let err = store.create_user("alice", "pass2", Role::Read).unwrap_err();
4786 assert!(matches!(err, AuthError::UserExists(_)));
4787 }
4788
4789 #[test]
4790 fn test_authenticate_and_validate() {
4791 let store = AuthStore::new(test_config());
4792 store.create_user("alice", "secret", Role::Write).unwrap();
4793
4794 let session = store.authenticate("alice", "secret").unwrap();
4795 assert!(session.token.starts_with("rs_"));
4796
4797 let (username, role) = store.validate_token(&session.token).unwrap();
4798 assert_eq!(username, "alice");
4799 assert_eq!(role, Role::Write);
4800 }
4801
4802 #[test]
4803 fn test_authenticate_wrong_password() {
4804 let store = AuthStore::new(test_config());
4805 store.create_user("alice", "secret", Role::Read).unwrap();
4806
4807 let err = store.authenticate("alice", "wrong").unwrap_err();
4808 assert!(matches!(err, AuthError::InvalidCredentials));
4809 }
4810
4811 #[test]
4812 fn test_api_key_lifecycle() {
4813 let store = AuthStore::new(test_config());
4814 store.create_user("alice", "pass", Role::Admin).unwrap();
4815
4816 let key = store
4817 .create_api_key("alice", "ci-token", Role::Write)
4818 .unwrap();
4819 assert!(key.key.starts_with("rk_"));
4820
4821 let (username, role) = store.validate_token(&key.key).unwrap();
4822 assert_eq!(username, "alice");
4823 assert_eq!(role, Role::Write);
4824
4825 store.revoke_api_key(&key.key).unwrap();
4826 assert!(store.validate_token(&key.key).is_none());
4827 }
4828
4829 #[test]
4830 fn test_api_key_role_exceeded() {
4831 let store = AuthStore::new(test_config());
4832 store.create_user("bob", "pass", Role::Read).unwrap();
4833
4834 let err = store
4835 .create_api_key("bob", "escalate", Role::Admin)
4836 .unwrap_err();
4837 assert!(matches!(err, AuthError::RoleExceeded { .. }));
4838 }
4839
4840 #[test]
4841 fn test_change_password() {
4842 let store = AuthStore::new(test_config());
4843 store.create_user("alice", "old", Role::Write).unwrap();
4844
4845 store.change_password("alice", "old", "new").unwrap();
4846
4847 assert!(store.authenticate("alice", "old").is_err());
4849 assert!(store.authenticate("alice", "new").is_ok());
4851 }
4852
4853 #[test]
4854 fn test_change_role() {
4855 let store = AuthStore::new(test_config());
4856 store.create_user("alice", "pass", Role::Admin).unwrap();
4857 store.create_api_key("alice", "key1", Role::Admin).unwrap();
4858
4859 store.change_role("alice", Role::Read).unwrap();
4860
4861 let users = store.list_users();
4863 let alice = users.iter().find(|u| u.username == "alice").unwrap();
4864 assert_eq!(alice.role, Role::Read);
4865
4866 assert_eq!(alice.api_keys[0].role, Role::Read);
4868 }
4869
4870 #[test]
4871 fn test_admin_user_mutations_are_policy_controlled_not_flag_guarded() {
4872 let store = AuthStore::new(test_config());
4873 store
4874 .create_admin_user("system", "pass", Role::Admin, None)
4875 .unwrap();
4876
4877 let uid = UserId::platform("system");
4878 store.change_password("system", "pass", "new").unwrap();
4879 store.change_role("system", Role::Read).unwrap();
4880 store.set_user_enabled(&uid, false).unwrap();
4881
4882 let key = store
4883 .create_api_key("system", "rotation", Role::Read)
4884 .unwrap();
4885 assert!(store.validate_token(&key.key).is_some());
4886 store.revoke_api_key(&key.key).unwrap();
4887 assert!(store.validate_token(&key.key).is_none());
4888 store.delete_user("system").unwrap();
4889 }
4890
4891 #[test]
4892 fn test_user_lifecycle_deny_policy_blocks_cloud_admin_deletion() {
4893 use crate::auth::policies::Policy;
4894
4895 let store = AuthStore::new(test_config());
4896 store
4897 .create_user("cloud-admin", "pass", Role::Admin)
4898 .unwrap();
4899 store
4900 .create_user("customer-admin", "pass", Role::Admin)
4901 .unwrap();
4902
4903 store
4904 .put_policy(
4905 Policy::from_json_str(
4906 r#"{
4907 "id": "customer-admin-allow-all",
4908 "version": 1,
4909 "statements": [{
4910 "effect": "allow",
4911 "actions": ["*"],
4912 "resources": ["*"]
4913 }]
4914 }"#,
4915 )
4916 .unwrap(),
4917 )
4918 .unwrap();
4919 store
4920 .put_policy(
4921 Policy::from_json_str(
4922 r#"{
4923 "id": "cloud-admin-protection",
4924 "version": 1,
4925 "statements": [{
4926 "effect": "deny",
4927 "actions": [
4928 "user:delete",
4929 "user:disable",
4930 "user:password:change",
4931 "user:role:update"
4932 ],
4933 "resources": ["user:cloud-admin"]
4934 }]
4935 }"#,
4936 )
4937 .unwrap(),
4938 )
4939 .unwrap();
4940 let customer = UserId::platform("customer-admin");
4941 store
4942 .attach_policy(
4943 PrincipalRef::User(customer.clone()),
4944 "customer-admin-allow-all",
4945 )
4946 .unwrap();
4947 store
4948 .attach_policy(
4949 PrincipalRef::User(customer.clone()),
4950 "cloud-admin-protection",
4951 )
4952 .unwrap();
4953
4954 let cloud_admin = UserId::platform("cloud-admin");
4955 assert!(!store.check_user_lifecycle_authz(
4956 &customer,
4957 Role::Admin,
4958 "user:delete",
4959 &cloud_admin,
4960 ));
4961 assert!(!store.check_user_lifecycle_authz(
4962 &customer,
4963 Role::Admin,
4964 "user:disable",
4965 &cloud_admin,
4966 ));
4967 assert!(!store.check_user_lifecycle_authz(
4968 &customer,
4969 Role::Admin,
4970 "user:password:change",
4971 &cloud_admin,
4972 ));
4973 assert!(!store.check_user_lifecycle_authz(
4974 &customer,
4975 Role::Admin,
4976 "user:role:update",
4977 &cloud_admin,
4978 ));
4979
4980 let another_user = UserId::platform("someone-else");
4981 assert!(store.check_user_lifecycle_authz(
4982 &customer,
4983 Role::Admin,
4984 "user:delete",
4985 &another_user,
4986 ));
4987 }
4988
4989 #[test]
4990 fn test_regular_user_mutations_still_work() {
4991 let store = AuthStore::new(test_config());
4992 store.create_user("alice", "old", Role::Admin).unwrap();
4993
4994 let uid = UserId::platform("alice");
4995 store.set_user_enabled(&uid, false).unwrap();
4996 assert!(matches!(
4997 store.authenticate("alice", "old"),
4998 Err(AuthError::InvalidCredentials)
4999 ));
5000
5001 store.set_user_enabled(&uid, true).unwrap();
5002 store.change_password("alice", "old", "new").unwrap();
5003 store.change_role("alice", Role::Read).unwrap();
5004 store.delete_user("alice").unwrap();
5005 assert!(matches!(
5006 store.authenticate("alice", "new"),
5007 Err(AuthError::InvalidCredentials)
5008 ));
5009 }
5010
5011 #[test]
5012 fn test_delete_user() {
5013 let store = AuthStore::new(test_config());
5014 store.create_user("alice", "pass", Role::Admin).unwrap();
5015 let key = store.create_api_key("alice", "key1", Role::Read).unwrap();
5016 let session = store.authenticate("alice", "pass").unwrap();
5017
5018 store.delete_user("alice").unwrap();
5019
5020 assert!(store.validate_token(&key.key).is_none());
5021 assert!(store.validate_token(&session.token).is_none());
5022 assert!(store.list_users().is_empty());
5023 }
5024
5025 #[test]
5026 fn test_revoke_session() {
5027 let store = AuthStore::new(test_config());
5028 store.create_user("alice", "pass", Role::Read).unwrap();
5029 let session = store.authenticate("alice", "pass").unwrap();
5030
5031 store.revoke_session(&session.token);
5032 assert!(store.validate_token(&session.token).is_none());
5033 }
5034
5035 #[test]
5036 fn test_password_hash_format() {
5037 let hash = hash_password("test");
5038 assert!(hash.starts_with("argon2id$"));
5039 let parts: Vec<&str> = hash.splitn(3, '$').collect();
5040 assert_eq!(parts.len(), 3);
5041 assert_eq!(parts[1].len(), 32);
5043 assert_eq!(parts[2].len(), 64);
5045 }
5046
5047 #[test]
5048 fn test_constant_time_eq() {
5049 assert!(constant_time_eq(b"hello", b"hello"));
5050 assert!(!constant_time_eq(b"hello", b"world"));
5051 assert!(!constant_time_eq(b"short", b"longer"));
5052 }
5053
5054 #[test]
5055 fn test_bootstrap_seals_permanently() {
5056 let store = AuthStore::new(test_config());
5057
5058 assert!(store.needs_bootstrap());
5059 assert!(!store.is_bootstrapped());
5060
5061 let result = store.bootstrap("admin", "secret");
5063 assert!(result.is_ok());
5064 let br = result.unwrap();
5065 assert_eq!(br.user.username, "admin");
5066 assert_eq!(br.user.role, Role::Admin);
5067 assert!(br.api_key.key.starts_with("rk_"));
5068 assert!(br.certificate.is_none());
5070
5071 assert!(!store.needs_bootstrap());
5073 assert!(store.is_bootstrapped());
5074
5075 let result = store.bootstrap("admin2", "secret2");
5077 assert!(result.is_err());
5078 let err = result.unwrap_err();
5079 assert!(err.to_string().contains("sealed permanently"));
5080
5081 assert_eq!(store.list_users().len(), 1);
5083 assert_eq!(store.list_users()[0].username, "admin");
5084 }
5085
5086 #[test]
5087 fn test_bootstrap_after_manual_user_creation() {
5088 let store = AuthStore::new(test_config());
5089
5090 store.create_user("existing", "pass", Role::Read).unwrap();
5092
5093 assert!(!store.needs_bootstrap()); }
5097
5098 #[test]
5103 fn test_same_username_two_tenants_distinct() {
5104 let store = AuthStore::new(test_config());
5105 store
5106 .create_user_in_tenant(Some("acme"), "alice", "pw-acme", Role::Write)
5107 .unwrap();
5108 store
5109 .create_user_in_tenant(Some("globex"), "alice", "pw-globex", Role::Read)
5110 .unwrap();
5111
5112 let users = store.list_users();
5114 assert_eq!(users.len(), 2);
5115
5116 assert!(store
5118 .authenticate_in_tenant(Some("acme"), "alice", "pw-acme")
5119 .is_ok());
5120 assert!(store
5121 .authenticate_in_tenant(Some("globex"), "alice", "pw-globex")
5122 .is_ok());
5123
5124 assert!(store
5126 .authenticate_in_tenant(Some("acme"), "alice", "pw-globex")
5127 .is_err());
5128 assert!(store
5129 .authenticate_in_tenant(Some("globex"), "alice", "pw-acme")
5130 .is_err());
5131 }
5132
5133 #[test]
5134 fn test_session_carries_tenant() {
5135 let store = AuthStore::new(test_config());
5136 store
5137 .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
5138 .unwrap();
5139 let session = store
5140 .authenticate_in_tenant(Some("acme"), "alice", "pw")
5141 .unwrap();
5142 assert_eq!(session.tenant_id.as_deref(), Some("acme"));
5143
5144 let (id, role) = store.validate_token_full(&session.token).unwrap();
5145 assert_eq!(id.tenant.as_deref(), Some("acme"));
5146 assert_eq!(id.username, "alice");
5147 assert_eq!(role, Role::Admin);
5148 }
5149
5150 #[test]
5151 fn test_platform_user_has_no_tenant() {
5152 let store = AuthStore::new(test_config());
5153 store.create_user("admin", "pw", Role::Admin).unwrap();
5154 let session = store.authenticate("admin", "pw").unwrap();
5155 assert!(session.tenant_id.is_none());
5156
5157 let (id, _) = store.validate_token_full(&session.token).unwrap();
5158 assert!(id.tenant.is_none());
5159 }
5160
5161 #[test]
5162 fn test_lookup_scram_verifier_global_resolves_platform() {
5163 let store = AuthStore::new(test_config());
5164 store.create_user("admin", "pw", Role::Admin).unwrap();
5165 store
5166 .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
5167 .unwrap();
5168
5169 let v = store.lookup_scram_verifier_global("admin");
5171 assert!(v.is_some());
5172
5173 let v_acme = store.lookup_scram_verifier(&UserId::scoped("acme", "admin"));
5175 assert!(v_acme.is_some());
5176
5177 assert_ne!(v.unwrap().salt, v_acme.unwrap().salt);
5179 }
5180
5181 #[test]
5182 fn test_delete_in_tenant_does_not_touch_other_tenant() {
5183 let store = AuthStore::new(test_config());
5184 store
5185 .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
5186 .unwrap();
5187 store
5188 .create_user_in_tenant(Some("globex"), "alice", "pw", Role::Admin)
5189 .unwrap();
5190
5191 store.delete_user_in_tenant(Some("acme"), "alice").unwrap();
5192
5193 assert!(store
5195 .authenticate_in_tenant(Some("globex"), "alice", "pw")
5196 .is_ok());
5197 assert!(store
5199 .authenticate_in_tenant(Some("acme"), "alice", "pw")
5200 .is_err());
5201 }
5202
5203 #[test]
5204 fn test_user_id_display() {
5205 assert_eq!(UserId::platform("admin").to_string(), "admin");
5206 assert_eq!(UserId::scoped("acme", "alice").to_string(), "acme/alice");
5207 }
5208
5209 fn enforcement_eval_ctx(role: Role) -> EvalContext {
5214 EvalContext {
5215 principal_tenant: None,
5216 current_tenant: None,
5217 peer_ip: None,
5218 mfa_present: false,
5219 now_ms: 1_700_000_000_000,
5220 principal_is_admin_role: role == Role::Admin,
5221 principal_is_platform_scoped: true,
5222 }
5223 }
5224
5225 fn install_unrelated_policy(store: &AuthStore) {
5229 store
5230 .put_policy(
5231 Policy::from_json_str(
5232 r#"{"id":"p-unrelated","version":1,"statements":[{"effect":"allow","actions":["select"],"resources":["table:public.other"]}]}"#,
5233 )
5234 .unwrap(),
5235 )
5236 .unwrap();
5237 }
5238
5239 #[test]
5240 fn enforcement_mode_default_for_new_store_is_legacy_rbac() {
5241 let store = AuthStore::new(test_config());
5246 assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::LegacyRbac);
5247 }
5248
5249 #[test]
5250 fn enforcement_mode_set_round_trips_and_returns_previous() {
5251 let store = AuthStore::new(test_config());
5252 let prev = store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5253 assert_eq!(prev, PolicyEnforcementMode::LegacyRbac);
5254 assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::PolicyOnly);
5255 let prev = store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5256 assert_eq!(prev, PolicyEnforcementMode::PolicyOnly);
5257 }
5258
5259 #[test]
5260 fn policy_only_mode_treats_no_matching_policy_as_deny() {
5261 let store = AuthStore::new(test_config());
5265 store.create_user("alice", "p", Role::Admin).unwrap();
5266 let uid = UserId::platform("alice");
5267 install_unrelated_policy(&store);
5268 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5269
5270 let resource = ResourceRef::new("table", "public.t");
5271 assert!(!store.check_policy_authz_with_role(
5274 &uid,
5275 "select",
5276 &resource,
5277 &enforcement_eval_ctx(Role::Admin),
5278 Role::Admin,
5279 ));
5280 }
5281
5282 #[test]
5283 fn legacy_rbac_mode_falls_back_to_role_decision() {
5284 let store = AuthStore::new(test_config());
5289 store.create_user("alice", "p", Role::Read).unwrap();
5290 let uid = UserId::platform("alice");
5291 install_unrelated_policy(&store);
5292 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5293
5294 let table = ResourceRef::new("table", "public.t");
5295 assert!(store.check_policy_authz_with_role(
5297 &uid,
5298 "select",
5299 &table,
5300 &enforcement_eval_ctx(Role::Read),
5301 Role::Read,
5302 ));
5303 assert!(!store.check_policy_authz_with_role(
5305 &uid,
5306 "insert",
5307 &table,
5308 &enforcement_eval_ctx(Role::Read),
5309 Role::Read,
5310 ));
5311 assert!(store.check_policy_authz_with_role(
5313 &uid,
5314 "insert",
5315 &table,
5316 &enforcement_eval_ctx(Role::Admin),
5317 Role::Admin,
5318 ));
5319 }
5320
5321 #[test]
5322 fn explicit_deny_wins_irrespective_of_mode_or_role() {
5323 let store = AuthStore::new(test_config());
5327 store.create_user("alice", "p", Role::Admin).unwrap();
5328 let uid = UserId::platform("alice");
5329 store
5330 .put_policy(
5331 Policy::from_json_str(
5332 r#"{"id":"p-deny-select","version":1,"statements":[{"effect":"deny","actions":["select"],"resources":["table:public.t"]}]}"#,
5333 )
5334 .unwrap(),
5335 )
5336 .unwrap();
5337 store
5338 .attach_policy(PrincipalRef::User(uid.clone()), "p-deny-select")
5339 .unwrap();
5340
5341 let resource = ResourceRef::new("table", "public.t");
5342 for mode in [
5343 PolicyEnforcementMode::LegacyRbac,
5344 PolicyEnforcementMode::PolicyOnly,
5345 ] {
5346 store.set_enforcement_mode(mode);
5347 assert!(
5348 !store.check_policy_authz_with_role(
5349 &uid,
5350 "select",
5351 &resource,
5352 &enforcement_eval_ctx(Role::Admin),
5353 Role::Admin,
5354 ),
5355 "explicit deny must win under mode {mode:?}"
5356 );
5357 }
5358 }
5359
5360 #[test]
5361 fn explicit_allow_wins_irrespective_of_mode_or_role() {
5362 let store = AuthStore::new(test_config());
5366 store.create_user("alice", "p", Role::Read).unwrap();
5367 let uid = UserId::platform("alice");
5368 store
5369 .put_policy(
5370 Policy::from_json_str(
5371 r#"{"id":"p-allow-create","version":1,"statements":[{"effect":"allow","actions":["create"],"resources":["table:public.t"]}]}"#,
5372 )
5373 .unwrap(),
5374 )
5375 .unwrap();
5376 store
5377 .attach_policy(PrincipalRef::User(uid.clone()), "p-allow-create")
5378 .unwrap();
5379 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5380
5381 let resource = ResourceRef::new("table", "public.t");
5382 assert!(store.check_policy_authz_with_role(
5383 &uid,
5384 "create",
5385 &resource,
5386 &enforcement_eval_ctx(Role::Read),
5387 Role::Read,
5388 ));
5389 }
5390
5391 #[test]
5392 fn take_legacy_rbac_warn_once_is_at_most_once_per_boot() {
5393 let store = AuthStore::new(test_config());
5394 assert!(store.take_legacy_rbac_warn_once());
5396 for _ in 0..3 {
5399 assert!(!store.take_legacy_rbac_warn_once());
5400 }
5401 }
5402
5403 #[test]
5404 fn take_legacy_rbac_warn_once_is_silent_under_policy_only() {
5405 let store = AuthStore::new(test_config());
5411 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5412 assert!(!store.take_legacy_rbac_warn_once());
5413 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5418 assert!(store.take_legacy_rbac_warn_once());
5419 assert!(!store.take_legacy_rbac_warn_once());
5420 }
5421
5422 #[test]
5423 fn strict_check_policy_authz_ignores_enforcement_mode() {
5424 let store = AuthStore::new(test_config());
5429 store.create_user("alice", "p", Role::Admin).unwrap();
5430 let uid = UserId::platform("alice");
5431 install_unrelated_policy(&store);
5432 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
5433
5434 let resource = ResourceRef::new("table", "public.t");
5435 assert!(!store.check_policy_authz(
5436 &uid,
5437 "select",
5438 &resource,
5439 &enforcement_eval_ctx(Role::Admin),
5440 ));
5441 }
5442}