1#![deny(clippy::disallowed_methods)]
7
8use std::borrow::Cow;
9use std::collections::{HashMap, HashSet};
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::sync::{Arc, RwLock};
12
13use crate::crypto::os_random;
14use crate::crypto::sha256::sha256;
15use crate::storage::encryption::argon2id::{derive_key, Argon2Params};
16use crate::storage::engine::pager::Pager;
17
18use super::column_policy_gate::{ColumnAccessRequest, ColumnPolicyGate, ColumnPolicyOutcome};
19use super::enforcement_mode::{legacy_rbac_decision, PolicyEnforcementMode};
20use super::managed_policy::{ManagedPolicyDecision, ManagedPolicyGate, PolicyOp};
21use super::policies::{self as iam_policies, EvalContext, Policy, ResourceRef, SimulationOutcome};
22use super::privileges::{
23 check_grant, Action, AuthzContext, AuthzError, Grant, GrantPrincipal, GrantsView,
24 PermissionCache, Resource, UserAttributes,
25};
26use super::registry::ConfigRegistry;
27use super::vault::{KeyPair, Vault, VaultState};
28use super::{now_ms, ApiKey, AuthConfig, AuthError, Role, Session, User, UserId};
29use crate::runtime::control_events::{
30 ControlEvent, ControlEventConfig, ControlEventCtx, ControlEventLedger, EventKind, Outcome,
31 Sensitivity,
32};
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub enum PrincipalRef {
41 User(UserId),
42 Group(String),
43}
44
45pub const PUBLIC_IAM_GROUP: &str = "__public__";
48
49#[derive(Debug, Clone, Default)]
52pub struct SimCtx {
53 pub current_tenant: Option<String>,
54 pub peer_ip: Option<std::net::IpAddr>,
55 pub mfa_present: bool,
56 pub now_ms: Option<u128>,
57}
58
59pub struct PolicyMutationControl<'a> {
66 pub ctx: &'a ControlEventCtx<'a>,
67 pub ledger: &'a dyn ControlEventLedger,
68 pub config: ControlEventConfig,
69 pub registry: Option<&'a ConfigRegistry>,
70 pub actor: &'a UserId,
71 pub eval_ctx: &'a EvalContext,
72}
73
74pub struct UserLifecycleControl<'a> {
76 pub ctx: &'a ControlEventCtx<'a>,
77 pub ledger: &'a dyn ControlEventLedger,
78 pub config: ControlEventConfig,
79}
80
81#[derive(Clone)]
82struct AuthStoreControlEvents {
83 ledger: Arc<dyn ControlEventLedger>,
84 config: ControlEventConfig,
85}
86
87pub struct BootstrapResult {
96 pub user: User,
97 pub api_key: ApiKey,
98 pub certificate: Option<String>,
100}
101
102impl std::fmt::Debug for BootstrapResult {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("BootstrapResult")
105 .field("user", &self.user)
106 .field("api_key", &"<redacted>")
107 .field(
108 "certificate",
109 &self.certificate.as_ref().map(|_| "<redacted>"),
110 )
111 .finish()
112 }
113}
114
115pub struct AuthStore {
123 users: RwLock<HashMap<UserId, User>>,
126 sessions: RwLock<HashMap<String, Session>>,
127 counted_session_tokens: RwLock<HashSet<String>>,
128 api_key_index: RwLock<HashMap<String, (UserId, Role)>>,
130 bootstrapped: AtomicBool,
132 config: AuthConfig,
133 vault: RwLock<Option<Vault>>,
135 pager: Option<Arc<Pager>>,
137 provided_certificate: Option<String>,
139 keypair: RwLock<Option<KeyPair>>,
142 vault_kv: RwLock<HashMap<String, String>>,
145 secret_key_init: std::sync::Mutex<()>,
146 plain_kv: RwLock<HashMap<String, String>>,
151 grants: RwLock<HashMap<UserId, Vec<Grant>>>,
156 public_grants: RwLock<Vec<Grant>>,
158 user_attributes: RwLock<HashMap<UserId, UserAttributes>>,
162 session_count_by_user: RwLock<HashMap<UserId, u32>>,
166 permission_cache: RwLock<HashMap<UserId, PermissionCache>>,
170 policies: RwLock<HashMap<String, Arc<Policy>>>,
174 user_attachments: RwLock<HashMap<UserId, Vec<String>>>,
177 group_attachments: RwLock<HashMap<String, Vec<String>>>,
181 iam_effective_cache: RwLock<HashMap<UserId, Vec<Arc<Policy>>>>,
184 iam_authorization_enabled: AtomicBool,
188 enforcement_mode: RwLock<PolicyEnforcementMode>,
198 legacy_rbac_boot_warn_emitted: AtomicBool,
203 visible_collections_cache: super::scope_cache::AuthCache,
213 control_events: RwLock<Option<AuthStoreControlEvents>>,
214}
215
216pub struct ConnectionLease {
219 store: Arc<AuthStore>,
220 principal: UserId,
221}
222
223impl Drop for ConnectionLease {
224 fn drop(&mut self) {
225 self.store.decrement_session_count(&self.principal);
226 }
227}
228
229fn auth_argon2_params() -> Argon2Params {
232 Argon2Params {
233 m_cost: 4 * 1024, t_cost: 3,
235 p: 1,
236 tag_len: 32,
237 }
238}
239
240fn policy_control_fields(
241 policy_id: &str,
242 policy: Option<&Policy>,
243 principal: Option<&PrincipalRef>,
244) -> HashMap<String, Sensitivity> {
245 let mut fields = HashMap::new();
246 fields.insert("policy_id".to_string(), Sensitivity::raw(policy_id));
247 if let Some(policy) = policy {
248 let effects = policy
249 .statements
250 .iter()
251 .map(|s| match s.effect {
252 iam_policies::Effect::Allow => "allow",
253 iam_policies::Effect::Deny => "deny",
254 })
255 .collect::<Vec<_>>()
256 .join(",");
257 let actions = policy
258 .statements
259 .iter()
260 .flat_map(|s| s.actions.iter())
261 .map(policy_action_pattern)
262 .collect::<Vec<_>>()
263 .join(",");
264 let resources = policy
265 .statements
266 .iter()
267 .flat_map(|s| s.resources.iter())
268 .map(policy_resource_pattern)
269 .collect::<Vec<_>>()
270 .join(",");
271 fields.insert("effect".to_string(), Sensitivity::raw(effects));
272 fields.insert("action".to_string(), Sensitivity::raw(actions));
273 fields.insert("resource".to_string(), Sensitivity::raw(resources));
274 if let Some(attrs) = policy_principal_attrs(policy) {
275 fields.insert("principal_attrs".to_string(), Sensitivity::raw(attrs));
276 }
277 }
278 if let Some(principal) = principal {
279 fields.insert(
280 "principal".to_string(),
281 Sensitivity::raw(policy_principal_label(principal)),
282 );
283 }
284 fields
285}
286
287fn policy_action_pattern(pattern: &iam_policies::ActionPattern) -> String {
288 match pattern {
289 iam_policies::ActionPattern::Exact(s) => s.clone(),
290 iam_policies::ActionPattern::Wildcard => "*".to_string(),
291 iam_policies::ActionPattern::Prefix(s) => format!("{s}:*"),
292 }
293}
294
295fn policy_resource_pattern(pattern: &iam_policies::ResourcePattern) -> String {
296 match pattern {
297 iam_policies::ResourcePattern::Exact { kind, name } => format!("{kind}:{name}"),
298 iam_policies::ResourcePattern::Glob(s) => s.clone(),
299 iam_policies::ResourcePattern::Wildcard => "*".to_string(),
300 }
301}
302
303fn policy_principal_attrs(policy: &Policy) -> Option<String> {
304 let mut attrs = Vec::new();
305 for statement in &policy.statements {
306 let Some(condition) = &statement.condition else {
307 continue;
308 };
309 if let Some(value) = condition.platform_scoped {
310 attrs.push(format!("platform_scoped={value}"));
311 }
312 if let Some(value) = condition.mfa {
313 attrs.push(format!("mfa={value}"));
314 }
315 if let Some(value) = condition.tenant_match {
316 attrs.push(format!("tenant_match={value}"));
317 }
318 }
319 if attrs.is_empty() {
320 None
321 } else {
322 Some(attrs.join(","))
323 }
324}
325
326fn policy_principal_label(principal: &PrincipalRef) -> String {
327 match principal {
328 PrincipalRef::User(uid) => format!("user:{uid}"),
329 PrincipalRef::Group(group) => format!("group:{group}"),
330 }
331}
332
333fn default_user_lifecycle_ctx<'a>() -> ControlEventCtx<'a> {
334 ControlEventCtx {
335 actor: crate::runtime::control_events::ActorRef::Anonymous,
336 scope: None,
337 request_id: None,
338 trace_id: None,
339 }
340}
341
342fn bootstrap_user_lifecycle_ctx<'a>() -> ControlEventCtx<'a> {
343 ControlEventCtx {
344 actor: crate::runtime::control_events::ActorRef::System("bootstrap"),
345 scope: None,
346 request_id: None,
347 trace_id: None,
348 }
349}
350
351fn user_resource(id: &UserId) -> String {
352 format!("user:{id}")
353}
354
355fn api_key_resource(api_key_id: &str) -> String {
356 format!("apikey:{api_key_id}")
357}
358
359fn api_key_id(key: &str) -> String {
363 match key.strip_prefix("rkh_") {
364 Some(hex) => hex.to_string(),
365 None => hex::encode(sha256(key.as_bytes())),
366 }
367}
368
369pub(crate) fn api_key_credential_id(token: &str) -> String {
372 format!("rkh_{}", api_key_id(token))
373}
374
375fn password_evidence() -> Sensitivity {
376 Sensitivity::redacted()
377}
378
379fn user_control_fields(
380 id: &UserId,
381 role: Option<Role>,
382 enabled: Option<bool>,
383 include_password: bool,
384) -> HashMap<String, Sensitivity> {
385 let mut fields = HashMap::new();
386 fields.insert(
387 "username".to_string(),
388 Sensitivity::raw(id.username.clone()),
389 );
390 fields.insert(
391 "tenant_id".to_string(),
392 Sensitivity::raw(id.tenant.clone().unwrap_or_default()),
393 );
394 if let Some(role) = role {
395 fields.insert("role".to_string(), Sensitivity::raw(role.as_str()));
396 }
397 if let Some(enabled) = enabled {
398 fields.insert("enabled".to_string(), Sensitivity::raw(enabled.to_string()));
399 }
400 if include_password {
401 fields.insert("password".to_string(), password_evidence());
402 }
403 fields
404}
405
406fn api_key_control_fields(
407 id: &UserId,
408 role: Role,
409 api_key_id: &str,
410) -> HashMap<String, Sensitivity> {
411 let mut fields = user_control_fields(id, Some(role), None, false);
412 fields.insert("api_key_id".to_string(), Sensitivity::raw(api_key_id));
413 fields.insert("api_key".to_string(), Sensitivity::redacted());
414 fields
415}
416
417fn user_error_is_denied(err: &AuthError) -> bool {
418 !matches!(err, AuthError::Internal(_))
419}
420
421impl AuthStore {
422 pub fn new(config: AuthConfig) -> Self {
427 Self {
428 users: RwLock::new(HashMap::new()),
429 sessions: RwLock::new(HashMap::new()),
430 counted_session_tokens: RwLock::new(HashSet::new()),
431 api_key_index: RwLock::new(HashMap::new()),
432 bootstrapped: AtomicBool::new(false),
433 config,
434 vault: RwLock::new(None),
435 pager: None,
436 provided_certificate: None,
437 keypair: RwLock::new(None),
438 vault_kv: RwLock::new(HashMap::new()),
439 secret_key_init: std::sync::Mutex::new(()),
440 plain_kv: RwLock::new(HashMap::new()),
441 grants: RwLock::new(HashMap::new()),
442 public_grants: RwLock::new(Vec::new()),
443 user_attributes: RwLock::new(HashMap::new()),
444 session_count_by_user: RwLock::new(HashMap::new()),
445 permission_cache: RwLock::new(HashMap::new()),
446 policies: RwLock::new(HashMap::new()),
447 user_attachments: RwLock::new(HashMap::new()),
448 group_attachments: RwLock::new(HashMap::new()),
449 iam_effective_cache: RwLock::new(HashMap::new()),
450 iam_authorization_enabled: AtomicBool::new(false),
451 enforcement_mode: RwLock::new(PolicyEnforcementMode::default_existing_install()),
452 legacy_rbac_boot_warn_emitted: AtomicBool::new(false),
453 visible_collections_cache: super::scope_cache::AuthCache::new(
454 super::scope_cache::DEFAULT_TTL,
455 ),
456 control_events: RwLock::new(None),
457 }
458 }
459
460 pub fn configure_control_events(
461 &self,
462 ledger: Arc<dyn ControlEventLedger>,
463 config: ControlEventConfig,
464 ) {
465 *self
466 .control_events
467 .write()
468 .unwrap_or_else(|e| e.into_inner()) = Some(AuthStoreControlEvents { ledger, config });
469 }
470
471 fn configured_control_events(&self) -> Option<AuthStoreControlEvents> {
472 self.control_events
473 .read()
474 .unwrap_or_else(|e| e.into_inner())
475 .clone()
476 }
477
478 pub fn with_vault(config: AuthConfig, pager: Arc<Pager>) -> Result<Self, AuthError> {
485 let certificate = crate::utils::env_with_file_fallback("REDDB_CERTIFICATE");
486 let passphrase = crate::utils::env_with_file_fallback("REDDB_VAULT_PASSPHRASE");
487 match (certificate, passphrase) {
488 (Some(_), Some(_)) => Err(AuthError::Internal(
489 "set either REDDB_CERTIFICATE or REDDB_VAULT_PASSPHRASE, not both".into(),
490 )),
491 (None, Some(passphrase)) => Self::with_vault_passphrase(config, pager, &passphrase),
492 (certificate, None) => {
493 Self::with_vault_optional_certificate(config, pager, certificate.as_deref())
494 }
495 }
496 }
497
498 pub fn with_vault_certificate(
499 config: AuthConfig,
500 pager: Arc<Pager>,
501 certificate_hex: &str,
502 ) -> Result<Self, AuthError> {
503 Self::with_vault_optional_certificate(config, pager, Some(certificate_hex))
504 }
505
506 pub fn with_vault_passphrase(
507 config: AuthConfig,
508 pager: Arc<Pager>,
509 passphrase: &str,
510 ) -> Result<Self, AuthError> {
511 let mut store = Self::new(config);
512 let vault = Vault::with_passphrase(&pager, passphrase)?;
513 if let Some(state) = vault.load(&pager)? {
514 store.restore_from_vault(state);
515 }
516 *store.vault.write().map_err(lock_err)? = Some(vault);
517 store.pager = Some(pager);
518 Ok(store)
519 }
520
521 fn with_vault_optional_certificate(
522 config: AuthConfig,
523 pager: Arc<Pager>,
524 certificate_hex: Option<&str>,
525 ) -> Result<Self, AuthError> {
526 let mut store = Self::new(config);
527
528 if Vault::has_saved_state(&pager) {
532 let vault = match certificate_hex {
533 Some(certificate) => Vault::with_certificate(&pager, certificate)?,
534 None => Vault::open(&pager)?,
535 };
536 if let Some(state) = vault.load(&pager)? {
537 store.restore_from_vault(state);
538 }
539 *store.vault.write().unwrap_or_else(|e| e.into_inner()) = Some(vault);
540 } else if let Some(certificate) = certificate_hex {
541 let vault = Vault::with_certificate(&pager, certificate)?;
542 *store.vault.write().unwrap_or_else(|e| e.into_inner()) = Some(vault);
543 }
544
545 store.pager = Some(pager);
546 store.provided_certificate =
547 certificate_hex.map(|certificate| certificate.trim().to_ascii_lowercase());
548 Ok(store)
549 }
550
551 pub fn config(&self) -> &AuthConfig {
552 &self.config
553 }
554
555 pub fn is_enabled(&self) -> bool {
556 self.config.enabled
557 }
558
559 pub fn needs_bootstrap(&self) -> bool {
561 !self.bootstrapped.load(Ordering::Acquire)
562 && self.users.read().map(|u| u.is_empty()).unwrap_or(true)
563 }
564
565 fn get_user_cloned(&self, id: &UserId) -> Option<User> {
567 self.users.read().ok().and_then(|m| m.get(id).cloned())
568 }
569
570 pub fn is_bootstrapped(&self) -> bool {
572 self.bootstrapped.load(Ordering::Acquire)
573 }
574
575 pub fn bootstrap(&self, username: &str, password: &str) -> Result<BootstrapResult, AuthError> {
586 if let Some(configured) = self.configured_control_events() {
587 let ctx = bootstrap_user_lifecycle_ctx();
588 let control = UserLifecycleControl {
589 ctx: &ctx,
590 ledger: configured.ledger.as_ref(),
591 config: configured.config,
592 };
593 self.bootstrap_with_control_events_inner(username, password, &control)
594 } else {
595 self.bootstrap_unaudited(username, password)
596 }
597 }
598
599 pub fn bootstrap_with_certificate_delivery(
602 &self,
603 username: &str,
604 password: &str,
605 mut deliver: impl FnMut(&str) -> Result<(), AuthError>,
606 ) -> Result<BootstrapResult, AuthError> {
607 let delivery: &mut dyn FnMut(&str) -> Result<(), AuthError> = &mut deliver;
608 if let Some(configured) = self.configured_control_events() {
609 let ctx = bootstrap_user_lifecycle_ctx();
610 let control = UserLifecycleControl {
611 ctx: &ctx,
612 ledger: configured.ledger.as_ref(),
613 config: configured.config,
614 };
615 self.bootstrap_with_control_events_inner_and_delivery(
616 username,
617 password,
618 &control,
619 Some(delivery),
620 )
621 } else {
622 self.bootstrap_unaudited_with_delivery(username, password, Some(delivery))
623 }
624 }
625
626 pub fn bootstrap_with_control_events(
627 &self,
628 username: &str,
629 password: &str,
630 ctx: &ControlEventCtx<'_>,
631 ledger: &dyn ControlEventLedger,
632 config: ControlEventConfig,
633 ) -> Result<BootstrapResult, AuthError> {
634 let system_ctx = ControlEventCtx {
635 actor: crate::runtime::control_events::ActorRef::System("bootstrap"),
636 scope: ctx.scope.clone(),
637 request_id: ctx.request_id.clone(),
638 trace_id: ctx.trace_id.clone(),
639 };
640 let control = UserLifecycleControl {
641 ctx: &system_ctx,
642 ledger,
643 config,
644 };
645 self.bootstrap_with_control_events_inner(username, password, &control)
646 }
647
648 pub fn bootstrap_system_admin(
653 &self,
654 username: &str,
655 password: &str,
656 ) -> Result<BootstrapResult, AuthError> {
657 self.bootstrap(username, password)
658 }
659
660 fn bootstrap_unaudited(
661 &self,
662 username: &str,
663 password: &str,
664 ) -> Result<BootstrapResult, AuthError> {
665 self.bootstrap_unaudited_with_delivery(username, password, None)
666 }
667
668 fn bootstrap_unaudited_with_delivery(
669 &self,
670 username: &str,
671 password: &str,
672 mut certificate_delivery: Option<&mut dyn FnMut(&str) -> Result<(), AuthError>>,
673 ) -> Result<BootstrapResult, AuthError> {
674 if self
676 .bootstrapped
677 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
678 .is_err()
679 {
680 return Err(AuthError::Forbidden(
681 "bootstrap already completed — sealed permanently".to_string(),
682 ));
683 }
684
685 {
687 let users = self.users.read().map_err(lock_err)?;
688 if !users.is_empty() {
689 return Err(AuthError::Forbidden(
690 "bootstrap already completed — users exist".to_string(),
691 ));
692 }
693 }
694
695 let user = self.create_user_in_tenant_unaudited(None, username, password, Role::Admin)?;
696 let key = self.create_api_key_in_tenant_unaudited(
697 None,
698 username,
699 "bootstrap",
700 Role::Admin,
701 None,
702 )?;
703
704 let credential_result = (|| -> Result<Option<String>, AuthError> {
707 let certificate = if let Some(ref pager) = self.pager {
708 let keypair = KeyPair::generate();
709 let mut vault = self.vault.write().map_err(lock_err)?;
710 let certificate = if vault.is_none() {
711 let certificate = keypair.certificate_hex();
712 if let Some(deliver) = certificate_delivery.as_mut() {
713 deliver(&certificate)?;
714 }
715 *vault = Some(Vault::with_certificate_bytes(pager, &keypair.certificate)?);
716 Some(certificate)
717 } else {
718 self.provided_certificate.clone()
719 };
720 *self.keypair.write().map_err(lock_err)? = Some(keypair);
721 drop(vault);
722 self.ensure_vault_secret_key_result()?;
723 certificate
724 } else {
725 None
726 };
727 *self.enforcement_mode.write().map_err(lock_err)? =
728 PolicyEnforcementMode::default_fresh_bootstrap();
729 self.vault_kv.write().map_err(lock_err)?.insert(
730 "red.config.policy.enforcement_mode".into(),
731 PolicyEnforcementMode::default_fresh_bootstrap()
732 .as_str()
733 .into(),
734 );
735 self.persist_to_vault_result()?;
736 Ok(certificate)
737 })();
738 let certificate = match credential_result {
739 Ok(certificate) => certificate,
740 Err(err) => {
741 self.rollback_bootstrap(&UserId::from_parts(None, username));
742 return Err(err);
743 }
744 };
745
746 Ok(BootstrapResult {
747 user,
748 api_key: key,
749 certificate,
750 })
751 }
752
753 fn bootstrap_with_control_events_inner(
754 &self,
755 username: &str,
756 password: &str,
757 control: &UserLifecycleControl<'_>,
758 ) -> Result<BootstrapResult, AuthError> {
759 self.bootstrap_with_control_events_inner_and_delivery(username, password, control, None)
760 }
761
762 fn bootstrap_with_control_events_inner_and_delivery(
763 &self,
764 username: &str,
765 password: &str,
766 control: &UserLifecycleControl<'_>,
767 certificate_delivery: Option<&mut dyn FnMut(&str) -> Result<(), AuthError>>,
768 ) -> Result<BootstrapResult, AuthError> {
769 let id = UserId::from_parts(None, username);
770 match self.bootstrap_unaudited_with_delivery(username, password, certificate_delivery) {
771 Ok(result) => {
772 let event_result = self.emit_user_lifecycle_allowed(
773 control,
774 EventKind::UserCreate,
775 "user.create",
776 &id,
777 user_control_fields(&id, Some(Role::Admin), Some(true), true),
778 );
779 if let Err(err) = event_result {
780 self.rollback_bootstrap(&id);
781 return Err(err);
782 }
783 Ok(result)
784 }
785 Err(err) => {
786 self.emit_user_lifecycle_outcome(
787 control,
788 if user_error_is_denied(&err) {
789 Outcome::Denied
790 } else {
791 Outcome::Error
792 },
793 EventKind::UserCreate,
794 "user.create",
795 &id,
796 Some(err.to_string()),
797 user_control_fields(&id, Some(Role::Admin), Some(true), true),
798 );
799 Err(err)
800 }
801 }
802 }
803
804 pub fn bootstrap_from_env(&self) -> Option<BootstrapResult> {
812 if !self.needs_bootstrap() {
813 return None;
814 }
815
816 let username = crate::utils::env_with_file_fallback("REDDB_USERNAME")?;
817 let password = crate::utils::env_with_file_fallback("REDDB_PASSWORD")?;
818
819 if username.is_empty() || password.is_empty() {
820 return None;
821 }
822 let mut certificate_output = if self.bootstrap_requires_certificate_delivery() {
823 let Some(path) = crate::utils::env_with_file_fallback("REDDB_BOOTSTRAP_CERT_OUT")
824 else {
825 tracing::error!(
826 target: "reddb::security",
827 "env bootstrap requires REDDB_BOOTSTRAP_CERT_OUT when the vault is enabled; refusing to emit the certificate to logs"
828 );
829 return None;
830 };
831 let mut options = std::fs::OpenOptions::new();
832 options.write(true).create_new(true);
833 #[cfg(unix)]
834 {
835 use std::os::unix::fs::OpenOptionsExt;
836 options.mode(0o600);
837 }
838 let file = match options.open(&path) {
839 Ok(file) => file,
840 Err(error) => {
841 tracing::error!(
842 target: "reddb::security",
843 path = %path,
844 error = %error,
845 "env bootstrap certificate output is not writable"
846 );
847 return None;
848 }
849 };
850 Some((path, file))
851 } else {
852 None
853 };
854
855 let mut certificate_staged = false;
856 let bootstrap_result = match certificate_output.as_mut() {
857 Some((path, file)) => {
858 self.bootstrap_with_certificate_delivery(&username, &password, |certificate| {
859 use std::io::Write;
860 writeln!(file, "{certificate}").map_err(|error| {
861 AuthError::Internal(format!(
862 "write bootstrap certificate {}: {error}",
863 path
864 ))
865 })?;
866 file.sync_all().map_err(|error| {
867 AuthError::Internal(format!("sync bootstrap certificate {}: {error}", path))
868 })?;
869 certificate_staged = true;
870 Ok(())
871 })
872 }
873 None => self.bootstrap(&username, &password),
874 };
875
876 match bootstrap_result {
877 Ok(result) => {
878 tracing::info!(
884 username = %reddb_wire::audit_safe_log_field(&username),
885 "bootstrapped admin user from REDDB_USERNAME/REDDB_PASSWORD"
886 );
887 if !certificate_staged {
888 if let (Some(cert), Some((path, file))) =
889 (result.certificate.as_deref(), certificate_output.as_mut())
890 {
891 use std::io::Write;
892 if let Err(error) = writeln!(file, "{cert}").and_then(|()| file.sync_all())
893 {
894 tracing::error!(
895 target: "reddb::security",
896 path = %path,
897 error = %error,
898 "could not write the vault certificate"
899 );
900 return None;
901 }
902 }
903 }
904 if let Some((path, _)) = certificate_output {
905 tracing::warn!(
906 target: "reddb::security",
907 path = %path,
908 "vault certificate written — save it: the ONLY way to unseal after a restart"
909 );
910 }
911 Some(result)
912 }
913 Err(e) => {
914 let output_path = certificate_output.as_ref().map(|(path, _)| path.clone());
915 drop(certificate_output);
916 if let Some(path) = output_path {
917 let _ = std::fs::remove_file(path);
918 }
919 tracing::error!(err = %e, "env bootstrap failed");
920 None
921 }
922 }
923 }
924
925 pub(crate) fn persist_to_vault_result(&self) -> Result<(), AuthError> {
931 let vault_guard = self.vault.read().unwrap_or_else(|e| e.into_inner());
932 if let (Some(ref vault), Some(ref pager)) = (&*vault_guard, &self.pager) {
933 let state = self.snapshot();
934 vault.save(pager, &state)?;
935 }
936 Ok(())
937 }
938
939 fn persist_to_vault(&self) {
945 if let Err(e) = self.persist_to_vault_result() {
946 tracing::error!(err = %e, "vault persist failed");
947 crate::telemetry::operator_event::OperatorEvent::SecretRotationFailed {
953 secret_ref: "auth_vault".to_string(),
954 error: e.to_string(),
955 }
956 .emit_global();
957 }
958 }
959
960 pub fn is_vault_backed(&self) -> bool {
962 self.pager.is_some()
963 && self
964 .vault
965 .read()
966 .map(|guard| guard.is_some())
967 .unwrap_or(false)
968 }
969
970 pub fn bootstrap_requires_certificate_delivery(&self) -> bool {
974 self.pager.is_some()
975 && self
976 .vault
977 .read()
978 .map(|vault| vault.is_none())
979 .unwrap_or(true)
980 }
981
982 pub fn vault_kv_get(&self, key: &str) -> Option<String> {
988 self.vault_kv
989 .read()
990 .ok()
991 .and_then(|kv| kv.get(key).cloned())
992 }
993
994 pub fn vault_kv_snapshot(&self) -> HashMap<String, String> {
996 self.vault_kv
997 .read()
998 .map(|kv| kv.clone())
999 .unwrap_or_default()
1000 }
1001
1002 pub fn vault_kv_export_encrypted(&self) -> Result<Option<String>, AuthError> {
1005 if !self.is_vault_backed() {
1006 return Err(AuthError::Forbidden(
1007 "vault KV export requires an enabled, unsealed vault".to_string(),
1008 ));
1009 }
1010 let kv = self.vault_kv_snapshot();
1011 if kv.is_empty() {
1012 return Ok(None);
1013 }
1014
1015 let vault_guard = self.vault.read().map_err(lock_err)?;
1016 let vault = vault_guard.as_ref().ok_or_else(|| {
1017 AuthError::Forbidden("vault KV export requires an enabled, unsealed vault".to_string())
1018 })?;
1019 let state = VaultState {
1020 users: Vec::new(),
1021 api_keys: Vec::new(),
1022 bootstrapped: false,
1023 master_secret: None,
1024 kv,
1025 };
1026 Ok(Some(vault.seal_logical_export(&state)?))
1027 }
1028
1029 pub fn vault_kv_try_import(
1032 &self,
1033 entries: HashMap<String, String>,
1034 ) -> Result<usize, AuthError> {
1035 if !self.is_vault_backed() {
1036 return Err(AuthError::Forbidden(
1037 "vault KV import requires an enabled, unsealed vault".to_string(),
1038 ));
1039 }
1040 if entries.is_empty() {
1041 return Ok(0);
1042 }
1043
1044 let mut previous = HashMap::new();
1045 {
1046 let mut kv = self.vault_kv.write().map_err(lock_err)?;
1047 for (key, value) in &entries {
1048 previous.insert(key.clone(), kv.insert(key.clone(), value.clone()));
1049 }
1050 }
1051
1052 if let Err(err) = self.persist_to_vault_result() {
1053 if let Ok(mut kv) = self.vault_kv.write() {
1054 for (key, old) in previous {
1055 match old {
1056 Some(value) => {
1057 kv.insert(key, value);
1058 }
1059 None => {
1060 kv.remove(&key);
1061 }
1062 }
1063 }
1064 }
1065 return Err(err);
1066 }
1067
1068 Ok(entries.len())
1069 }
1070
1071 pub fn vault_kv_try_import_placeholders(&self, keys: &[String]) -> Result<usize, AuthError> {
1074 let entries = keys
1075 .iter()
1076 .map(|key| (key.clone(), "false".to_string()))
1077 .collect();
1078 self.vault_kv_try_import(entries)
1079 }
1080
1081 pub fn vault_kv_set(&self, key: String, value: String) {
1083 if let Ok(mut kv) = self.vault_kv.write() {
1084 kv.insert(key, value);
1085 }
1086 self.persist_to_vault();
1087 }
1088
1089 pub fn vault_kv_try_set(&self, key: String, value: String) -> Result<(), AuthError> {
1092 if !self.is_vault_backed() {
1093 return Err(AuthError::Forbidden(
1094 "SET SECRET requires an enabled, unsealed vault".to_string(),
1095 ));
1096 }
1097
1098 let previous = {
1099 let mut kv = self.vault_kv.write().map_err(lock_err)?;
1100 kv.insert(key.clone(), value)
1101 };
1102
1103 if let Err(err) = self.persist_to_vault_result() {
1104 if let Ok(mut kv) = self.vault_kv.write() {
1105 match previous {
1106 Some(value) => {
1107 kv.insert(key, value);
1108 }
1109 None => {
1110 kv.remove(&key);
1111 }
1112 }
1113 }
1114 return Err(err);
1115 }
1116
1117 Ok(())
1118 }
1119
1120 pub fn vault_kv_delete(&self, key: &str) -> bool {
1122 let existed = self
1123 .vault_kv
1124 .write()
1125 .map(|mut kv| kv.remove(key).is_some())
1126 .unwrap_or(false);
1127 if existed {
1128 self.persist_to_vault();
1129 }
1130 existed
1131 }
1132
1133 pub fn vault_kv_try_delete(&self, key: &str) -> Result<bool, AuthError> {
1136 if !self.is_vault_backed() {
1137 return Err(AuthError::Forbidden(
1138 "DELETE SECRET requires an enabled, unsealed vault".to_string(),
1139 ));
1140 }
1141
1142 let removed = {
1143 let mut kv = self.vault_kv.write().map_err(lock_err)?;
1144 kv.remove(key)
1145 };
1146
1147 if removed.is_none() {
1148 return Ok(false);
1149 }
1150
1151 if let Err(err) = self.persist_to_vault_result() {
1152 if let Ok(mut kv) = self.vault_kv.write() {
1153 if let Some(value) = removed {
1154 kv.insert(key.to_string(), value);
1155 }
1156 }
1157 return Err(err);
1158 }
1159
1160 Ok(true)
1161 }
1162
1163 pub fn vault_kv_keys(&self) -> Vec<String> {
1165 self.vault_kv
1166 .read()
1167 .map(|kv| kv.keys().cloned().collect())
1168 .unwrap_or_default()
1169 }
1170
1171 pub fn plain_kv_get(&self, key: &str) -> Option<String> {
1182 self.plain_kv
1183 .read()
1184 .ok()
1185 .and_then(|kv| kv.get(key).cloned())
1186 }
1187
1188 pub fn plain_kv_snapshot(&self) -> HashMap<String, String> {
1190 self.plain_kv
1191 .read()
1192 .map(|kv| kv.clone())
1193 .unwrap_or_default()
1194 }
1195
1196 pub fn plain_kv_set(&self, key: String, value: String) {
1198 if let Ok(mut kv) = self.plain_kv.write() {
1199 kv.insert(key, value);
1200 }
1201 }
1202
1203 pub fn plain_kv_delete(&self, key: &str) -> bool {
1205 self.plain_kv
1206 .write()
1207 .map(|mut kv| kv.remove(key).is_some())
1208 .unwrap_or(false)
1209 }
1210
1211 pub fn plain_kv_keys(&self) -> Vec<String> {
1213 self.plain_kv
1214 .read()
1215 .map(|kv| kv.keys().cloned().collect())
1216 .unwrap_or_default()
1217 }
1218
1219 pub fn vault_secret_key(&self) -> Option<[u8; 32]> {
1222 let hex_str = self.vault_kv_get("red.secret.aes_key")?;
1223 let bytes = hex::decode(hex_str).ok()?;
1224 if bytes.len() == 32 {
1225 let mut key = [0u8; 32];
1226 key.copy_from_slice(&bytes);
1227 Some(key)
1228 } else {
1229 None
1230 }
1231 }
1232
1233 pub fn ensure_vault_secret_key(&self) {
1235 if let Err(err) = self.ensure_vault_secret_key_result() {
1236 tracing::error!(error = %err, "vault secret key initialization failed");
1237 }
1238 }
1239
1240 pub(crate) fn ensure_vault_secret_key_result(&self) -> Result<(), AuthError> {
1241 let _guard = self.secret_key_init.lock().map_err(lock_err)?;
1242 if self.vault_secret_key().is_some() {
1243 return Ok(());
1244 }
1245 if self.vault_kv_get("red.secret.aes_key").is_some() {
1246 return Err(AuthError::Internal(
1247 "stored vault secret key is invalid".into(),
1248 ));
1249 }
1250 let value = hex::encode(random_bytes(32));
1251 if self.is_vault_backed() {
1252 self.vault_kv_try_set("red.secret.aes_key".into(), value)
1253 } else {
1254 self.vault_kv
1255 .write()
1256 .map_err(lock_err)?
1257 .insert("red.secret.aes_key".into(), value);
1258 Ok(())
1259 }
1260 }
1261
1262 fn snapshot(&self) -> VaultState {
1264 let users_guard = self.users.read().unwrap_or_else(|e| e.into_inner());
1265 let users: Vec<User> = users_guard.values().cloned().collect();
1266
1267 let mut api_keys = Vec::new();
1270 for user in &users {
1271 let owner = UserId::from_parts(user.tenant_id.as_deref(), &user.username);
1272 for key in &user.api_keys {
1273 api_keys.push((owner.clone(), key.clone()));
1274 }
1275 }
1276
1277 let master_secret = self
1279 .keypair
1280 .read()
1281 .ok()
1282 .and_then(|guard| guard.as_ref().map(|kp| kp.master_secret.clone()));
1283
1284 let kv = self.vault_kv.read().map(|m| m.clone()).unwrap_or_default();
1285
1286 VaultState {
1287 users,
1288 api_keys,
1289 bootstrapped: self.bootstrapped.load(Ordering::Acquire),
1290 master_secret,
1291 kv,
1292 }
1293 }
1294
1295 fn restore_from_vault(&mut self, state: VaultState) {
1297 if state.bootstrapped {
1299 self.bootstrapped.store(true, Ordering::Release);
1300 }
1301
1302 if let Some(secret) = state.master_secret {
1304 let kp = KeyPair::from_master_secret(secret);
1305 if let Ok(mut guard) = self.keypair.write() {
1306 *guard = Some(kp);
1307 }
1308 }
1309
1310 if let Ok(mut kv) = self.vault_kv.write() {
1312 *kv = state.kv;
1313 }
1314
1315 let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
1317 let mut idx = self
1318 .api_key_index
1319 .write()
1320 .unwrap_or_else(|e| e.into_inner());
1321
1322 for mut user in state.users {
1323 let id = UserId::from_parts(user.tenant_id.as_deref(), &user.username);
1324 for key in &mut user.api_keys {
1328 if !key.key.starts_with("rkh_") {
1329 key.key = api_key_credential_id(&key.key);
1330 }
1331 }
1332 for key in &user.api_keys {
1334 idx.insert(key.key.clone(), (id.clone(), key.role));
1335 }
1336 users.insert(id, user);
1337 }
1338 drop(idx);
1339 drop(users);
1340
1341 self.rehydrate_acl();
1342 self.rehydrate_iam();
1343 }
1344
1345 pub fn create_user(
1353 &self,
1354 username: &str,
1355 password: &str,
1356 role: Role,
1357 ) -> Result<User, AuthError> {
1358 self.create_user_in_tenant(None, username, password, role)
1359 }
1360
1361 pub fn create_user_with_control_events(
1362 &self,
1363 username: &str,
1364 password: &str,
1365 role: Role,
1366 ctx: &ControlEventCtx<'_>,
1367 ledger: &dyn ControlEventLedger,
1368 config: ControlEventConfig,
1369 ) -> Result<User, AuthError> {
1370 self.create_user_in_tenant_with_control_events(
1371 None, username, password, role, ctx, ledger, config,
1372 )
1373 }
1374
1375 pub fn create_user_in_tenant(
1380 &self,
1381 tenant_id: Option<&str>,
1382 username: &str,
1383 password: &str,
1384 role: Role,
1385 ) -> Result<User, AuthError> {
1386 if let Some(configured) = self.configured_control_events() {
1387 let ctx = default_user_lifecycle_ctx();
1388 let control = UserLifecycleControl {
1389 ctx: &ctx,
1390 ledger: configured.ledger.as_ref(),
1391 config: configured.config,
1392 };
1393 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1394 } else {
1395 self.create_user_in_tenant_unaudited(tenant_id, username, password, role)
1396 }
1397 }
1398
1399 #[allow(clippy::too_many_arguments)]
1400 pub fn create_user_in_tenant_with_control_events(
1401 &self,
1402 tenant_id: Option<&str>,
1403 username: &str,
1404 password: &str,
1405 role: Role,
1406 ctx: &ControlEventCtx<'_>,
1407 ledger: &dyn ControlEventLedger,
1408 config: ControlEventConfig,
1409 ) -> Result<User, AuthError> {
1410 let control = UserLifecycleControl {
1411 ctx,
1412 ledger,
1413 config,
1414 };
1415 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1416 }
1417
1418 pub fn create_admin_user(
1419 &self,
1420 username: &str,
1421 password: &str,
1422 role: Role,
1423 tenant_id: Option<&str>,
1424 ) -> Result<User, AuthError> {
1425 if let Some(configured) = self.configured_control_events() {
1426 let ctx = default_user_lifecycle_ctx();
1427 let control = UserLifecycleControl {
1428 ctx: &ctx,
1429 ledger: configured.ledger.as_ref(),
1430 config: configured.config,
1431 };
1432 self.create_user_in_tenant_controlled(tenant_id, username, password, role, &control)
1433 } else {
1434 self.create_user_in_tenant_unaudited(tenant_id, username, password, role)
1435 }
1436 }
1437
1438 fn create_user_in_tenant_unaudited(
1439 &self,
1440 tenant_id: Option<&str>,
1441 username: &str,
1442 password: &str,
1443 role: Role,
1444 ) -> Result<User, AuthError> {
1445 let id = UserId::from_parts(tenant_id, username);
1446 let mut users = self.users.write().map_err(lock_err)?;
1447 if users.contains_key(&id) {
1448 return Err(AuthError::UserExists(id.to_string()));
1449 }
1450
1451 let now = now_ms();
1452 let user = User {
1453 username: username.to_string(),
1454 tenant_id: tenant_id.map(|s| s.to_string()),
1455 password_hash: hash_password(password),
1456 scram_verifier: Some(make_scram_verifier(password)),
1457 role,
1458 api_keys: Vec::new(),
1459 created_at: now,
1460 updated_at: now,
1461 enabled: true,
1462 };
1463 users.insert(id, user.clone());
1464 drop(users); self.persist_to_vault();
1466 Ok(user)
1467 }
1468
1469 fn create_user_in_tenant_controlled(
1470 &self,
1471 tenant_id: Option<&str>,
1472 username: &str,
1473 password: &str,
1474 role: Role,
1475 control: &UserLifecycleControl<'_>,
1476 ) -> Result<User, AuthError> {
1477 let id = UserId::from_parts(tenant_id, username);
1478 match self.create_user_in_tenant_unaudited(tenant_id, username, password, role) {
1479 Ok(user) => {
1480 if let Err(err) = self.emit_user_lifecycle_allowed(
1481 control,
1482 EventKind::UserCreate,
1483 "user.create",
1484 &id,
1485 user_control_fields(&id, Some(role), Some(true), true),
1486 ) {
1487 self.rollback_create_user(&id);
1488 return Err(err);
1489 }
1490 Ok(user)
1491 }
1492 Err(err) => {
1493 self.emit_user_lifecycle_outcome(
1494 control,
1495 if user_error_is_denied(&err) {
1496 Outcome::Denied
1497 } else {
1498 Outcome::Error
1499 },
1500 EventKind::UserCreate,
1501 "user.create",
1502 &id,
1503 Some(err.to_string()),
1504 user_control_fields(&id, Some(role), Some(true), true),
1505 );
1506 Err(err)
1507 }
1508 }
1509 }
1510
1511 pub fn lookup_scram_verifier(&self, id: &UserId) -> Option<crate::auth::scram::ScramVerifier> {
1517 if crate::auth::self_lock_guard::is_synthetic_principal(&id.username) {
1519 return None;
1520 }
1521 let users = self.users.read().ok()?;
1522 users
1526 .get(id)
1527 .filter(|u| u.enabled)
1528 .and_then(|u| u.scram_verifier.clone())
1529 }
1530
1531 pub fn lookup_scram_verifier_global(
1536 &self,
1537 username: &str,
1538 ) -> Option<crate::auth::scram::ScramVerifier> {
1539 self.lookup_scram_verifier(&UserId::platform(username))
1540 }
1541
1542 pub fn list_users(&self) -> Vec<User> {
1548 let users = match self.users.read() {
1549 Ok(g) => g,
1550 Err(_) => return Vec::new(),
1551 };
1552 users
1553 .values()
1554 .filter(|u| !crate::auth::self_lock_guard::is_synthetic_principal(&u.username))
1555 .map(|u| User {
1556 password_hash: String::new(), ..u.clone()
1558 })
1559 .collect()
1560 }
1561
1562 pub fn list_users_scoped(&self, tenant_filter: Option<Option<&str>>) -> Vec<User> {
1569 let users = match self.users.read() {
1570 Ok(g) => g,
1571 Err(_) => return Vec::new(),
1572 };
1573 users
1574 .values()
1575 .filter(|u| match tenant_filter {
1576 None => true,
1577 Some(t) => u.tenant_id.as_deref() == t,
1578 })
1579 .filter(|u| !crate::auth::self_lock_guard::is_synthetic_principal(&u.username))
1580 .map(|u| User {
1581 password_hash: String::new(), ..u.clone()
1583 })
1584 .collect()
1585 }
1586
1587 pub fn user_lifecycle_resource(uid: &UserId) -> ResourceRef {
1592 let resource = ResourceRef::new("user", uid.username.clone());
1593 match &uid.tenant {
1594 Some(tenant) => resource.with_tenant(tenant.clone()),
1595 None => resource,
1596 }
1597 }
1598
1599 pub fn eval_context_for_principal(
1600 &self,
1601 principal: &UserId,
1602 role: Role,
1603 current_tenant: Option<String>,
1604 ) -> EvalContext {
1605 EvalContext {
1606 principal_tenant: principal.tenant.clone(),
1607 current_tenant,
1608 peer_ip: None,
1609 mfa_present: false,
1610 now_ms: now_ms(),
1611 principal_is_admin_role: role == Role::Admin,
1612 principal_is_platform_scoped: principal.tenant.is_none(),
1613 }
1614 }
1615
1616 pub fn check_user_lifecycle_authz(
1623 &self,
1624 actor: &UserId,
1625 actor_role: Role,
1626 action: &str,
1627 target: &UserId,
1628 ) -> bool {
1629 let resource = Self::user_lifecycle_resource(target);
1630 let ctx = self.eval_context_for_principal(actor, actor_role, target.tenant.clone());
1631 if actor.tenant.is_some() && actor.tenant != target.tenant {
1632 return false;
1633 }
1634 if self.has_explicit_user_lifecycle_deny(actor, actor_role, action, target) {
1635 return false;
1636 }
1637 actor_role.can_admin()
1638 || self.check_policy_authz_with_role(actor, action, &resource, &ctx, actor_role)
1639 }
1640
1641 pub fn has_explicit_user_lifecycle_deny(
1647 &self,
1648 actor: &UserId,
1649 actor_role: Role,
1650 action: &str,
1651 target: &UserId,
1652 ) -> bool {
1653 let resource = Self::user_lifecycle_resource(target);
1654 let ctx = self.eval_context_for_principal(actor, actor_role, target.tenant.clone());
1655 let pols = self.effective_policies(actor);
1656 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
1657 matches!(
1658 iam_policies::evaluate(&refs, action, &resource, &ctx),
1659 iam_policies::Decision::Deny { .. }
1660 )
1661 }
1662
1663 pub fn get_user(&self, tenant_id: Option<&str>, username: &str) -> Option<User> {
1664 let id = UserId::from_parts(tenant_id, username);
1665 self.get_user_cloned(&id).map(|u| User {
1666 password_hash: String::new(),
1667 ..u
1668 })
1669 }
1670
1671 pub fn delete_user(&self, username: &str) -> Result<(), AuthError> {
1676 self.delete_user_in_tenant(None, username)
1677 }
1678
1679 pub fn delete_user_with_control_events(
1680 &self,
1681 username: &str,
1682 ctx: &ControlEventCtx<'_>,
1683 ledger: &dyn ControlEventLedger,
1684 config: ControlEventConfig,
1685 ) -> Result<(), AuthError> {
1686 self.delete_user_in_tenant_with_control_events(None, username, ctx, ledger, config)
1687 }
1688
1689 pub fn delete_user_in_tenant(
1692 &self,
1693 tenant_id: Option<&str>,
1694 username: &str,
1695 ) -> Result<(), AuthError> {
1696 if let Some(configured) = self.configured_control_events() {
1697 let ctx = default_user_lifecycle_ctx();
1698 let control = UserLifecycleControl {
1699 ctx: &ctx,
1700 ledger: configured.ledger.as_ref(),
1701 config: configured.config,
1702 };
1703 self.delete_user_in_tenant_controlled(tenant_id, username, &control)
1704 } else {
1705 self.delete_user_in_tenant_unaudited(tenant_id, username)
1706 }
1707 }
1708
1709 pub fn delete_user_in_tenant_with_control_events(
1710 &self,
1711 tenant_id: Option<&str>,
1712 username: &str,
1713 ctx: &ControlEventCtx<'_>,
1714 ledger: &dyn ControlEventLedger,
1715 config: ControlEventConfig,
1716 ) -> Result<(), AuthError> {
1717 let control = UserLifecycleControl {
1718 ctx,
1719 ledger,
1720 config,
1721 };
1722 self.delete_user_in_tenant_controlled(tenant_id, username, &control)
1723 }
1724
1725 fn delete_user_in_tenant_unaudited(
1726 &self,
1727 tenant_id: Option<&str>,
1728 username: &str,
1729 ) -> Result<(), AuthError> {
1730 let id = UserId::from_parts(tenant_id, username);
1731 let mut users = self.users.write().map_err(lock_err)?;
1732 let user = users
1733 .remove(&id)
1734 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1735
1736 if let Ok(mut idx) = self.api_key_index.write() {
1738 for api_key in &user.api_keys {
1739 idx.remove(&api_key.key);
1740 }
1741 }
1742
1743 self.purge_sessions_for(&id);
1744
1745 self.persist_to_vault();
1746 Ok(())
1747 }
1748
1749 fn delete_user_in_tenant_controlled(
1750 &self,
1751 tenant_id: Option<&str>,
1752 username: &str,
1753 control: &UserLifecycleControl<'_>,
1754 ) -> Result<(), AuthError> {
1755 let id = UserId::from_parts(tenant_id, username);
1756 let rollback = self.user_delete_rollback_snapshot(&id, tenant_id, username);
1757 match self.delete_user_in_tenant_unaudited(tenant_id, username) {
1758 Ok(()) => {
1759 if let Err(err) = self.emit_user_lifecycle_allowed(
1760 control,
1761 EventKind::UserDelete,
1762 "user.delete",
1763 &id,
1764 user_control_fields(&id, rollback.as_ref().map(|r| r.0.role), None, false),
1765 ) {
1766 if let Some((user, sessions)) = rollback {
1767 self.restore_deleted_user(&id, user, sessions);
1768 }
1769 return Err(err);
1770 }
1771 Ok(())
1772 }
1773 Err(err) => {
1774 self.emit_user_lifecycle_outcome(
1775 control,
1776 if user_error_is_denied(&err) {
1777 Outcome::Denied
1778 } else {
1779 Outcome::Error
1780 },
1781 EventKind::UserDelete,
1782 "user.delete",
1783 &id,
1784 Some(err.to_string()),
1785 user_control_fields(&id, rollback.as_ref().map(|r| r.0.role), None, false),
1786 );
1787 Err(err)
1788 }
1789 }
1790 }
1791
1792 pub fn change_password(
1796 &self,
1797 username: &str,
1798 old_password: &str,
1799 new_password: &str,
1800 ) -> Result<(), AuthError> {
1801 self.change_password_in_tenant(None, username, old_password, new_password)
1802 }
1803
1804 pub fn change_password_with_control_events(
1805 &self,
1806 username: &str,
1807 old_password: &str,
1808 new_password: &str,
1809 ctx: &ControlEventCtx<'_>,
1810 ledger: &dyn ControlEventLedger,
1811 config: ControlEventConfig,
1812 ) -> Result<(), AuthError> {
1813 self.change_password_in_tenant_with_control_events(
1814 None,
1815 username,
1816 old_password,
1817 new_password,
1818 ctx,
1819 ledger,
1820 config,
1821 )
1822 }
1823
1824 #[allow(clippy::too_many_arguments)]
1825 pub fn change_password_in_tenant(
1826 &self,
1827 tenant_id: Option<&str>,
1828 username: &str,
1829 old_password: &str,
1830 new_password: &str,
1831 ) -> Result<(), AuthError> {
1832 if let Some(configured) = self.configured_control_events() {
1833 let ctx = default_user_lifecycle_ctx();
1834 let control = UserLifecycleControl {
1835 ctx: &ctx,
1836 ledger: configured.ledger.as_ref(),
1837 config: configured.config,
1838 };
1839 self.change_password_in_tenant_controlled(
1840 tenant_id,
1841 username,
1842 old_password,
1843 new_password,
1844 &control,
1845 )
1846 } else {
1847 self.change_password_in_tenant_unaudited(
1848 tenant_id,
1849 username,
1850 old_password,
1851 new_password,
1852 )
1853 }
1854 }
1855
1856 #[allow(clippy::too_many_arguments)]
1857 pub fn change_password_in_tenant_with_control_events(
1858 &self,
1859 tenant_id: Option<&str>,
1860 username: &str,
1861 old_password: &str,
1862 new_password: &str,
1863 ctx: &ControlEventCtx<'_>,
1864 ledger: &dyn ControlEventLedger,
1865 config: ControlEventConfig,
1866 ) -> Result<(), AuthError> {
1867 let control = UserLifecycleControl {
1868 ctx,
1869 ledger,
1870 config,
1871 };
1872 self.change_password_in_tenant_controlled(
1873 tenant_id,
1874 username,
1875 old_password,
1876 new_password,
1877 &control,
1878 )
1879 }
1880
1881 fn change_password_in_tenant_unaudited(
1882 &self,
1883 tenant_id: Option<&str>,
1884 username: &str,
1885 old_password: &str,
1886 new_password: &str,
1887 ) -> Result<(), AuthError> {
1888 let id = UserId::from_parts(tenant_id, username);
1889 let mut users = self.users.write().map_err(lock_err)?;
1890 let user = users
1891 .get_mut(&id)
1892 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1893
1894 if !verify_password(old_password, &user.password_hash) {
1895 return Err(AuthError::InvalidCredentials);
1896 }
1897
1898 user.password_hash = hash_password(new_password);
1899 user.scram_verifier = Some(make_scram_verifier(new_password));
1900 user.updated_at = now_ms();
1901 drop(users); self.persist_to_vault();
1903 Ok(())
1904 }
1905
1906 pub fn set_password_by_admin(&self, id: &UserId, new_password: &str) -> Result<(), AuthError> {
1911 if new_password.is_empty() {
1912 return Err(AuthError::Forbidden("password cannot be empty".to_string()));
1913 }
1914 {
1915 let mut users = self.users.write().map_err(lock_err)?;
1916 let user = users
1917 .get_mut(id)
1918 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
1919 user.password_hash = hash_password(new_password);
1920 user.scram_verifier = Some(make_scram_verifier(new_password));
1921 user.updated_at = now_ms();
1922 }
1923 self.purge_sessions_for(id);
1924 self.persist_to_vault();
1925 Ok(())
1926 }
1927
1928 fn change_password_in_tenant_controlled(
1929 &self,
1930 tenant_id: Option<&str>,
1931 username: &str,
1932 old_password: &str,
1933 new_password: &str,
1934 control: &UserLifecycleControl<'_>,
1935 ) -> Result<(), AuthError> {
1936 let id = UserId::from_parts(tenant_id, username);
1937 let previous = self.get_user_cloned(&id);
1938 match self.change_password_in_tenant_unaudited(
1939 tenant_id,
1940 username,
1941 old_password,
1942 new_password,
1943 ) {
1944 Ok(()) => {
1945 if let Err(err) = self.emit_user_lifecycle_allowed(
1946 control,
1947 EventKind::UserUpdate,
1948 "user.update",
1949 &id,
1950 user_control_fields(
1951 &id,
1952 previous.as_ref().map(|u| u.role),
1953 previous.as_ref().map(|u| u.enabled),
1954 true,
1955 ),
1956 ) {
1957 self.restore_user_snapshot(&id, previous);
1958 return Err(err);
1959 }
1960 Ok(())
1961 }
1962 Err(err) => {
1963 self.emit_user_lifecycle_outcome(
1964 control,
1965 if user_error_is_denied(&err) {
1966 Outcome::Denied
1967 } else {
1968 Outcome::Error
1969 },
1970 EventKind::UserUpdate,
1971 "user.update",
1972 &id,
1973 Some(err.to_string()),
1974 user_control_fields(
1975 &id,
1976 previous.as_ref().map(|u| u.role),
1977 previous.as_ref().map(|u| u.enabled),
1978 true,
1979 ),
1980 );
1981 Err(err)
1982 }
1983 }
1984 }
1985
1986 pub fn change_role(&self, username: &str, new_role: Role) -> Result<(), AuthError> {
1989 self.change_role_in_tenant(None, username, new_role)
1990 }
1991
1992 pub fn change_role_in_tenant(
1993 &self,
1994 tenant_id: Option<&str>,
1995 username: &str,
1996 new_role: Role,
1997 ) -> Result<(), AuthError> {
1998 if let Some(configured) = self.configured_control_events() {
1999 let ctx = default_user_lifecycle_ctx();
2000 let control = UserLifecycleControl {
2001 ctx: &ctx,
2002 ledger: configured.ledger.as_ref(),
2003 config: configured.config,
2004 };
2005 self.change_role_in_tenant_controlled(tenant_id, username, new_role, &control)
2006 } else {
2007 self.change_role_in_tenant_unaudited(tenant_id, username, new_role)
2008 }
2009 }
2010
2011 pub fn change_role_in_tenant_with_control_events(
2012 &self,
2013 tenant_id: Option<&str>,
2014 username: &str,
2015 new_role: Role,
2016 ctx: &ControlEventCtx<'_>,
2017 ledger: &dyn ControlEventLedger,
2018 config: ControlEventConfig,
2019 ) -> Result<(), AuthError> {
2020 let control = UserLifecycleControl {
2021 ctx,
2022 ledger,
2023 config,
2024 };
2025 self.change_role_in_tenant_controlled(tenant_id, username, new_role, &control)
2026 }
2027
2028 fn change_role_in_tenant_unaudited(
2029 &self,
2030 tenant_id: Option<&str>,
2031 username: &str,
2032 new_role: Role,
2033 ) -> Result<(), AuthError> {
2034 let id = UserId::from_parts(tenant_id, username);
2035 let mut users = self.users.write().map_err(lock_err)?;
2036 let user = users
2037 .get_mut(&id)
2038 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
2039
2040 let prior_role = user.role;
2041 user.role = new_role;
2042 user.updated_at = now_ms();
2043
2044 if new_role == Role::Admin && prior_role != Role::Admin {
2049 crate::telemetry::operator_event::OperatorEvent::AdminCapabilityGranted {
2050 granted_to: id.to_string(),
2051 capability: "Role::Admin".to_string(),
2052 granted_by: "auth_store::change_role".to_string(),
2053 }
2054 .emit_global();
2055 }
2056
2057 for key in &mut user.api_keys {
2059 if key.role > new_role {
2060 key.role = new_role;
2061 }
2062 }
2063
2064 if let Ok(mut idx) = self.api_key_index.write() {
2066 for key in &user.api_keys {
2067 if let Some(entry) = idx.get_mut(&key.key) {
2068 entry.1 = key.role;
2069 }
2070 }
2071 }
2072
2073 if prior_role != new_role {
2077 self.purge_sessions_for(&id);
2078 }
2079
2080 self.persist_to_vault();
2081 Ok(())
2082 }
2083
2084 fn change_role_in_tenant_controlled(
2085 &self,
2086 tenant_id: Option<&str>,
2087 username: &str,
2088 new_role: Role,
2089 control: &UserLifecycleControl<'_>,
2090 ) -> Result<(), AuthError> {
2091 let id = UserId::from_parts(tenant_id, username);
2092 let previous = self.get_user_cloned(&id);
2093 match self.change_role_in_tenant_unaudited(tenant_id, username, new_role) {
2094 Ok(()) => {
2095 if let Err(err) = self.emit_user_lifecycle_allowed(
2096 control,
2097 EventKind::UserUpdate,
2098 "user.update",
2099 &id,
2100 user_control_fields(
2101 &id,
2102 Some(new_role),
2103 previous.as_ref().map(|u| u.enabled),
2104 false,
2105 ),
2106 ) {
2107 self.restore_user_snapshot(&id, previous);
2108 return Err(err);
2109 }
2110 Ok(())
2111 }
2112 Err(err) => {
2113 self.emit_user_lifecycle_outcome(
2114 control,
2115 if user_error_is_denied(&err) {
2116 Outcome::Denied
2117 } else {
2118 Outcome::Error
2119 },
2120 EventKind::UserUpdate,
2121 "user.update",
2122 &id,
2123 Some(err.to_string()),
2124 user_control_fields(
2125 &id,
2126 Some(new_role),
2127 previous.as_ref().map(|u| u.enabled),
2128 false,
2129 ),
2130 );
2131 Err(err)
2132 }
2133 }
2134 }
2135
2136 pub fn authenticate(&self, username: &str, password: &str) -> Result<Session, AuthError> {
2148 self.authenticate_in_tenant(None, username, password)
2149 }
2150
2151 pub fn authenticate_in_tenant(
2161 &self,
2162 tenant_id: Option<&str>,
2163 username: &str,
2164 password: &str,
2165 ) -> Result<Session, AuthError> {
2166 let id = UserId::from_parts(tenant_id, username);
2167 match self.authenticate_in_tenant_unaudited(tenant_id, username, password) {
2168 Ok(session) => {
2169 if let Err(err) = self.emit_auth_event(
2170 EventKind::AuthLogin,
2171 Outcome::Allowed,
2172 &id,
2173 Some(session.role),
2174 None,
2175 ) {
2176 if let Ok(mut sessions) = self.sessions.write() {
2177 sessions.remove(&session.token);
2178 }
2179 return Err(err);
2180 }
2181 Ok(session)
2182 }
2183 Err(err) => {
2184 let outcome = if matches!(err, AuthError::InvalidCredentials) {
2185 Outcome::Denied
2186 } else {
2187 Outcome::Error
2188 };
2189 let _ = self.emit_auth_event(
2191 EventKind::AuthLoginFailed,
2192 outcome,
2193 &id,
2194 None,
2195 Some(&err.to_string()),
2196 );
2197 Err(err)
2198 }
2199 }
2200 }
2201
2202 fn emit_auth_event(
2205 &self,
2206 kind: EventKind,
2207 outcome: Outcome,
2208 id: &UserId,
2209 role: Option<Role>,
2210 reason: Option<&str>,
2211 ) -> Result<(), AuthError> {
2212 let Some(configured) = self.configured_control_events() else {
2213 return Ok(());
2214 };
2215 let ctx = ControlEventCtx {
2216 actor: crate::runtime::control_events::ActorRef::User(id),
2217 scope: id.tenant.as_deref().map(Cow::Borrowed),
2218 request_id: None,
2219 trace_id: None,
2220 };
2221 let mut fields: HashMap<String, Sensitivity> = HashMap::new();
2222 fields.insert("principal".to_string(), Sensitivity::raw(id.to_string()));
2223 if let Some(role) = role {
2224 fields.insert("role".to_string(), Sensitivity::raw(role.as_str()));
2225 }
2226 let event = ControlEvent {
2227 kind,
2228 outcome,
2229 action: Cow::Borrowed(kind.as_str()),
2230 resource: Some(format!("principal:{id}")),
2231 reason: reason.map(str::to_string),
2232 matched_policy_id: None,
2233 fields,
2234 };
2235 match configured.ledger.emit(&ctx, event) {
2236 Ok(_) => Ok(()),
2237 Err(err) if configured.config.require_persistence() => Err(AuthError::Internal(
2238 format!("control-event ledger refused {}: {err}", kind.as_str()),
2239 )),
2240 Err(err) => {
2241 tracing::warn!(
2242 target: "reddb::security",
2243 kind = kind.as_str(),
2244 error = %err,
2245 "control-event ledger refused an auth event"
2246 );
2247 Ok(())
2248 }
2249 }
2250 }
2251
2252 fn authenticate_in_tenant_unaudited(
2253 &self,
2254 tenant_id: Option<&str>,
2255 username: &str,
2256 password: &str,
2257 ) -> Result<Session, AuthError> {
2258 if crate::auth::self_lock_guard::is_synthetic_principal(username) {
2262 return Err(AuthError::InvalidCredentials);
2263 }
2264 let id = UserId::from_parts(tenant_id, username);
2265 let users = self.users.read().map_err(lock_err)?;
2266 let user = users.get(&id).ok_or(AuthError::InvalidCredentials)?;
2267
2268 if !user.enabled {
2269 return Err(AuthError::InvalidCredentials);
2270 }
2271
2272 let stored_hash = user.password_hash.clone();
2273 if !verify_password(password, &stored_hash) {
2274 return Err(AuthError::InvalidCredentials);
2275 }
2276
2277 let token = match self.keypair.read().ok().and_then(|g| {
2279 g.as_ref().map(|kp| {
2280 let token_id = random_hex(16);
2281 let sig = kp.sign(format!("session:{}", token_id).as_bytes());
2282 format!("rs_{}{}", token_id, hex::encode(&sig[..16]))
2284 })
2285 }) {
2286 Some(signed_token) => signed_token,
2287 None => generate_session_token(),
2288 };
2289
2290 let now = now_ms();
2291 let session = Session {
2292 token,
2293 username: username.to_string(),
2294 tenant_id: user.tenant_id.clone(),
2295 role: user.role,
2296 created_at: now,
2297 expires_at: now + (self.config.session_ttl_secs as u128 * 1000),
2298 };
2299
2300 drop(users); let mut sessions = self.sessions.write().map_err(lock_err)?;
2303 sessions.insert(session.token.clone(), session.clone());
2304 drop(sessions);
2305
2306 if password_hash_needs_upgrade(&stored_hash) {
2309 let upgraded = hash_password(password);
2310 let mut rehashed = false;
2311 if let Ok(mut users) = self.users.write() {
2312 if let Some(user) = users.get_mut(&id) {
2313 if user.password_hash == stored_hash {
2315 user.password_hash = upgraded;
2316 rehashed = true;
2317 }
2318 }
2319 }
2320 if rehashed {
2321 self.persist_to_vault();
2322 }
2323 }
2324
2325 Ok(session)
2326 }
2327
2328 pub fn validate_token(&self, token: &str) -> Option<(String, Role)> {
2340 self.validate_token_full(token)
2341 .map(|(id, role)| (id.username, role))
2342 }
2343
2344 pub fn validate_token_full(&self, token: &str) -> Option<(UserId, Role)> {
2347 let resolved = self.resolve_token_unchecked(token)?;
2348 if !self.principal_is_active(&resolved.0) {
2352 return None;
2353 }
2354 Some(resolved)
2355 }
2356
2357 fn resolve_token_unchecked(&self, token: &str) -> Option<(UserId, Role)> {
2359 if token.starts_with("rs_") {
2361 if let Ok(sessions) = self.sessions.read() {
2362 if let Some(session) = sessions.get(token) {
2363 let now = now_ms();
2364 if now < session.expires_at {
2365 return Some((
2366 UserId::from_parts(session.tenant_id.as_deref(), &session.username),
2367 session.role,
2368 ));
2369 }
2370 }
2371 }
2372 return None;
2373 }
2374
2375 if token.starts_with("rk_") {
2378 let id = api_key_credential_id(token);
2379 let (owner, role) = self
2380 .api_key_index
2381 .read()
2382 .ok()
2383 .and_then(|idx| idx.get(&id).cloned())?;
2384 if self.api_key_expired(&owner, &id) {
2385 return None;
2386 }
2387 return Some((owner, role));
2388 }
2389
2390 None
2391 }
2392
2393 fn api_key_expired(&self, owner: &UserId, id: &str) -> bool {
2396 let Ok(users) = self.users.read() else {
2397 return true;
2398 };
2399 users
2400 .get(owner)
2401 .and_then(|user| user.api_keys.iter().find(|key| key.key == id))
2402 .map(|key| key.expires_at.is_some_and(|deadline| now_ms() >= deadline))
2403 .unwrap_or(true)
2404 }
2405
2406 pub fn active_user_role(&self, id: &UserId) -> Option<Role> {
2412 if !self.principal_is_active(id) {
2413 return None;
2414 }
2415 self.users.read().ok()?.get(id).map(|user| user.role)
2416 }
2417
2418 fn principal_is_active(&self, id: &UserId) -> bool {
2421 let enabled = match self.users.read() {
2422 Ok(users) => users.get(id).map(|u| u.enabled).unwrap_or(false),
2423 Err(_) => false,
2424 };
2425 if !enabled {
2426 return false;
2427 }
2428 match self.user_attributes(id).valid_until {
2429 Some(deadline) => now_ms() < deadline,
2430 None => true,
2431 }
2432 }
2433
2434 pub fn create_api_key(
2442 &self,
2443 username: &str,
2444 name: &str,
2445 role: Role,
2446 ) -> Result<ApiKey, AuthError> {
2447 self.create_api_key_in_tenant(None, username, name, role)
2448 }
2449
2450 pub fn create_api_key_with_control_events(
2451 &self,
2452 username: &str,
2453 name: &str,
2454 role: Role,
2455 ctx: &ControlEventCtx<'_>,
2456 ledger: &dyn ControlEventLedger,
2457 config: ControlEventConfig,
2458 ) -> Result<ApiKey, AuthError> {
2459 self.create_api_key_in_tenant_with_control_events(
2460 None, username, name, role, ctx, ledger, config,
2461 )
2462 }
2463
2464 pub fn create_api_key_in_tenant(
2465 &self,
2466 tenant_id: Option<&str>,
2467 username: &str,
2468 name: &str,
2469 role: Role,
2470 ) -> Result<ApiKey, AuthError> {
2471 self.create_api_key_in_tenant_expiring(tenant_id, username, name, role, None)
2472 }
2473
2474 pub fn create_api_key_in_tenant_expiring(
2477 &self,
2478 tenant_id: Option<&str>,
2479 username: &str,
2480 name: &str,
2481 role: Role,
2482 expires_at: Option<u128>,
2483 ) -> Result<ApiKey, AuthError> {
2484 if let Some(configured) = self.configured_control_events() {
2485 let ctx = default_user_lifecycle_ctx();
2486 let control = UserLifecycleControl {
2487 ctx: &ctx,
2488 ledger: configured.ledger.as_ref(),
2489 config: configured.config,
2490 };
2491 self.create_api_key_in_tenant_controlled(
2492 tenant_id, username, name, role, expires_at, &control,
2493 )
2494 } else {
2495 self.create_api_key_in_tenant_unaudited(tenant_id, username, name, role, expires_at)
2496 }
2497 }
2498
2499 #[allow(clippy::too_many_arguments)]
2500 pub fn create_api_key_in_tenant_with_control_events(
2501 &self,
2502 tenant_id: Option<&str>,
2503 username: &str,
2504 name: &str,
2505 role: Role,
2506 ctx: &ControlEventCtx<'_>,
2507 ledger: &dyn ControlEventLedger,
2508 config: ControlEventConfig,
2509 ) -> Result<ApiKey, AuthError> {
2510 let control = UserLifecycleControl {
2511 ctx,
2512 ledger,
2513 config,
2514 };
2515 self.create_api_key_in_tenant_controlled(tenant_id, username, name, role, None, &control)
2516 }
2517
2518 fn create_api_key_in_tenant_unaudited(
2519 &self,
2520 tenant_id: Option<&str>,
2521 username: &str,
2522 name: &str,
2523 role: Role,
2524 expires_at: Option<u128>,
2525 ) -> Result<ApiKey, AuthError> {
2526 let id = UserId::from_parts(tenant_id, username);
2527 let mut users = self.users.write().map_err(lock_err)?;
2528 let user = users
2529 .get_mut(&id)
2530 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
2531
2532 if role > user.role {
2534 return Err(AuthError::RoleExceeded {
2535 requested: role,
2536 ceiling: user.role,
2537 });
2538 }
2539
2540 let secret = generate_api_key();
2541 let stored = ApiKey {
2542 key: api_key_credential_id(&secret),
2543 name: name.to_string(),
2544 role,
2545 created_at: now_ms(),
2546 expires_at,
2547 };
2548
2549 user.api_keys.push(stored.clone());
2550 user.updated_at = now_ms();
2551
2552 if let Ok(mut idx) = self.api_key_index.write() {
2554 idx.insert(stored.key.clone(), (id.clone(), stored.role));
2555 }
2556
2557 drop(users); self.persist_to_vault();
2559 Ok(ApiKey {
2561 key: secret,
2562 ..stored
2563 })
2564 }
2565
2566 fn create_api_key_in_tenant_controlled(
2567 &self,
2568 tenant_id: Option<&str>,
2569 username: &str,
2570 name: &str,
2571 role: Role,
2572 expires_at: Option<u128>,
2573 control: &UserLifecycleControl<'_>,
2574 ) -> Result<ApiKey, AuthError> {
2575 let id = UserId::from_parts(tenant_id, username);
2576 match self.create_api_key_in_tenant_unaudited(tenant_id, username, name, role, expires_at) {
2577 Ok(api_key) => {
2578 let key_id = api_key_id(&api_key.key);
2579 if let Err(err) = self.emit_api_key_allowed(
2580 control,
2581 EventKind::ApiKeyCreate,
2582 "apikey.create",
2583 &key_id,
2584 api_key_control_fields(&id, role, &key_id),
2585 ) {
2586 self.rollback_create_api_key(&id, &api_key.key);
2587 return Err(err);
2588 }
2589 Ok(api_key)
2590 }
2591 Err(err) => {
2592 self.emit_user_lifecycle_outcome(
2593 control,
2594 if user_error_is_denied(&err) {
2595 Outcome::Denied
2596 } else {
2597 Outcome::Error
2598 },
2599 EventKind::ApiKeyCreate,
2600 "apikey.create",
2601 &id,
2602 Some(err.to_string()),
2603 user_control_fields(&id, Some(role), None, false),
2604 );
2605 Err(err)
2606 }
2607 }
2608 }
2609
2610 pub fn api_key_owner(&self, key: &str) -> Option<UserId> {
2612 let key_id = api_key_credential_id(key);
2613 self.api_key_index
2614 .read()
2615 .ok()
2616 .and_then(|index| index.get(&key_id).map(|(id, _)| id.clone()))
2617 }
2618
2619 pub fn revoke_api_key(&self, key: &str) -> Result<(), AuthError> {
2621 if let Some(configured) = self.configured_control_events() {
2622 let ctx = default_user_lifecycle_ctx();
2623 let control = UserLifecycleControl {
2624 ctx: &ctx,
2625 ledger: configured.ledger.as_ref(),
2626 config: configured.config,
2627 };
2628 self.revoke_api_key_controlled(key, &control)
2629 } else {
2630 self.revoke_api_key_unaudited(key)
2631 }
2632 }
2633
2634 pub fn revoke_api_key_with_control_events(
2635 &self,
2636 key: &str,
2637 ctx: &ControlEventCtx<'_>,
2638 ledger: &dyn ControlEventLedger,
2639 config: ControlEventConfig,
2640 ) -> Result<(), AuthError> {
2641 let control = UserLifecycleControl {
2642 ctx,
2643 ledger,
2644 config,
2645 };
2646 self.revoke_api_key_controlled(key, &control)
2647 }
2648
2649 fn revoke_api_key_unaudited(&self, key: &str) -> Result<(), AuthError> {
2650 let key_id = api_key_credential_id(key);
2651 let key = key_id.as_str();
2652 let mut users = self.users.write().map_err(lock_err)?;
2653
2654 let owner_id: UserId = {
2658 if let Ok(idx) = self.api_key_index.read() {
2659 if let Some((id, _)) = idx.get(key) {
2660 id.clone()
2661 } else {
2662 return Err(AuthError::KeyNotFound(key.to_string()));
2663 }
2664 } else {
2665 let owner = users
2666 .iter()
2667 .find(|(_, u)| u.api_keys.iter().any(|k| k.key == key));
2668 match owner {
2669 Some((id, _)) => id.clone(),
2670 None => return Err(AuthError::KeyNotFound(key.to_string())),
2671 }
2672 }
2673 };
2674
2675 let user = users
2676 .get_mut(&owner_id)
2677 .ok_or_else(|| AuthError::KeyNotFound(key.to_string()))?;
2678 user.api_keys.retain(|k| k.key != key);
2679 user.updated_at = now_ms();
2680
2681 if let Ok(mut idx) = self.api_key_index.write() {
2683 idx.remove(key);
2684 }
2685
2686 self.persist_to_vault();
2687 Ok(())
2688 }
2689
2690 fn revoke_api_key_controlled(
2691 &self,
2692 key: &str,
2693 control: &UserLifecycleControl<'_>,
2694 ) -> Result<(), AuthError> {
2695 let key_id = api_key_id(key);
2696 let rollback = self.api_key_rollback_snapshot(key);
2697 let id = rollback
2698 .as_ref()
2699 .map(|r| r.0.clone())
2700 .unwrap_or_else(|| UserId::from_parts(None, ""));
2701 let role = rollback.as_ref().map(|r| r.1.role).unwrap_or(Role::Read);
2702 match self.revoke_api_key_unaudited(key) {
2703 Ok(()) => {
2704 if let Err(err) = self.emit_api_key_allowed(
2705 control,
2706 EventKind::ApiKeyRevoke,
2707 "apikey.revoke",
2708 &key_id,
2709 api_key_control_fields(&id, role, &key_id),
2710 ) {
2711 if let Some((owner, api_key)) = rollback {
2712 self.restore_api_key(&owner, api_key);
2713 }
2714 return Err(err);
2715 }
2716 Ok(())
2717 }
2718 Err(err) => {
2719 self.emit_user_lifecycle_outcome(
2720 control,
2721 if user_error_is_denied(&err) {
2722 Outcome::Denied
2723 } else {
2724 Outcome::Error
2725 },
2726 EventKind::ApiKeyRevoke,
2727 "apikey.revoke",
2728 &id,
2729 Some(err.to_string()),
2730 api_key_control_fields(&id, role, &key_id),
2731 );
2732 Err(err)
2733 }
2734 }
2735 }
2736
2737 pub fn revoke_session(&self, token: &str) {
2743 if let Some(session) = self.remove_session_without_count(token) {
2744 if self.take_counted_session(token) {
2745 let uid = UserId::from_parts(session.tenant_id.as_deref(), &session.username);
2746 self.decrement_session_count(&uid);
2747 }
2748 }
2749 }
2750
2751 fn remove_session_without_count(&self, token: &str) -> Option<Session> {
2752 self.sessions.write().ok()?.remove(token)
2753 }
2754
2755 fn take_counted_session(&self, token: &str) -> bool {
2756 self.counted_session_tokens
2757 .write()
2758 .map(|mut tokens| tokens.remove(token))
2759 .unwrap_or(false)
2760 }
2761
2762 pub fn purge_expired_sessions(&self) -> usize {
2764 let now = now_ms();
2765 if let Ok(mut sessions) = self.sessions.write() {
2766 let expired: Vec<Session> = sessions
2767 .values()
2768 .filter(|session| session.expires_at <= now)
2769 .cloned()
2770 .collect();
2771 sessions.retain(|_, session| session.expires_at > now);
2772 drop(sessions);
2773 for session in &expired {
2774 if self.take_counted_session(&session.token) {
2775 let uid = UserId::from_parts(session.tenant_id.as_deref(), &session.username);
2776 self.decrement_session_count(&uid);
2777 }
2778 }
2779 return expired.len();
2780 }
2781 0
2782 }
2783
2784 pub fn grant(
2804 &self,
2805 granter: &UserId,
2806 granter_role: Role,
2807 principal: GrantPrincipal,
2808 resource: Resource,
2809 actions: Vec<Action>,
2810 with_grant_option: bool,
2811 tenant: Option<String>,
2812 ) -> Result<(), AuthError> {
2813 if granter_role != Role::Admin {
2814 return Err(AuthError::Forbidden(format!(
2815 "GRANT requires Admin role; granter `{}` has `{:?}`",
2816 granter, granter_role
2817 )));
2818 }
2819
2820 if granter.tenant.is_some() && granter.tenant != tenant {
2824 return Err(AuthError::Forbidden(format!(
2825 "cross-tenant GRANT denied: granter tenant `{:?}` != grant tenant `{:?}`",
2826 granter.tenant, tenant
2827 )));
2828 }
2829
2830 let mut actions_set = std::collections::BTreeSet::new();
2831 for a in actions {
2832 actions_set.insert(a);
2833 }
2834 let g = Grant {
2835 principal: principal.clone(),
2836 resource,
2837 actions: actions_set,
2838 with_grant_option,
2839 granted_by: granter.to_string(),
2840 granted_at: now_ms(),
2841 tenant,
2842 columns: None,
2843 };
2844
2845 match &principal {
2846 GrantPrincipal::User(uid) => {
2847 self.grants
2848 .write()
2849 .unwrap_or_else(|e| e.into_inner())
2850 .entry(uid.clone())
2851 .or_default()
2852 .push(g.clone());
2853 self.invalidate_permission_cache(Some(uid));
2854 }
2855 GrantPrincipal::Public => {
2856 self.public_grants
2857 .write()
2858 .unwrap_or_else(|e| e.into_inner())
2859 .push(g.clone());
2860 self.invalidate_permission_cache(None);
2861 }
2862 GrantPrincipal::Group(_) => {
2863 return Err(AuthError::Forbidden(
2864 "GROUP principals are not yet supported; use a USER or PUBLIC".to_string(),
2865 ));
2866 }
2867 }
2868
2869 self.invalidate_visible_collections_for_tenant(g.tenant.as_deref());
2874
2875 self.persist_acl_to_kv();
2876 Ok(())
2877 }
2878
2879 pub fn revoke(
2882 &self,
2883 granter: &UserId,
2884 granter_role: Role,
2885 principal: &GrantPrincipal,
2886 resource: &Resource,
2887 actions: &[Action],
2888 ) -> Result<usize, AuthError> {
2889 if granter_role != Role::Admin {
2890 return Err(AuthError::Forbidden(format!(
2891 "REVOKE requires Admin role; granter `{}` has `{:?}`",
2892 granter, granter_role
2893 )));
2894 }
2895
2896 if let GrantPrincipal::User(uid) = principal {
2902 if granter.tenant.is_some() && granter.tenant != uid.tenant {
2903 return Err(AuthError::Forbidden(format!(
2904 "cross-tenant REVOKE denied: granter tenant `{:?}` != principal tenant `{:?}`",
2905 granter.tenant, uid.tenant
2906 )));
2907 }
2908 }
2909
2910 let removed = match principal {
2911 GrantPrincipal::User(uid) => {
2912 let mut g = self.grants.write().unwrap_or_else(|e| e.into_inner());
2913 let before = g.get(uid).map(|v| v.len()).unwrap_or(0);
2914 if let Some(list) = g.get_mut(uid) {
2915 list.retain(|gr| {
2916 !(gr.resource == *resource
2917 && (actions.iter().any(|a| gr.actions.contains(a))
2918 || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2919 });
2920 }
2921 let after = g.get(uid).map(|v| v.len()).unwrap_or(0);
2922 drop(g);
2923 self.invalidate_permission_cache(Some(uid));
2924 before - after
2925 }
2926 GrantPrincipal::Public => {
2927 let mut p = self
2928 .public_grants
2929 .write()
2930 .unwrap_or_else(|e| e.into_inner());
2931 let before = p.len();
2932 p.retain(|gr| {
2933 !(gr.resource == *resource
2934 && (actions.iter().any(|a| gr.actions.contains(a))
2935 || (gr.actions.contains(&Action::All) && !actions.is_empty())))
2936 });
2937 let after = p.len();
2938 drop(p);
2939 self.invalidate_permission_cache(None);
2940 before - after
2941 }
2942 GrantPrincipal::Group(_) => 0,
2943 };
2944
2945 if removed > 0 {
2946 match principal {
2952 GrantPrincipal::User(uid) => {
2953 self.invalidate_visible_collections_for_tenant(uid.tenant.as_deref());
2954 }
2955 GrantPrincipal::Public | GrantPrincipal::Group(_) => {
2956 self.invalidate_visible_collections_cache();
2957 }
2958 }
2959 self.persist_acl_to_kv();
2960 }
2961 Ok(removed)
2962 }
2963
2964 pub fn visible_collections_for_scope(
2978 &self,
2979 tenant: Option<&str>,
2980 role: Role,
2981 principal: &str,
2982 all_collections: &[String],
2983 ) -> std::collections::HashSet<String> {
2984 let key = super::scope_cache::ScopeKey::new(tenant, principal, role);
2985 if let Some(hit) = self.visible_collections_cache.get(&key) {
2986 return hit;
2987 }
2988 let ctx = AuthzContext {
2991 principal,
2992 effective_role: role,
2993 tenant,
2994 };
2995 let mut visible = std::collections::HashSet::new();
2996 for collection in all_collections {
2997 let resource = Resource::table_from_name(collection);
2998 if self.check_grant(&ctx, Action::Select, &resource).is_ok() {
2999 visible.insert(collection.clone());
3000 }
3001 }
3002 self.visible_collections_cache.insert(key, visible.clone());
3003 visible
3004 }
3005
3006 pub fn auth_cache_stats(&self) -> super::scope_cache::AuthCacheStats {
3010 self.visible_collections_cache.stats()
3011 }
3012
3013 pub fn invalidate_visible_collections_cache(&self) {
3017 self.visible_collections_cache.invalidate_all();
3018 }
3019
3020 pub fn invalidate_visible_collections_for_tenant(&self, tenant: Option<&str>) {
3023 self.visible_collections_cache.invalidate_tenant(tenant);
3024 }
3025
3026 pub fn effective_grants(&self, uid: &UserId) -> Vec<Grant> {
3029 let mut out = Vec::new();
3030 if let Ok(g) = self.grants.read() {
3031 if let Some(list) = g.get(uid) {
3032 out.extend(list.iter().cloned());
3033 }
3034 }
3035 if let Ok(p) = self.public_grants.read() {
3036 out.extend(p.iter().cloned());
3037 }
3038 out
3039 }
3040
3041 pub fn check_grant(
3044 &self,
3045 ctx: &AuthzContext<'_>,
3046 action: Action,
3047 resource: &Resource,
3048 ) -> Result<(), AuthzError> {
3049 if ctx.effective_role == Role::Admin {
3050 return Ok(());
3051 }
3052
3053 let uid = UserId::from_parts(ctx.tenant, ctx.principal);
3054
3055 if let Ok(cache) = self.permission_cache.read() {
3057 if let Some(pc) = cache.get(&uid) {
3058 if pc.allows(resource, action) {
3059 return Ok(());
3060 }
3061 }
3062 }
3063
3064 let user_grants = self
3066 .grants
3067 .read()
3068 .ok()
3069 .and_then(|g| g.get(&uid).cloned())
3070 .unwrap_or_default();
3071 let any_user_grants = self
3072 .grants
3073 .read()
3074 .ok()
3075 .map(|g| g.values().any(|list| !list.is_empty()))
3076 .unwrap_or(false);
3077 let public_grants = self
3078 .public_grants
3079 .read()
3080 .ok()
3081 .map(|p| p.clone())
3082 .unwrap_or_default();
3083 if user_grants.is_empty() && public_grants.is_empty() && any_user_grants {
3084 return Err(AuthzError::PermissionDenied {
3085 action,
3086 resource: resource.clone(),
3087 principal: ctx.principal.to_string(),
3088 });
3089 }
3090 let view = GrantsView {
3091 user_grants: &user_grants,
3092 public_grants: &public_grants,
3093 };
3094 let result = check_grant(ctx, action, resource, &view);
3095
3096 if result.is_ok() {
3097 let pc = PermissionCache::build(&user_grants, &public_grants);
3098 if let Ok(mut cache) = self.permission_cache.write() {
3099 cache.insert(uid, pc);
3100 }
3101 }
3102 result
3103 }
3104
3105 pub fn set_user_attributes(
3111 &self,
3112 uid: &UserId,
3113 attrs: UserAttributes,
3114 ) -> Result<(), AuthError> {
3115 let users = self.users.read().map_err(lock_err)?;
3116 if !users.contains_key(uid) {
3117 return Err(AuthError::UserNotFound(uid.to_string()));
3118 }
3119 drop(users);
3120
3121 self.user_attributes
3122 .write()
3123 .unwrap_or_else(|e| e.into_inner())
3124 .insert(uid.clone(), attrs);
3125 self.invalidate_iam_cache(Some(uid));
3126 self.persist_acl_to_kv();
3127 Ok(())
3128 }
3129
3130 pub fn user_attributes(&self, uid: &UserId) -> UserAttributes {
3133 self.user_attributes
3134 .read()
3135 .ok()
3136 .and_then(|m| m.get(uid).cloned())
3137 .unwrap_or_default()
3138 }
3139
3140 pub fn add_user_to_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
3141 if group.trim().is_empty() {
3142 return Err(AuthError::Forbidden("group name cannot be empty".into()));
3143 }
3144 let mut attrs = self.user_attributes(uid);
3145 if !attrs.groups.iter().any(|g| g == group) {
3146 attrs.groups.push(group.to_string());
3147 attrs.groups.sort();
3148 }
3149 self.set_user_attributes(uid, attrs)
3150 }
3151
3152 pub fn remove_user_from_group(&self, uid: &UserId, group: &str) -> Result<(), AuthError> {
3153 let mut attrs = self.user_attributes(uid);
3154 attrs.groups.retain(|g| g != group);
3155 self.set_user_attributes(uid, attrs)
3156 }
3157
3158 pub fn set_user_enabled(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
3160 if let Some(configured) = self.configured_control_events() {
3161 let ctx = default_user_lifecycle_ctx();
3162 let control = UserLifecycleControl {
3163 ctx: &ctx,
3164 ledger: configured.ledger.as_ref(),
3165 config: configured.config,
3166 };
3167 self.set_user_enabled_controlled(uid, enabled, &control)
3168 } else {
3169 self.set_user_enabled_unaudited(uid, enabled)
3170 }
3171 }
3172
3173 pub fn disable_user(
3174 &self,
3175 username: &str,
3176 ctx: &ControlEventCtx<'_>,
3177 ledger: &dyn ControlEventLedger,
3178 config: ControlEventConfig,
3179 ) -> Result<(), AuthError> {
3180 self.disable_user_in_tenant(None, username, ctx, ledger, config)
3181 }
3182
3183 pub fn disable_user_in_tenant(
3184 &self,
3185 tenant_id: Option<&str>,
3186 username: &str,
3187 ctx: &ControlEventCtx<'_>,
3188 ledger: &dyn ControlEventLedger,
3189 config: ControlEventConfig,
3190 ) -> Result<(), AuthError> {
3191 let uid = UserId::from_parts(tenant_id, username);
3192 let control = UserLifecycleControl {
3193 ctx,
3194 ledger,
3195 config,
3196 };
3197 self.set_user_enabled_controlled(&uid, false, &control)
3198 }
3199
3200 fn set_user_enabled_unaudited(&self, uid: &UserId, enabled: bool) -> Result<(), AuthError> {
3201 let mut users = self.users.write().map_err(lock_err)?;
3202 let user = users
3203 .get_mut(uid)
3204 .ok_or_else(|| AuthError::UserNotFound(uid.to_string()))?;
3205 user.enabled = enabled;
3206 user.updated_at = now_ms();
3207 drop(users);
3208 if !enabled {
3209 self.purge_sessions_for(uid);
3210 }
3211 self.persist_to_vault();
3212 Ok(())
3213 }
3214
3215 fn purge_sessions_for(&self, uid: &UserId) {
3218 if let Ok(mut sessions) = self.sessions.write() {
3219 let tokens: Vec<String> = sessions
3220 .iter()
3221 .filter(|(_, session)| {
3222 session.username == uid.username && session.tenant_id == uid.tenant
3223 })
3224 .map(|(token, _)| token.clone())
3225 .collect();
3226 for token in &tokens {
3227 sessions.remove(token);
3228 }
3229 drop(sessions);
3230 for token in tokens {
3231 if self.take_counted_session(&token) {
3232 self.decrement_session_count(uid);
3233 }
3234 }
3235 }
3236 }
3237
3238 fn set_user_enabled_controlled(
3239 &self,
3240 uid: &UserId,
3241 enabled: bool,
3242 control: &UserLifecycleControl<'_>,
3243 ) -> Result<(), AuthError> {
3244 let previous = self.get_user_cloned(uid);
3245 let kind = if enabled {
3246 EventKind::UserUpdate
3247 } else {
3248 EventKind::UserDisable
3249 };
3250 let action = if enabled {
3251 "user.update"
3252 } else {
3253 "user.disable"
3254 };
3255 match self.set_user_enabled_unaudited(uid, enabled) {
3256 Ok(()) => {
3257 if let Err(err) = self.emit_user_lifecycle_allowed(
3258 control,
3259 kind,
3260 action,
3261 uid,
3262 user_control_fields(
3263 uid,
3264 previous.as_ref().map(|u| u.role),
3265 Some(enabled),
3266 false,
3267 ),
3268 ) {
3269 self.restore_user_snapshot(uid, previous);
3270 return Err(err);
3271 }
3272 Ok(())
3273 }
3274 Err(err) => {
3275 self.emit_user_lifecycle_outcome(
3276 control,
3277 if user_error_is_denied(&err) {
3278 Outcome::Denied
3279 } else {
3280 Outcome::Error
3281 },
3282 kind,
3283 action,
3284 uid,
3285 Some(err.to_string()),
3286 user_control_fields(
3287 uid,
3288 previous.as_ref().map(|u| u.role),
3289 Some(enabled),
3290 false,
3291 ),
3292 );
3293 Err(err)
3294 }
3295 }
3296 }
3297
3298 fn emit_user_lifecycle_allowed(
3299 &self,
3300 control: &UserLifecycleControl<'_>,
3301 kind: EventKind,
3302 action: &'static str,
3303 id: &UserId,
3304 fields: HashMap<String, Sensitivity>,
3305 ) -> Result<(), AuthError> {
3306 self.emit_user_lifecycle_event(control, Outcome::Allowed, kind, action, id, None, fields)
3307 }
3308
3309 fn emit_api_key_allowed(
3310 &self,
3311 control: &UserLifecycleControl<'_>,
3312 kind: EventKind,
3313 action: &'static str,
3314 api_key_id: &str,
3315 fields: HashMap<String, Sensitivity>,
3316 ) -> Result<(), AuthError> {
3317 self.emit_control_event(
3318 control,
3319 Outcome::Allowed,
3320 kind,
3321 action,
3322 Some(api_key_resource(api_key_id)),
3323 None,
3324 fields,
3325 )
3326 }
3327
3328 #[allow(clippy::too_many_arguments)]
3329 fn emit_user_lifecycle_outcome(
3330 &self,
3331 control: &UserLifecycleControl<'_>,
3332 outcome: Outcome,
3333 kind: EventKind,
3334 action: &'static str,
3335 id: &UserId,
3336 reason: Option<String>,
3337 fields: HashMap<String, Sensitivity>,
3338 ) {
3339 let _ = self.emit_user_lifecycle_event(control, outcome, kind, action, id, reason, fields);
3340 }
3341
3342 #[allow(clippy::too_many_arguments)]
3343 fn emit_user_lifecycle_event(
3344 &self,
3345 control: &UserLifecycleControl<'_>,
3346 outcome: Outcome,
3347 kind: EventKind,
3348 action: &'static str,
3349 id: &UserId,
3350 reason: Option<String>,
3351 fields: HashMap<String, Sensitivity>,
3352 ) -> Result<(), AuthError> {
3353 self.emit_control_event(
3354 control,
3355 outcome,
3356 kind,
3357 action,
3358 Some(user_resource(id)),
3359 reason,
3360 fields,
3361 )
3362 }
3363
3364 #[allow(clippy::too_many_arguments)]
3365 fn emit_control_event(
3366 &self,
3367 control: &UserLifecycleControl<'_>,
3368 outcome: Outcome,
3369 kind: EventKind,
3370 action: &'static str,
3371 resource: Option<String>,
3372 reason: Option<String>,
3373 fields: HashMap<String, Sensitivity>,
3374 ) -> Result<(), AuthError> {
3375 let event = ControlEvent {
3376 kind,
3377 outcome,
3378 action: Cow::Borrowed(action),
3379 resource,
3380 reason,
3381 matched_policy_id: None,
3382 fields,
3383 };
3384 match control.ledger.emit(control.ctx, event) {
3385 Ok(_) => Ok(()),
3386 Err(err) if control.config.require_persistence() => {
3387 Err(AuthError::Internal(err.to_string()))
3388 }
3389 Err(_) => Ok(()),
3390 }
3391 }
3392
3393 fn rollback_create_user(&self, id: &UserId) {
3394 let removed = self
3395 .users
3396 .write()
3397 .unwrap_or_else(|e| e.into_inner())
3398 .remove(id);
3399 if let Some(user) = removed {
3400 self.remove_api_key_index_entries(&user);
3401 }
3402 self.persist_to_vault();
3403 }
3404
3405 fn rollback_bootstrap(&self, id: &UserId) {
3406 self.bootstrapped.store(false, Ordering::Release);
3407 self.rollback_create_user(id);
3408 }
3409
3410 fn restore_user_snapshot(&self, id: &UserId, previous: Option<User>) {
3411 match previous {
3412 Some(user) => {
3413 self.users
3414 .write()
3415 .unwrap_or_else(|e| e.into_inner())
3416 .insert(id.clone(), user.clone());
3417 self.index_user_api_keys(id, &user);
3418 }
3419 None => {
3420 self.rollback_create_user(id);
3421 return;
3422 }
3423 }
3424 self.persist_to_vault();
3425 }
3426
3427 fn user_delete_rollback_snapshot(
3428 &self,
3429 id: &UserId,
3430 tenant_id: Option<&str>,
3431 username: &str,
3432 ) -> Option<(User, Vec<(String, Session)>)> {
3433 let user = self.get_user_cloned(id)?;
3434 let sessions = self
3435 .sessions
3436 .read()
3437 .map(|sessions| {
3438 sessions
3439 .iter()
3440 .filter(|(_, session)| {
3441 session.username == username && session.tenant_id.as_deref() == tenant_id
3442 })
3443 .map(|(token, session)| (token.clone(), session.clone()))
3444 .collect()
3445 })
3446 .unwrap_or_default();
3447 Some((user, sessions))
3448 }
3449
3450 fn restore_deleted_user(&self, id: &UserId, user: User, sessions: Vec<(String, Session)>) {
3451 self.users
3452 .write()
3453 .unwrap_or_else(|e| e.into_inner())
3454 .insert(id.clone(), user.clone());
3455 self.index_user_api_keys(id, &user);
3456 if !sessions.is_empty() {
3457 let mut guard = self.sessions.write().unwrap_or_else(|e| e.into_inner());
3458 for (token, session) in sessions {
3459 guard.insert(token, session);
3460 }
3461 }
3462 self.persist_to_vault();
3463 }
3464
3465 fn rollback_create_api_key(&self, id: &UserId, key: &str) {
3466 let key_id = api_key_credential_id(key);
3467 let key = key_id.as_str();
3468 if let Ok(mut users) = self.users.write() {
3469 if let Some(user) = users.get_mut(id) {
3470 user.api_keys.retain(|api_key| api_key.key != key);
3471 user.updated_at = now_ms();
3472 }
3473 }
3474 if let Ok(mut idx) = self.api_key_index.write() {
3475 idx.remove(key);
3476 }
3477 self.persist_to_vault();
3478 }
3479
3480 fn api_key_rollback_snapshot(&self, key: &str) -> Option<(UserId, ApiKey)> {
3481 let key = api_key_credential_id(key);
3482 let users = self.users.read().ok()?;
3483 users.iter().find_map(|(id, user)| {
3484 user.api_keys
3485 .iter()
3486 .find(|api_key| api_key.key == key)
3487 .cloned()
3488 .map(|api_key| (id.clone(), api_key))
3489 })
3490 }
3491
3492 fn restore_api_key(&self, id: &UserId, api_key: ApiKey) {
3493 if let Ok(mut users) = self.users.write() {
3494 if let Some(user) = users.get_mut(id) {
3495 if !user
3496 .api_keys
3497 .iter()
3498 .any(|candidate| candidate.key == api_key.key)
3499 {
3500 user.api_keys.push(api_key.clone());
3501 user.updated_at = now_ms();
3502 }
3503 }
3504 }
3505 if let Ok(mut idx) = self.api_key_index.write() {
3506 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
3507 }
3508 self.persist_to_vault();
3509 }
3510
3511 fn remove_api_key_index_entries(&self, user: &User) {
3512 if let Ok(mut idx) = self.api_key_index.write() {
3513 for api_key in &user.api_keys {
3514 idx.remove(&api_key.key);
3515 }
3516 }
3517 }
3518
3519 fn index_user_api_keys(&self, id: &UserId, user: &User) {
3520 if let Ok(mut idx) = self.api_key_index.write() {
3521 for api_key in &user.api_keys {
3522 idx.insert(api_key.key.clone(), (id.clone(), api_key.role));
3523 }
3524 }
3525 }
3526
3527 pub fn authenticate_with_attrs(
3537 &self,
3538 tenant_id: Option<&str>,
3539 username: &str,
3540 password: &str,
3541 ) -> Result<Session, AuthError> {
3542 let uid = UserId::from_parts(tenant_id, username);
3543 let session = self.authenticate_active(tenant_id, username, password)?;
3544 if let Err(error) = self.reserve_connection_slot(&uid) {
3545 self.remove_session_without_count(&session.token);
3546 return Err(error);
3547 }
3548 let mut tokens = self.counted_session_tokens.write().map_err(|_| {
3549 self.remove_session_without_count(&session.token);
3550 self.decrement_session_count(&uid);
3551 AuthError::Internal("session admission lock poisoned".to_string())
3552 })?;
3553 tokens.insert(session.token.clone());
3554 Ok(session)
3555 }
3556
3557 pub fn authenticate_active(
3561 &self,
3562 tenant_id: Option<&str>,
3563 username: &str,
3564 password: &str,
3565 ) -> Result<Session, AuthError> {
3566 let uid = UserId::from_parts(tenant_id, username);
3567 if let Some(deadline) = self.user_attributes(&uid).valid_until {
3568 if now_ms() >= deadline {
3569 return Err(AuthError::Forbidden(format!(
3570 "account `{uid}` expired (VALID UNTIL exceeded)"
3571 )));
3572 }
3573 }
3574 self.authenticate_in_tenant(tenant_id, username, password)
3575 }
3576
3577 pub fn authenticate_connection(
3580 self: &Arc<Self>,
3581 tenant_id: Option<&str>,
3582 username: &str,
3583 password: &str,
3584 ) -> Result<(Session, ConnectionLease), AuthError> {
3585 let session = self.authenticate_active(tenant_id, username, password)?;
3586 let principal = UserId::from_parts(tenant_id, username);
3587 if let Err(error) = self.reserve_connection_slot(&principal) {
3588 self.remove_session_without_count(&session.token);
3589 return Err(error);
3590 }
3591 self.remove_session_without_count(&session.token);
3595 let lease = ConnectionLease {
3596 store: Arc::clone(self),
3597 principal,
3598 };
3599 Ok((session, lease))
3600 }
3601
3602 pub fn acquire_connection_lease(
3605 self: &Arc<Self>,
3606 principal: &UserId,
3607 ) -> Result<ConnectionLease, AuthError> {
3608 if !self.principal_is_active(principal) {
3609 return Err(AuthError::InvalidCredentials);
3610 }
3611 self.reserve_connection_slot(principal)?;
3612 Ok(ConnectionLease {
3613 store: Arc::clone(self),
3614 principal: principal.clone(),
3615 })
3616 }
3617
3618 fn reserve_connection_slot(&self, uid: &UserId) -> Result<(), AuthError> {
3619 let attrs = self.user_attributes(uid);
3620 if let Some(deadline) = attrs.valid_until {
3621 if now_ms() >= deadline {
3622 return Err(AuthError::Forbidden(format!(
3623 "account `{uid}` expired (VALID UNTIL exceeded)"
3624 )));
3625 }
3626 }
3627 let mut counts = self
3628 .session_count_by_user
3629 .write()
3630 .map_err(|_| AuthError::Internal("connection admission lock poisoned".to_string()))?;
3631 let current = counts.get(uid).copied().unwrap_or(0);
3632 if let Some(limit) = attrs.connection_limit {
3633 if current >= limit {
3634 return Err(AuthError::Forbidden(format!(
3635 "account `{uid}` exceeded CONNECTION LIMIT ({limit})"
3636 )));
3637 }
3638 }
3639 counts.insert(uid.clone(), current.saturating_add(1));
3640 Ok(())
3641 }
3642
3643 pub fn decrement_session_count(&self, uid: &UserId) {
3646 if let Ok(mut counts) = self.session_count_by_user.write() {
3647 if let Some(c) = counts.get_mut(uid) {
3648 *c = c.saturating_sub(1);
3649 }
3650 }
3651 }
3652
3653 pub fn rehydrate_acl(&self) {
3660 let kv_snapshot: Vec<(String, String)> = self
3661 .vault_kv
3662 .read()
3663 .map(|kv| {
3664 kv.iter()
3665 .filter(|(k, _)| {
3666 k.starts_with("red.acl.grants.")
3667 || k.starts_with("red.acl.attrs.")
3668 || k == &"red.acl.public_grants"
3669 })
3670 .map(|(k, v)| (k.clone(), v.clone()))
3671 .collect()
3672 })
3673 .unwrap_or_default();
3674
3675 for (k, v) in kv_snapshot {
3676 if k == "red.acl.public_grants" {
3677 if let Some(parsed) = decode_grants_blob(&v) {
3678 *self
3679 .public_grants
3680 .write()
3681 .unwrap_or_else(|e| e.into_inner()) = parsed;
3682 }
3683 } else if let Some(suffix) = k.strip_prefix("red.acl.grants.") {
3684 if let Some(uid) = decode_uid(suffix) {
3685 if let Some(mut parsed) = decode_grants_blob(&v) {
3686 for g in parsed.iter_mut() {
3689 g.principal = GrantPrincipal::User(uid.clone());
3690 }
3691 self.grants
3692 .write()
3693 .unwrap_or_else(|e| e.into_inner())
3694 .insert(uid, parsed);
3695 }
3696 }
3697 } else if let Some(suffix) = k.strip_prefix("red.acl.attrs.") {
3698 if let Some(uid) = decode_uid(suffix) {
3699 if let Some(parsed) = decode_attrs_blob(&v) {
3700 self.user_attributes
3701 .write()
3702 .unwrap_or_else(|e| e.into_inner())
3703 .insert(uid, parsed);
3704 }
3705 }
3706 }
3707 }
3708
3709 self.permission_cache
3710 .write()
3711 .unwrap_or_else(|e| e.into_inner())
3712 .clear();
3713 }
3714
3715 fn persist_acl_to_kv(&self) {
3717 let public = self
3718 .public_grants
3719 .read()
3720 .ok()
3721 .map(|p| encode_grants_blob(&p))
3722 .unwrap_or_default();
3723 self.vault_kv_set("red.acl.public_grants".to_string(), public);
3724
3725 let snapshot: Vec<(UserId, Vec<Grant>)> = self
3726 .grants
3727 .read()
3728 .ok()
3729 .map(|g| g.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3730 .unwrap_or_default();
3731 for (uid, list) in snapshot {
3732 let key = format!("red.acl.grants.{}", encode_uid(&uid));
3733 let val = encode_grants_blob(&list);
3734 self.vault_kv_set(key, val);
3735 }
3736
3737 let attrs_snapshot: Vec<(UserId, UserAttributes)> = self
3738 .user_attributes
3739 .read()
3740 .ok()
3741 .map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
3742 .unwrap_or_default();
3743 for (uid, attrs) in attrs_snapshot {
3744 let key = format!("red.acl.attrs.{}", encode_uid(&uid));
3745 let val = encode_attrs_blob(&attrs);
3746 self.vault_kv_set(key, val);
3747 }
3748 }
3749
3750 fn invalidate_permission_cache(&self, uid: Option<&UserId>) {
3751 if let Ok(mut cache) = self.permission_cache.write() {
3752 match uid {
3753 Some(u) => {
3754 cache.remove(u);
3755 }
3756 None => cache.clear(),
3757 }
3758 }
3759 }
3760
3761 pub fn put_policy_with_control_events(
3770 &self,
3771 p: Policy,
3772 control: &PolicyMutationControl<'_>,
3773 ) -> Result<(), AuthError> {
3774 let policy_id = p.id.clone();
3775 let kind = if self.get_policy(&policy_id).is_some() {
3776 EventKind::PolicyUpdate
3777 } else {
3778 EventKind::PolicyCreate
3779 };
3780
3781 if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
3782 let err = AuthError::Forbidden(format!("policy id `{}` is reserved", p.id));
3783 self.emit_policy_error(
3784 control,
3785 kind,
3786 "policy:put",
3787 &policy_id,
3788 Some(&p),
3789 None,
3790 &err,
3791 );
3792 return Err(err);
3793 }
3794
3795 if let Some(ManagedPolicyDecision::Deny {
3796 entry_id,
3797 matched_action,
3798 matched_resource,
3799 reason,
3800 ..
3801 }) = self.managed_policy_decision(&policy_id, PolicyOp::Put, control)
3802 {
3803 self.emit_policy_denied(
3804 control,
3805 kind,
3806 "policy:put",
3807 &policy_id,
3808 Some(&p),
3809 None,
3810 &reason.to_string(),
3811 Some(entry_id),
3812 Some((matched_action, matched_resource)),
3813 );
3814 return Err(Self::managed_policy_error(&policy_id, &reason.to_string()));
3815 }
3816
3817 let previous = self.get_policy(&policy_id);
3818 let was_enabled = self.iam_authorization_enabled();
3819 match self.put_policy_internal(p.clone()) {
3820 Ok(()) => match self.emit_policy_allowed(
3821 control,
3822 kind,
3823 "policy:put",
3824 &policy_id,
3825 Some(&p),
3826 None,
3827 None,
3828 ) {
3829 Ok(()) => Ok(()),
3830 Err(err) => {
3831 self.restore_policy_put(&policy_id, previous, was_enabled);
3832 Err(err)
3833 }
3834 },
3835 Err(err) => {
3836 self.emit_policy_error(
3837 control,
3838 kind,
3839 "policy:put",
3840 &policy_id,
3841 Some(&p),
3842 None,
3843 &err,
3844 );
3845 Err(err)
3846 }
3847 }
3848 }
3849
3850 pub fn delete_policy_with_control_events(
3851 &self,
3852 id: &str,
3853 control: &PolicyMutationControl<'_>,
3854 ) -> Result<(), AuthError> {
3855 let existing = self.get_policy(id);
3856 if let Some(ManagedPolicyDecision::Deny {
3857 entry_id,
3858 matched_action,
3859 matched_resource,
3860 reason,
3861 ..
3862 }) = self.managed_policy_decision(id, PolicyOp::Drop, control)
3863 {
3864 self.emit_policy_denied(
3865 control,
3866 EventKind::PolicyDelete,
3867 "policy:drop",
3868 id,
3869 existing.as_deref(),
3870 None,
3871 &reason.to_string(),
3872 Some(entry_id),
3873 Some((matched_action, matched_resource)),
3874 );
3875 return Err(Self::managed_policy_error(id, &reason.to_string()));
3876 }
3877
3878 let user_attachments = self
3879 .user_attachments
3880 .read()
3881 .map(|m| m.clone())
3882 .unwrap_or_default();
3883 let group_attachments = self
3884 .group_attachments
3885 .read()
3886 .map(|m| m.clone())
3887 .unwrap_or_default();
3888 let was_enabled = self.iam_authorization_enabled();
3889 match self.delete_policy(id) {
3890 Ok(()) => match self.emit_policy_allowed(
3891 control,
3892 EventKind::PolicyDelete,
3893 "policy:drop",
3894 id,
3895 existing.as_deref(),
3896 None,
3897 None,
3898 ) {
3899 Ok(()) => Ok(()),
3900 Err(err) => {
3901 if let Some(policy) = existing {
3902 self.restore_policy_delete(
3903 id,
3904 policy,
3905 user_attachments,
3906 group_attachments,
3907 was_enabled,
3908 );
3909 }
3910 Err(err)
3911 }
3912 },
3913 Err(err) => {
3914 self.emit_policy_error(
3915 control,
3916 EventKind::PolicyDelete,
3917 "policy:drop",
3918 id,
3919 existing.as_deref(),
3920 None,
3921 &err,
3922 );
3923 Err(err)
3924 }
3925 }
3926 }
3927
3928 pub fn attach_policy_with_control_events(
3929 &self,
3930 principal: PrincipalRef,
3931 policy_id: &str,
3932 control: &PolicyMutationControl<'_>,
3933 ) -> Result<(), AuthError> {
3934 let existing = self.get_policy(policy_id);
3935 if let Some(ManagedPolicyDecision::Deny {
3936 entry_id,
3937 matched_action,
3938 matched_resource,
3939 reason,
3940 ..
3941 }) = self.managed_policy_decision(policy_id, PolicyOp::Attach, control)
3942 {
3943 self.emit_policy_denied(
3944 control,
3945 EventKind::PolicyAttach,
3946 "policy:attach",
3947 policy_id,
3948 existing.as_deref(),
3949 Some(&principal),
3950 &reason.to_string(),
3951 Some(entry_id),
3952 Some((matched_action, matched_resource)),
3953 );
3954 return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
3955 }
3956
3957 if let Some(err_msg) = self.check_self_lock_invariant_for_attach(policy_id) {
3958 self.emit_policy_denied(
3959 control,
3960 EventKind::PolicyAttach,
3961 "policy:attach",
3962 policy_id,
3963 existing.as_deref(),
3964 Some(&principal),
3965 &err_msg,
3966 None,
3967 None,
3968 );
3969 return Err(AuthError::Forbidden(err_msg));
3970 }
3971
3972 let user_attachments = self
3973 .user_attachments
3974 .read()
3975 .map(|m| m.clone())
3976 .unwrap_or_default();
3977 let group_attachments = self
3978 .group_attachments
3979 .read()
3980 .map(|m| m.clone())
3981 .unwrap_or_default();
3982 match self.attach_policy(principal.clone(), policy_id) {
3983 Ok(()) => match self.emit_policy_allowed(
3984 control,
3985 EventKind::PolicyAttach,
3986 "policy:attach",
3987 policy_id,
3988 existing.as_deref(),
3989 Some(&principal),
3990 None,
3991 ) {
3992 Ok(()) => Ok(()),
3993 Err(err) => {
3994 self.restore_policy_attachments(user_attachments, group_attachments);
3995 Err(err)
3996 }
3997 },
3998 Err(err) => {
3999 self.emit_policy_error(
4000 control,
4001 EventKind::PolicyAttach,
4002 "policy:attach",
4003 policy_id,
4004 existing.as_deref(),
4005 Some(&principal),
4006 &err,
4007 );
4008 Err(err)
4009 }
4010 }
4011 }
4012
4013 pub fn detach_policy_with_control_events(
4014 &self,
4015 principal: PrincipalRef,
4016 policy_id: &str,
4017 control: &PolicyMutationControl<'_>,
4018 ) -> Result<(), AuthError> {
4019 let existing = self.get_policy(policy_id);
4020 if let Some(ManagedPolicyDecision::Deny {
4021 entry_id,
4022 matched_action,
4023 matched_resource,
4024 reason,
4025 ..
4026 }) = self.managed_policy_decision(policy_id, PolicyOp::Detach, control)
4027 {
4028 self.emit_policy_denied(
4029 control,
4030 EventKind::PolicyDetach,
4031 "policy:detach",
4032 policy_id,
4033 existing.as_deref(),
4034 Some(&principal),
4035 &reason.to_string(),
4036 Some(entry_id),
4037 Some((matched_action, matched_resource)),
4038 );
4039 return Err(Self::managed_policy_error(policy_id, &reason.to_string()));
4040 }
4041 let Some(existing_policy) = existing else {
4042 let err = AuthError::Forbidden(format!("policy `{policy_id}` not found"));
4043 self.emit_policy_error(
4044 control,
4045 EventKind::PolicyDetach,
4046 "policy:detach",
4047 policy_id,
4048 None,
4049 Some(&principal),
4050 &err,
4051 );
4052 return Err(err);
4053 };
4054
4055 let user_attachments = self
4056 .user_attachments
4057 .read()
4058 .map(|m| m.clone())
4059 .unwrap_or_default();
4060 let group_attachments = self
4061 .group_attachments
4062 .read()
4063 .map(|m| m.clone())
4064 .unwrap_or_default();
4065 match self.detach_policy(principal.clone(), policy_id) {
4066 Ok(()) => match self.emit_policy_allowed(
4067 control,
4068 EventKind::PolicyDetach,
4069 "policy:detach",
4070 policy_id,
4071 Some(existing_policy.as_ref()),
4072 Some(&principal),
4073 None,
4074 ) {
4075 Ok(()) => Ok(()),
4076 Err(err) => {
4077 self.restore_policy_attachments(user_attachments, group_attachments);
4078 Err(err)
4079 }
4080 },
4081 Err(err) => {
4082 self.emit_policy_error(
4083 control,
4084 EventKind::PolicyDetach,
4085 "policy:detach",
4086 policy_id,
4087 Some(existing_policy.as_ref()),
4088 Some(&principal),
4089 &err,
4090 );
4091 Err(err)
4092 }
4093 }
4094 }
4095
4096 fn managed_policy_decision(
4097 &self,
4098 policy_id: &str,
4099 op: PolicyOp,
4100 control: &PolicyMutationControl<'_>,
4101 ) -> Option<ManagedPolicyDecision> {
4102 control.registry.map(|registry| {
4103 ManagedPolicyGate::new(registry).check_mutation(
4104 self,
4105 control.actor,
4106 control.eval_ctx,
4107 policy_id,
4108 op,
4109 )
4110 })
4111 }
4112
4113 fn check_self_lock_invariant_for_attach(&self, _policy_id: &str) -> Option<String> {
4120 use crate::auth::self_lock_guard::{
4121 check_self_lock_invariant, format_block_error, InvariantOutcome,
4122 };
4123 let policies: Vec<Arc<Policy>> = match self.policies.read() {
4124 Ok(map) => map.values().cloned().collect(),
4125 Err(_) => return None,
4126 };
4127 let outcome = check_self_lock_invariant(&policies);
4128 match &outcome {
4129 InvariantOutcome::Ok => None,
4130 InvariantOutcome::Blocked { .. } => format_block_error(&outcome),
4131 }
4132 }
4133
4134 fn managed_policy_error(policy_id: &str, reason: &str) -> AuthError {
4135 AuthError::Forbidden(format!(
4136 "managed policy mutation blocked for `{policy_id}`: {reason}"
4137 ))
4138 }
4139
4140 fn emit_policy_allowed(
4141 &self,
4142 control: &PolicyMutationControl<'_>,
4143 kind: EventKind,
4144 action: &'static str,
4145 policy_id: &str,
4146 policy: Option<&Policy>,
4147 principal: Option<&PrincipalRef>,
4148 matched_policy_id: Option<String>,
4149 ) -> Result<(), AuthError> {
4150 let event = ControlEvent {
4151 kind,
4152 outcome: Outcome::Allowed,
4153 action: std::borrow::Cow::Borrowed(action),
4154 resource: Some(format!("policy:{policy_id}")),
4155 reason: None,
4156 matched_policy_id,
4157 fields: policy_control_fields(policy_id, policy, principal),
4158 };
4159 match control.ledger.emit(control.ctx, event) {
4160 Ok(_) => Ok(()),
4161 Err(err) if control.config.require_persistence() => {
4162 Err(AuthError::Internal(err.to_string()))
4163 }
4164 Err(_) => Ok(()),
4165 }
4166 }
4167
4168 #[allow(clippy::too_many_arguments)]
4169 fn emit_policy_denied(
4170 &self,
4171 control: &PolicyMutationControl<'_>,
4172 kind: EventKind,
4173 action: &'static str,
4174 policy_id: &str,
4175 policy: Option<&Policy>,
4176 principal: Option<&PrincipalRef>,
4177 reason: &str,
4178 matched_policy_id: Option<String>,
4179 matched: Option<(String, String)>,
4180 ) {
4181 let mut fields = policy_control_fields(policy_id, policy, principal);
4182 if let Some((matched_action, matched_resource)) = matched {
4183 fields.insert(
4184 "matched_action".to_string(),
4185 Sensitivity::raw(matched_action),
4186 );
4187 fields.insert(
4188 "matched_resource".to_string(),
4189 Sensitivity::raw(matched_resource),
4190 );
4191 }
4192 let event = ControlEvent {
4193 kind,
4194 outcome: Outcome::Denied,
4195 action: std::borrow::Cow::Borrowed(action),
4196 resource: Some(format!("policy:{policy_id}")),
4197 reason: Some(reason.to_string()),
4198 matched_policy_id,
4199 fields,
4200 };
4201 let _ = control.ledger.emit(control.ctx, event);
4202 }
4203
4204 fn emit_policy_error(
4205 &self,
4206 control: &PolicyMutationControl<'_>,
4207 kind: EventKind,
4208 action: &'static str,
4209 policy_id: &str,
4210 policy: Option<&Policy>,
4211 principal: Option<&PrincipalRef>,
4212 err: &AuthError,
4213 ) {
4214 let event = ControlEvent {
4215 kind,
4216 outcome: Outcome::Error,
4217 action: std::borrow::Cow::Borrowed(action),
4218 resource: Some(format!("policy:{policy_id}")),
4219 reason: Some(err.to_string()),
4220 matched_policy_id: None,
4221 fields: policy_control_fields(policy_id, policy, principal),
4222 };
4223 let _ = control.ledger.emit(control.ctx, event);
4224 }
4225
4226 fn restore_policy_put(
4227 &self,
4228 policy_id: &str,
4229 previous: Option<Arc<Policy>>,
4230 was_enabled: bool,
4231 ) {
4232 let mut policies = self.policies.write().unwrap_or_else(|e| e.into_inner());
4233 match previous {
4234 Some(policy) => {
4235 policies.insert(policy_id.to_string(), policy);
4236 }
4237 None => {
4238 policies.remove(policy_id);
4239 }
4240 }
4241 drop(policies);
4242 self.iam_authorization_enabled
4243 .store(was_enabled, Ordering::Release);
4244 self.iam_effective_cache
4245 .write()
4246 .unwrap_or_else(|e| e.into_inner())
4247 .clear();
4248 self.invalidate_visible_collections_cache();
4249 self.persist_iam_to_kv();
4250 }
4251
4252 fn restore_policy_delete(
4253 &self,
4254 policy_id: &str,
4255 policy: Arc<Policy>,
4256 user_attachments: HashMap<UserId, Vec<String>>,
4257 group_attachments: HashMap<String, Vec<String>>,
4258 was_enabled: bool,
4259 ) {
4260 self.policies
4261 .write()
4262 .unwrap_or_else(|e| e.into_inner())
4263 .insert(policy_id.to_string(), policy);
4264 self.restore_policy_attachments(user_attachments, group_attachments);
4265 self.iam_authorization_enabled
4266 .store(was_enabled, Ordering::Release);
4267 self.persist_iam_to_kv();
4268 }
4269
4270 fn restore_policy_attachments(
4271 &self,
4272 user_attachments: HashMap<UserId, Vec<String>>,
4273 group_attachments: HashMap<String, Vec<String>>,
4274 ) {
4275 *self
4276 .user_attachments
4277 .write()
4278 .unwrap_or_else(|e| e.into_inner()) = user_attachments;
4279 *self
4280 .group_attachments
4281 .write()
4282 .unwrap_or_else(|e| e.into_inner()) = group_attachments;
4283 self.iam_effective_cache
4284 .write()
4285 .unwrap_or_else(|e| e.into_inner())
4286 .clear();
4287 self.invalidate_visible_collections_cache();
4288 self.persist_iam_to_kv();
4289 }
4290
4291 pub fn put_policy(&self, p: Policy) -> Result<(), AuthError> {
4296 if p.id.starts_with("_grant_") || p.id.starts_with("_default_") {
4297 return Err(AuthError::Forbidden(format!(
4298 "policy id `{}` is reserved",
4299 p.id
4300 )));
4301 }
4302 self.put_policy_internal(p)
4303 }
4304
4305 pub fn put_policy_internal(&self, p: Policy) -> Result<(), AuthError> {
4310 p.validate()
4311 .map_err(|e| AuthError::Forbidden(format!("invalid policy `{}`: {e}", p.id)))?;
4312 let id = p.id.clone();
4313 self.policies
4314 .write()
4315 .unwrap_or_else(|e| e.into_inner())
4316 .insert(id, Arc::new(p));
4317 self.iam_authorization_enabled
4318 .store(true, Ordering::Release);
4319 self.iam_effective_cache
4320 .write()
4321 .unwrap_or_else(|e| e.into_inner())
4322 .clear();
4323 self.invalidate_visible_collections_cache();
4327 self.persist_iam_to_kv();
4328 Ok(())
4329 }
4330
4331 pub fn iam_authorization_enabled(&self) -> bool {
4336 self.iam_authorization_enabled.load(Ordering::Acquire)
4337 }
4338
4339 pub fn delete_policy(&self, id: &str) -> Result<(), AuthError> {
4341 let removed = self
4342 .policies
4343 .write()
4344 .unwrap_or_else(|e| e.into_inner())
4345 .remove(id)
4346 .is_some();
4347 if !removed {
4348 return Err(AuthError::Forbidden(format!("policy `{id}` not found")));
4349 }
4350 if let Ok(mut ua) = self.user_attachments.write() {
4352 for ids in ua.values_mut() {
4353 ids.retain(|p| p != id);
4354 }
4355 ua.retain(|_, v| !v.is_empty());
4356 }
4357 if let Ok(mut ga) = self.group_attachments.write() {
4358 for ids in ga.values_mut() {
4359 ids.retain(|p| p != id);
4360 }
4361 ga.retain(|_, v| !v.is_empty());
4362 }
4363 self.iam_effective_cache
4364 .write()
4365 .unwrap_or_else(|e| e.into_inner())
4366 .clear();
4367 self.invalidate_visible_collections_cache();
4370 self.persist_iam_to_kv();
4371 Ok(())
4372 }
4373
4374 pub fn list_policies(&self) -> Vec<Arc<Policy>> {
4376 let map = match self.policies.read() {
4377 Ok(g) => g,
4378 Err(_) => return Vec::new(),
4379 };
4380 let mut out: Vec<Arc<Policy>> = map.values().cloned().collect();
4381 out.sort_by(|a, b| a.id.cmp(&b.id));
4382 out
4383 }
4384
4385 pub fn get_policy(&self, id: &str) -> Option<Arc<Policy>> {
4387 self.policies.read().ok().and_then(|m| m.get(id).cloned())
4388 }
4389
4390 pub fn group_policies(&self, group: &str) -> Vec<Arc<Policy>> {
4392 let policies = self.policies.read();
4393 let attachments = self.group_attachments.read();
4394 let mut out = Vec::new();
4395 if let (Ok(p_map), Ok(ga_map)) = (policies, attachments) {
4396 if let Some(ids) = ga_map.get(group) {
4397 for id in ids {
4398 if let Some(p) = p_map.get(id) {
4399 out.push(p.clone());
4400 }
4401 }
4402 }
4403 }
4404 out.sort_by(|a, b| a.id.cmp(&b.id));
4405 out
4406 }
4407
4408 pub fn delete_synthetic_grant_policies(
4412 &self,
4413 principal: &GrantPrincipal,
4414 resource: &Resource,
4415 actions: &[Action],
4416 ) -> usize {
4417 let attached = match principal {
4418 GrantPrincipal::User(uid) => self
4419 .user_attachments
4420 .read()
4421 .ok()
4422 .and_then(|m| m.get(uid).cloned())
4423 .unwrap_or_default(),
4424 GrantPrincipal::Group(group) => self
4425 .group_attachments
4426 .read()
4427 .ok()
4428 .and_then(|m| m.get(group).cloned())
4429 .unwrap_or_default(),
4430 GrantPrincipal::Public => self
4431 .group_attachments
4432 .read()
4433 .ok()
4434 .and_then(|m| m.get(PUBLIC_IAM_GROUP).cloned())
4435 .unwrap_or_default(),
4436 };
4437 if attached.is_empty() {
4438 return 0;
4439 }
4440
4441 let mut delete_ids = Vec::new();
4442 if let Ok(policies) = self.policies.read() {
4443 for id in attached {
4444 let Some(policy) = policies.get(&id) else {
4445 continue;
4446 };
4447 if !policy.id.starts_with("_grant_") {
4448 continue;
4449 }
4450 if synthetic_grant_matches(policy, resource, actions) {
4451 delete_ids.push(policy.id.clone());
4452 }
4453 }
4454 }
4455
4456 let mut deleted = 0usize;
4457 for id in delete_ids {
4458 if self.delete_policy(&id).is_ok() {
4459 deleted += 1;
4460 }
4461 }
4462 deleted
4463 }
4464
4465 pub fn apply_policy_break_glass(&self, boot_ts_ms: u128) -> Result<(), AuthError> {
4473 use crate::auth::self_lock_guard::{
4474 break_glass_audit_fields, unlock_policy, PLATFORM_OWNER_UNLOCK_POLICY_ID,
4475 PLATFORM_OWNER_USERNAME,
4476 };
4477
4478 let policy = unlock_policy();
4482 self.put_policy_internal(policy)?;
4483
4484 let owner_id = UserId::platform(PLATFORM_OWNER_USERNAME);
4489 {
4490 let mut users = self.users.write().unwrap_or_else(|e| e.into_inner());
4491 users.entry(owner_id.clone()).or_insert_with(|| User {
4492 username: PLATFORM_OWNER_USERNAME.to_string(),
4493 tenant_id: None,
4494 password_hash: String::new(),
4499 scram_verifier: None,
4500 role: Role::Admin,
4501 api_keys: Vec::new(),
4502 created_at: boot_ts_ms,
4503 updated_at: boot_ts_ms,
4504 enabled: false,
4505 });
4506 }
4507
4508 self.attach_policy(
4512 PrincipalRef::User(owner_id.clone()),
4513 PLATFORM_OWNER_UNLOCK_POLICY_ID,
4514 )?;
4515
4516 let fields_raw = break_glass_audit_fields(boot_ts_ms);
4521 let mut fields: HashMap<String, Sensitivity> = HashMap::new();
4522 for (k, v) in fields_raw {
4523 fields.insert(k, Sensitivity::raw(v));
4524 }
4525 if let Some(configured) = self.configured_control_events() {
4526 let ctx = ControlEventCtx {
4527 actor: crate::runtime::control_events::ActorRef::System("break_glass"),
4528 scope: None,
4529 request_id: None,
4530 trace_id: None,
4531 };
4532 let event = ControlEvent {
4533 kind: EventKind::PolicyBreakGlass,
4534 outcome: Outcome::Allowed,
4535 action: std::borrow::Cow::Borrowed("policy:break_glass"),
4536 resource: Some(format!("policy:{PLATFORM_OWNER_UNLOCK_POLICY_ID}")),
4537 reason: Some("REDDB_POLICY_BREAK_GLASS=1 at boot".into()),
4538 matched_policy_id: None,
4539 fields,
4540 };
4541 let _ = configured.ledger.emit(&ctx, event);
4542 }
4543 tracing::warn!(
4544 policy_id = PLATFORM_OWNER_UNLOCK_POLICY_ID,
4545 principal = PLATFORM_OWNER_USERNAME,
4546 boot_ts_ms = %boot_ts_ms,
4547 "REDDB_POLICY_BREAK_GLASS=1 — installed/refreshed platform-owner unlock policy and rebound to synthetic principal"
4548 );
4549 Ok(())
4550 }
4551
4552 pub fn attach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
4555 if !self
4556 .policies
4557 .read()
4558 .map(|m| m.contains_key(policy_id))
4559 .unwrap_or(false)
4560 {
4561 return Err(AuthError::Forbidden(format!(
4562 "policy `{policy_id}` not found"
4563 )));
4564 }
4565 match &principal {
4566 PrincipalRef::User(uid) => {
4567 let mut ua = self
4568 .user_attachments
4569 .write()
4570 .unwrap_or_else(|e| e.into_inner());
4571 let list = ua.entry(uid.clone()).or_default();
4572 if !list.iter().any(|p| p == policy_id) {
4573 list.push(policy_id.to_string());
4574 }
4575 drop(ua);
4576 self.invalidate_iam_cache(Some(uid));
4577 }
4578 PrincipalRef::Group(g) => {
4579 let mut ga = self
4580 .group_attachments
4581 .write()
4582 .unwrap_or_else(|e| e.into_inner());
4583 let list = ga.entry(g.clone()).or_default();
4584 if !list.iter().any(|p| p == policy_id) {
4585 list.push(policy_id.to_string());
4586 }
4587 drop(ga);
4588 self.invalidate_iam_cache(None);
4589 }
4590 }
4591 self.persist_iam_to_kv();
4592 Ok(())
4593 }
4594
4595 pub fn detach_policy(&self, principal: PrincipalRef, policy_id: &str) -> Result<(), AuthError> {
4597 match &principal {
4598 PrincipalRef::User(uid) => {
4599 if let Ok(mut ua) = self.user_attachments.write() {
4600 if let Some(list) = ua.get_mut(uid) {
4601 list.retain(|p| p != policy_id);
4602 if list.is_empty() {
4603 ua.remove(uid);
4604 }
4605 }
4606 }
4607 self.invalidate_iam_cache(Some(uid));
4608 }
4609 PrincipalRef::Group(g) => {
4610 if let Ok(mut ga) = self.group_attachments.write() {
4611 if let Some(list) = ga.get_mut(g) {
4612 list.retain(|p| p != policy_id);
4613 if list.is_empty() {
4614 ga.remove(g);
4615 }
4616 }
4617 }
4618 self.invalidate_iam_cache(None);
4619 }
4620 }
4621 self.persist_iam_to_kv();
4622 Ok(())
4623 }
4624
4625 pub fn effective_policies(&self, user: &UserId) -> Vec<Arc<Policy>> {
4629 if let Ok(cache) = self.iam_effective_cache.read() {
4630 if let Some(hit) = cache.get(user) {
4631 return hit.clone();
4632 }
4633 }
4634 let policies = self.policies.read();
4635 let user_attachments = self.user_attachments.read();
4636 let group_attachments = self.group_attachments.read();
4637 let mut groups = self
4638 .user_attributes
4639 .read()
4640 .ok()
4641 .and_then(|m| m.get(user).map(|attrs| attrs.groups.clone()))
4642 .unwrap_or_default();
4643 groups.insert(0, PUBLIC_IAM_GROUP.to_string());
4644 let mut out: Vec<Arc<Policy>> = Vec::new();
4645 if let (Ok(p_map), Ok(ua_map), Ok(ga_map)) = (policies, user_attachments, group_attachments)
4646 {
4647 for group in groups {
4648 if let Some(ids) = ga_map.get(&group) {
4649 for id in ids {
4650 if let Some(p) = p_map.get(id) {
4651 out.push(p.clone());
4652 }
4653 }
4654 }
4655 }
4656 if let Some(ids) = ua_map.get(user) {
4657 for id in ids {
4658 if let Some(p) = p_map.get(id) {
4659 out.push(p.clone());
4660 }
4661 }
4662 }
4663 }
4664 if let Ok(mut cache) = self.iam_effective_cache.write() {
4665 cache.insert(user.clone(), out.clone());
4666 }
4667 out
4668 }
4669
4670 pub fn simulate(
4673 &self,
4674 principal: &UserId,
4675 action: &str,
4676 resource: &ResourceRef,
4677 ctx_extras: SimCtx,
4678 ) -> SimulationOutcome {
4679 let user_role = self
4680 .users
4681 .read()
4682 .ok()
4683 .and_then(|u| u.get(principal).map(|u| u.role));
4684 let principal_is_admin_role = user_role == Some(Role::Admin);
4685 let now = ctx_extras.now_ms.unwrap_or_else(now_ms);
4686 let ctx = EvalContext {
4687 principal_tenant: principal.tenant.clone(),
4688 current_tenant: ctx_extras.current_tenant,
4689 peer_ip: ctx_extras.peer_ip,
4690 mfa_present: ctx_extras.mfa_present,
4691 now_ms: now,
4692 principal_is_admin_role,
4693 principal_is_platform_scoped: principal.tenant.is_none(),
4694 };
4695 let pols = self.effective_policies(principal);
4696 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4697 iam_policies::simulate(&refs, action, resource, &ctx)
4698 }
4699
4700 pub fn check_policy_authz(
4711 &self,
4712 principal: &UserId,
4713 action: &str,
4714 resource: &ResourceRef,
4715 ctx: &EvalContext,
4716 ) -> bool {
4717 let pols = self.effective_policies(principal);
4718 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4719 let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4720 matches!(
4721 decision,
4722 iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass
4723 )
4724 }
4725
4726 pub fn check_policy_authz_with_role(
4742 &self,
4743 principal: &UserId,
4744 action: &str,
4745 resource: &ResourceRef,
4746 ctx: &EvalContext,
4747 role: Role,
4748 ) -> bool {
4749 let pols = self.effective_policies(principal);
4750 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4751 let decision = iam_policies::evaluate(&refs, action, resource, ctx);
4752 match decision {
4753 iam_policies::Decision::Allow { .. } | iam_policies::Decision::AdminBypass => true,
4754 iam_policies::Decision::Deny { .. } => false,
4755 iam_policies::Decision::DefaultDeny => match self.enforcement_mode() {
4756 PolicyEnforcementMode::LegacyRbac => legacy_rbac_decision(role, action),
4757 PolicyEnforcementMode::PolicyOnly => false,
4758 },
4759 }
4760 }
4761
4762 pub fn enforcement_mode(&self) -> PolicyEnforcementMode {
4769 *self
4770 .enforcement_mode
4771 .read()
4772 .unwrap_or_else(|e| e.into_inner())
4773 }
4774
4775 pub fn set_enforcement_mode(&self, mode: PolicyEnforcementMode) -> PolicyEnforcementMode {
4781 let prev = {
4782 let mut guard = self
4783 .enforcement_mode
4784 .write()
4785 .unwrap_or_else(|e| e.into_inner());
4786 let prev = *guard;
4787 *guard = mode;
4788 prev
4789 };
4790 self.vault_kv_set(
4791 "red.config.policy.enforcement_mode".to_string(),
4792 mode.as_str().to_string(),
4793 );
4794 prev
4795 }
4796
4797 pub fn take_legacy_rbac_warn_once(&self) -> bool {
4803 if self.enforcement_mode() != PolicyEnforcementMode::LegacyRbac {
4804 return false;
4805 }
4806 !self
4807 .legacy_rbac_boot_warn_emitted
4808 .swap(true, Ordering::AcqRel)
4809 }
4810
4811 pub fn check_column_projection_authz(
4815 &self,
4816 principal: &UserId,
4817 request: &ColumnAccessRequest,
4818 ctx: &EvalContext,
4819 ) -> ColumnPolicyOutcome {
4820 let pols = self.effective_policies(principal);
4821 let refs: Vec<&Policy> = pols.iter().map(|p| p.as_ref()).collect();
4822 ColumnPolicyGate::new(&refs).evaluate(request, ctx)
4823 }
4824
4825 fn invalidate_iam_cache(&self, uid: Option<&UserId>) {
4826 if let Ok(mut cache) = self.iam_effective_cache.write() {
4827 match uid {
4828 Some(u) => {
4829 cache.remove(u);
4830 }
4831 None => cache.clear(),
4832 }
4833 }
4834 }
4835
4836 pub fn invalidate_all_iam_cache(&self) {
4840 self.invalidate_iam_cache(None);
4841 }
4842
4843 pub fn rehydrate_iam(&self) {
4851 let mut enabled = self
4852 .vault_kv_get("red.iam.enabled")
4853 .map(|v| v == "true")
4854 .unwrap_or(false);
4855 if let Some(blob) = self.vault_kv_get("red.iam.policies") {
4857 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4858 if let Some(obj) = val.as_object() {
4859 let mut map = HashMap::new();
4860 for (id, body) in obj.iter() {
4861 let s = body.to_string_compact();
4862 if let Ok(p) = Policy::from_json_str(&s) {
4863 map.insert(id.clone(), Arc::new(p));
4864 }
4865 }
4866 if !map.is_empty() {
4867 enabled = true;
4868 }
4869 *self.policies.write().unwrap_or_else(|e| e.into_inner()) = map;
4870 }
4871 }
4872 }
4873 if let Some(blob) = self.vault_kv_get("red.iam.attachments.users") {
4875 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4876 if let Some(obj) = val.as_object() {
4877 let mut map: HashMap<UserId, Vec<String>> = HashMap::new();
4878 for (encoded_uid, ids_v) in obj.iter() {
4879 let Some(uid) = decode_uid(encoded_uid) else {
4880 continue;
4881 };
4882 if let Some(arr) = ids_v.as_array() {
4883 let ids: Vec<String> = arr
4884 .iter()
4885 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4886 .collect();
4887 map.insert(uid, ids);
4888 }
4889 }
4890 *self
4891 .user_attachments
4892 .write()
4893 .unwrap_or_else(|e| e.into_inner()) = map;
4894 }
4895 }
4896 }
4897 if let Some(blob) = self.vault_kv_get("red.iam.attachments.groups") {
4899 if let Ok(val) = crate::serde_json::from_str::<crate::serde_json::Value>(&blob) {
4900 if let Some(obj) = val.as_object() {
4901 let mut map: HashMap<String, Vec<String>> = HashMap::new();
4902 for (g, ids_v) in obj.iter() {
4903 if let Some(arr) = ids_v.as_array() {
4904 let ids: Vec<String> = arr
4905 .iter()
4906 .filter_map(|v| v.as_str().map(|s| s.to_string()))
4907 .collect();
4908 map.insert(g.clone(), ids);
4909 }
4910 }
4911 *self
4912 .group_attachments
4913 .write()
4914 .unwrap_or_else(|e| e.into_inner()) = map;
4915 }
4916 }
4917 }
4918 self.iam_authorization_enabled
4919 .store(enabled, Ordering::Release);
4920 self.invalidate_iam_cache(None);
4921
4922 if let Some(stored) = self.vault_kv_get("red.config.policy.enforcement_mode") {
4929 if let Some(mode) = PolicyEnforcementMode::parse(&stored) {
4930 *self
4931 .enforcement_mode
4932 .write()
4933 .unwrap_or_else(|e| e.into_inner()) = mode;
4934 }
4935 }
4936
4937 if self.take_legacy_rbac_warn_once() {
4942 tracing::warn!(
4943 target: "reddb::auth::enforcement_mode",
4944 mode = "legacy_rbac",
4945 hard_version = crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4946 "policy enforcement_mode=legacy_rbac (transitional). Run \
4947 `MIGRATE POLICY MODE TO 'policy_only'` (next slice S5B) \
4948 before {} to flip this install over to the strict posture.",
4949 crate::auth::enforcement_mode::POLICY_ONLY_HARD_VERSION,
4950 );
4951 }
4952 }
4953
4954 fn persist_iam_to_kv(&self) {
4957 let enabled = if self.iam_authorization_enabled() {
4958 "true"
4959 } else {
4960 "false"
4961 };
4962 self.vault_kv_set("red.iam.enabled".to_string(), enabled.to_string());
4963
4964 let policies_obj = {
4966 let map = self.policies.read().unwrap_or_else(|e| e.into_inner());
4967 let mut obj = crate::serde_json::Map::new();
4968 for (id, p) in map.iter() {
4969 let s = p.to_json_string();
4970 if let Ok(v) = crate::serde_json::from_str::<crate::serde_json::Value>(&s) {
4971 obj.insert(id.clone(), v);
4972 }
4973 }
4974 crate::serde_json::Value::Object(obj).to_string_compact()
4975 };
4976 self.vault_kv_set("red.iam.policies".to_string(), policies_obj);
4977
4978 let users_obj = {
4980 let map = self
4981 .user_attachments
4982 .read()
4983 .unwrap_or_else(|e| e.into_inner());
4984 let mut obj = crate::serde_json::Map::new();
4985 for (uid, ids) in map.iter() {
4986 let arr = crate::serde_json::Value::Array(
4987 ids.iter()
4988 .map(|s| crate::serde_json::Value::String(s.clone()))
4989 .collect(),
4990 );
4991 obj.insert(encode_uid(uid), arr);
4992 }
4993 crate::serde_json::Value::Object(obj).to_string_compact()
4994 };
4995 self.vault_kv_set("red.iam.attachments.users".to_string(), users_obj);
4996
4997 let groups_obj = {
4999 let map = self
5000 .group_attachments
5001 .read()
5002 .unwrap_or_else(|e| e.into_inner());
5003 let mut obj = crate::serde_json::Map::new();
5004 for (g, ids) in map.iter() {
5005 let arr = crate::serde_json::Value::Array(
5006 ids.iter()
5007 .map(|s| crate::serde_json::Value::String(s.clone()))
5008 .collect(),
5009 );
5010 obj.insert(g.clone(), arr);
5011 }
5012 crate::serde_json::Value::Object(obj).to_string_compact()
5013 };
5014 self.vault_kv_set("red.iam.attachments.groups".to_string(), groups_obj);
5015 }
5016}
5017
5018fn synthetic_grant_matches(policy: &Policy, resource: &Resource, actions: &[Action]) -> bool {
5019 policy.statements.iter().any(|st| {
5020 st.effect == crate::auth::policies::Effect::Allow
5021 && st.condition.is_none()
5022 && grant_actions_overlap(&st.actions, actions)
5023 && grant_resource_matches(&st.resources, resource)
5024 })
5025}
5026
5027fn grant_actions_overlap(
5028 patterns: &[crate::auth::policies::ActionPattern],
5029 actions: &[Action],
5030) -> bool {
5031 if actions.contains(&Action::All) {
5032 return true;
5033 }
5034 patterns.iter().any(|pat| match pat {
5035 crate::auth::policies::ActionPattern::Wildcard => true,
5036 crate::auth::policies::ActionPattern::Exact(s) => {
5037 actions.iter().any(|a| s.eq_ignore_ascii_case(a.as_str()))
5038 }
5039 crate::auth::policies::ActionPattern::Prefix(_) => false,
5040 })
5041}
5042
5043fn grant_resource_matches(
5044 patterns: &[crate::auth::policies::ResourcePattern],
5045 resource: &Resource,
5046) -> bool {
5047 let expected = grant_resource_pattern(resource);
5048 patterns.iter().any(|pat| pat == &expected)
5049}
5050
5051fn grant_resource_pattern(resource: &Resource) -> crate::auth::policies::ResourcePattern {
5052 use crate::auth::policies::ResourcePattern;
5053
5054 match resource {
5055 Resource::Database => ResourcePattern::Glob("table:*".to_string()),
5056 Resource::Schema(s) => ResourcePattern::Glob(format!("table:{s}.*")),
5057 Resource::Table { schema, table } => ResourcePattern::Exact {
5058 kind: "table".to_string(),
5059 name: match schema {
5060 Some(s) => format!("{s}.{table}"),
5061 None => table.clone(),
5062 },
5063 },
5064 Resource::Function { schema, name } => ResourcePattern::Exact {
5065 kind: "function".to_string(),
5066 name: match schema {
5067 Some(s) => format!("{s}.{name}"),
5068 None => name.clone(),
5069 },
5070 },
5071 }
5072}
5073
5074fn encode_uid(uid: &UserId) -> String {
5090 match &uid.tenant {
5091 Some(t) => format!("{}/{}", t, uid.username),
5092 None => format!("*/{}", uid.username),
5093 }
5094}
5095
5096fn decode_uid(s: &str) -> Option<UserId> {
5097 let (tenant, username) = s.split_once('/')?;
5098 Some(if tenant == "*" {
5099 UserId::platform(username)
5100 } else {
5101 UserId::scoped(tenant, username)
5102 })
5103}
5104
5105fn encode_resource(r: &Resource) -> String {
5106 match r {
5107 Resource::Database => "db".into(),
5108 Resource::Schema(s) => format!("schema:{}", s),
5109 Resource::Table { schema, table } => {
5110 format!("table:{}:{}", schema.as_deref().unwrap_or("*"), table)
5111 }
5112 Resource::Function { schema, name } => {
5113 format!("func:{}:{}", schema.as_deref().unwrap_or("*"), name)
5114 }
5115 }
5116}
5117
5118fn decode_resource(s: &str) -> Option<Resource> {
5119 if s == "db" {
5120 return Some(Resource::Database);
5121 }
5122 if let Some(rest) = s.strip_prefix("schema:") {
5123 return Some(Resource::Schema(rest.to_string()));
5124 }
5125 if let Some(rest) = s.strip_prefix("table:") {
5126 let (schema, table) = rest.split_once(':')?;
5127 return Some(Resource::Table {
5128 schema: if schema == "*" {
5129 None
5130 } else {
5131 Some(schema.to_string())
5132 },
5133 table: table.to_string(),
5134 });
5135 }
5136 if let Some(rest) = s.strip_prefix("func:") {
5137 let (schema, name) = rest.split_once(':')?;
5138 return Some(Resource::Function {
5139 schema: if schema == "*" {
5140 None
5141 } else {
5142 Some(schema.to_string())
5143 },
5144 name: name.to_string(),
5145 });
5146 }
5147 None
5148}
5149
5150fn encode_grants_blob(grants: &[Grant]) -> String {
5151 let mut out = String::new();
5152 for g in grants {
5153 let actions: Vec<&str> = g.actions.iter().map(|a| a.as_str()).collect();
5154 out.push_str(&format!(
5155 "GRANT|{}|{}|{}|{}|{}|{}\n",
5156 encode_resource(&g.resource),
5157 actions.join(","),
5158 g.with_grant_option,
5159 g.tenant.as_deref().unwrap_or("*"),
5160 g.granted_by,
5161 g.granted_at,
5162 ));
5163 }
5164 out
5165}
5166
5167fn decode_grants_blob(s: &str) -> Option<Vec<Grant>> {
5168 let mut out = Vec::new();
5169 for line in s.lines() {
5170 if line.is_empty() {
5171 continue;
5172 }
5173 let parts: Vec<&str> = line.split('|').collect();
5174 if parts.len() != 7 || parts[0] != "GRANT" {
5175 return None;
5176 }
5177 let resource = decode_resource(parts[1])?;
5178 let mut actions = std::collections::BTreeSet::new();
5179 for token in parts[2].split(',') {
5180 if let Some(a) = Action::from_keyword(token) {
5181 actions.insert(a);
5182 }
5183 }
5184 let with_grant_option = parts[3] == "true";
5185 let tenant = if parts[4] == "*" {
5186 None
5187 } else {
5188 Some(parts[4].to_string())
5189 };
5190 let granted_by = parts[5].to_string();
5191 let granted_at: u128 = parts[6].parse().unwrap_or(0);
5192 out.push(Grant {
5193 principal: GrantPrincipal::Public,
5196 resource,
5197 actions,
5198 with_grant_option,
5199 granted_by,
5200 granted_at,
5201 tenant,
5202 columns: None,
5203 });
5204 }
5205 Some(out)
5206}
5207
5208fn encode_attrs_blob(a: &UserAttributes) -> String {
5209 let valid = a
5210 .valid_until
5211 .map(|t| t.to_string())
5212 .unwrap_or_else(|| "*".into());
5213 let limit = a
5214 .connection_limit
5215 .map(|l| l.to_string())
5216 .unwrap_or_else(|| "*".into());
5217 let path = a.search_path.clone().unwrap_or_else(|| "*".into());
5218 let groups = if a.groups.is_empty() {
5219 "*".to_string()
5220 } else {
5221 a.groups.join(",")
5222 };
5223 format!("ATTR|{}|{}|{}|{}\n", valid, limit, path, groups)
5224}
5225
5226fn decode_attrs_blob(s: &str) -> Option<UserAttributes> {
5227 let line = s.lines().next()?;
5228 let parts: Vec<&str> = line.split('|').collect();
5229 if !(parts.len() == 4 || parts.len() == 5) || parts[0] != "ATTR" {
5230 return None;
5231 }
5232 let groups = if parts.get(4).copied().unwrap_or("*") == "*" {
5233 Vec::new()
5234 } else {
5235 parts[4]
5236 .split(',')
5237 .filter(|g| !g.is_empty())
5238 .map(|g| g.to_string())
5239 .collect()
5240 };
5241 Some(UserAttributes {
5242 valid_until: if parts[1] == "*" {
5243 None
5244 } else {
5245 parts[1].parse().ok()
5246 },
5247 connection_limit: if parts[2] == "*" {
5248 None
5249 } else {
5250 parts[2].parse().ok()
5251 },
5252 search_path: if parts[3] == "*" {
5253 None
5254 } else {
5255 Some(parts[3].to_string())
5256 },
5257 groups,
5258 })
5259}
5260
5261fn make_scram_verifier(password: &str) -> crate::auth::scram::ScramVerifier {
5271 let salt = random_bytes(16);
5272 crate::auth::scram::ScramVerifier::from_password(
5273 password,
5274 salt,
5275 crate::auth::scram::DEFAULT_ITER,
5276 )
5277}
5278
5279const PASSWORD_M_COST_KIB: u32 = 19 * 1024;
5284const PASSWORD_T_COST: u32 = 2;
5285const PASSWORD_P_COST: u32 = 1;
5286const PASSWORD_TAG_LEN: usize = 32;
5287
5288fn password_hasher() -> argon2::Argon2<'static> {
5289 let params = argon2::Params::new(
5290 PASSWORD_M_COST_KIB,
5291 PASSWORD_T_COST,
5292 PASSWORD_P_COST,
5293 Some(PASSWORD_TAG_LEN),
5294 )
5295 .expect("Argon2id parameters are constant and valid");
5296 argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params)
5297}
5298
5299pub(crate) fn hash_password(password: &str) -> String {
5311 use argon2::PasswordHasher;
5312 password_hasher()
5315 .hash_password(password.as_bytes())
5316 .expect("Argon2id hashing cannot fail for valid parameters and a working CSPRNG")
5317 .to_string()
5318}
5319
5320pub(crate) fn password_hash_needs_upgrade(stored_hash: &str) -> bool {
5323 stored_hash.starts_with("argon2id$")
5324}
5325
5326pub(crate) fn verify_password(password: &str, stored_hash: &str) -> bool {
5331 if let Some(rest) = stored_hash.strip_prefix("argon2id$") {
5332 return verify_legacy_password(password, rest);
5333 }
5334 use argon2::{PasswordHash, PasswordVerifier};
5335 let Ok(parsed) = stored_hash.parse::<PasswordHash>() else {
5336 return false;
5337 };
5338 password_hasher()
5339 .verify_password(password.as_bytes(), &parsed)
5340 .is_ok()
5341}
5342
5343fn verify_legacy_password(password: &str, salt_and_hash: &str) -> bool {
5346 let Some((salt_hex, hash_hex)) = salt_and_hash.split_once('$') else {
5347 return false;
5348 };
5349 let (Ok(salt), Ok(expected)) = (hex::decode(salt_hex), hex::decode(hash_hex)) else {
5350 return false;
5351 };
5352 let computed = derive_key(password.as_bytes(), &salt, &auth_argon2_params());
5353 constant_time_eq(&computed, &expected)
5354}
5355
5356fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
5358 crate::crypto::constant_time_eq(a, b)
5359}
5360
5361fn generate_session_token() -> String {
5366 format!("rs_{}", hex::encode(random_bytes(32)))
5367}
5368
5369fn generate_api_key() -> String {
5370 format!("rk_{}", hex::encode(random_bytes(32)))
5371}
5372
5373fn random_hex(n: usize) -> String {
5375 hex::encode(random_bytes(n))
5376}
5377
5378pub(crate) fn random_bytes(n: usize) -> Vec<u8> {
5381 let mut buf = vec![0u8; n.max(32)];
5382 if let Err(err) = os_random::fill_bytes(&mut buf) {
5386 panic!("OS CSPRNG unavailable; refusing to generate credential material: {err}");
5387 }
5388 let digest = sha256(&buf);
5390 if n <= 32 {
5391 digest[..n].to_vec()
5392 } else {
5393 let mut out = Vec::with_capacity(n);
5395 let mut prev = digest;
5396 while out.len() < n {
5397 out.extend_from_slice(&prev[..std::cmp::min(32, n - out.len())]);
5398 prev = sha256(&prev);
5399 }
5400 out
5401 }
5402}
5403
5404fn lock_err<T>(_: T) -> AuthError {
5409 AuthError::Internal("lock poisoned".to_string())
5410}
5411
5412#[cfg(test)]
5417mod tests {
5418 use super::*;
5419
5420 fn test_config() -> AuthConfig {
5421 AuthConfig {
5422 enabled: true,
5423 session_ttl_secs: 60,
5424 require_auth: true,
5425 auto_encrypt_storage: false,
5426 vault_enabled: false,
5427 cert: Default::default(),
5428 oauth: Default::default(),
5429 }
5430 }
5431
5432 #[test]
5433 fn test_create_and_list_users() {
5434 let store = AuthStore::new(test_config());
5435 store.create_user("alice", "pass1", Role::Admin).unwrap();
5436 store.create_user("bob", "pass2", Role::Read).unwrap();
5437
5438 let users = store.list_users();
5439 assert_eq!(users.len(), 2);
5440 for u in &users {
5442 assert!(u.password_hash.is_empty());
5443 }
5444 }
5445
5446 #[test]
5447 fn test_create_duplicate_user() {
5448 let store = AuthStore::new(test_config());
5449 store.create_user("alice", "pass", Role::Admin).unwrap();
5450 let err = store.create_user("alice", "pass2", Role::Read).unwrap_err();
5451 assert!(matches!(err, AuthError::UserExists(_)));
5452 }
5453
5454 struct RecordingLedger(std::sync::Mutex<Vec<(String, String)>>);
5455
5456 impl crate::runtime::control_events::ControlEventLedger for RecordingLedger {
5457 fn emit(
5458 &self,
5459 _ctx: &ControlEventCtx<'_>,
5460 event: ControlEvent,
5461 ) -> Result<
5462 crate::runtime::control_events::EventId,
5463 crate::runtime::control_events::ControlEventError,
5464 > {
5465 self.0.lock().unwrap().push((
5466 event.kind.as_str().to_string(),
5467 event.outcome.as_str().to_string(),
5468 ));
5469 Ok(crate::runtime::control_events::EventId("evt".to_string()))
5470 }
5471 }
5472
5473 #[test]
5474 fn logins_land_in_the_control_event_ledger() {
5475 let store = AuthStore::new(test_config());
5476 store.create_user("dave", "pw", Role::Read).unwrap();
5477 let ledger = std::sync::Arc::new(RecordingLedger(std::sync::Mutex::new(Vec::new())));
5478 store.configure_control_events(
5479 ledger.clone(),
5480 crate::runtime::control_events::ControlEventConfig::default(),
5481 );
5482 store.authenticate("dave", "pw").expect("login");
5483 store
5484 .authenticate("dave", "nope")
5485 .expect_err("bad password");
5486 store
5487 .authenticate("nobody", "pw")
5488 .expect_err("unknown user");
5489 let seen = ledger.0.lock().unwrap().clone();
5490 let allowed = Outcome::Allowed.as_str().to_string();
5491 let denied = Outcome::Denied.as_str().to_string();
5492 assert!(
5493 seen.contains(&("auth.login".to_string(), allowed)),
5494 "success not recorded: {seen:?}"
5495 );
5496 assert_eq!(
5497 seen.iter()
5498 .filter(|(k, o)| k == "auth.login_failed" && *o == denied)
5499 .count(),
5500 2,
5501 "both refusals must be recorded: {seen:?}"
5502 );
5503 }
5504
5505 #[test]
5506 fn api_keys_are_hashed_at_rest_and_expire() {
5507 let store = AuthStore::new(test_config());
5508 store.create_user("erin", "pw", Role::Write).unwrap();
5509 let key = store
5510 .create_api_key_in_tenant_expiring(None, "erin", "ci", Role::Read, Some(now_ms() + 60))
5511 .unwrap();
5512 assert!(key.key.starts_with("rk_"));
5513
5514 let stored = store
5515 .users
5516 .read()
5517 .unwrap()
5518 .get(&UserId::from_parts(None, "erin"))
5519 .unwrap()
5520 .api_keys[0]
5521 .clone();
5522 assert!(stored.key.starts_with("rkh_"), "{}", stored.key);
5523 assert_ne!(stored.key, key.key, "the secret must not be stored");
5524 assert_eq!(stored.key, api_key_credential_id(&key.key));
5525
5526 assert!(store.validate_token(&key.key).is_some());
5527 std::thread::sleep(std::time::Duration::from_millis(80));
5528 assert!(
5529 store.validate_token(&key.key).is_none(),
5530 "an expired key must not validate"
5531 );
5532
5533 store.revoke_api_key(&stored.key).unwrap();
5535 assert!(store.revoke_api_key(&key.key).is_err());
5536 }
5537
5538 #[test]
5539 fn revoke_is_clamped_to_the_granter_tenant() {
5540 use crate::auth::privileges::{Action, GrantPrincipal, Resource};
5541 let store = AuthStore::new(test_config());
5542 let acme_admin = UserId::from_parts(Some("acme"), "adm");
5543 let globex_user = UserId::from_parts(Some("globex"), "u");
5544 let err = store
5545 .revoke(
5546 &acme_admin,
5547 Role::Admin,
5548 &GrantPrincipal::User(globex_user),
5549 &Resource::table_from_name("orders"),
5550 &[Action::Select],
5551 )
5552 .expect_err("a tenant admin must not touch another tenant's grants");
5553 assert!(err.to_string().contains("cross-tenant"), "{err}");
5554 store
5556 .revoke(
5557 &UserId::from_parts(None, "platform"),
5558 Role::Admin,
5559 &GrantPrincipal::User(UserId::from_parts(Some("globex"), "u")),
5560 &Resource::table_from_name("orders"),
5561 &[Action::Select],
5562 )
5563 .expect("platform admin revokes anywhere");
5564 }
5565
5566 #[test]
5567 fn role_change_revokes_sessions_minted_under_the_old_role() {
5568 let store = AuthStore::new(test_config());
5569 store.create_user("carol", "pw", Role::Write).unwrap();
5570 let session = store.authenticate("carol", "pw").unwrap();
5571 assert!(store.validate_token(&session.token).is_some());
5572 store.change_role("carol", Role::Read).unwrap();
5573 assert!(
5574 store.validate_token(&session.token).is_none(),
5575 "a demotion must not keep riding on the old session"
5576 );
5577 }
5578
5579 #[test]
5580 fn test_authenticate_and_validate() {
5581 let store = AuthStore::new(test_config());
5582 store.create_user("alice", "secret", Role::Write).unwrap();
5583
5584 let session = store.authenticate("alice", "secret").unwrap();
5585 assert!(session.token.starts_with("rs_"));
5586
5587 let (username, role) = store.validate_token(&session.token).unwrap();
5588 assert_eq!(username, "alice");
5589 assert_eq!(role, Role::Write);
5590 }
5591
5592 #[test]
5593 fn test_authenticate_wrong_password() {
5594 let store = AuthStore::new(test_config());
5595 store.create_user("alice", "secret", Role::Read).unwrap();
5596
5597 let err = store.authenticate("alice", "wrong").unwrap_err();
5598 assert!(matches!(err, AuthError::InvalidCredentials));
5599 }
5600
5601 #[test]
5602 fn test_api_key_lifecycle() {
5603 let store = AuthStore::new(test_config());
5604 store.create_user("alice", "pass", Role::Admin).unwrap();
5605
5606 let key = store
5607 .create_api_key("alice", "ci-token", Role::Write)
5608 .unwrap();
5609 assert!(key.key.starts_with("rk_"));
5610
5611 let (username, role) = store.validate_token(&key.key).unwrap();
5612 assert_eq!(username, "alice");
5613 assert_eq!(role, Role::Write);
5614
5615 store.revoke_api_key(&key.key).unwrap();
5616 assert!(store.validate_token(&key.key).is_none());
5617 }
5618
5619 #[test]
5620 fn test_api_key_role_exceeded() {
5621 let store = AuthStore::new(test_config());
5622 store.create_user("bob", "pass", Role::Read).unwrap();
5623
5624 let err = store
5625 .create_api_key("bob", "escalate", Role::Admin)
5626 .unwrap_err();
5627 assert!(matches!(err, AuthError::RoleExceeded { .. }));
5628 }
5629
5630 #[test]
5631 fn test_change_password() {
5632 let store = AuthStore::new(test_config());
5633 store.create_user("alice", "old", Role::Write).unwrap();
5634
5635 store.change_password("alice", "old", "new").unwrap();
5636
5637 assert!(store.authenticate("alice", "old").is_err());
5639 assert!(store.authenticate("alice", "new").is_ok());
5641 }
5642
5643 #[test]
5644 fn test_change_role() {
5645 let store = AuthStore::new(test_config());
5646 store.create_user("alice", "pass", Role::Admin).unwrap();
5647 store.create_api_key("alice", "key1", Role::Admin).unwrap();
5648
5649 store.change_role("alice", Role::Read).unwrap();
5650
5651 let users = store.list_users();
5653 let alice = users.iter().find(|u| u.username == "alice").unwrap();
5654 assert_eq!(alice.role, Role::Read);
5655
5656 assert_eq!(alice.api_keys[0].role, Role::Read);
5658 }
5659
5660 #[test]
5661 fn test_admin_user_mutations_are_policy_controlled_not_flag_guarded() {
5662 let store = AuthStore::new(test_config());
5663 store
5664 .create_admin_user("system", "pass", Role::Admin, None)
5665 .unwrap();
5666
5667 let uid = UserId::platform("system");
5668 store.change_password("system", "pass", "new").unwrap();
5669 store.change_role("system", Role::Read).unwrap();
5670 store.set_user_enabled(&uid, false).unwrap();
5671
5672 let key = store
5673 .create_api_key("system", "rotation", Role::Read)
5674 .unwrap();
5675 assert!(store.validate_token(&key.key).is_none());
5679 store.set_user_enabled(&uid, true).unwrap();
5680 assert!(store.validate_token(&key.key).is_some());
5681 store.revoke_api_key(&key.key).unwrap();
5682 assert!(store.validate_token(&key.key).is_none());
5683 store.delete_user("system").unwrap();
5684 }
5685
5686 #[test]
5687 fn test_user_lifecycle_deny_policy_blocks_cloud_admin_deletion() {
5688 use crate::auth::policies::Policy;
5689
5690 let store = AuthStore::new(test_config());
5691 store
5692 .create_user("cloud-admin", "pass", Role::Admin)
5693 .unwrap();
5694 store
5695 .create_user("customer-admin", "pass", Role::Admin)
5696 .unwrap();
5697
5698 store
5699 .put_policy(
5700 Policy::from_json_str(
5701 r#"{
5702 "id": "customer-admin-allow-all",
5703 "version": 1,
5704 "statements": [{
5705 "effect": "allow",
5706 "actions": ["*"],
5707 "resources": ["*"]
5708 }]
5709 }"#,
5710 )
5711 .unwrap(),
5712 )
5713 .unwrap();
5714 store
5715 .put_policy(
5716 Policy::from_json_str(
5717 r#"{
5718 "id": "cloud-admin-protection",
5719 "version": 1,
5720 "statements": [{
5721 "effect": "deny",
5722 "actions": [
5723 "user:delete",
5724 "user:disable",
5725 "user:password:change",
5726 "user:role:update"
5727 ],
5728 "resources": ["user:cloud-admin"]
5729 }]
5730 }"#,
5731 )
5732 .unwrap(),
5733 )
5734 .unwrap();
5735 let customer = UserId::platform("customer-admin");
5736 store
5737 .attach_policy(
5738 PrincipalRef::User(customer.clone()),
5739 "customer-admin-allow-all",
5740 )
5741 .unwrap();
5742 store
5743 .attach_policy(
5744 PrincipalRef::User(customer.clone()),
5745 "cloud-admin-protection",
5746 )
5747 .unwrap();
5748
5749 let cloud_admin = UserId::platform("cloud-admin");
5750 assert!(!store.check_user_lifecycle_authz(
5751 &customer,
5752 Role::Admin,
5753 "user:delete",
5754 &cloud_admin,
5755 ));
5756 assert!(!store.check_user_lifecycle_authz(
5757 &customer,
5758 Role::Admin,
5759 "user:disable",
5760 &cloud_admin,
5761 ));
5762 assert!(!store.check_user_lifecycle_authz(
5763 &customer,
5764 Role::Admin,
5765 "user:password:change",
5766 &cloud_admin,
5767 ));
5768 assert!(!store.check_user_lifecycle_authz(
5769 &customer,
5770 Role::Admin,
5771 "user:role:update",
5772 &cloud_admin,
5773 ));
5774
5775 let another_user = UserId::platform("someone-else");
5776 assert!(store.check_user_lifecycle_authz(
5777 &customer,
5778 Role::Admin,
5779 "user:delete",
5780 &another_user,
5781 ));
5782 }
5783
5784 #[test]
5785 fn admin_user_lifecycle_keeps_role_baseline_in_policy_only_mode() {
5786 let store = AuthStore::new(test_config());
5787 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
5788 store.create_user("admin", "pass", Role::Admin).unwrap();
5789 store.create_user("alice", "pass", Role::Read).unwrap();
5790
5791 assert!(store.check_user_lifecycle_authz(
5792 &UserId::platform("admin"),
5793 Role::Admin,
5794 "user:update",
5795 &UserId::platform("alice"),
5796 ));
5797 }
5798
5799 #[test]
5800 fn test_regular_user_mutations_still_work() {
5801 let store = AuthStore::new(test_config());
5802 store.create_user("alice", "old", Role::Admin).unwrap();
5803
5804 let uid = UserId::platform("alice");
5805 store.set_user_enabled(&uid, false).unwrap();
5806 assert!(matches!(
5807 store.authenticate("alice", "old"),
5808 Err(AuthError::InvalidCredentials)
5809 ));
5810
5811 store.set_user_enabled(&uid, true).unwrap();
5812 store.change_password("alice", "old", "new").unwrap();
5813 store.change_role("alice", Role::Read).unwrap();
5814 store.delete_user("alice").unwrap();
5815 assert!(matches!(
5816 store.authenticate("alice", "new"),
5817 Err(AuthError::InvalidCredentials)
5818 ));
5819 }
5820
5821 #[test]
5822 fn test_delete_user() {
5823 let store = AuthStore::new(test_config());
5824 store.create_user("alice", "pass", Role::Admin).unwrap();
5825 let key = store.create_api_key("alice", "key1", Role::Read).unwrap();
5826 let session = store.authenticate("alice", "pass").unwrap();
5827
5828 store.delete_user("alice").unwrap();
5829
5830 assert!(store.validate_token(&key.key).is_none());
5831 assert!(store.validate_token(&session.token).is_none());
5832 assert!(store.list_users().is_empty());
5833 }
5834
5835 #[test]
5836 fn test_revoke_session() {
5837 let store = AuthStore::new(test_config());
5838 store.create_user("alice", "pass", Role::Read).unwrap();
5839 let session = store.authenticate("alice", "pass").unwrap();
5840
5841 store.revoke_session(&session.token);
5842 assert!(store.validate_token(&session.token).is_none());
5843 }
5844
5845 #[test]
5846 fn test_password_hash_format() {
5847 let hash = hash_password("test");
5852 assert!(
5853 hash.starts_with("$argon2id$v=19$"),
5854 "expected a PHC-encoded Argon2id hash, got: {hash}"
5855 );
5856 assert!(
5857 hash.contains(&format!(
5858 "m={PASSWORD_M_COST_KIB},t={PASSWORD_T_COST},p={PASSWORD_P_COST}"
5859 )),
5860 "the PHC string must carry the cost parameters: {hash}"
5861 );
5862 assert!(verify_password("test", &hash));
5863 assert!(!verify_password("wrong", &hash));
5864 assert!(
5865 !password_hash_needs_upgrade(&hash),
5866 "a freshly minted hash must not be flagged for upgrade"
5867 );
5868 }
5869
5870 #[test]
5871 fn legacy_password_hashes_still_verify_and_upgrade_on_login() {
5872 let legacy = {
5876 let salt = random_bytes(16);
5877 let hash = derive_key(b"hunter2", &salt, &auth_argon2_params());
5878 format!("argon2id${}${}", hex::encode(&salt), hex::encode(&hash))
5879 };
5880 assert!(verify_password("hunter2", &legacy));
5881 assert!(!verify_password("wrong", &legacy));
5882 assert!(password_hash_needs_upgrade(&legacy));
5883
5884 let store = AuthStore::new(test_config());
5885 store
5886 .create_user("erin", "placeholder", Role::Write)
5887 .unwrap();
5888 {
5889 let mut users = store.users.write().unwrap();
5890 users
5891 .get_mut(&UserId::platform("erin"))
5892 .unwrap()
5893 .password_hash = legacy.clone();
5894 }
5895
5896 store
5897 .authenticate("erin", "hunter2")
5898 .expect("a legacy hash must still authenticate");
5899
5900 let stored = store
5901 .users
5902 .read()
5903 .unwrap()
5904 .get(&UserId::platform("erin"))
5905 .unwrap()
5906 .password_hash
5907 .clone();
5908 assert!(
5909 stored.starts_with("$argon2id$v=19$"),
5910 "a successful login must upgrade the stored hash, got: {stored}"
5911 );
5912 assert!(store.authenticate("erin", "hunter2").is_ok());
5913 }
5914
5915 #[test]
5916 fn test_constant_time_eq() {
5917 assert!(constant_time_eq(b"hello", b"hello"));
5918 assert!(!constant_time_eq(b"hello", b"world"));
5919 assert!(!constant_time_eq(b"short", b"longer"));
5920 }
5921
5922 #[test]
5923 fn test_bootstrap_seals_permanently() {
5924 let store = AuthStore::new(test_config());
5925
5926 assert!(store.needs_bootstrap());
5927 assert!(!store.is_bootstrapped());
5928
5929 let result = store.bootstrap("admin", "secret");
5931 assert!(result.is_ok());
5932 let br = result.unwrap();
5933 assert_eq!(br.user.username, "admin");
5934 assert_eq!(br.user.role, Role::Admin);
5935 assert!(br.api_key.key.starts_with("rk_"));
5936 assert!(br.certificate.is_none());
5938
5939 assert!(!store.needs_bootstrap());
5941 assert!(store.is_bootstrapped());
5942
5943 let result = store.bootstrap("admin2", "secret2");
5945 assert!(result.is_err());
5946 let err = result.unwrap_err();
5947 assert!(err.to_string().contains("sealed permanently"));
5948
5949 assert_eq!(store.list_users().len(), 1);
5951 assert_eq!(store.list_users()[0].username, "admin");
5952 }
5953
5954 #[test]
5955 fn certificate_delivery_failure_precedes_vault_activation() {
5956 use crate::storage::engine::pager::PagerConfig;
5957 use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
5958
5959 static COUNTER: AtomicU64 = AtomicU64::new(0);
5960 let id = COUNTER.fetch_add(1, AtomicOrdering::Relaxed);
5961 let directory = std::env::temp_dir().join(format!(
5962 "reddb-auth-delivery-failure-{}-{id}",
5963 std::process::id()
5964 ));
5965 std::fs::create_dir_all(&directory).expect("temp directory");
5966 let pager = Arc::new(
5967 Pager::open(directory.join("test.rdb"), PagerConfig::default()).expect("pager"),
5968 );
5969 let mut config = test_config();
5970 config.vault_enabled = true;
5971 let store = AuthStore::with_vault_optional_certificate(config, Arc::clone(&pager), None)
5972 .expect("fresh vault store");
5973
5974 let error = store
5975 .bootstrap_with_certificate_delivery("admin", "secret", |_| {
5976 Err(AuthError::Internal(
5977 "simulated delivery failure".to_string(),
5978 ))
5979 })
5980 .expect_err("delivery failure must abort bootstrap");
5981 assert!(error.to_string().contains("simulated delivery failure"));
5982 assert!(store.needs_bootstrap());
5983 assert!(store.list_users().is_empty());
5984 assert!(!Vault::has_saved_state(&pager));
5985
5986 drop(store);
5987 drop(pager);
5988 let _ = std::fs::remove_dir_all(directory);
5989 }
5990
5991 #[test]
5992 fn test_bootstrap_after_manual_user_creation() {
5993 let store = AuthStore::new(test_config());
5994
5995 store.create_user("existing", "pass", Role::Read).unwrap();
5997
5998 assert!(!store.needs_bootstrap()); }
6002
6003 #[test]
6008 fn test_same_username_two_tenants_distinct() {
6009 let store = AuthStore::new(test_config());
6010 store
6011 .create_user_in_tenant(Some("acme"), "alice", "pw-acme", Role::Write)
6012 .unwrap();
6013 store
6014 .create_user_in_tenant(Some("globex"), "alice", "pw-globex", Role::Read)
6015 .unwrap();
6016
6017 let users = store.list_users();
6019 assert_eq!(users.len(), 2);
6020
6021 assert!(store
6023 .authenticate_in_tenant(Some("acme"), "alice", "pw-acme")
6024 .is_ok());
6025 assert!(store
6026 .authenticate_in_tenant(Some("globex"), "alice", "pw-globex")
6027 .is_ok());
6028
6029 assert!(store
6031 .authenticate_in_tenant(Some("acme"), "alice", "pw-globex")
6032 .is_err());
6033 assert!(store
6034 .authenticate_in_tenant(Some("globex"), "alice", "pw-acme")
6035 .is_err());
6036 }
6037
6038 #[test]
6039 fn test_session_carries_tenant() {
6040 let store = AuthStore::new(test_config());
6041 store
6042 .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
6043 .unwrap();
6044 let session = store
6045 .authenticate_in_tenant(Some("acme"), "alice", "pw")
6046 .unwrap();
6047 assert_eq!(session.tenant_id.as_deref(), Some("acme"));
6048
6049 let (id, role) = store.validate_token_full(&session.token).unwrap();
6050 assert_eq!(id.tenant.as_deref(), Some("acme"));
6051 assert_eq!(id.username, "alice");
6052 assert_eq!(role, Role::Admin);
6053 }
6054
6055 #[test]
6056 fn test_platform_user_has_no_tenant() {
6057 let store = AuthStore::new(test_config());
6058 store.create_user("admin", "pw", Role::Admin).unwrap();
6059 let session = store.authenticate("admin", "pw").unwrap();
6060 assert!(session.tenant_id.is_none());
6061
6062 let (id, _) = store.validate_token_full(&session.token).unwrap();
6063 assert!(id.tenant.is_none());
6064 }
6065
6066 #[test]
6067 fn test_lookup_scram_verifier_global_resolves_platform() {
6068 let store = AuthStore::new(test_config());
6069 store.create_user("admin", "pw", Role::Admin).unwrap();
6070 store
6071 .create_user_in_tenant(Some("acme"), "admin", "pw", Role::Admin)
6072 .unwrap();
6073
6074 let v = store.lookup_scram_verifier_global("admin");
6076 assert!(v.is_some());
6077
6078 let v_acme = store.lookup_scram_verifier(&UserId::scoped("acme", "admin"));
6080 assert!(v_acme.is_some());
6081
6082 assert_ne!(v.unwrap().salt, v_acme.unwrap().salt);
6084 }
6085
6086 #[test]
6087 fn test_delete_in_tenant_does_not_touch_other_tenant() {
6088 let store = AuthStore::new(test_config());
6089 store
6090 .create_user_in_tenant(Some("acme"), "alice", "pw", Role::Admin)
6091 .unwrap();
6092 store
6093 .create_user_in_tenant(Some("globex"), "alice", "pw", Role::Admin)
6094 .unwrap();
6095
6096 store.delete_user_in_tenant(Some("acme"), "alice").unwrap();
6097
6098 assert!(store
6100 .authenticate_in_tenant(Some("globex"), "alice", "pw")
6101 .is_ok());
6102 assert!(store
6104 .authenticate_in_tenant(Some("acme"), "alice", "pw")
6105 .is_err());
6106 }
6107
6108 #[test]
6109 fn test_user_id_display() {
6110 assert_eq!(UserId::platform("admin").to_string(), "admin");
6111 assert_eq!(UserId::scoped("acme", "alice").to_string(), "acme/alice");
6112 }
6113
6114 fn enforcement_eval_ctx(role: Role) -> EvalContext {
6119 EvalContext {
6120 principal_tenant: None,
6121 current_tenant: None,
6122 peer_ip: None,
6123 mfa_present: false,
6124 now_ms: 1_700_000_000_000,
6125 principal_is_admin_role: role == Role::Admin,
6126 principal_is_platform_scoped: true,
6127 }
6128 }
6129
6130 fn install_unrelated_policy(store: &AuthStore) {
6134 store
6135 .put_policy(
6136 Policy::from_json_str(
6137 r#"{"id":"p-unrelated","version":1,"statements":[{"effect":"allow","actions":["select"],"resources":["table:public.other"]}]}"#,
6138 )
6139 .unwrap(),
6140 )
6141 .unwrap();
6142 }
6143
6144 #[test]
6145 fn enforcement_mode_default_for_new_store_is_legacy_rbac() {
6146 let store = AuthStore::new(test_config());
6151 assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::LegacyRbac);
6152 }
6153
6154 #[test]
6155 fn enforcement_mode_set_round_trips_and_returns_previous() {
6156 let store = AuthStore::new(test_config());
6157 let prev = store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
6158 assert_eq!(prev, PolicyEnforcementMode::LegacyRbac);
6159 assert_eq!(store.enforcement_mode(), PolicyEnforcementMode::PolicyOnly);
6160 let prev = store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6161 assert_eq!(prev, PolicyEnforcementMode::PolicyOnly);
6162 }
6163
6164 #[test]
6165 fn policy_only_mode_treats_no_matching_policy_as_deny() {
6166 let store = AuthStore::new(test_config());
6170 store.create_user("alice", "p", Role::Admin).unwrap();
6171 let uid = UserId::platform("alice");
6172 install_unrelated_policy(&store);
6173 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
6174
6175 let resource = ResourceRef::new("table", "public.t");
6176 assert!(!store.check_policy_authz_with_role(
6179 &uid,
6180 "select",
6181 &resource,
6182 &enforcement_eval_ctx(Role::Admin),
6183 Role::Admin,
6184 ));
6185 }
6186
6187 #[test]
6188 fn legacy_rbac_mode_falls_back_to_role_decision() {
6189 let store = AuthStore::new(test_config());
6194 store.create_user("alice", "p", Role::Read).unwrap();
6195 let uid = UserId::platform("alice");
6196 install_unrelated_policy(&store);
6197 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6198
6199 let table = ResourceRef::new("table", "public.t");
6200 assert!(store.check_policy_authz_with_role(
6202 &uid,
6203 "select",
6204 &table,
6205 &enforcement_eval_ctx(Role::Read),
6206 Role::Read,
6207 ));
6208 assert!(!store.check_policy_authz_with_role(
6210 &uid,
6211 "insert",
6212 &table,
6213 &enforcement_eval_ctx(Role::Read),
6214 Role::Read,
6215 ));
6216 assert!(store.check_policy_authz_with_role(
6218 &uid,
6219 "insert",
6220 &table,
6221 &enforcement_eval_ctx(Role::Admin),
6222 Role::Admin,
6223 ));
6224 }
6225
6226 #[test]
6227 fn explicit_deny_wins_irrespective_of_mode_or_role() {
6228 let store = AuthStore::new(test_config());
6232 store.create_user("alice", "p", Role::Admin).unwrap();
6233 let uid = UserId::platform("alice");
6234 store
6235 .put_policy(
6236 Policy::from_json_str(
6237 r#"{"id":"p-deny-select","version":1,"statements":[{"effect":"deny","actions":["select"],"resources":["table:public.t"]}]}"#,
6238 )
6239 .unwrap(),
6240 )
6241 .unwrap();
6242 store
6243 .attach_policy(PrincipalRef::User(uid.clone()), "p-deny-select")
6244 .unwrap();
6245
6246 let resource = ResourceRef::new("table", "public.t");
6247 for mode in [
6248 PolicyEnforcementMode::LegacyRbac,
6249 PolicyEnforcementMode::PolicyOnly,
6250 ] {
6251 store.set_enforcement_mode(mode);
6252 assert!(
6253 !store.check_policy_authz_with_role(
6254 &uid,
6255 "select",
6256 &resource,
6257 &enforcement_eval_ctx(Role::Admin),
6258 Role::Admin,
6259 ),
6260 "explicit deny must win under mode {mode:?}"
6261 );
6262 }
6263 }
6264
6265 #[test]
6266 fn explicit_allow_wins_irrespective_of_mode_or_role() {
6267 let store = AuthStore::new(test_config());
6271 store.create_user("alice", "p", Role::Read).unwrap();
6272 let uid = UserId::platform("alice");
6273 store
6274 .put_policy(
6275 Policy::from_json_str(
6276 r#"{"id":"p-allow-create","version":1,"statements":[{"effect":"allow","actions":["create"],"resources":["table:public.t"]}]}"#,
6277 )
6278 .unwrap(),
6279 )
6280 .unwrap();
6281 store
6282 .attach_policy(PrincipalRef::User(uid.clone()), "p-allow-create")
6283 .unwrap();
6284 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
6285
6286 let resource = ResourceRef::new("table", "public.t");
6287 assert!(store.check_policy_authz_with_role(
6288 &uid,
6289 "create",
6290 &resource,
6291 &enforcement_eval_ctx(Role::Read),
6292 Role::Read,
6293 ));
6294 }
6295
6296 #[test]
6297 fn take_legacy_rbac_warn_once_is_at_most_once_per_boot() {
6298 let store = AuthStore::new(test_config());
6299 assert!(store.take_legacy_rbac_warn_once());
6301 for _ in 0..3 {
6304 assert!(!store.take_legacy_rbac_warn_once());
6305 }
6306 }
6307
6308 #[test]
6309 fn take_legacy_rbac_warn_once_is_silent_under_policy_only() {
6310 let store = AuthStore::new(test_config());
6316 store.set_enforcement_mode(PolicyEnforcementMode::PolicyOnly);
6317 assert!(!store.take_legacy_rbac_warn_once());
6318 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6323 assert!(store.take_legacy_rbac_warn_once());
6324 assert!(!store.take_legacy_rbac_warn_once());
6325 }
6326
6327 #[test]
6328 fn strict_check_policy_authz_ignores_enforcement_mode() {
6329 let store = AuthStore::new(test_config());
6334 store.create_user("alice", "p", Role::Admin).unwrap();
6335 let uid = UserId::platform("alice");
6336 install_unrelated_policy(&store);
6337 store.set_enforcement_mode(PolicyEnforcementMode::LegacyRbac);
6338
6339 let resource = ResourceRef::new("table", "public.t");
6340 assert!(!store.check_policy_authz(
6341 &uid,
6342 "select",
6343 &resource,
6344 &enforcement_eval_ctx(Role::Admin),
6345 ));
6346 }
6347
6348 #[test]
6349 fn disabling_a_user_invalidates_its_api_keys_and_sessions() {
6350 let store = AuthStore::new(test_config());
6351 store.create_user("carol", "pw", Role::Write).unwrap();
6352 let key = store
6353 .create_api_key("carol", "ci", Role::Write)
6354 .unwrap()
6355 .key;
6356 let session = store.authenticate("carol", "pw").unwrap();
6357 assert!(store.validate_token(&key).is_some());
6358 assert!(store.validate_token(&session.token).is_some());
6359
6360 let uid = UserId::platform("carol");
6361 store.set_user_enabled(&uid, false).unwrap();
6362
6363 assert!(
6367 store.validate_token(&key).is_none(),
6368 "an API key must stop working the moment its owner is disabled"
6369 );
6370 assert!(
6371 store.validate_token(&session.token).is_none(),
6372 "live sessions must not survive disabling the account"
6373 );
6374 assert!(
6375 store.lookup_scram_verifier(&uid).is_none(),
6376 "SCRAM must not authenticate a disabled account"
6377 );
6378 }
6379
6380 #[test]
6381 fn admin_password_reset_applies_and_revokes_sessions() {
6382 let store = AuthStore::new(test_config());
6383 store.create_user("dave", "old", Role::Write).unwrap();
6384 let session = store.authenticate("dave", "old").unwrap();
6385
6386 store
6387 .set_password_by_admin(&UserId::platform("dave"), "rotated")
6388 .expect("admin reset");
6389
6390 assert!(
6391 store.authenticate("dave", "old").is_err(),
6392 "the old password must stop working after a reset"
6393 );
6394 assert!(
6395 store.authenticate("dave", "rotated").is_ok(),
6396 "the new password must work after a reset"
6397 );
6398 assert!(
6399 store.validate_token(&session.token).is_none(),
6400 "a password rotation must revoke live sessions"
6401 );
6402 }
6403}