lean_ctx/core/trust/
policy.rs1use serde::{Deserialize, Serialize};
4
5use super::classification::{ClassifiedData, SecurityClassification};
6use super::identity::IdentityContext;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
10pub enum PolicyDecision {
11 Allow,
13 Deny,
15 Audit,
17}
18
19#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
22pub struct PolicyRule {
23 pub condition: String,
25 pub action: String,
27 pub evidence_required: bool,
29}
30
31impl PolicyRule {
32 #[must_use]
34 pub fn new(
35 condition: impl Into<String>,
36 action: impl Into<String>,
37 evidence_required: bool,
38 ) -> Self {
39 Self {
40 condition: condition.into(),
41 action: action.into(),
42 evidence_required,
43 }
44 }
45}
46
47pub trait PolicyContext {
49 fn classification(&self) -> Option<SecurityClassification>;
51
52 fn identity(&self) -> Option<&IdentityContext>;
54}
55
56impl PolicyContext for IdentityContext {
57 fn classification(&self) -> Option<SecurityClassification> {
58 None
59 }
60
61 fn identity(&self) -> Option<&IdentityContext> {
62 Some(self)
63 }
64}
65
66impl PolicyContext for SecurityClassification {
67 fn classification(&self) -> Option<SecurityClassification> {
68 Some(*self)
69 }
70
71 fn identity(&self) -> Option<&IdentityContext> {
72 None
73 }
74}
75
76impl<T> PolicyContext for ClassifiedData<T> {
77 fn classification(&self) -> Option<SecurityClassification> {
78 Some(self.classification)
79 }
80
81 fn identity(&self) -> Option<&IdentityContext> {
82 None
83 }
84}
85
86pub trait PolicyEvaluator {
88 fn evaluate(&self, context: &dyn PolicyContext, action: &str) -> PolicyDecision;
90}
91
92#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
94pub struct AllowAll;
95
96impl PolicyEvaluator for AllowAll {
97 fn evaluate(&self, _context: &dyn PolicyContext, _action: &str) -> PolicyDecision {
98 PolicyDecision::Allow
99 }
100}
101
102#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
104pub struct DenyAll;
105
106impl PolicyEvaluator for DenyAll {
107 fn evaluate(&self, _context: &dyn PolicyContext, _action: &str) -> PolicyDecision {
108 PolicyDecision::Deny
109 }
110}
111
112#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
115pub struct ClassificationBased {
116 pub maximum_allowed: SecurityClassification,
118 #[serde(default)]
120 pub rules: Vec<PolicyRule>,
121}
122
123impl ClassificationBased {
124 #[must_use]
126 pub fn new(maximum_allowed: SecurityClassification) -> Self {
127 Self {
128 maximum_allowed,
129 rules: Vec::new(),
130 }
131 }
132
133 #[must_use]
135 pub fn from_rules(maximum_allowed: SecurityClassification, rules: Vec<PolicyRule>) -> Self {
136 Self {
137 maximum_allowed,
138 rules,
139 }
140 }
141
142 #[must_use]
144 pub fn with_rule(mut self, rule: PolicyRule) -> Self {
145 self.rules.push(rule);
146 self
147 }
148
149 pub fn add_rule(&mut self, rule: PolicyRule) {
151 self.rules.push(rule);
152 }
153}
154
155impl PolicyEvaluator for ClassificationBased {
156 fn evaluate(&self, context: &dyn PolicyContext, action: &str) -> PolicyDecision {
157 let Some(classification) = context.classification() else {
158 return PolicyDecision::Audit;
161 };
162
163 if classification > self.maximum_allowed {
164 return PolicyDecision::Deny;
165 }
166
167 for rule in &self.rules {
168 if action_matches(&rule.action, action)
169 && condition_matches(&rule.condition, classification)
170 && rule.evidence_required
171 {
172 return PolicyDecision::Audit;
173 }
174 }
175 PolicyDecision::Allow
176 }
177}
178
179fn action_matches(pattern: &str, action: &str) -> bool {
180 let pattern = pattern.trim();
181 pattern == "*"
182 || pattern.eq_ignore_ascii_case(action.trim())
183 || pattern
184 .strip_suffix('*')
185 .is_some_and(|prefix| action.trim().starts_with(prefix))
186}
187
188fn condition_matches(condition: &str, classification: SecurityClassification) -> bool {
189 let condition = condition.trim();
190 if condition.is_empty() || condition == "*" || condition.eq_ignore_ascii_case("always") {
191 return true;
192 }
193
194 let (operator, value) = if let Some(value) = condition.strip_prefix("classification>=") {
195 (">=", value)
196 } else if let Some(value) = condition.strip_prefix("classification<=") {
197 ("<=", value)
198 } else if let Some(value) = condition.strip_prefix("classification=") {
199 ("=", value)
200 } else if let Some(value) = condition.strip_prefix("classification:") {
201 ("=", value)
202 } else {
203 ("=", condition)
204 };
205
206 let Some(required) = SecurityClassification::parse(value) else {
207 return false;
208 };
209 match operator {
210 ">=" => classification >= required,
211 "<=" => classification <= required,
212 "=" => classification == required,
213 _ => false,
214 }
215}