1use parking_lot::RwLock;
19use std::collections::{HashMap, HashSet, VecDeque};
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub enum Permission {
28 Read,
29 Write,
30 Delete,
31 List,
32 Admin,
33 Share,
34 Execute(String),
36}
37
38impl Permission {
39 fn matches(&self, other: &Permission) -> bool {
40 match (self, other) {
41 (Permission::Execute(a), Permission::Execute(b)) => a == b,
42 _ => std::mem::discriminant(self) == std::mem::discriminant(other),
43 }
44 }
45}
46
47impl std::fmt::Display for Permission {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Permission::Read => write!(f, "Read"),
51 Permission::Write => write!(f, "Write"),
52 Permission::Delete => write!(f, "Delete"),
53 Permission::List => write!(f, "List"),
54 Permission::Admin => write!(f, "Admin"),
55 Permission::Share => write!(f, "Share"),
56 Permission::Execute(op) => write!(f, "Execute({op})"),
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct SacRole {
64 pub name: String,
65 pub permissions: Vec<Permission>,
66 pub inherits: Vec<String>,
68}
69
70#[derive(Debug, Clone)]
72pub struct SubjectAttributes {
73 pub subject_id: String,
74 pub roles: Vec<String>,
75 pub attributes: Vec<(String, String)>,
77 pub clearance_level: u8,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum PolicyEffect {
84 Allow,
85 Deny,
86}
87
88#[derive(Debug, Clone)]
90pub struct ResourcePolicy {
91 pub resource_pattern: String,
93 pub required_permission: Permission,
95 pub required_roles: Vec<String>,
98 pub required_attributes: Vec<(String, String)>,
100 pub min_clearance: u8,
102 pub deny_list: Vec<String>,
104 pub allow_list: Vec<String>,
106 pub effect: PolicyEffect,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum AccessDecision {
112 Allowed { reason: String },
113 Denied { reason: String },
114 NotApplicable,
115}
116
117#[derive(Debug, Clone)]
119pub struct SacAuditEntry {
120 pub subject_id: String,
121 pub resource: String,
122 pub permission: Permission,
123 pub decision: AccessDecision,
124 pub timestamp: u64,
126 pub policy_matched: Option<String>,
128}
129
130#[derive(Debug, Clone)]
132pub struct AclConfig {
133 pub default_effect: PolicyEffect,
135 pub enable_audit: bool,
136 pub max_audit_entries: usize,
137 pub role_inheritance_depth: u8,
139}
140
141impl Default for AclConfig {
142 fn default() -> Self {
143 AclConfig {
144 default_effect: PolicyEffect::Deny,
145 enable_audit: true,
146 max_audit_entries: 10_000,
147 role_inheritance_depth: 16,
148 }
149 }
150}
151
152#[derive(Debug, Clone)]
154pub struct AclStats {
155 pub decisions_made: u64,
156 pub allows: u64,
157 pub denies: u64,
158 pub subjects_registered: usize,
159 pub policies_count: usize,
160}
161
162#[derive(Debug, thiserror::Error)]
164pub enum AclError {
165 #[error("subject not found: {0}")]
166 SubjectNotFound(String),
167 #[error("role not found: {0}")]
168 RoleNotFound(String),
169 #[error("policy conflict on resource '{resource}': policies {policies:?}")]
170 PolicyConflict {
171 resource: String,
172 policies: Vec<String>,
173 },
174 #[error("cyclic role inheritance detected: {0:?}")]
175 CyclicRoleInheritance(Vec<String>),
176 #[error("invalid resource pattern: {0}")]
177 InvalidPattern(String),
178}
179
180fn matches_pattern(pattern: &str, resource: &str) -> bool {
190 if pattern == "*" {
191 return true;
192 }
193 if let Some(prefix) = pattern.strip_suffix("/*") {
194 return resource.starts_with(prefix);
195 }
196 if let Some(suffix) = pattern.strip_prefix("*.") {
197 return resource.ends_with(suffix);
198 }
199 pattern == resource
200}
201
202struct AclState {
207 subjects: HashMap<String, SubjectAttributes>,
208 roles: HashMap<String, SacRole>,
209 policies: Vec<ResourcePolicy>,
210 audit_log: VecDeque<SacAuditEntry>,
211 decisions_made: u64,
212 allows: u64,
213 denies: u64,
214}
215
216impl AclState {
217 fn new() -> Self {
218 AclState {
219 subjects: HashMap::new(),
220 roles: HashMap::new(),
221 policies: Vec::new(),
222 audit_log: VecDeque::new(),
223 decisions_made: 0,
224 allows: 0,
225 denies: 0,
226 }
227 }
228
229 fn expand_roles(
232 &self,
233 start_roles: &[String],
234 max_depth: u8,
235 ) -> Result<HashSet<String>, AclError> {
236 let mut visited: HashSet<String> = HashSet::new();
237 let mut queue: VecDeque<(String, u8)> = VecDeque::new();
238
239 for r in start_roles {
240 queue.push_back((r.clone(), 0));
241 }
242
243 while let Some((role_name, depth)) = queue.pop_front() {
244 if visited.contains(&role_name) {
245 continue;
246 }
247 visited.insert(role_name.clone());
248
249 if depth >= max_depth {
250 continue;
251 }
252
253 if let Some(role) = self.roles.get(&role_name) {
254 for parent in &role.inherits {
255 if !visited.contains(parent) {
256 queue.push_back((parent.clone(), depth + 1));
257 }
258 }
259 }
260 }
261
262 Ok(visited)
263 }
264
265 fn validate_no_cycle(&self, role_name: &str) -> Result<(), AclError> {
268 let mut color: HashMap<&str, u8> = HashMap::new();
269 let mut cycle_path: Vec<String> = Vec::new();
270
271 fn dfs<'a>(
272 name: &'a str,
273 roles: &'a HashMap<String, SacRole>,
274 color: &mut HashMap<&'a str, u8>,
275 path: &mut Vec<String>,
276 ) -> bool {
277 color.insert(name, 1);
278 path.push(name.to_string());
279
280 if let Some(role) = roles.get(name) {
281 for parent in &role.inherits {
282 let parent_color = *color.get(parent.as_str()).unwrap_or(&0);
283 if parent_color == 1 {
284 path.push(parent.clone());
285 return true; }
287 if parent_color == 0 && dfs(parent.as_str(), roles, color, path) {
288 return true;
289 }
290 }
291 }
292
293 path.pop();
294 color.insert(name, 2);
295 false
296 }
297
298 if dfs(role_name, &self.roles, &mut color, &mut cycle_path) {
299 return Err(AclError::CyclicRoleInheritance(cycle_path));
300 }
301 Ok(())
302 }
303
304 fn append_audit(&mut self, entry: SacAuditEntry, max_entries: usize, enabled: bool) {
306 if !enabled {
307 return;
308 }
309 if self.audit_log.len() >= max_entries {
310 self.audit_log.pop_front();
311 }
312 self.audit_log.push_back(entry);
313 }
314}
315
316pub struct StorageAccessController {
322 config: AclConfig,
323 state: RwLock<AclState>,
324}
325
326impl StorageAccessController {
327 pub fn new(config: AclConfig) -> Self {
329 StorageAccessController {
330 config,
331 state: RwLock::new(AclState::new()),
332 }
333 }
334
335 pub fn with_defaults() -> Self {
337 Self::new(AclConfig::default())
338 }
339
340 pub fn register_subject(&self, subject: SubjectAttributes) -> Result<(), AclError> {
344 let mut state = self.state.write();
345 state.subjects.insert(subject.subject_id.clone(), subject);
346 Ok(())
347 }
348
349 pub fn register_role(&self, role: SacRole) -> Result<(), AclError> {
354 {
355 let mut state = self.state.write();
356 state.roles.insert(role.name.clone(), role.clone());
358 let result = state.validate_no_cycle(&role.name);
359 if result.is_err() {
360 state.roles.remove(&role.name);
362 return result;
363 }
364 }
365 Ok(())
366 }
367
368 pub fn add_policy(&self, policy: ResourcePolicy) -> Result<(), AclError> {
372 if policy.resource_pattern.is_empty() {
373 return Err(AclError::InvalidPattern(
374 "resource_pattern must not be empty".to_string(),
375 ));
376 }
377 let mut state = self.state.write();
378 state.policies.push(policy);
379 Ok(())
380 }
381
382 pub fn remove_policy(&self, resource_pattern: &str) -> Result<(), AclError> {
386 let mut state = self.state.write();
387 let before = state.policies.len();
388 state
389 .policies
390 .retain(|p| p.resource_pattern != resource_pattern);
391 if state.policies.len() == before {
392 return Err(AclError::InvalidPattern(format!(
393 "no policy with pattern '{resource_pattern}'"
394 )));
395 }
396 Ok(())
397 }
398
399 pub fn check_access(
406 &self,
407 subject_id: &str,
408 resource: &str,
409 permission: Permission,
410 current_ts: u64,
411 ) -> Result<AccessDecision, AclError> {
412 let max_depth = self.config.role_inheritance_depth;
413 let default_effect = self.config.default_effect.clone();
414 let enable_audit = self.config.enable_audit;
415 let max_audit = self.config.max_audit_entries;
416
417 let decision = {
419 let state = self.state.read();
420
421 let subject = state
422 .subjects
423 .get(subject_id)
424 .ok_or_else(|| AclError::SubjectNotFound(subject_id.to_string()))?;
425
426 let effective_roles = state.expand_roles(&subject.roles, max_depth)?;
427
428 let matching: Vec<&ResourcePolicy> = state
431 .policies
432 .iter()
433 .filter(|p| {
434 matches_pattern(&p.resource_pattern, resource)
435 && p.required_permission.matches(&permission)
436 })
437 .collect();
438
439 if matching.is_empty() {
440 let decision = match default_effect {
441 PolicyEffect::Allow => AccessDecision::Allowed {
442 reason: "no matching policy; default-allow".to_string(),
443 },
444 PolicyEffect::Deny => AccessDecision::Denied {
445 reason: "no matching policy; default-deny".to_string(),
446 },
447 };
448 (decision, None)
449 } else {
450 evaluate_policies(subject, subject_id, &effective_roles, &matching)
451 }
452 };
453
454 {
456 let mut state = self.state.write();
457 state.decisions_made += 1;
458 match &decision.0 {
459 AccessDecision::Allowed { .. } => state.allows += 1,
460 AccessDecision::Denied { .. } => state.denies += 1,
461 AccessDecision::NotApplicable => {}
462 }
463 state.append_audit(
464 SacAuditEntry {
465 subject_id: subject_id.to_string(),
466 resource: resource.to_string(),
467 permission: permission.clone(),
468 decision: decision.0.clone(),
469 timestamp: current_ts,
470 policy_matched: decision.1.clone(),
471 },
472 max_audit,
473 enable_audit,
474 );
475 }
476
477 Ok(decision.0)
478 }
479
480 pub fn effective_permissions(
487 &self,
488 subject_id: &str,
489 resource: &str,
490 ) -> Result<Vec<Permission>, AclError> {
491 let ts = 0u64; let execute_ops: Vec<String> = {
495 let state = self.state.read();
496 state
497 .policies
498 .iter()
499 .filter(|p| matches_pattern(&p.resource_pattern, resource))
500 .filter_map(|p| {
501 if let Permission::Execute(op) = &p.required_permission {
502 Some(op.clone())
503 } else {
504 None
505 }
506 })
507 .collect()
508 };
509
510 let candidates: Vec<Permission> = {
511 let mut v = vec![
512 Permission::Read,
513 Permission::Write,
514 Permission::Delete,
515 Permission::List,
516 Permission::Admin,
517 Permission::Share,
518 ];
519 for op in execute_ops {
520 v.push(Permission::Execute(op));
521 }
522 v
523 };
524
525 let mut allowed = Vec::new();
526 for perm in candidates {
527 let decision = self.check_access(subject_id, resource, perm.clone(), ts)?;
528 if matches!(decision, AccessDecision::Allowed { .. }) {
529 allowed.push(perm);
530 }
531 }
532 Ok(allowed)
533 }
534
535 pub fn roles_for_subject(&self, subject_id: &str) -> Result<Vec<String>, AclError> {
537 let state = self.state.read();
538 let subject = state
539 .subjects
540 .get(subject_id)
541 .ok_or_else(|| AclError::SubjectNotFound(subject_id.to_string()))?;
542 let expanded = state.expand_roles(&subject.roles, self.config.role_inheritance_depth)?;
543 let mut result: Vec<String> = expanded.into_iter().collect();
544 result.sort();
545 Ok(result)
546 }
547
548 pub fn audit_log_entries(
555 &self,
556 subject_id: Option<&str>,
557 resource: Option<&str>,
558 ) -> Vec<SacAuditEntry> {
559 let state = self.state.read();
560 state
561 .audit_log
562 .iter()
563 .filter(|e| {
564 subject_id.is_none_or(|s| e.subject_id == s)
565 && resource.is_none_or(|r| e.resource == r)
566 })
567 .cloned()
568 .collect()
569 }
570
571 pub fn stats(&self) -> AclStats {
575 let state = self.state.read();
576 AclStats {
577 decisions_made: state.decisions_made,
578 allows: state.allows,
579 denies: state.denies,
580 subjects_registered: state.subjects.len(),
581 policies_count: state.policies.len(),
582 }
583 }
584
585 pub fn audit_log_len(&self) -> usize {
587 self.state.read().audit_log.len()
588 }
589
590 pub fn with_audit_log<F, T>(&self, f: F) -> T
592 where
593 F: FnOnce(&VecDeque<SacAuditEntry>) -> T,
594 {
595 let state = self.state.read();
596 f(&state.audit_log)
597 }
598}
599
600fn evaluate_policies(
612 subject: &SubjectAttributes,
613 subject_id: &str,
614 effective_roles: &HashSet<String>,
615 policies: &[&ResourcePolicy],
616) -> (AccessDecision, Option<String>) {
617 for policy in policies {
619 if policy.allow_list.iter().any(|id| id == subject_id) {
620 return (
621 AccessDecision::Allowed {
622 reason: format!(
623 "subject '{subject_id}' is on the allow_list of policy '{}'",
624 policy.resource_pattern
625 ),
626 },
627 Some(policy.resource_pattern.clone()),
628 );
629 }
630 }
631
632 for policy in policies {
634 if policy.deny_list.iter().any(|id| id == subject_id) {
635 return (
636 AccessDecision::Denied {
637 reason: format!(
638 "subject '{subject_id}' is on the deny_list of policy '{}'",
639 policy.resource_pattern
640 ),
641 },
642 Some(policy.resource_pattern.clone()),
643 );
644 }
645 }
646
647 let result = policies
652 .iter()
653 .map(|policy| evaluate_single_policy(subject, policy, effective_roles))
654 .next();
655
656 result.unwrap_or((AccessDecision::NotApplicable, None))
657}
658
659fn evaluate_single_policy(
661 subject: &SubjectAttributes,
662 policy: &ResourcePolicy,
663 effective_roles: &HashSet<String>,
664) -> (AccessDecision, Option<String>) {
665 if !policy.required_roles.is_empty() {
667 let has_role = policy
668 .required_roles
669 .iter()
670 .any(|r| effective_roles.contains(r.as_str()));
671 if !has_role {
672 return (
673 AccessDecision::Denied {
674 reason: format!(
675 "subject lacks required roles {:?} (policy '{}')",
676 policy.required_roles, policy.resource_pattern
677 ),
678 },
679 Some(policy.resource_pattern.clone()),
680 );
681 }
682 }
683
684 for (key, value) in &policy.required_attributes {
686 let has_attr = subject
687 .attributes
688 .iter()
689 .any(|(k, v)| k == key && v == value);
690 if !has_attr {
691 return (
692 AccessDecision::Denied {
693 reason: format!(
694 "subject missing attribute '{}={}' (policy '{}')",
695 key, value, policy.resource_pattern
696 ),
697 },
698 Some(policy.resource_pattern.clone()),
699 );
700 }
701 }
702
703 if subject.clearance_level < policy.min_clearance {
705 return (
706 AccessDecision::Denied {
707 reason: format!(
708 "subject clearance {} < required {} (policy '{}')",
709 subject.clearance_level, policy.min_clearance, policy.resource_pattern
710 ),
711 },
712 Some(policy.resource_pattern.clone()),
713 );
714 }
715
716 match policy.effect {
718 PolicyEffect::Allow => (
719 AccessDecision::Allowed {
720 reason: format!("policy '{}' granted access", policy.resource_pattern),
721 },
722 Some(policy.resource_pattern.clone()),
723 ),
724 PolicyEffect::Deny => (
725 AccessDecision::Denied {
726 reason: format!("policy '{}' denied access", policy.resource_pattern),
727 },
728 Some(policy.resource_pattern.clone()),
729 ),
730 }
731}
732
733#[cfg(test)]
738mod tests {
739 use super::*;
740
741 struct Xorshift64(u64);
744
745 impl Xorshift64 {
746 fn new(seed: u64) -> Self {
747 Xorshift64(if seed == 0 { 0xdeadbeef_cafebabe } else { seed })
748 }
749 fn next(&mut self) -> u64 {
750 let mut x = self.0;
751 x ^= x << 13;
752 x ^= x >> 7;
753 x ^= x << 17;
754 self.0 = x;
755 x
756 }
757 fn next_u8(&mut self) -> u8 {
758 (self.next() & 0xff) as u8
759 }
760 fn next_usize(&mut self, max: usize) -> usize {
761 (self.next() % max as u64) as usize
762 }
763 }
764
765 fn make_controller() -> StorageAccessController {
768 StorageAccessController::with_defaults()
769 }
770
771 fn subject(id: &str, roles: &[&str]) -> SubjectAttributes {
772 SubjectAttributes {
773 subject_id: id.to_string(),
774 roles: roles.iter().map(|s| s.to_string()).collect(),
775 attributes: vec![],
776 clearance_level: 0,
777 }
778 }
779
780 fn subject_with_attrs(
781 id: &str,
782 roles: &[&str],
783 attrs: &[(&str, &str)],
784 clearance: u8,
785 ) -> SubjectAttributes {
786 SubjectAttributes {
787 subject_id: id.to_string(),
788 roles: roles.iter().map(|s| s.to_string()).collect(),
789 attributes: attrs
790 .iter()
791 .map(|(k, v)| (k.to_string(), v.to_string()))
792 .collect(),
793 clearance_level: clearance,
794 }
795 }
796
797 fn role(name: &str, perms: Vec<Permission>, inherits: &[&str]) -> SacRole {
798 SacRole {
799 name: name.to_string(),
800 permissions: perms,
801 inherits: inherits.iter().map(|s| s.to_string()).collect(),
802 }
803 }
804
805 fn allow_policy(pattern: &str, perm: Permission, roles: &[&str]) -> ResourcePolicy {
806 ResourcePolicy {
807 resource_pattern: pattern.to_string(),
808 required_permission: perm,
809 required_roles: roles.iter().map(|s| s.to_string()).collect(),
810 required_attributes: vec![],
811 min_clearance: 0,
812 deny_list: vec![],
813 allow_list: vec![],
814 effect: PolicyEffect::Allow,
815 }
816 }
817
818 fn deny_policy(pattern: &str, perm: Permission, roles: &[&str]) -> ResourcePolicy {
819 ResourcePolicy {
820 resource_pattern: pattern.to_string(),
821 required_permission: perm,
822 required_roles: roles.iter().map(|s| s.to_string()).collect(),
823 required_attributes: vec![],
824 min_clearance: 0,
825 deny_list: vec![],
826 allow_list: vec![],
827 effect: PolicyEffect::Deny,
828 }
829 }
830
831 #[test]
834 fn test_pattern_wildcard_all() {
835 assert!(matches_pattern("*", "anything/at/all.json"));
836 }
837
838 #[test]
839 fn test_pattern_prefix_wildcard() {
840 assert!(matches_pattern("data/*", "data/block1"));
841 assert!(matches_pattern("data/*", "data/nested"));
842 assert!(!matches_pattern("data/*", "other/block1"));
843 }
844
845 #[test]
846 fn test_pattern_suffix_wildcard() {
847 assert!(matches_pattern("*.json", "config.json"));
848 assert!(matches_pattern("*.json", "schema.json"));
849 assert!(!matches_pattern("*.json", "config.toml"));
850 }
851
852 #[test]
853 fn test_pattern_exact() {
854 assert!(matches_pattern("exact/path", "exact/path"));
855 assert!(!matches_pattern("exact/path", "exact/other"));
856 }
857
858 #[test]
861 fn test_register_subject_ok() {
862 let ctrl = make_controller();
863 ctrl.register_subject(subject("alice", &["reader"]))
864 .expect("register failed");
865 let s = ctrl.stats();
866 assert_eq!(s.subjects_registered, 1);
867 }
868
869 #[test]
870 fn test_register_subject_overwrite() {
871 let ctrl = make_controller();
872 ctrl.register_subject(subject("alice", &["reader"]))
873 .unwrap();
874 ctrl.register_subject(subject("alice", &["admin"])).unwrap();
875 let s = ctrl.stats();
876 assert_eq!(s.subjects_registered, 1);
877 }
878
879 #[test]
882 fn test_register_role_ok() {
883 let ctrl = make_controller();
884 ctrl.register_role(role("reader", vec![Permission::Read], &[]))
885 .unwrap();
886 let s = ctrl.stats();
887 assert_eq!(s.subjects_registered, 0);
889 }
890
891 #[test]
892 fn test_register_role_cyclic_two() {
893 let ctrl = make_controller();
894 ctrl.register_role(role("a", vec![], &["b"])).unwrap();
895 let err = ctrl.register_role(role("b", vec![], &["a"])).unwrap_err();
896 assert!(
897 matches!(err, AclError::CyclicRoleInheritance(_)),
898 "expected cyclic error, got {err:?}"
899 );
900 }
901
902 #[test]
903 fn test_register_role_cyclic_self() {
904 let ctrl = make_controller();
905 let err = ctrl
906 .register_role(role("loop_role", vec![], &["loop_role"]))
907 .unwrap_err();
908 assert!(matches!(err, AclError::CyclicRoleInheritance(_)));
909 }
910
911 #[test]
912 fn test_register_role_cyclic_three() {
913 let ctrl = make_controller();
914 ctrl.register_role(role("x", vec![], &["y"])).unwrap();
915 ctrl.register_role(role("y", vec![], &["z"])).unwrap();
916 let err = ctrl.register_role(role("z", vec![], &["x"])).unwrap_err();
917 assert!(matches!(err, AclError::CyclicRoleInheritance(_)));
918 }
919
920 #[test]
923 fn test_add_policy_ok() {
924 let ctrl = make_controller();
925 ctrl.add_policy(allow_policy("*", Permission::Read, &[]))
926 .unwrap();
927 assert_eq!(ctrl.stats().policies_count, 1);
928 }
929
930 #[test]
931 fn test_add_policy_empty_pattern_error() {
932 let ctrl = make_controller();
933 let err = ctrl
934 .add_policy(allow_policy("", Permission::Read, &[]))
935 .unwrap_err();
936 assert!(matches!(err, AclError::InvalidPattern(_)));
937 }
938
939 #[test]
940 fn test_remove_policy_ok() {
941 let ctrl = make_controller();
942 ctrl.add_policy(allow_policy("data/*", Permission::Read, &[]))
943 .unwrap();
944 ctrl.remove_policy("data/*").unwrap();
945 assert_eq!(ctrl.stats().policies_count, 0);
946 }
947
948 #[test]
949 fn test_remove_policy_not_found_error() {
950 let ctrl = make_controller();
951 let err = ctrl.remove_policy("nonexistent/*").unwrap_err();
952 assert!(matches!(err, AclError::InvalidPattern(_)));
953 }
954
955 #[test]
958 fn test_default_deny_no_policy() {
959 let ctrl = StorageAccessController::new(AclConfig {
960 default_effect: PolicyEffect::Deny,
961 ..AclConfig::default()
962 });
963 ctrl.register_subject(subject("bob", &[])).unwrap();
964 let d = ctrl
965 .check_access("bob", "secret.json", Permission::Read, 1)
966 .unwrap();
967 assert!(matches!(d, AccessDecision::Denied { .. }));
968 }
969
970 #[test]
971 fn test_default_allow_no_policy() {
972 let ctrl = StorageAccessController::new(AclConfig {
973 default_effect: PolicyEffect::Allow,
974 ..AclConfig::default()
975 });
976 ctrl.register_subject(subject("guest", &[])).unwrap();
977 let d = ctrl
978 .check_access("guest", "public.json", Permission::Read, 1)
979 .unwrap();
980 assert!(matches!(d, AccessDecision::Allowed { .. }));
981 }
982
983 #[test]
986 fn test_allow_via_role() {
987 let ctrl = make_controller();
988 ctrl.register_role(role("reader", vec![Permission::Read], &[]))
989 .unwrap();
990 ctrl.register_subject(subject("alice", &["reader"]))
991 .unwrap();
992 ctrl.add_policy(allow_policy("data/*", Permission::Read, &["reader"]))
993 .unwrap();
994 let d = ctrl
995 .check_access("alice", "data/file.bin", Permission::Read, 1)
996 .unwrap();
997 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
998 }
999
1000 #[test]
1001 fn test_deny_wrong_role() {
1002 let ctrl = make_controller();
1003 ctrl.register_role(role("reader", vec![Permission::Read], &[]))
1004 .unwrap();
1005 ctrl.register_subject(subject("dave", &["reader"])).unwrap();
1006 ctrl.add_policy(allow_policy("admin/*", Permission::Write, &["admin"]))
1007 .unwrap();
1008 let d = ctrl
1009 .check_access("dave", "admin/config", Permission::Write, 1)
1010 .unwrap();
1011 assert!(matches!(d, AccessDecision::Denied { .. }), "{d:?}");
1013 }
1014
1015 #[test]
1018 fn test_deny_list_overrides_role() {
1019 let ctrl = make_controller();
1020 ctrl.register_role(role("admin", vec![Permission::Admin], &[]))
1021 .unwrap();
1022 ctrl.register_subject(subject("mallory", &["admin"]))
1023 .unwrap();
1024
1025 let mut policy = allow_policy("*", Permission::Admin, &["admin"]);
1026 policy.deny_list.push("mallory".to_string());
1027 ctrl.add_policy(policy).unwrap();
1028
1029 let d = ctrl
1030 .check_access("mallory", "anything", Permission::Admin, 1)
1031 .unwrap();
1032 assert!(matches!(d, AccessDecision::Denied { .. }), "{d:?}");
1033 }
1034
1035 #[test]
1036 fn test_deny_list_does_not_affect_others() {
1037 let ctrl = make_controller();
1038 ctrl.register_role(role("admin", vec![], &[])).unwrap();
1039 ctrl.register_subject(subject("alice", &["admin"])).unwrap();
1040 ctrl.register_subject(subject("mallory", &["admin"]))
1041 .unwrap();
1042
1043 let mut policy = allow_policy("*", Permission::Admin, &["admin"]);
1044 policy.deny_list.push("mallory".to_string());
1045 ctrl.add_policy(policy).unwrap();
1046
1047 let d = ctrl
1048 .check_access("alice", "anything", Permission::Admin, 1)
1049 .unwrap();
1050 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1051 }
1052
1053 #[test]
1056 fn test_allow_list_overrides_deny_list() {
1057 let ctrl = make_controller();
1058 ctrl.register_subject(subject("super_user", &[])).unwrap();
1059
1060 let mut policy = deny_policy("*", Permission::Delete, &[]);
1061 policy.allow_list.push("super_user".to_string());
1062 policy.deny_list.push("super_user".to_string()); ctrl.add_policy(policy).unwrap();
1064
1065 let d = ctrl
1066 .check_access("super_user", "anything", Permission::Delete, 1)
1067 .unwrap();
1068 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1069 }
1070
1071 #[test]
1072 fn test_allow_list_bypasses_role_check() {
1073 let ctrl = make_controller();
1074 ctrl.register_subject(subject("vip", &[])).unwrap();
1076
1077 let mut policy = allow_policy("secret/*", Permission::Read, &["admin"]);
1078 policy.allow_list.push("vip".to_string());
1079 ctrl.add_policy(policy).unwrap();
1080
1081 let d = ctrl
1082 .check_access("vip", "secret/file", Permission::Read, 1)
1083 .unwrap();
1084 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1085 }
1086
1087 #[test]
1090 fn test_attribute_match_allow() {
1091 let ctrl = make_controller();
1092 ctrl.register_role(role("engineer", vec![], &[])).unwrap();
1093 ctrl.register_subject(subject_with_attrs(
1094 "eng_alice",
1095 &["engineer"],
1096 &[("department", "eng"), ("team", "storage")],
1097 0,
1098 ))
1099 .unwrap();
1100
1101 let mut policy = allow_policy("eng/*", Permission::Write, &["engineer"]);
1102 policy
1103 .required_attributes
1104 .push(("department".to_string(), "eng".to_string()));
1105 ctrl.add_policy(policy).unwrap();
1106
1107 let d = ctrl
1108 .check_access("eng_alice", "eng/data", Permission::Write, 1)
1109 .unwrap();
1110 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1111 }
1112
1113 #[test]
1114 fn test_attribute_mismatch_deny() {
1115 let ctrl = make_controller();
1116 ctrl.register_role(role("engineer", vec![], &[])).unwrap();
1117 ctrl.register_subject(subject_with_attrs(
1118 "sales_bob",
1119 &["engineer"],
1120 &[("department", "sales")],
1121 0,
1122 ))
1123 .unwrap();
1124
1125 let mut policy = allow_policy("eng/*", Permission::Write, &["engineer"]);
1126 policy
1127 .required_attributes
1128 .push(("department".to_string(), "eng".to_string()));
1129 ctrl.add_policy(policy).unwrap();
1130
1131 let d = ctrl
1132 .check_access("sales_bob", "eng/data", Permission::Write, 1)
1133 .unwrap();
1134 assert!(matches!(d, AccessDecision::Denied { .. }), "{d:?}");
1135 }
1136
1137 #[test]
1138 fn test_attribute_multiple_required_all_must_match() {
1139 let ctrl = make_controller();
1140 ctrl.register_role(role("dev", vec![], &[])).unwrap();
1141 ctrl.register_subject(subject_with_attrs(
1142 "user_partial",
1143 &["dev"],
1144 &[("dept", "eng")], 0,
1146 ))
1147 .unwrap();
1148
1149 let mut policy = allow_policy("*", Permission::Read, &["dev"]);
1150 policy
1151 .required_attributes
1152 .push(("dept".to_string(), "eng".to_string()));
1153 policy
1154 .required_attributes
1155 .push(("clearance".to_string(), "secret".to_string()));
1156 ctrl.add_policy(policy).unwrap();
1157
1158 let d = ctrl
1159 .check_access("user_partial", "anything", Permission::Read, 1)
1160 .unwrap();
1161 assert!(matches!(d, AccessDecision::Denied { .. }), "{d:?}");
1162 }
1163
1164 #[test]
1167 fn test_clearance_sufficient() {
1168 let ctrl = make_controller();
1169 ctrl.register_role(role("agent", vec![], &[])).unwrap();
1170 ctrl.register_subject(subject_with_attrs("spy", &["agent"], &[], 200))
1171 .unwrap();
1172
1173 let mut policy = allow_policy("classified/*", Permission::Read, &["agent"]);
1174 policy.min_clearance = 100;
1175 ctrl.add_policy(policy).unwrap();
1176
1177 let d = ctrl
1178 .check_access("spy", "classified/mission", Permission::Read, 1)
1179 .unwrap();
1180 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1181 }
1182
1183 #[test]
1184 fn test_clearance_insufficient() {
1185 let ctrl = make_controller();
1186 ctrl.register_role(role("agent", vec![], &[])).unwrap();
1187 ctrl.register_subject(subject_with_attrs("rookie", &["agent"], &[], 50))
1188 .unwrap();
1189
1190 let mut policy = allow_policy("classified/*", Permission::Read, &["agent"]);
1191 policy.min_clearance = 100;
1192 ctrl.add_policy(policy).unwrap();
1193
1194 let d = ctrl
1195 .check_access("rookie", "classified/mission", Permission::Read, 1)
1196 .unwrap();
1197 assert!(matches!(d, AccessDecision::Denied { .. }), "{d:?}");
1198 }
1199
1200 #[test]
1201 fn test_clearance_exact_boundary() {
1202 let ctrl = make_controller();
1203 ctrl.register_role(role("r", vec![], &[])).unwrap();
1204 ctrl.register_subject(subject_with_attrs("u", &["r"], &[], 100))
1205 .unwrap();
1206
1207 let mut policy = allow_policy("*", Permission::Read, &["r"]);
1208 policy.min_clearance = 100;
1209 ctrl.add_policy(policy).unwrap();
1210
1211 let d = ctrl
1212 .check_access("u", "resource", Permission::Read, 1)
1213 .unwrap();
1214 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1215 }
1216
1217 #[test]
1220 fn test_role_inheritance_one_hop() {
1221 let ctrl = make_controller();
1222 ctrl.register_role(role("base", vec![Permission::Read], &[]))
1223 .unwrap();
1224 ctrl.register_role(role("derived", vec![], &["base"]))
1225 .unwrap();
1226 ctrl.register_subject(subject("user", &["derived"]))
1227 .unwrap();
1228 ctrl.add_policy(allow_policy("*", Permission::Read, &["base"]))
1229 .unwrap();
1230
1231 let d = ctrl
1232 .check_access("user", "file.txt", Permission::Read, 1)
1233 .unwrap();
1234 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1235 }
1236
1237 #[test]
1238 fn test_role_inheritance_two_hops() {
1239 let ctrl = make_controller();
1240 ctrl.register_role(role("root", vec![], &[])).unwrap();
1241 ctrl.register_role(role("middle", vec![], &["root"]))
1242 .unwrap();
1243 ctrl.register_role(role("leaf", vec![], &["middle"]))
1244 .unwrap();
1245 ctrl.register_subject(subject("u", &["leaf"])).unwrap();
1246 ctrl.add_policy(allow_policy("*", Permission::Admin, &["root"]))
1247 .unwrap();
1248
1249 let d = ctrl.check_access("u", "sys", Permission::Admin, 1).unwrap();
1250 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1251 }
1252
1253 #[test]
1254 fn test_roles_for_subject_bfs() {
1255 let ctrl = make_controller();
1256 ctrl.register_role(role("a", vec![], &[])).unwrap();
1257 ctrl.register_role(role("b", vec![], &["a"])).unwrap();
1258 ctrl.register_role(role("c", vec![], &["b"])).unwrap();
1259 ctrl.register_subject(subject("u", &["c"])).unwrap();
1260
1261 let roles = ctrl.roles_for_subject("u").unwrap();
1262 assert!(roles.contains(&"a".to_string()));
1263 assert!(roles.contains(&"b".to_string()));
1264 assert!(roles.contains(&"c".to_string()));
1265 assert_eq!(roles.len(), 3);
1266 }
1267
1268 #[test]
1269 fn test_roles_for_subject_diamond_inheritance() {
1270 let ctrl = make_controller();
1273 ctrl.register_role(role("a", vec![], &[])).unwrap();
1274 ctrl.register_role(role("b", vec![], &["a"])).unwrap();
1275 ctrl.register_role(role("c", vec![], &["a"])).unwrap();
1276 ctrl.register_role(role("d", vec![], &["b", "c"])).unwrap();
1277 ctrl.register_subject(subject("u", &["d"])).unwrap();
1278
1279 let roles = ctrl.roles_for_subject("u").unwrap();
1280 assert_eq!(roles.len(), 4);
1282 assert!(roles.contains(&"a".to_string()));
1283 }
1284
1285 #[test]
1286 fn test_roles_for_subject_not_found() {
1287 let ctrl = make_controller();
1288 let err = ctrl.roles_for_subject("ghost").unwrap_err();
1289 assert!(matches!(err, AclError::SubjectNotFound(_)));
1290 }
1291
1292 #[test]
1295 fn test_effective_permissions_read_write() {
1296 let ctrl = make_controller();
1297 ctrl.register_role(role("editor", vec![], &[])).unwrap();
1298 ctrl.register_subject(subject("ed", &["editor"])).unwrap();
1299 ctrl.add_policy(allow_policy("docs/*", Permission::Read, &["editor"]))
1300 .unwrap();
1301 ctrl.add_policy(allow_policy("docs/*", Permission::Write, &["editor"]))
1302 .unwrap();
1303 ctrl.add_policy(allow_policy("docs/*", Permission::Delete, &["admin"]))
1305 .unwrap();
1306
1307 let perms = ctrl.effective_permissions("ed", "docs/file.md").unwrap();
1308 assert!(perms.contains(&Permission::Read), "{perms:?}");
1309 assert!(perms.contains(&Permission::Write), "{perms:?}");
1310 assert!(!perms.contains(&Permission::Delete), "{perms:?}");
1311 }
1312
1313 #[test]
1314 fn test_effective_permissions_execute_custom() {
1315 let ctrl = make_controller();
1316 ctrl.register_role(role("runner", vec![], &[])).unwrap();
1317 ctrl.register_subject(subject("svc", &["runner"])).unwrap();
1318 ctrl.add_policy(allow_policy(
1319 "jobs/*",
1320 Permission::Execute("compress".to_string()),
1321 &["runner"],
1322 ))
1323 .unwrap();
1324
1325 let perms = ctrl.effective_permissions("svc", "jobs/task1").unwrap();
1326 assert!(
1327 perms.contains(&Permission::Execute("compress".to_string())),
1328 "{perms:?}"
1329 );
1330 }
1331
1332 #[test]
1333 fn test_effective_permissions_no_policies() {
1334 let ctrl = make_controller();
1335 ctrl.register_subject(subject("nobody", &[])).unwrap();
1336 let perms = ctrl
1337 .effective_permissions("nobody", "some/resource")
1338 .unwrap();
1339 assert!(perms.is_empty(), "{perms:?}");
1341 }
1342
1343 #[test]
1346 fn test_audit_log_entries_captured() {
1347 let ctrl = make_controller();
1348 ctrl.register_subject(subject("u", &[])).unwrap();
1349 ctrl.check_access("u", "res", Permission::Read, 42).unwrap();
1350 let entries = ctrl.audit_log_entries(None, None);
1351 assert_eq!(entries.len(), 1);
1352 assert_eq!(entries[0].subject_id, "u");
1353 assert_eq!(entries[0].resource, "res");
1354 assert_eq!(entries[0].timestamp, 42);
1355 }
1356
1357 #[test]
1358 fn test_audit_log_filter_by_subject() {
1359 let ctrl = make_controller();
1360 ctrl.register_subject(subject("alice", &[])).unwrap();
1361 ctrl.register_subject(subject("bob", &[])).unwrap();
1362 ctrl.check_access("alice", "r", Permission::Read, 1)
1363 .unwrap();
1364 ctrl.check_access("bob", "r", Permission::Read, 2).unwrap();
1365 ctrl.check_access("alice", "r", Permission::Write, 3)
1366 .unwrap();
1367
1368 let alice_entries = ctrl.audit_log_entries(Some("alice"), None);
1369 assert_eq!(alice_entries.len(), 2);
1370 for e in &alice_entries {
1371 assert_eq!(e.subject_id, "alice");
1372 }
1373 }
1374
1375 #[test]
1376 fn test_audit_log_filter_by_resource() {
1377 let ctrl = make_controller();
1378 ctrl.register_subject(subject("u", &[])).unwrap();
1379 ctrl.check_access("u", "res_a", Permission::Read, 1)
1380 .unwrap();
1381 ctrl.check_access("u", "res_b", Permission::Read, 2)
1382 .unwrap();
1383 ctrl.check_access("u", "res_a", Permission::Write, 3)
1384 .unwrap();
1385
1386 let entries = ctrl.audit_log_entries(None, Some("res_a"));
1387 assert_eq!(entries.len(), 2);
1388 for e in &entries {
1389 assert_eq!(e.resource, "res_a");
1390 }
1391 }
1392
1393 #[test]
1394 fn test_audit_log_max_capacity_eviction() {
1395 let ctrl = StorageAccessController::new(AclConfig {
1396 max_audit_entries: 5,
1397 ..AclConfig::default()
1398 });
1399 ctrl.register_subject(subject("u", &[])).unwrap();
1400 for i in 0..10u64 {
1401 ctrl.check_access("u", "r", Permission::Read, i).unwrap();
1402 }
1403 assert_eq!(ctrl.audit_log_len(), 5);
1404 let entries = ctrl.audit_log_entries(None, None);
1406 assert_eq!(entries[0].timestamp, 5);
1407 assert_eq!(entries[4].timestamp, 9);
1408 }
1409
1410 #[test]
1411 fn test_audit_log_disabled() {
1412 let ctrl = StorageAccessController::new(AclConfig {
1413 enable_audit: false,
1414 ..AclConfig::default()
1415 });
1416 ctrl.register_subject(subject("u", &[])).unwrap();
1417 ctrl.check_access("u", "r", Permission::Read, 1).unwrap();
1418 assert_eq!(ctrl.audit_log_len(), 0);
1419 }
1420
1421 #[test]
1424 fn test_stats_counts() {
1425 let ctrl = make_controller();
1426 ctrl.register_subject(subject("u", &[])).unwrap();
1427 for i in 0..4u64 {
1429 ctrl.check_access("u", "r", Permission::Read, i).unwrap();
1430 }
1431 let s = ctrl.stats();
1432 assert_eq!(s.decisions_made, 4);
1433 assert_eq!(s.denies, 4);
1434 assert_eq!(s.allows, 0);
1435 }
1436
1437 #[test]
1438 fn test_stats_allows() {
1439 let ctrl = make_controller();
1440 ctrl.register_role(role("r", vec![], &[])).unwrap();
1441 ctrl.register_subject(subject("u", &["r"])).unwrap();
1442 ctrl.add_policy(allow_policy("*", Permission::Read, &["r"]))
1443 .unwrap();
1444 ctrl.check_access("u", "x", Permission::Read, 1).unwrap();
1445 ctrl.check_access("u", "y", Permission::Read, 2).unwrap();
1446 let s = ctrl.stats();
1447 assert_eq!(s.allows, 2);
1448 assert_eq!(s.denies, 0);
1449 }
1450
1451 #[test]
1454 fn test_check_access_subject_not_found() {
1455 let ctrl = make_controller();
1456 let err = ctrl
1457 .check_access("ghost", "res", Permission::Read, 0)
1458 .unwrap_err();
1459 assert!(matches!(err, AclError::SubjectNotFound(_)));
1460 }
1461
1462 #[test]
1463 fn test_roles_for_subject_unknown() {
1464 let ctrl = make_controller();
1465 let err = ctrl.roles_for_subject("unknown").unwrap_err();
1466 assert!(matches!(err, AclError::SubjectNotFound(_)));
1467 }
1468
1469 #[test]
1472 fn test_execute_permission_specific_op() {
1473 let ctrl = make_controller();
1474 ctrl.register_role(role("worker", vec![], &[])).unwrap();
1475 ctrl.register_subject(subject("w", &["worker"])).unwrap();
1476 ctrl.add_policy(allow_policy(
1477 "jobs/*",
1478 Permission::Execute("replicate".to_string()),
1479 &["worker"],
1480 ))
1481 .unwrap();
1482
1483 let d = ctrl
1485 .check_access(
1486 "w",
1487 "jobs/task",
1488 Permission::Execute("replicate".to_string()),
1489 1,
1490 )
1491 .unwrap();
1492 assert!(matches!(d, AccessDecision::Allowed { .. }), "{d:?}");
1493
1494 let d2 = ctrl
1496 .check_access(
1497 "w",
1498 "jobs/task",
1499 Permission::Execute("compress".to_string()),
1500 2,
1501 )
1502 .unwrap();
1503 assert!(matches!(d2, AccessDecision::Denied { .. }), "{d2:?}");
1505 }
1506
1507 #[test]
1510 fn test_multiple_policies_first_applicable_wins() {
1511 let ctrl = make_controller();
1512 ctrl.register_role(role("staff", vec![], &[])).unwrap();
1513 ctrl.register_subject(subject("emp", &["staff"])).unwrap();
1514
1515 ctrl.add_policy(allow_policy("data/*", Permission::Read, &["staff"]))
1517 .unwrap();
1518 ctrl.add_policy(deny_policy("data/*", Permission::Write, &[]))
1520 .unwrap();
1521
1522 let read_d = ctrl
1523 .check_access("emp", "data/file", Permission::Read, 1)
1524 .unwrap();
1525 let write_d = ctrl
1526 .check_access("emp", "data/file", Permission::Write, 2)
1527 .unwrap();
1528
1529 assert!(
1530 matches!(read_d, AccessDecision::Allowed { .. }),
1531 "{read_d:?}"
1532 );
1533 assert!(
1534 matches!(write_d, AccessDecision::Denied { .. }),
1535 "{write_d:?}"
1536 );
1537 }
1538
1539 #[test]
1542 fn test_deny_effect_policy() {
1543 let ctrl = StorageAccessController::new(AclConfig {
1544 default_effect: PolicyEffect::Allow, ..AclConfig::default()
1546 });
1547 ctrl.register_role(role("r", vec![], &[])).unwrap();
1548 ctrl.register_subject(subject("u", &["r"])).unwrap();
1549 ctrl.add_policy(deny_policy("forbidden/*", Permission::Delete, &["r"]))
1550 .unwrap();
1551
1552 let d = ctrl
1553 .check_access("u", "forbidden/file", Permission::Delete, 1)
1554 .unwrap();
1555 assert!(matches!(d, AccessDecision::Denied { .. }), "{d:?}");
1556 }
1557
1558 #[test]
1561 fn test_concurrent_access() {
1562 use std::sync::Arc;
1563 use std::thread;
1564
1565 let ctrl = Arc::new(make_controller());
1566 ctrl.register_role(role("reader", vec![], &[])).unwrap();
1567 ctrl.register_subject(subject("concurrent_user", &["reader"]))
1568 .unwrap();
1569 ctrl.add_policy(allow_policy("shared/*", Permission::Read, &["reader"]))
1570 .unwrap();
1571
1572 let handles: Vec<_> = (0..8)
1573 .map(|i| {
1574 let c = Arc::clone(&ctrl);
1575 thread::spawn(move || {
1576 for j in 0..50u64 {
1577 let d = c
1578 .check_access(
1579 "concurrent_user",
1580 "shared/resource",
1581 Permission::Read,
1582 i * 50 + j,
1583 )
1584 .unwrap();
1585 assert!(matches!(d, AccessDecision::Allowed { .. }));
1586 }
1587 })
1588 })
1589 .collect();
1590
1591 for h in handles {
1592 h.join().expect("thread panicked");
1593 }
1594
1595 assert_eq!(ctrl.stats().decisions_made, 400);
1596 }
1597
1598 #[test]
1601 fn test_xorshift_rng_coverage() {
1602 let mut rng = Xorshift64::new(42);
1604 let values: Vec<u64> = (0..1000).map(|_| rng.next()).collect();
1605 let unique: HashSet<u64> = values.iter().cloned().collect();
1607 assert!(unique.len() > 990, "RNG appears degenerate");
1608 }
1609
1610 #[test]
1611 fn test_random_clearance_decisions() {
1612 let mut rng = Xorshift64::new(0xc0ffee);
1613 ctrl_register_role_shared_test(&mut rng);
1617 }
1618
1619 fn ctrl_register_role_shared_test(rng: &mut Xorshift64) {
1620 for i in 0..20usize {
1621 let clearance = rng.next_u8();
1622 let min_req = rng.next_u8();
1623
1624 let ctrl = make_controller();
1626 ctrl.register_role(role("classified_reader", vec![], &[]))
1627 .unwrap();
1628 let sid = format!("agent_{i}");
1629 ctrl.register_subject(subject_with_attrs(
1630 &sid,
1631 &["classified_reader"],
1632 &[],
1633 clearance,
1634 ))
1635 .unwrap();
1636
1637 let pat = format!("vault_{i:04}/*");
1639 let mut p = allow_policy(&pat, Permission::Read, &["classified_reader"]);
1640 p.min_clearance = min_req;
1641 ctrl.add_policy(p).unwrap();
1642
1643 let resource = format!("vault_{i:04}/secret");
1644 let d = ctrl
1645 .check_access(&sid, &resource, Permission::Read, i as u64)
1646 .unwrap();
1647 if clearance >= min_req {
1648 assert!(
1649 matches!(d, AccessDecision::Allowed { .. }),
1650 "i={i} clearance={clearance} min={min_req} → {d:?}"
1651 );
1652 } else {
1653 assert!(
1654 matches!(d, AccessDecision::Denied { .. }),
1655 "i={i} clearance={clearance} min={min_req} → {d:?}"
1656 );
1657 }
1658 }
1659 }
1660
1661 #[test]
1662 fn test_random_allow_deny_list_membership() {
1663 let mut rng = Xorshift64::new(0x1337);
1664 let ctrl = make_controller();
1665 let subjects: Vec<String> = (0..10)
1666 .map(|i| {
1667 let sid = format!("s{i}");
1668 ctrl.register_subject(subject(&sid, &[])).unwrap();
1669 sid
1670 })
1671 .collect();
1672
1673 let allowed_idx = rng.next_usize(subjects.len());
1674 let denied_idx = (allowed_idx + 1) % subjects.len();
1675
1676 let mut policy = allow_policy("*", Permission::List, &[]);
1677 policy.allow_list.push(subjects[allowed_idx].clone());
1678 policy.deny_list.push(subjects[denied_idx].clone());
1679 ctrl.add_policy(policy).unwrap();
1680
1681 let d_allow = ctrl
1682 .check_access(&subjects[allowed_idx], "res", Permission::List, 1)
1683 .unwrap();
1684 let d_deny = ctrl
1685 .check_access(&subjects[denied_idx], "res", Permission::List, 2)
1686 .unwrap();
1687
1688 assert!(
1689 matches!(d_allow, AccessDecision::Allowed { .. }),
1690 "{d_allow:?}"
1691 );
1692 assert!(
1693 matches!(d_deny, AccessDecision::Denied { .. }),
1694 "{d_deny:?}"
1695 );
1696 }
1697
1698 #[test]
1701 fn test_permission_matches_same_variant() {
1702 assert!(Permission::Read.matches(&Permission::Read));
1703 assert!(Permission::Admin.matches(&Permission::Admin));
1704 assert!(
1705 Permission::Execute("op".to_string()).matches(&Permission::Execute("op".to_string()))
1706 );
1707 }
1708
1709 #[test]
1710 fn test_permission_matches_different_execute_ops() {
1711 assert!(
1712 !Permission::Execute("a".to_string()).matches(&Permission::Execute("b".to_string()))
1713 );
1714 }
1715
1716 #[test]
1717 fn test_permission_matches_different_variants() {
1718 assert!(!Permission::Read.matches(&Permission::Write));
1719 assert!(!Permission::Delete.matches(&Permission::Admin));
1720 }
1721
1722 #[test]
1725 fn test_permission_display() {
1726 assert_eq!(Permission::Read.to_string(), "Read");
1727 assert_eq!(
1728 Permission::Execute("run_gc".to_string()).to_string(),
1729 "Execute(run_gc)"
1730 );
1731 }
1732
1733 #[test]
1736 fn test_acl_config_defaults() {
1737 let cfg = AclConfig::default();
1738 assert_eq!(cfg.default_effect, PolicyEffect::Deny);
1739 assert!(cfg.enable_audit);
1740 assert_eq!(cfg.max_audit_entries, 10_000);
1741 assert_eq!(cfg.role_inheritance_depth, 16);
1742 }
1743
1744 #[test]
1747 fn test_with_audit_log_closure() {
1748 let ctrl = make_controller();
1749 ctrl.register_subject(subject("u", &[])).unwrap();
1750 ctrl.check_access("u", "r", Permission::Read, 99).unwrap();
1751
1752 let ts = ctrl.with_audit_log(|log| log.back().map(|e| e.timestamp));
1753 assert_eq!(ts, Some(99));
1754 }
1755}