1use std::collections::HashMap;
5
6use regex::Regex;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use sha2::{Digest, Sha256};
10
11use crate::canonicalize;
12use crate::guard::NegativeCapability;
13
14#[derive(Clone, Debug, Serialize, Deserialize, Default)]
15pub struct PolicyQuery {
16 pub subject: String,
17 #[serde(default)]
18 pub instance: Option<String>,
19 pub action: String,
20 #[serde(default)]
21 pub target: Option<String>,
22 #[serde(default)]
23 pub context: HashMap<String, Value>,
24 #[serde(default)]
25 pub negative_capabilities: Vec<NegativeCapability>,
26 #[serde(default)]
27 pub enforcement_level: Option<String>,
28 #[serde(default)]
29 pub now: Option<String>,
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
33pub struct PolicyDecision {
34 pub decision_version: String,
35 pub policy_engine: String,
36 pub engine_version: Option<String>,
37 pub trust_domain: String,
38 pub subject: String,
39 #[serde(skip_serializing_if = "Option::is_none", default)]
40 pub instance: Option<String>,
41 pub action: String,
42 #[serde(skip_serializing_if = "Option::is_none", default)]
43 pub target: Option<String>,
44 pub decision: String,
45 #[serde(skip_serializing_if = "Option::is_none", default)]
46 pub rule_id: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none", default)]
48 pub reason: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none", default)]
50 pub approval: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none", default)]
52 pub proof_required: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none", default)]
54 pub constraints_applied: Option<Vec<Value>>,
55 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub negative_capabilities_consulted: Option<Vec<NegativeCapability>>,
57 #[serde(skip_serializing_if = "Option::is_none", default)]
58 pub enforcement_level: Option<String>,
59 pub evaluated_at: String,
60 #[serde(skip_serializing_if = "Option::is_none", default)]
61 pub policy_manifest_hash: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none", default)]
63 pub context: Option<HashMap<String, Value>>,
64}
65
66#[derive(Clone, Debug, Serialize, Deserialize)]
67pub struct PolicyRule {
68 pub id: String,
69 pub effect: String,
70 #[serde(default)]
71 pub action: Option<String>,
72 #[serde(default)]
73 pub action_pattern: Option<String>,
74 #[serde(default)]
75 pub subject_pattern: Option<String>,
76 #[serde(default)]
77 pub target_patterns: Option<Vec<String>>,
78 #[serde(default)]
79 pub approval: Option<String>,
80 #[serde(default)]
81 pub proof_required: Option<String>,
82 #[serde(default)]
83 pub constraints: Option<Vec<Value>>,
84 #[serde(default)]
85 pub reason: Option<String>,
86}
87
88#[derive(Clone, Debug, Serialize, Deserialize)]
89pub struct PolicyManifest {
90 pub policy_version: String,
91 pub trust_domain: String,
92 #[serde(default)]
93 pub engine_hint: Option<String>,
94 pub rules: Vec<PolicyRule>,
95 #[serde(default)]
96 pub negative_capabilities: Vec<NegativeCapability>,
97 #[serde(default)]
98 pub continuous_reevaluation: Option<ContinuousReeval>,
99 #[serde(default)]
100 pub quorum_defaults: Option<QuorumDefaults>,
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize)]
104pub struct ContinuousReeval {
105 pub triggers: Vec<String>,
106}
107
108#[derive(Clone, Debug, Serialize, Deserialize)]
109pub struct QuorumDefaults {
110 pub min_approvers: u32,
111 pub of: Vec<String>,
112}
113
114pub trait PolicyEngineImpl {
120 fn evaluate(&self, query: &PolicyQuery) -> PolicyDecision;
121}
122
123pub struct NativePolicyEngine {
124 policy: PolicyManifest,
125 manifest_hash: String,
126}
127
128impl PolicyEngineImpl for NativePolicyEngine {
129 fn evaluate(&self, query: &PolicyQuery) -> PolicyDecision {
130 NativePolicyEngine::evaluate(self, query)
131 }
132}
133
134pub fn evaluate_with_engine(
144 hint: Option<&str>,
145 native: &NativePolicyEngine,
146 cedar: Option<&dyn PolicyEngineImpl>,
147 rego: Option<&dyn PolicyEngineImpl>,
148 query: &PolicyQuery,
149) -> PolicyDecision {
150 match hint {
151 Some("cedar") => match cedar {
152 Some(eng) => eng.evaluate(query),
153 None => unavailable_decision("cedar", query, native.policy.trust_domain.as_str()),
154 },
155 Some("rego") => match rego {
156 Some(eng) => eng.evaluate(query),
157 None => unavailable_decision("rego", query, native.policy.trust_domain.as_str()),
158 },
159 _ => native.evaluate(query),
160 }
161}
162
163fn unavailable_decision(engine: &str, query: &PolicyQuery, trust_domain: &str) -> PolicyDecision {
164 PolicyDecision {
165 decision_version: "1".into(),
166 policy_engine: engine.into(),
167 engine_version: Some(format!("{engine}-stub")),
168 trust_domain: trust_domain.into(),
169 subject: query.subject.clone(),
170 instance: query.instance.clone(),
171 action: query.action.clone(),
172 target: query.target.clone(),
173 decision: "deny".into(),
174 rule_id: None,
175 reason: Some(format!(
176 "{engine} engine not configured for this dispatcher (no adapter supplied)"
177 )),
178 approval: None,
179 proof_required: None,
180 constraints_applied: None,
181 negative_capabilities_consulted: None,
182 enforcement_level: query.enforcement_level.clone(),
183 evaluated_at: now_iso8601(),
184 policy_manifest_hash: None,
185 context: if query.context.is_empty() {
186 None
187 } else {
188 Some(query.context.clone())
189 },
190 }
191}
192
193impl NativePolicyEngine {
194 pub fn new(policy: PolicyManifest) -> Self {
195 let canonical_value = serde_json::to_value(&policy).unwrap_or(Value::Null);
196 let canonical = canonicalize(&canonical_value).unwrap_or_default();
197 let digest: [u8; 32] = Sha256::digest(canonical.as_bytes()).into();
198 let hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
199 let manifest_hash = format!("sha256-{}", hex);
200 NativePolicyEngine {
201 policy,
202 manifest_hash,
203 }
204 }
205
206 pub fn evaluate(&self, query: &PolicyQuery) -> PolicyDecision {
207 let now = query.now.clone().unwrap_or_else(now_iso8601);
208 let neg_caps = if query.negative_capabilities.is_empty() {
209 self.policy.negative_capabilities.clone()
210 } else {
211 query.negative_capabilities.clone()
212 };
213 for neg in &neg_caps {
214 if negative_matches(neg, query) {
215 return self.decision(
216 query,
217 "deny",
218 neg.reason
219 .clone()
220 .unwrap_or_else(|| format!("denied by negative_capability {}", neg.name)),
221 None,
222 None,
223 None,
224 None,
225 Some(&neg_caps),
226 &now,
227 );
228 }
229 }
230 for rule in &self.policy.rules {
231 if !rule_matches(rule, query) {
232 continue;
233 }
234 let reason = rule
235 .reason
236 .clone()
237 .unwrap_or_else(|| format!("matched rule {}", rule.id));
238 match rule.effect.as_str() {
239 "allow" => {
240 return self.decision(
241 query,
242 "allow",
243 reason,
244 Some(rule.id.clone()),
245 rule.constraints.clone(),
246 rule.proof_required.clone(),
247 rule.approval.clone(),
248 Some(&neg_caps),
249 &now,
250 );
251 }
252 "deny" => {
253 return self.decision(
254 query,
255 "deny",
256 reason,
257 Some(rule.id.clone()),
258 None,
259 None,
260 None,
261 Some(&neg_caps),
262 &now,
263 );
264 }
265 "escalate" => {
266 let decision = if rule.approval.as_deref() == Some("quorum") {
267 "escalate"
268 } else {
269 "approval-required"
270 };
271 return self.decision(
272 query,
273 decision,
274 reason,
275 Some(rule.id.clone()),
276 rule.constraints.clone(),
277 rule.proof_required.clone(),
278 rule.approval.clone().or_else(|| Some("required".into())),
279 Some(&neg_caps),
280 &now,
281 );
282 }
283 "log_only" => {
284 return self.decision(
285 query,
286 "log-only",
287 reason,
288 Some(rule.id.clone()),
289 rule.constraints.clone(),
290 rule.proof_required.clone(),
291 None,
292 Some(&neg_caps),
293 &now,
294 );
295 }
296 _ => continue,
297 }
298 }
299 self.decision(
300 query,
301 "deny",
302 "no matching rule (default deny)".into(),
303 None,
304 None,
305 None,
306 None,
307 Some(&neg_caps),
308 &now,
309 )
310 }
311
312 pub fn continuous_triggers(&self) -> Vec<String> {
313 self.policy
314 .continuous_reevaluation
315 .as_ref()
316 .map(|c| c.triggers.clone())
317 .unwrap_or_default()
318 }
319
320 pub fn quorum_defaults(&self) -> Option<&QuorumDefaults> {
321 self.policy.quorum_defaults.as_ref()
322 }
323
324 pub fn manifest_hash(&self) -> &str {
325 &self.manifest_hash
326 }
327
328 #[allow(clippy::too_many_arguments)]
329 fn decision(
330 &self,
331 query: &PolicyQuery,
332 decision: &str,
333 reason: String,
334 rule_id: Option<String>,
335 constraints: Option<Vec<Value>>,
336 proof: Option<String>,
337 approval: Option<String>,
338 neg_caps: Option<&[NegativeCapability]>,
339 now: &str,
340 ) -> PolicyDecision {
341 PolicyDecision {
342 decision_version: "1".into(),
343 policy_engine: "native".into(),
344 engine_version: Some("tf-policy-native-0.1.0".into()),
345 trust_domain: self.policy.trust_domain.clone(),
346 subject: query.subject.clone(),
347 instance: query.instance.clone(),
348 action: query.action.clone(),
349 target: query.target.clone(),
350 decision: decision.into(),
351 rule_id,
352 reason: Some(reason),
353 approval,
354 proof_required: proof,
355 constraints_applied: constraints.filter(|c| !c.is_empty()),
356 negative_capabilities_consulted: neg_caps.map(|c| c.to_vec()).filter(|v| !v.is_empty()),
357 enforcement_level: query.enforcement_level.clone(),
358 evaluated_at: now.into(),
359 policy_manifest_hash: Some(self.manifest_hash.clone()),
360 context: if query.context.is_empty() {
361 None
362 } else {
363 Some(query.context.clone())
364 },
365 }
366 }
367}
368
369fn rule_matches(rule: &PolicyRule, query: &PolicyQuery) -> bool {
370 if let Some(action) = &rule.action {
371 if action != &query.action {
372 return false;
373 }
374 }
375 if let Some(pattern) = &rule.action_pattern {
376 let re = match Regex::new(pattern) {
377 Ok(r) => r,
378 Err(_) => return false,
379 };
380 if !re.is_match(&query.action) {
381 return false;
382 }
383 }
384 if let Some(pattern) = &rule.subject_pattern {
385 let re = match Regex::new(pattern) {
386 Ok(r) => r,
387 Err(_) => return false,
388 };
389 if !re.is_match(&query.subject) {
390 return false;
391 }
392 }
393 if let Some(targets) = &rule.target_patterns {
394 if !targets.is_empty() {
395 let Some(target) = &query.target else {
396 return false;
397 };
398 if !targets.iter().any(|p| glob_match(p, target)) {
399 return false;
400 }
401 }
402 }
403 true
404}
405
406fn negative_matches(neg: &NegativeCapability, q: &PolicyQuery) -> bool {
407 if neg.name != q.action {
408 return false;
409 }
410 let Some(target_pattern) = neg.target.as_deref() else {
411 return true;
412 };
413 let Some(query_target) = q.target.as_deref() else {
414 return false;
415 };
416 glob_match(target_pattern, query_target)
417}
418
419fn glob_match(pattern: &str, value: &str) -> bool {
420 let mut re = String::from("^");
421 let bytes = pattern.as_bytes();
422 let mut i = 0;
423 while i < bytes.len() {
424 let b = bytes[i];
425 match b {
426 b'*' => {
427 if i + 1 < bytes.len() && bytes[i + 1] == b'*' {
428 re.push_str(".*");
429 i += 2;
430 } else {
431 re.push_str("[^/]*");
432 i += 1;
433 }
434 }
435 b'.' | b'+' | b'^' | b'$' | b'{' | b'}' | b'(' | b')' | b'|' | b'[' | b']' | b'\\' => {
436 re.push('\\');
437 re.push(b as char);
438 i += 1;
439 }
440 _ => {
441 re.push(b as char);
442 i += 1;
443 }
444 }
445 }
446 re.push('$');
447 Regex::new(&re).map(|r| r.is_match(value)).unwrap_or(false)
448}
449
450fn now_iso8601() -> String {
451 let secs = std::time::SystemTime::now()
452 .duration_since(std::time::UNIX_EPOCH)
453 .unwrap_or_default()
454 .as_secs() as i64;
455 let (year, month, day, hour, minute, second) = secs_to_ymdhms(secs);
456 format!(
457 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
458 year, month, day, hour, minute, second
459 )
460}
461
462fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
463 let days = secs.div_euclid(86_400);
464 let time = secs.rem_euclid(86_400);
465 let hour = (time / 3600) as u32;
466 let minute = ((time % 3600) / 60) as u32;
467 let second = (time % 60) as u32;
468 let z = days + 719_468;
469 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
470 let doe = (z - era * 146_097) as u64;
471 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
472 let y = yoe as i64 + era * 400;
473 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
474 let mp = (5 * doy + 2) / 153;
475 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
476 let m = if mp < 10 {
477 (mp + 3) as u32
478 } else {
479 (mp - 9) as u32
480 };
481 let year = if m <= 2 { y + 1 } else { y };
482 (year as i32, m, d, hour, minute, second)
483}