Skip to main content

ipfrs_storage/
storage_access_controller.rs

1//! Role-based and attribute-based access control (RBAC + ABAC) for storage operations.
2//!
3//! This module provides `StorageAccessController`, a production-grade engine that
4//! evaluates access requests against a set of resource policies.  Decisions are
5//! made via a deterministic algorithm:
6//!
7//! 1. Explicit **allow-list** membership → `Allowed`
8//! 2. Explicit **deny-list** membership → `Denied`
9//! 3. **Role** check  (required_roles, incl. inherited via BFS)
10//! 4. **Attribute** check (all required key-value pairs must be present)
11//! 5. **Clearance** check  (subject.clearance_level >= policy.min_clearance)
12//! 6. **Permission** check (required_permission must match the requested one)
13//! 7. Return the policy **effect** (Allow / Deny); no-match → `default_effect`
14//!
15//! An append-only audit log is maintained for every evaluated decision when
16//! `AclConfig::enable_audit` is `true`.
17
18use parking_lot::RwLock;
19use std::collections::{HashMap, HashSet, VecDeque};
20
21// ─────────────────────────────────────────────────────────────────────────────
22// Public types
23// ─────────────────────────────────────────────────────────────────────────────
24
25/// Atomic storage operation that a subject may wish to perform.
26#[derive(Debug, Clone, PartialEq, Eq, Hash)]
27pub enum Permission {
28    Read,
29    Write,
30    Delete,
31    List,
32    Admin,
33    Share,
34    /// Arbitrary named operation (e.g. `"compress"`, `"replicate"`).
35    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/// A named role carrying a set of permissions and optional parent roles.
62#[derive(Debug, Clone)]
63pub struct SacRole {
64    pub name: String,
65    pub permissions: Vec<Permission>,
66    /// Names of roles this role inherits permissions from.
67    pub inherits: Vec<String>,
68}
69
70/// Identity and attributes of a subject (user / service account).
71#[derive(Debug, Clone)]
72pub struct SubjectAttributes {
73    pub subject_id: String,
74    pub roles: Vec<String>,
75    /// Arbitrary key-value metadata (e.g. `("department", "eng")`).
76    pub attributes: Vec<(String, String)>,
77    /// Numeric clearance level: 0 = public, 255 = top secret.
78    pub clearance_level: u8,
79}
80
81/// Determines what happens when a policy matches a request.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum PolicyEffect {
84    Allow,
85    Deny,
86}
87
88/// A policy governing which subjects may perform an operation on a resource.
89#[derive(Debug, Clone)]
90pub struct ResourcePolicy {
91    /// Glob-like pattern: `"*"`, `"dir/*"`, `"*.json"`, or exact path.
92    pub resource_pattern: String,
93    /// The operation guarded by this policy.
94    pub required_permission: Permission,
95    /// Subject must hold at least one of these roles (incl. inherited).
96    /// Empty = any role is accepted.
97    pub required_roles: Vec<String>,
98    /// All of these key-value attributes must be present on the subject.
99    pub required_attributes: Vec<(String, String)>,
100    /// Subject's clearance_level must be >= this value.
101    pub min_clearance: u8,
102    /// Subjects in this list are always denied, regardless of other checks.
103    pub deny_list: Vec<String>,
104    /// Subjects in this list are always allowed (overrides deny_list).
105    pub allow_list: Vec<String>,
106    pub effect: PolicyEffect,
107}
108
109/// Final access decision with a human-readable reason.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum AccessDecision {
112    Allowed { reason: String },
113    Denied { reason: String },
114    NotApplicable,
115}
116
117/// One entry in the audit log.
118#[derive(Debug, Clone)]
119pub struct SacAuditEntry {
120    pub subject_id: String,
121    pub resource: String,
122    pub permission: Permission,
123    pub decision: AccessDecision,
124    /// Unix-epoch milliseconds (caller-supplied).
125    pub timestamp: u64,
126    /// Name of the policy whose effect decided the outcome (if any).
127    pub policy_matched: Option<String>,
128}
129
130/// Controller-wide configuration.
131#[derive(Debug, Clone)]
132pub struct AclConfig {
133    /// What to do when no policy matches. Defaults to `PolicyEffect::Deny`.
134    pub default_effect: PolicyEffect,
135    pub enable_audit: bool,
136    pub max_audit_entries: usize,
137    /// Maximum BFS hops when resolving role inheritance chains.
138    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/// Aggregated statistics snapshot.
153#[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/// Errors produced by the access controller.
163#[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
180// ─────────────────────────────────────────────────────────────────────────────
181// Internal helpers
182// ─────────────────────────────────────────────────────────────────────────────
183
184/// Simple glob matching:
185/// - `"*"` matches everything
186/// - `"prefix/*"` matches anything that starts with `"prefix/"`
187/// - `"*.ext"` matches anything that ends with `".ext"`
188/// - exact match otherwise
189fn 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
202// ─────────────────────────────────────────────────────────────────────────────
203// Internal mutable state
204// ─────────────────────────────────────────────────────────────────────────────
205
206struct 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    /// BFS role expansion — returns the full set of role names reachable from
230    /// `start_roles` honouring the `max_depth` limit.
231    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    /// Validate that no role introduces a cycle in the inheritance graph.
266    /// Uses DFS coloring: white (0) / gray (1) / black (2).
267    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; // cycle
286                    }
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    /// Append an entry to the audit log, evicting oldest if over capacity.
305    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
316// ─────────────────────────────────────────────────────────────────────────────
317// Public controller
318// ─────────────────────────────────────────────────────────────────────────────
319
320/// Thread-safe role-based and attribute-based access controller.
321pub struct StorageAccessController {
322    config: AclConfig,
323    state: RwLock<AclState>,
324}
325
326impl StorageAccessController {
327    /// Create a new controller with the supplied configuration.
328    pub fn new(config: AclConfig) -> Self {
329        StorageAccessController {
330            config,
331            state: RwLock::new(AclState::new()),
332        }
333    }
334
335    /// Create a controller with default (deny-by-default, audit-enabled) settings.
336    pub fn with_defaults() -> Self {
337        Self::new(AclConfig::default())
338    }
339
340    // ── Mutation ─────────────────────────────────────────────────────────────
341
342    /// Register (or replace) a subject identity and its attributes.
343    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    /// Register (or replace) a role definition.
350    ///
351    /// Returns [`AclError::CyclicRoleInheritance`] if adding this role would
352    /// introduce a cycle in the inheritance graph.
353    pub fn register_role(&self, role: SacRole) -> Result<(), AclError> {
354        {
355            let mut state = self.state.write();
356            // Insert tentatively so validate_no_cycle can see the new edges.
357            state.roles.insert(role.name.clone(), role.clone());
358            let result = state.validate_no_cycle(&role.name);
359            if result.is_err() {
360                // Rollback
361                state.roles.remove(&role.name);
362                return result;
363            }
364        }
365        Ok(())
366    }
367
368    /// Append a resource policy.
369    ///
370    /// Validates the resource pattern is non-empty.
371    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    /// Remove all policies whose `resource_pattern` equals `resource_pattern`.
383    ///
384    /// Returns `Err(AclError::InvalidPattern)` if no policies matched.
385    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    // ── Core evaluation ───────────────────────────────────────────────────────
400
401    /// Evaluate whether `subject_id` may perform `permission` on `resource`.
402    ///
403    /// `current_ts` is a caller-supplied Unix timestamp (milliseconds) used
404    /// only for audit log entries.
405    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        // We hold a read lock for evaluation then upgrade to write for stats/audit.
418        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            // Collect matching policies (those whose pattern covers the resource
429            // AND whose required_permission matches the requested one).
430            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        // Write stats and audit.
455        {
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    /// Return all permissions the subject is granted (i.e. yields `Allowed`)
481    /// for the given resource across the known permission variants.
482    ///
483    /// The set of probed permissions is:
484    /// `[Read, Write, Delete, List, Admin, Share]` plus any `Execute` ops that
485    /// appear in registered policies for this resource.
486    pub fn effective_permissions(
487        &self,
488        subject_id: &str,
489        resource: &str,
490    ) -> Result<Vec<Permission>, AclError> {
491        let ts = 0u64; // audit entries for this call use ts=0
492
493        // Collect Execute operation names from matching policies.
494        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    /// Return all role names the subject holds, including inherited roles (BFS).
536    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    // ── Audit ─────────────────────────────────────────────────────────────────
549
550    /// Query the audit log.
551    ///
552    /// Both filters are optional; omitting them returns the full log.
553    /// Entries are returned in insertion order (oldest first).
554    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    // ── Stats ─────────────────────────────────────────────────────────────────
572
573    /// Return a snapshot of controller-wide statistics.
574    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    /// Current number of entries in the audit log.
586    pub fn audit_log_len(&self) -> usize {
587        self.state.read().audit_log.len()
588    }
589
590    /// Direct read access to the audit log via a closure (avoids cloning).
591    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
600// ─────────────────────────────────────────────────────────────────────────────
601// Policy evaluation (pure function — no locks required)
602// ─────────────────────────────────────────────────────────────────────────────
603
604/// Evaluate a non-empty list of matching policies against a subject, returning
605/// `(decision, policy_pattern_that_decided)`.
606///
607/// Algorithm (first-match wins in the following priority order):
608///   1. allow_list  → Allowed
609///   2. deny_list   → Denied
610///   3. Roles + Attributes + Clearance + Permission → effect
611fn evaluate_policies(
612    subject: &SubjectAttributes,
613    subject_id: &str,
614    effective_roles: &HashSet<String>,
615    policies: &[&ResourcePolicy],
616) -> (AccessDecision, Option<String>) {
617    // Pass 1 – explicit allow_list (highest priority)
618    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    // Pass 2 – explicit deny_list
633    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    // Pass 3 – full RBAC+ABAC evaluation on each matching policy.
648    // We evaluate each policy and return the first applicable decision.
649    // A policy is "applicable" when all its constraints are satisfied; a
650    // constraint failure is itself a `Denied` decision.
651    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
659/// Evaluate one policy against a subject, returning an `(AccessDecision, pattern)` pair.
660fn evaluate_single_policy(
661    subject: &SubjectAttributes,
662    policy: &ResourcePolicy,
663    effective_roles: &HashSet<String>,
664) -> (AccessDecision, Option<String>) {
665    // Role check: if required_roles is non-empty the subject must hold at least one.
666    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    // Attribute check: every (key, value) pair must be present.
685    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    // Clearance check
704    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    // All checks passed – return the policy's effect.
717    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// ─────────────────────────────────────────────────────────────────────────────
734// Tests
735// ─────────────────────────────────────────────────────────────────────────────
736
737#[cfg(test)]
738mod tests {
739    use super::*;
740
741    // ── Inline xorshift64 PRNG (no rand crate) ────────────────────────────────
742
743    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    // ── Helpers ───────────────────────────────────────────────────────────────
766
767    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    // ── Pattern matching unit tests ───────────────────────────────────────────
832
833    #[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    // ── Subject registration ──────────────────────────────────────────────────
859
860    #[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    // ── Role registration ─────────────────────────────────────────────────────
880
881    #[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        // Stats.policies_count refers to policies, not roles.
888        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    // ── Policy management ─────────────────────────────────────────────────────
921
922    #[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    // ── Default-deny / default-allow ─────────────────────────────────────────
956
957    #[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    // ── Allow via role ────────────────────────────────────────────────────────
984
985    #[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        // Policy matches but dave lacks "admin" role → Denied
1012        assert!(matches!(d, AccessDecision::Denied { .. }), "{d:?}");
1013    }
1014
1015    // ── Deny list ─────────────────────────────────────────────────────────────
1016
1017    #[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    // ── Allow list ────────────────────────────────────────────────────────────
1054
1055    #[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()); // also on deny – allow wins
1063        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        // No roles registered; policy requires "admin" role
1075        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    // ── Attribute matching ────────────────────────────────────────────────────
1088
1089    #[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")], // missing "clearance"="secret"
1145            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    // ── Clearance level ───────────────────────────────────────────────────────
1165
1166    #[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    // ── Role inheritance (BFS) ────────────────────────────────────────────────
1218
1219    #[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        // a ← b ← d
1271        // a ← c ← d
1272        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        // Should contain a, b, c, d – no duplicates.
1281        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    // ── Effective permissions ─────────────────────────────────────────────────
1293
1294    #[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        // Delete allowed for admin only (not editor)
1304        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        // Default-deny: nothing is allowed
1340        assert!(perms.is_empty(), "{perms:?}");
1341    }
1342
1343    // ── Audit log ─────────────────────────────────────────────────────────────
1344
1345    #[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        // Oldest entries should have been evicted; newest should remain.
1405        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    // ── Statistics ────────────────────────────────────────────────────────────
1422
1423    #[test]
1424    fn test_stats_counts() {
1425        let ctrl = make_controller();
1426        ctrl.register_subject(subject("u", &[])).unwrap();
1427        // default-deny → all denied
1428        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    // ── Error cases ───────────────────────────────────────────────────────────
1452
1453    #[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    // ── Execute permission ────────────────────────────────────────────────────
1470
1471    #[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        // Correct op
1484        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        // Wrong op (same family but different string)
1495        let d2 = ctrl
1496            .check_access(
1497                "w",
1498                "jobs/task",
1499                Permission::Execute("compress".to_string()),
1500                2,
1501            )
1502            .unwrap();
1503        // no policy matches "compress" → default deny
1504        assert!(matches!(d2, AccessDecision::Denied { .. }), "{d2:?}");
1505    }
1506
1507    // ── Multiple policies on same resource ────────────────────────────────────
1508
1509    #[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        // Policy 1: allow staff read on data/*
1516        ctrl.add_policy(allow_policy("data/*", Permission::Read, &["staff"]))
1517            .unwrap();
1518        // Policy 2: deny everyone write on data/*
1519        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    // ── Deny policy effect ────────────────────────────────────────────────────
1540
1541    #[test]
1542    fn test_deny_effect_policy() {
1543        let ctrl = StorageAccessController::new(AclConfig {
1544            default_effect: PolicyEffect::Allow, // default allow
1545            ..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    // ── Concurrency / thread safety ───────────────────────────────────────────
1559
1560    #[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    // ── Random / property-style checks ───────────────────────────────────────
1599
1600    #[test]
1601    fn test_xorshift_rng_coverage() {
1602        // Smoke-test the inline PRNG used by other tests
1603        let mut rng = Xorshift64::new(42);
1604        let values: Vec<u64> = (0..1000).map(|_| rng.next()).collect();
1605        // All values should be distinct (with overwhelming probability)
1606        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        // Use a fresh controller per iteration to avoid cross-policy interference
1614        // from earlier iterations' policies matching later resources (e.g.
1615        // "agent_docs_1/*" prefix-matches "agent_docs_10/file").
1616        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            // Fresh controller per iteration guarantees exactly one policy matches.
1625            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            // Use zero-padded resource names to avoid prefix ambiguity in patterns.
1638            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    // ── Permission::matches self-test ─────────────────────────────────────────
1699
1700    #[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    // ── Permission::Display ───────────────────────────────────────────────────
1723
1724    #[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    // ── AclConfig defaults ────────────────────────────────────────────────────
1734
1735    #[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    // ── with_audit_log closure ────────────────────────────────────────────────
1745
1746    #[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}