Skip to main content

sova_scope/
matrix.rs

1//! Declarative role matrix builder.
2
3use crate::types::{Ability, Capabilities, ResourceRef};
4use std::collections::HashMap;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7pub enum ResourcePattern {
8    All,
9    Kind(&'static str),
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum Condition {
14    Always,
15    Owner,
16}
17
18#[derive(Clone, Debug)]
19struct Rule {
20    role: &'static str,
21    ability: Ability,
22    resource: ResourcePattern,
23    condition: Condition,
24    allow: bool,
25}
26
27#[derive(Clone, Debug, Default)]
28pub struct RoleMatrix {
29    rules: Vec<Rule>,
30    allow_all_roles: Vec<&'static str>,
31}
32
33impl RoleMatrix {
34    pub fn evaluate(
35        &self,
36        role: &str,
37        ability: Ability,
38        resource: Option<ResourceRef>,
39        is_owner: bool,
40    ) -> bool {
41        if self.allow_all_roles.contains(&role) {
42            return true;
43        }
44
45        let mut decided: Option<bool> = None;
46        for rule in &self.rules {
47            if rule.role != role || rule.ability != ability {
48                continue;
49            }
50            if !resource_matches(rule.resource, resource) {
51                continue;
52            }
53            let ok = match rule.condition {
54                Condition::Always => true,
55                Condition::Owner => is_owner,
56            };
57            if ok {
58                decided = Some(rule.allow);
59            }
60        }
61
62        decided.unwrap_or(false)
63    }
64
65    pub fn capabilities(
66        &self,
67        role: &str,
68        resource: Option<ResourceRef>,
69        is_owner: bool,
70    ) -> Capabilities {
71        Capabilities {
72            view: self.evaluate(role, Ability::View, resource, is_owner),
73            create: self.evaluate(role, Ability::Create, resource, is_owner),
74            update: self.evaluate(role, Ability::Update, resource, is_owner),
75            delete: self.evaluate(role, Ability::Delete, resource, is_owner),
76            manage: self.evaluate(role, Ability::Manage, resource, is_owner),
77        }
78    }
79}
80
81fn resource_matches(pattern: ResourcePattern, resource: Option<ResourceRef>) -> bool {
82    match pattern {
83        ResourcePattern::All => true,
84        ResourcePattern::Kind(kind) => resource.is_some_and(|r| r.kind == kind),
85    }
86}
87
88#[derive(Default)]
89pub struct MatrixBuilder {
90    kinds: HashMap<&'static str, RoleMatrix>,
91}
92
93impl MatrixBuilder {
94    pub fn allow_all(&mut self, scope_kind: &'static str, role: &'static str) -> &mut Self {
95        self.kinds
96            .entry(scope_kind)
97            .or_default()
98            .allow_all_roles
99            .push(role);
100        self
101    }
102
103    pub fn allow(
104        &mut self,
105        scope_kind: &'static str,
106        role: &'static str,
107        ability: Ability,
108        resource: ResourcePattern,
109        condition: Condition,
110    ) -> &mut Self {
111        self.kinds
112            .entry(scope_kind)
113            .or_default()
114            .rules
115            .push(Rule {
116                role,
117                ability,
118                resource,
119                condition,
120                allow: true,
121            });
122        self
123    }
124
125    pub fn deny(
126        &mut self,
127        scope_kind: &'static str,
128        role: &'static str,
129        ability: Ability,
130        resource: ResourcePattern,
131        condition: Condition,
132    ) -> &mut Self {
133        self.kinds
134            .entry(scope_kind)
135            .or_default()
136            .rules
137            .push(Rule {
138                role,
139                ability,
140                resource,
141                condition,
142                allow: false,
143            });
144        self
145    }
146
147    pub fn build(self) -> HashMap<&'static str, RoleMatrix> {
148        self.kinds
149    }
150}
151
152// Ergonomics aliases used in docs / atlas config.