confium_pki/delegation/
constraint.rs1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "kind", rename_all = "snake_case")]
13pub enum Constraint {
14 ModelBound {
16 model_id: String,
18 },
19 NameBound {
21 name_pattern: String,
23 },
24 TimeBound {
26 not_before: DateTime<Utc>,
28 not_after: DateTime<Utc>,
30 },
31 CountBound {
33 max_issuances: u32,
35 },
36 GeographicBound {
38 regions: Vec<String>,
40 },
41 SubjectBound {
43 subjects: Vec<String>,
45 },
46}
47
48impl Constraint {
49 pub fn satisfies(&self, value: &ScopeValue) -> bool {
53 match (self, value) {
54 (Constraint::ModelBound { model_id }, ScopeValue::ModelId(actual)) => {
55 model_id == actual
56 }
57 (Constraint::NameBound { name_pattern }, ScopeValue::Name(actual)) => {
58 glob_match(name_pattern, actual)
59 }
60 (
61 Constraint::TimeBound {
62 not_before,
63 not_after,
64 },
65 ScopeValue::Time(when),
66 ) => when >= not_before && when <= not_after,
67 (Constraint::CountBound { max_issuances }, ScopeValue::Count(used)) => {
68 *used <= *max_issuances
69 }
70 (Constraint::GeographicBound { regions }, ScopeValue::Region(actual)) => {
71 regions.iter().any(|r| r == actual)
72 }
73 (Constraint::SubjectBound { subjects }, ScopeValue::Subject(actual)) => {
74 subjects.iter().any(|s| s == actual)
75 }
76 _ => false,
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub enum ScopeValue<'a> {
84 ModelId(&'a str),
86 Name(&'a str),
88 Time(DateTime<Utc>),
90 Count(u32),
92 Region(&'a str),
94 Subject(&'a str),
96}
97
98fn glob_match(pattern: &str, value: &str) -> bool {
100 fn helper(p: &[u8], v: &[u8]) -> bool {
101 match (p.first(), v.first()) {
102 (Some(b'*'), _) => helper(&p[1..], v) || (!v.is_empty() && helper(p, &v[1..])),
103 (Some(b'?'), Some(_)) => helper(&p[1..], &v[1..]),
104 (Some(pc), Some(vc)) if pc == vc => helper(&p[1..], &v[1..]),
105 (None, None) => true,
106 _ => false,
107 }
108 }
109 helper(pattern.as_bytes(), value.as_bytes())
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn model_bound_matches() {
118 let c = Constraint::ModelBound {
119 model_id: "FM-2026-A".into(),
120 };
121 assert!(c.satisfies(&ScopeValue::ModelId("FM-2026-A")));
122 assert!(!c.satisfies(&ScopeValue::ModelId("FM-2026-B")));
123 }
124
125 #[test]
126 fn name_bound_glob_matches() {
127 let c = Constraint::NameBound {
128 name_pattern: "*.example.com".into(),
129 };
130 assert!(c.satisfies(&ScopeValue::Name("www.example.com")));
131 assert!(c.satisfies(&ScopeValue::Name("api.example.com")));
132 assert!(!c.satisfies(&ScopeValue::Name("example.com")));
133 assert!(!c.satisfies(&ScopeValue::Name("evil.org")));
134 }
135
136 #[test]
137 fn geographic_bound_matches() {
138 let c = Constraint::GeographicBound {
139 regions: vec!["europe".into(), "americas".into()],
140 };
141 assert!(c.satisfies(&ScopeValue::Region("europe")));
142 assert!(c.satisfies(&ScopeValue::Region("americas")));
143 assert!(!c.satisfies(&ScopeValue::Region("asia-pacific")));
144 }
145}