Skip to main content

logicaffeine_language/analysis/
policy.rs

1//! Security Policy Registry.
2//!
3//! Stores predicate and capability definitions parsed from `## Policy` blocks.
4//! These are used to generate security methods on structs and enforce them
5//! with the `Check` statement.
6
7use std::collections::HashMap;
8use logicaffeine_base::Symbol;
9
10/// Condition in a policy definition.
11/// Represents the predicate logic for security rules.
12#[derive(Debug, Clone)]
13pub enum PolicyCondition {
14    /// Field comparison: `the user's role equals "admin"`
15    FieldEquals {
16        field: Symbol,
17        value: Symbol,
18        /// Whether the value came from a string literal (needs quotes in codegen)
19        is_string_literal: bool,
20    },
21    /// Boolean field: `the user's verified equals true`
22    FieldBool {
23        field: Symbol,
24        value: bool,
25    },
26    /// Predicate call: `the user is admin`
27    Predicate {
28        subject: Symbol,
29        predicate: Symbol,
30    },
31    /// Whole-subject vs object field: `the user equals the document's owner`
32    /// (the object field shares the subject's type). Compiles to `self == &object.field`.
33    ObjectFieldEquals {
34        subject: Symbol,
35        object: Symbol,
36        field: Symbol,
37    },
38    /// Cross-field comparison: `the user's name equals the document's owner`.
39    /// Compiles to `self.<subject_field> == <object>.<object_field>`.
40    SubjectFieldEqualsObjectField {
41        subject_field: Symbol,
42        object: Symbol,
43        object_field: Symbol,
44    },
45    /// Logical OR: `A OR B`
46    Or(Box<PolicyCondition>, Box<PolicyCondition>),
47    /// Logical AND: `A AND B`
48    And(Box<PolicyCondition>, Box<PolicyCondition>),
49}
50
51/// A predicate definition: `A User is admin if the user's role equals "admin".`
52#[derive(Debug, Clone)]
53pub struct PredicateDef {
54    /// The type this predicate applies to (e.g., "User")
55    pub subject_type: Symbol,
56    /// The predicate name (e.g., "admin")
57    pub predicate_name: Symbol,
58    /// The condition that must be true
59    pub condition: PolicyCondition,
60}
61
62/// A capability definition: `A User can publish the Document if...`
63#[derive(Debug, Clone)]
64pub struct CapabilityDef {
65    /// The type that has this capability (e.g., "User")
66    pub subject_type: Symbol,
67    /// The action name (e.g., "publish")
68    pub action: Symbol,
69    /// The object type the action applies to (e.g., "Document")
70    pub object_type: Symbol,
71    /// The condition that must be true
72    pub condition: PolicyCondition,
73}
74
75/// Registry for security policies defined in `## Policy` blocks.
76#[derive(Debug, Default, Clone)]
77pub struct PolicyRegistry {
78    /// Predicates indexed by subject type
79    predicates: HashMap<Symbol, Vec<PredicateDef>>,
80    /// Capabilities indexed by subject type
81    capabilities: HashMap<Symbol, Vec<CapabilityDef>>,
82}
83
84impl PolicyRegistry {
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Register a predicate definition
90    pub fn register_predicate(&mut self, def: PredicateDef) {
91        self.predicates
92            .entry(def.subject_type)
93            .or_insert_with(Vec::new)
94            .push(def);
95    }
96
97    /// Register a capability definition
98    pub fn register_capability(&mut self, def: CapabilityDef) {
99        self.capabilities
100            .entry(def.subject_type)
101            .or_insert_with(Vec::new)
102            .push(def);
103    }
104
105    /// Get predicates for a type
106    pub fn get_predicates(&self, subject_type: Symbol) -> Option<&[PredicateDef]> {
107        self.predicates.get(&subject_type).map(|v| v.as_slice())
108    }
109
110    /// Get capabilities for a type
111    pub fn get_capabilities(&self, subject_type: Symbol) -> Option<&[CapabilityDef]> {
112        self.capabilities.get(&subject_type).map(|v| v.as_slice())
113    }
114
115    /// Check if a type has any predicates
116    pub fn has_predicates(&self, subject_type: Symbol) -> bool {
117        self.predicates.contains_key(&subject_type)
118    }
119
120    /// Check if a type has any capabilities
121    pub fn has_capabilities(&self, subject_type: Symbol) -> bool {
122        self.capabilities.contains_key(&subject_type)
123    }
124
125    /// Iterate over all types with predicates (for codegen)
126    pub fn iter_predicates(&self) -> impl Iterator<Item = (&Symbol, &Vec<PredicateDef>)> {
127        self.predicates.iter()
128    }
129
130    /// Iterate over all types with capabilities (for codegen)
131    pub fn iter_capabilities(&self) -> impl Iterator<Item = (&Symbol, &Vec<CapabilityDef>)> {
132        self.capabilities.iter()
133    }
134
135    /// Check if registry has any policies
136    pub fn is_empty(&self) -> bool {
137        self.predicates.is_empty() && self.capabilities.is_empty()
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use logicaffeine_base::Interner;
145
146    #[test]
147    fn registry_stores_predicates() {
148        let mut interner = Interner::new();
149        let mut registry = PolicyRegistry::new();
150
151        let user = interner.intern("User");
152        let admin = interner.intern("admin");
153        let role = interner.intern("role");
154        let admin_val = interner.intern("admin");
155
156        let def = PredicateDef {
157            subject_type: user,
158            predicate_name: admin,
159            condition: PolicyCondition::FieldEquals {
160                field: role,
161                value: admin_val,
162                is_string_literal: true,
163            },
164        };
165
166        registry.register_predicate(def);
167
168        assert!(registry.has_predicates(user));
169        let preds = registry.get_predicates(user).unwrap();
170        assert_eq!(preds.len(), 1);
171        assert_eq!(preds[0].predicate_name, admin);
172    }
173
174    #[test]
175    fn registry_stores_capabilities() {
176        let mut interner = Interner::new();
177        let mut registry = PolicyRegistry::new();
178
179        let user = interner.intern("User");
180        let doc = interner.intern("Document");
181        let publish = interner.intern("publish");
182        let admin = interner.intern("admin");
183        let user_var = interner.intern("user");
184
185        let def = CapabilityDef {
186            subject_type: user,
187            action: publish,
188            object_type: doc,
189            condition: PolicyCondition::Predicate {
190                subject: user_var,
191                predicate: admin,
192            },
193        };
194
195        registry.register_capability(def);
196
197        assert!(registry.has_capabilities(user));
198        let caps = registry.get_capabilities(user).unwrap();
199        assert_eq!(caps.len(), 1);
200        assert_eq!(caps[0].action, publish);
201        assert_eq!(caps[0].object_type, doc);
202    }
203}