Skip to main content

steel_core/permission/
set.rs

1use std::mem;
2
3use super::{PermissionContext, PermissionExpr, PermissionKey, PermissionRuleContext};
4
5/// Resolved state of one matching permission rule.
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum PermissionState {
8    /// Explicitly grants the matching permission.
9    Allow,
10    /// Explicitly rejects the matching permission.
11    Deny,
12}
13
14/// One permission rule.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct PermissionEntry {
17    key: PermissionKey,
18    context: PermissionRuleContext,
19    state: PermissionState,
20}
21
22impl PermissionEntry {
23    /// Creates one global permission rule.
24    #[must_use]
25    pub const fn new(key: PermissionKey, state: PermissionState) -> Self {
26        Self {
27            key,
28            context: PermissionRuleContext::Global,
29            state,
30        }
31    }
32
33    /// Creates one contextual permission rule.
34    #[must_use]
35    pub const fn new_with_context(
36        key: PermissionKey,
37        context: PermissionRuleContext,
38        state: PermissionState,
39    ) -> Self {
40        Self {
41            key,
42            context,
43            state,
44        }
45    }
46
47    /// Creates a global allow rule.
48    #[must_use]
49    pub const fn allow(key: PermissionKey) -> Self {
50        Self::new(key, PermissionState::Allow)
51    }
52
53    /// Creates a contextual allow rule.
54    #[must_use]
55    pub const fn allow_with_context(key: PermissionKey, context: PermissionRuleContext) -> Self {
56        Self::new_with_context(key, context, PermissionState::Allow)
57    }
58
59    /// Creates a global deny rule.
60    #[must_use]
61    pub const fn deny(key: PermissionKey) -> Self {
62        Self::new(key, PermissionState::Deny)
63    }
64
65    /// Creates a contextual deny rule.
66    #[must_use]
67    pub const fn deny_with_context(key: PermissionKey, context: PermissionRuleContext) -> Self {
68        Self::new_with_context(key, context, PermissionState::Deny)
69    }
70
71    /// Returns the key pattern matched by this rule.
72    #[must_use]
73    pub const fn key(&self) -> &PermissionKey {
74        &self.key
75    }
76
77    /// Returns the runtime context constraint.
78    #[must_use]
79    pub const fn context(&self) -> &PermissionRuleContext {
80        &self.context
81    }
82
83    /// Returns whether this rule allows or denies.
84    #[must_use]
85    pub const fn state(&self) -> PermissionState {
86        self.state
87    }
88}
89
90/// A flat effective permission set with deterministic conflict resolution.
91#[derive(Clone, Debug, Default, PartialEq, Eq)]
92pub struct PermissionSet {
93    entries: Vec<PermissionEntry>,
94    sources: Vec<PermissionResolutionSource>,
95}
96
97impl PermissionSet {
98    /// Creates an empty set. Unset permissions resolve to `None` and are denied by `allows*`.
99    #[must_use]
100    pub const fn new() -> Self {
101        Self {
102            entries: Vec::new(),
103            sources: Vec::new(),
104        }
105    }
106
107    /// Creates a set of direct subject rules.
108    #[must_use]
109    pub fn from_entries(entries: impl IntoIterator<Item = PermissionEntry>) -> Self {
110        let entries = entries.into_iter().collect::<Vec<_>>();
111        let sources = vec![PermissionResolutionSource::Subject; entries.len()];
112        Self { entries, sources }
113    }
114
115    /// Returns all rules in insertion order.
116    #[must_use]
117    pub fn entries(&self) -> &[PermissionEntry] {
118        &self.entries
119    }
120
121    /// Adds a direct subject rule.
122    pub fn push(&mut self, entry: PermissionEntry) {
123        self.push_with_source(entry, PermissionResolutionSource::Subject);
124    }
125
126    /// Adds a global direct allow.
127    pub fn allow(&mut self, key: PermissionKey) {
128        self.push(PermissionEntry::allow(key));
129    }
130
131    /// Adds a contextual direct allow.
132    pub fn allow_in(&mut self, key: PermissionKey, context: PermissionRuleContext) {
133        self.push(PermissionEntry::allow_with_context(key, context));
134    }
135
136    /// Adds a global direct deny.
137    pub fn deny(&mut self, key: PermissionKey) {
138        self.push(PermissionEntry::deny(key));
139    }
140
141    /// Adds a contextual direct deny.
142    pub fn deny_in(&mut self, key: PermissionKey, context: PermissionRuleContext) {
143        self.push(PermissionEntry::deny_with_context(key, context));
144    }
145
146    /// Replaces the exact global rule for `key`.
147    pub fn set(&mut self, key: PermissionKey, state: PermissionState) {
148        self.set_in(key, PermissionRuleContext::Global, state);
149    }
150
151    /// Replaces the exact rule for `key` and `context`.
152    pub fn set_in(
153        &mut self,
154        key: PermissionKey,
155        context: PermissionRuleContext,
156        state: PermissionState,
157    ) {
158        self.retain_entries(|entry| entry.key != key || entry.context != context);
159        self.push(PermissionEntry::new_with_context(key, context, state));
160    }
161
162    /// Removes the exact global rule for `key`.
163    pub fn unset(&mut self, key: &PermissionKey) -> bool {
164        self.unset_in(key, &PermissionRuleContext::Global)
165    }
166
167    /// Removes the exact rule for `key` and `context`.
168    pub fn unset_in(&mut self, key: &PermissionKey, context: &PermissionRuleContext) -> bool {
169        let old_len = self.entries.len();
170        self.retain_entries(|entry| entry.key() != key || entry.context() != context);
171        self.entries.len() != old_len
172    }
173
174    /// Resolves one key globally.
175    #[must_use]
176    pub fn resolve_key(&self, key: &PermissionKey) -> Option<PermissionState> {
177        self.resolve_key_in(key, &PermissionContext::global())
178    }
179
180    /// Resolves one key in an active context.
181    #[must_use]
182    pub fn resolve_key_in(
183        &self,
184        key: &PermissionKey,
185        context: &PermissionContext,
186    ) -> Option<PermissionState> {
187        self.best_key_candidate(key, context)
188            .map(|candidate| candidate.state)
189    }
190
191    /// Resolves one key globally and returns the winning rule details.
192    #[must_use]
193    pub fn resolve_key_detailed(&self, key: &PermissionKey) -> Option<PermissionResolution> {
194        self.resolve_key_in_detailed(key, &PermissionContext::global())
195    }
196
197    /// Resolves one key in an active context and returns the winning rule details.
198    #[must_use]
199    pub fn resolve_key_in_detailed(
200        &self,
201        key: &PermissionKey,
202        context: &PermissionContext,
203    ) -> Option<PermissionResolution> {
204        self.best_key_candidate(key, context)
205            .map(|candidate| self.permission_resolution(candidate))
206    }
207
208    /// Resolves a child key that may inherit a broad parent grant.
209    #[must_use]
210    pub fn resolve_scoped_key(
211        &self,
212        parent: &PermissionKey,
213        key: &PermissionKey,
214    ) -> Option<PermissionState> {
215        self.resolve_scoped_key_in(parent, key, &PermissionContext::global())
216    }
217
218    /// Resolves a child key with a parent fallback in an active context.
219    #[must_use]
220    pub fn resolve_scoped_key_in(
221        &self,
222        parent: &PermissionKey,
223        key: &PermissionKey,
224        context: &PermissionContext,
225    ) -> Option<PermissionState> {
226        self.best_scoped_key_candidate(parent, key, context)
227            .map(|candidate| candidate.state)
228    }
229
230    /// Resolves a scoped child globally and returns the winning rule details.
231    #[must_use]
232    pub fn resolve_scoped_key_detailed(
233        &self,
234        parent: &PermissionKey,
235        key: &PermissionKey,
236    ) -> Option<PermissionResolution> {
237        self.resolve_scoped_key_in_detailed(parent, key, &PermissionContext::global())
238    }
239
240    /// Resolves a scoped child in an active context and returns winning rule details.
241    #[must_use]
242    pub fn resolve_scoped_key_in_detailed(
243        &self,
244        parent: &PermissionKey,
245        key: &PermissionKey,
246        context: &PermissionContext,
247    ) -> Option<PermissionResolution> {
248        self.best_scoped_key_candidate(parent, key, context)
249            .map(|candidate| self.permission_resolution(candidate))
250    }
251
252    /// Returns whether one global key resolves to allow.
253    #[must_use]
254    pub fn allows_key(&self, key: &PermissionKey) -> bool {
255        self.resolve_key(key) == Some(PermissionState::Allow)
256    }
257
258    /// Returns whether one key resolves to allow in `context`.
259    #[must_use]
260    pub fn allows_key_in(&self, key: &PermissionKey, context: &PermissionContext) -> bool {
261        self.resolve_key_in(key, context) == Some(PermissionState::Allow)
262    }
263
264    /// Returns whether one scoped child resolves to allow globally.
265    #[must_use]
266    pub fn allows_scoped_key(&self, parent: &PermissionKey, key: &PermissionKey) -> bool {
267        self.resolve_scoped_key(parent, key) == Some(PermissionState::Allow)
268    }
269
270    /// Returns whether one scoped child resolves to allow in `context`.
271    #[must_use]
272    pub fn allows_scoped_key_in(
273        &self,
274        parent: &PermissionKey,
275        key: &PermissionKey,
276        context: &PermissionContext,
277    ) -> bool {
278        self.resolve_scoped_key_in(parent, key, context) == Some(PermissionState::Allow)
279    }
280
281    /// Returns whether a global permission expression resolves to allow.
282    #[must_use]
283    pub fn allows(&self, permission: &PermissionExpr) -> bool {
284        self.allows_in(permission, &PermissionContext::global())
285    }
286
287    /// Returns whether an expression resolves to allow in `context`.
288    #[must_use]
289    pub fn allows_in(&self, permission: &PermissionExpr, context: &PermissionContext) -> bool {
290        self.resolve_in(permission, context) == Some(PermissionState::Allow)
291    }
292
293    /// Resolves a permission expression globally.
294    #[must_use]
295    pub fn resolve(&self, permission: &PermissionExpr) -> Option<PermissionState> {
296        self.resolve_in(permission, &PermissionContext::global())
297    }
298
299    /// Resolves a permission expression in `context`.
300    #[must_use]
301    pub fn resolve_in(
302        &self,
303        permission: &PermissionExpr,
304        context: &PermissionContext,
305    ) -> Option<PermissionState> {
306        match permission {
307            PermissionExpr::Key(key) => self.resolve_key_in(key, context),
308            PermissionExpr::ScopedKey { parent, key } => {
309                self.resolve_scoped_key_in(parent, key, context)
310            }
311            PermissionExpr::All(children) => resolve_all(children, self, context),
312            PermissionExpr::Any(children) => resolve_any(children, self, context),
313        }
314    }
315
316    pub(super) fn push_group(&mut self, entry: PermissionEntry, group: &str, group_priority: i32) {
317        self.push_with_source(
318            entry,
319            PermissionResolutionSource::Group {
320                name: group.to_owned(),
321                priority: group_priority,
322            },
323        );
324    }
325
326    fn push_with_source(&mut self, entry: PermissionEntry, source: PermissionResolutionSource) {
327        self.entries.push(entry);
328        self.sources.push(source);
329    }
330
331    fn retain_entries(&mut self, mut keep: impl FnMut(&PermissionEntry) -> bool) {
332        let entries = mem::take(&mut self.entries);
333        let sources = mem::take(&mut self.sources);
334        for (entry, source) in entries.into_iter().zip(sources) {
335            if keep(&entry) {
336                self.entries.push(entry);
337                self.sources.push(source);
338            }
339        }
340    }
341
342    fn best_key_candidate(
343        &self,
344        key: &PermissionKey,
345        context: &PermissionContext,
346    ) -> Option<PermissionCandidate> {
347        let mut best = None;
348        for (index, (entry, source)) in self.entries.iter().zip(&self.sources).enumerate() {
349            if !entry.context.matches(context) || !entry.key.matches(key) {
350                continue;
351            }
352            push_candidate(
353                &mut best,
354                index,
355                entry.key.specificity(),
356                entry.context.specificity(),
357                source,
358                entry.state,
359            );
360        }
361        best
362    }
363
364    fn best_scoped_key_candidate(
365        &self,
366        parent: &PermissionKey,
367        key: &PermissionKey,
368        context: &PermissionContext,
369    ) -> Option<PermissionCandidate> {
370        let mut best = None;
371        let parent_scopes_key = parent.scopes(key);
372        for (index, (entry, source)) in self.entries.iter().zip(&self.sources).enumerate() {
373            if !entry.context.matches(context) {
374                continue;
375            }
376            let matches_parent = parent_scopes_key && entry.key.matches(parent);
377            let matches_key = entry.key.matches(key);
378            if !matches_parent && !matches_key {
379                continue;
380            }
381
382            let mut specificity = entry.key.specificity();
383            if matches_key && !matches_parent {
384                specificity += 1;
385            }
386            push_candidate(
387                &mut best,
388                index,
389                specificity,
390                entry.context.specificity(),
391                source,
392                entry.state,
393            );
394        }
395        best
396    }
397
398    fn permission_resolution(&self, candidate: PermissionCandidate) -> PermissionResolution {
399        PermissionResolution {
400            entry: self.entries[candidate.entry_index].clone(),
401            source: candidate.source,
402            key_specificity: candidate.key_specificity,
403            context_specificity: candidate.context_specificity,
404        }
405    }
406}
407
408fn resolve_all(
409    children: &[PermissionExpr],
410    permissions: &PermissionSet,
411    context: &PermissionContext,
412) -> Option<PermissionState> {
413    if children.is_empty() {
414        return Some(PermissionState::Allow);
415    }
416    let mut saw_unset = false;
417    for child in children {
418        match permissions.resolve_in(child, context) {
419            Some(PermissionState::Allow) => {}
420            Some(PermissionState::Deny) => return Some(PermissionState::Deny),
421            None => saw_unset = true,
422        }
423    }
424    if saw_unset {
425        None
426    } else {
427        Some(PermissionState::Allow)
428    }
429}
430
431fn resolve_any(
432    children: &[PermissionExpr],
433    permissions: &PermissionSet,
434    context: &PermissionContext,
435) -> Option<PermissionState> {
436    if children.is_empty() {
437        return Some(PermissionState::Deny);
438    }
439    let mut saw_unset = false;
440    for child in children {
441        match permissions.resolve_in(child, context) {
442            Some(PermissionState::Allow) => return Some(PermissionState::Allow),
443            Some(PermissionState::Deny) => {}
444            None => saw_unset = true,
445        }
446    }
447    if saw_unset {
448        None
449    } else {
450        Some(PermissionState::Deny)
451    }
452}
453
454/// Origin of a rule in an effective permission set.
455#[derive(Clone, Debug, PartialEq, Eq)]
456pub enum PermissionResolutionSource {
457    /// A configured permission group contributed the rule.
458    Group {
459        /// Group name.
460        name: String,
461        /// Priority used for equally specific group conflicts.
462        priority: i32,
463    },
464    /// A direct subject override contributed the rule.
465    Subject,
466}
467
468impl PermissionResolutionSource {
469    /// Returns the contributing group name.
470    #[must_use]
471    pub fn group_name(&self) -> Option<&str> {
472        match self {
473            Self::Group { name, .. } => Some(name),
474            Self::Subject => None,
475        }
476    }
477
478    /// Returns the contributing group priority.
479    #[must_use]
480    pub const fn group_priority(&self) -> Option<i32> {
481        match self {
482            Self::Group { priority, .. } => Some(*priority),
483            Self::Subject => None,
484        }
485    }
486
487    pub(super) const fn rank(&self) -> usize {
488        match self {
489            Self::Group { .. } => 0,
490            Self::Subject => 1,
491        }
492    }
493
494    pub(super) const fn tie_priority(&self) -> i32 {
495        match self {
496            Self::Group { priority, .. } => *priority,
497            Self::Subject => 0,
498        }
499    }
500}
501
502/// Detailed winning permission rule.
503#[derive(Clone, Debug, PartialEq, Eq)]
504pub struct PermissionResolution {
505    entry: PermissionEntry,
506    source: PermissionResolutionSource,
507    key_specificity: usize,
508    context_specificity: usize,
509}
510
511impl PermissionResolution {
512    /// Returns the complete winning rule.
513    #[must_use]
514    pub const fn entry(&self) -> &PermissionEntry {
515        &self.entry
516    }
517
518    /// Returns where the winning rule came from.
519    #[must_use]
520    pub const fn source(&self) -> &PermissionResolutionSource {
521        &self.source
522    }
523
524    /// Returns the winning allow or deny state.
525    #[must_use]
526    pub const fn state(&self) -> PermissionState {
527        self.entry.state()
528    }
529
530    /// Returns the winning key pattern.
531    #[must_use]
532    pub const fn key(&self) -> &PermissionKey {
533        self.entry.key()
534    }
535
536    /// Returns the winning context constraint.
537    #[must_use]
538    pub const fn context(&self) -> &PermissionRuleContext {
539        self.entry.context()
540    }
541
542    /// Returns the key-specificity rank used during resolution.
543    #[must_use]
544    pub const fn key_specificity(&self) -> usize {
545        self.key_specificity
546    }
547
548    /// Returns the context-specificity rank used during resolution.
549    #[must_use]
550    pub const fn context_specificity(&self) -> usize {
551        self.context_specificity
552    }
553}
554
555#[derive(Clone, Debug, PartialEq, Eq)]
556struct PermissionCandidate {
557    entry_index: usize,
558    key_specificity: usize,
559    context_specificity: usize,
560    source: PermissionResolutionSource,
561    state: PermissionState,
562}
563
564impl PermissionCandidate {
565    const fn order(&self) -> (usize, usize, usize, i32) {
566        (
567            self.key_specificity,
568            self.context_specificity,
569            self.source.rank(),
570            self.source.tie_priority(),
571        )
572    }
573}
574
575fn push_candidate(
576    best: &mut Option<PermissionCandidate>,
577    entry_index: usize,
578    key_specificity: usize,
579    context_specificity: usize,
580    source: &PermissionResolutionSource,
581    state: PermissionState,
582) {
583    let candidate = PermissionCandidate {
584        entry_index,
585        key_specificity,
586        context_specificity,
587        source: source.clone(),
588        state,
589    };
590    match best {
591        None => *best = Some(candidate),
592        Some(current) if candidate.order() > current.order() => *best = Some(candidate),
593        Some(current)
594            if candidate.order() == current.order()
595                && current.state == PermissionState::Allow
596                && state == PermissionState::Deny =>
597        {
598            *best = Some(candidate);
599        }
600        _ => {}
601    }
602}