1use std::collections::{BTreeMap, HashMap};
14
15use didbot_policy::PolicyId;
16
17use crate::records::{Includes, PolicyBinding};
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct Boundary {
24 pub principals: Vec<String>,
26}
27
28impl Boundary {
29 #[must_use]
31 pub fn of(principals: Vec<String>) -> Self {
32 Self { principals }
33 }
34
35 #[must_use]
37 pub fn alone(did: &str) -> Self {
38 Self::of(vec![did.to_owned()])
39 }
40
41 #[must_use]
44 pub fn server_wide() -> Self {
45 Self::default()
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Coverage {
52 pub subjects: Vec<String>,
54 pub includes: Includes,
56 pub excludes: Vec<String>,
58}
59
60impl Coverage {
61 #[must_use]
63 pub fn of(binding: &PolicyBinding) -> Self {
64 Self {
65 subjects: binding.subjects.clone(),
66 includes: binding.includes,
67 excludes: binding.excludes.clone(),
68 }
69 }
70
71 #[must_use]
76 pub fn covers(&self, boundary: &Boundary, server: &str) -> bool {
77 let chain = &boundary.principals;
78 let names = |list: &[String], did: &str| list.iter().any(|named| named == did);
79 if names(&self.excludes, server) || chain.iter().any(|did| names(&self.excludes, did)) {
80 return false;
81 }
82 let account = chain.first().map(String::as_str);
83 let by_subject =
84 self.includes.subjects && account.is_some_and(|did| names(&self.subjects, did));
85 let by_descent = self.includes.descendants
86 && (chain.iter().skip(1).any(|did| names(&self.subjects, did))
87 || (account != Some(server) && names(&self.subjects, server)));
88 by_subject || by_descent
89 }
90}
91
92#[derive(Debug, Clone, Default)]
99pub struct BindingIndex {
100 server: String,
101 bound: HashMap<PolicyId, Vec<Coverage>>,
102 names: HashMap<PolicyId, String>,
105}
106
107impl BindingIndex {
108 #[must_use]
116 pub fn of(
117 server: &str,
118 bindings: &BTreeMap<String, PolicyBinding>,
119 ids: &BTreeMap<String, Vec<String>>,
120 registered: &HashMap<String, PolicyId>,
121 ) -> (Self, Vec<String>) {
122 let mut coverages: HashMap<&str, Vec<Coverage>> = HashMap::new();
123 for binding in bindings.values() {
124 for policy in &binding.policies {
125 coverages
126 .entry(policy.key.as_str())
127 .or_default()
128 .push(Coverage::of(binding));
129 }
130 }
131 let mut bound = HashMap::new();
132 let mut unbound = Vec::new();
133 for (key, ids) in ids {
134 let coverage = coverages.remove(key.as_str()).unwrap_or_default();
135 if coverage.is_empty() {
136 unbound.push(key.clone());
137 }
138 for id in ids {
139 if let Some(policy) = registered.get(id) {
140 bound.insert(*policy, coverage.clone());
141 }
142 }
143 }
144 unbound.sort();
145 let names = registered
146 .iter()
147 .map(|(id, policy)| (*policy, id.clone()))
148 .collect();
149 (
150 Self {
151 server: server.to_owned(),
152 bound,
153 names,
154 },
155 unbound,
156 )
157 }
158
159 #[must_use]
161 pub fn name_of(&self, policy: PolicyId) -> Option<&str> {
162 self.names.get(&policy).map(String::as_str)
163 }
164
165 #[must_use]
167 pub fn applies(&self, policy: PolicyId, boundary: &Boundary) -> bool {
168 match self.bound.get(&policy) {
169 None => true,
170 Some(coverages) => coverages
171 .iter()
172 .any(|coverage| coverage.covers(boundary, &self.server)),
173 }
174 }
175
176 #[must_use]
178 pub fn len(&self) -> usize {
179 self.bound.len()
180 }
181
182 #[must_use]
184 pub fn is_empty(&self) -> bool {
185 self.bound.is_empty()
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 const SERVER: &str = "did:web:agents.example";
194 const POOL: &str = "did:web:pool.agents.example";
195 const HOST: &str = "did:web:host.agents.example";
196 const AGENT: &str = "did:web:agent.agents.example";
197
198 fn coverage(subjects: &[&str], includes: &[&str], excludes: &[&str]) -> Coverage {
199 Coverage {
200 subjects: subjects.iter().map(|s| (*s).to_owned()).collect(),
201 includes: Includes {
202 subjects: includes.contains(&"subjects"),
203 descendants: includes.contains(&"descendants"),
204 },
205 excludes: excludes.iter().map(|s| (*s).to_owned()).collect(),
206 }
207 }
208
209 #[test]
212 fn coverage_follows_the_chain_and_the_exclusions() {
213 let agent = Boundary::of(vec![AGENT.into(), HOST.into(), POOL.into()]);
214 let host = Boundary::of(vec![HOST.into(), POOL.into()]);
215 let pool = Boundary::alone(POOL);
216
217 let subjects_only = coverage(&[POOL], &["subjects"], &[]);
218 assert!(subjects_only.covers(&pool, SERVER));
219 assert!(!subjects_only.covers(&host, SERVER));
220
221 let descendants_only = coverage(&[POOL], &["descendants"], &[]);
222 assert!(!descendants_only.covers(&pool, SERVER));
223 assert!(descendants_only.covers(&host, SERVER));
224 assert!(descendants_only.covers(&agent, SERVER));
225
226 let carved = coverage(&[POOL], &["descendants"], &[HOST]);
227 assert!(!carved.covers(&host, SERVER), "the excluded account");
228 assert!(!carved.covers(&agent, SERVER), "and everything beneath it");
229
230 let whole_server = coverage(&[SERVER], &["descendants"], &[]);
231 assert!(whole_server.covers(&agent, SERVER));
232 assert!(whole_server.covers(&Boundary::alone("did:web:root.agents.example"), SERVER));
233 assert!(whole_server.covers(&Boundary::server_wide(), SERVER));
234 assert!(!coverage(&[SERVER], &["subjects"], &[]).covers(&agent, SERVER));
235
236 let elsewhere = coverage(
237 &["did:web:other.example"],
238 &["subjects", "descendants"],
239 &[],
240 );
241 assert!(!elsewhere.covers(&agent, SERVER));
242 assert!(!elsewhere.covers(&Boundary::server_wide(), SERVER));
243 }
244}