didbot_policy/index.rs
1//! The applicability index: the tree that turns a subject into an ordered
2//! list of candidate policies.
3//!
4//! `plan/policy.md`: "The tree indexes; the list evaluates [...] a write
5//! matching nothing has paid only for a walk it already owed." Two
6//! independent trees are built per subject shape — one from each policy's
7//! decision applicability, one from its observation surface — because a
8//! policy can be told about an attempt without being asked to decide it.
9
10use std::collections::HashMap;
11use std::sync::Arc;
12
13use crate::outcome::Evaluator;
14use crate::outcome::Outcome;
15use crate::policy::{
16 Applicability, CompiledId, EvaluatorId, KindSet, PolicyDeclaration, PolicyId, Predicate, Scope,
17};
18use crate::subject::{Change, Subject, SubjectKind, WriteAction};
19
20/// Split one path into the segments this index routes on: `.` separates
21/// them, and an `[index]` subscript is a segment of its own rather than
22/// part of the field name it follows.
23///
24/// Both halves of the convention exist because the write path that produces
25/// a [`Change::path`] writes an array element as `tags[0]` and a nested one
26/// as `labels.values[0].val` (see `didbot_pds::policy::diff`). Splitting on
27/// `.` alone would make `tags[0]` a single opaque segment that the declared
28/// prefix `tags` could never be an ancestor of — so a policy guarding a
29/// field would silently stop guarding it the moment that field held an
30/// array, while the identical policy over a scalar field worked. A prefix
31/// can still name a specific element (`tags[0]`) by writing the subscript
32/// out; it segments the same way on both sides.
33fn segments(path: &str) -> Vec<&str> {
34 let mut out = Vec::new();
35 for part in path.split('.') {
36 let mut start = 0;
37 for (i, c) in part.char_indices() {
38 if c == '[' && i > start {
39 out.push(&part[start..i]);
40 start = i;
41 } else if c == ']' {
42 out.push(&part[start..=i]);
43 start = i + c.len_utf8();
44 }
45 }
46 if start < part.len() || start == 0 {
47 out.push(&part[start..]);
48 }
49 }
50 out
51}
52
53/// A trie over changed-path segments, as [`segments`] splits them.
54///
55/// `here` holds policies whose declared prefix ends at this node — which
56/// includes the root, for `Scope::Any` path prefixes, so those match a write
57/// regardless of its diff, even one with no changed paths.
58#[derive(Debug, Default)]
59struct PathNode {
60 here: Vec<PolicyId>,
61 children: HashMap<String, PathNode>,
62}
63
64impl PathNode {
65 fn insert_here(&mut self, policy: PolicyId) {
66 self.here.push(policy);
67 }
68
69 fn insert_at(&mut self, prefix: &str, policy: PolicyId) {
70 let mut node = self;
71 for segment in segments(prefix) {
72 node = node.children.entry(segment.to_string()).or_default();
73 }
74 node.here.push(policy);
75 }
76
77 /// Collect every policy whose declared prefix is an ancestor of, or
78 /// equal to, `path` — walking exactly the segments `path` has, never
79 /// more.
80 fn collect(&self, path: &str, out: &mut Vec<PolicyId>) {
81 let mut node = self;
82 for segment in segments(path) {
83 match node.children.get(segment) {
84 Some(next) => {
85 node = next;
86 out.extend_from_slice(&node.here);
87 }
88 None => return,
89 }
90 }
91 }
92}
93
94/// One collection's trie, plus the action and account-kind filters each
95/// entry was declared with. Both are indexed in parallel with a
96/// synthetic per-policy mask recorded at insertion — kept as side tables
97/// rather than inline in [`PathNode`] so the trie stays generic over "a
98/// policy matched here" and each filter test stays one lookup, not a scan.
99#[derive(Debug, Default)]
100struct CollectionTrie {
101 paths: PathNode,
102 actions: HashMap<PolicyId, crate::policy::ActionSet>,
103 kinds: HashMap<PolicyId, KindSet>,
104}
105
106/// The write-subject index: collection, then changed path, then action.
107#[derive(Debug, Default)]
108struct WriteIndex {
109 /// Policies declared for a specific set of collections.
110 by_collection: HashMap<String, CollectionTrie>,
111 /// Policies declared with `Scope::Any` collections — checked for every
112 /// write regardless of its collection.
113 any_collection: CollectionTrie,
114}
115
116impl WriteIndex {
117 fn insert(
118 &mut self,
119 policy: PolicyId,
120 collections: &Scope<String>,
121 kinds: KindSet,
122 actions: crate::policy::ActionSet,
123 path_prefixes: &Scope<String>,
124 ) {
125 let apply = |trie: &mut CollectionTrie| {
126 trie.actions.insert(policy, actions);
127 trie.kinds.insert(policy, kinds);
128 match path_prefixes {
129 Scope::Any => trie.paths.insert_here(policy),
130 Scope::Only(prefixes) => {
131 for prefix in prefixes {
132 trie.paths.insert_at(prefix, policy);
133 }
134 }
135 }
136 };
137 match collections {
138 Scope::Any => apply(&mut self.any_collection),
139 Scope::Only(names) => {
140 for name in names {
141 apply(self.by_collection.entry(name.clone()).or_default());
142 }
143 }
144 }
145 }
146
147 /// A write matching no policy costs exactly this: at most two hash
148 /// lookups (the specific collection, the `Scope::Any` bucket) and,
149 /// only when one of those exists, a walk bounded by the write's own
150 /// changed paths — never by the number of policies registered.
151 ///
152 /// `changes` carries before/after values (see [`Change`]); this index
153 /// only ever reads `change.path` out of it — routing is on where a
154 /// write touched, not on what it changed to. A value predicate belongs
155 /// in the evaluator that a matched policy names, with the full
156 /// `Change` and [`Subject`] available to it.
157 fn candidates(
158 &self,
159 collection: &str,
160 kind: SubjectKind<'_>,
161 action: WriteAction,
162 changes: &[Change<'_>],
163 ) -> Vec<PolicyId> {
164 let mut out = Vec::new();
165 for trie in [
166 Some(&self.any_collection),
167 self.by_collection.get(collection),
168 ]
169 .into_iter()
170 .flatten()
171 {
172 let mut matched = trie.paths.here.clone();
173 for change in changes {
174 trie.paths.collect(change.path, &mut matched);
175 }
176 matched.retain(|policy| {
177 trie.actions
178 .get(policy)
179 .is_some_and(|set| set.contains(action))
180 && trie.kinds.get(policy).is_some_and(|set| set.contains(kind))
181 });
182 out.extend(matched);
183 }
184 out.sort_unstable_by_key(|p| p.index());
185 out.dedup();
186 out
187 }
188}
189
190/// The index for a subject keyed on its client alone: a grant or a token.
191#[derive(Debug, Default)]
192struct ClientIndex {
193 by_client: HashMap<String, Vec<PolicyId>>,
194 any_client: Vec<PolicyId>,
195}
196
197impl ClientIndex {
198 fn insert(&mut self, policy: PolicyId, client_ids: &Scope<String>) {
199 match client_ids {
200 Scope::Any => self.any_client.push(policy),
201 Scope::Only(ids) => {
202 for id in ids {
203 self.by_client.entry(id.clone()).or_default().push(policy);
204 }
205 }
206 }
207 }
208
209 fn candidates(&self, client_id: &str) -> Vec<PolicyId> {
210 let mut out = self.any_client.clone();
211 if let Some(specific) = self.by_client.get(client_id) {
212 out.extend_from_slice(specific);
213 }
214 out.sort_unstable_by_key(|p| p.index());
215 out.dedup();
216 out
217 }
218}
219
220/// One tree per subject shape.
221#[derive(Debug, Default)]
222struct Trees {
223 write: WriteIndex,
224 blob: Vec<PolicyId>,
225 grant: ClientIndex,
226 token: ClientIndex,
227 create: Vec<PolicyId>,
228}
229
230impl Trees {
231 fn insert(&mut self, policy: PolicyId, applicability: &Applicability) {
232 match applicability {
233 Applicability::Write {
234 kinds,
235 collections,
236 actions,
237 path_prefixes,
238 } => self
239 .write
240 .insert(policy, collections, *kinds, *actions, path_prefixes),
241 Applicability::Blob => self.blob.push(policy),
242 Applicability::Grant { client_ids } => self.grant.insert(policy, client_ids),
243 Applicability::Token { client_ids } => self.token.insert(policy, client_ids),
244 Applicability::Create => self.create.push(policy),
245 }
246 }
247
248 fn candidates(&self, subject: &Subject<'_>) -> Vec<PolicyId> {
249 match subject {
250 Subject::Write {
251 attributes,
252 collection,
253 action,
254 diff,
255 ..
256 } => self
257 .write
258 .candidates(collection, attributes.kind, *action, diff.changes),
259 Subject::Blob { .. } => self.blob.clone(),
260 Subject::Grant { client_id, .. } => self.grant.candidates(client_id.as_str()),
261 Subject::Token { client_id, .. } => self.token.candidates(client_id.as_str()),
262 Subject::Create { .. } => self.create.clone(),
263 }
264 }
265}
266
267/// The pair of trees every subject is routed through: decision and
268/// observation.
269#[derive(Debug, Default)]
270struct ShapeIndex {
271 decision: Trees,
272 observation: Trees,
273}
274
275/// The built applicability index: every registered policy's predicate, and
276/// the decision and observation trees to route a subject through.
277///
278/// Built once, from a fixed set of [`PolicyDeclaration`]s, via
279/// [`PolicyTreeBuilder`]. `PolicyId` order is registration order, which is
280/// this server's presentational evaluation order — see `PolicyId`'s doc
281/// comment.
282pub struct PolicyTree {
283 evaluators: Vec<Arc<dyn Evaluator>>,
284 predicates: Vec<Predicate>,
285 index: ShapeIndex,
286}
287
288impl PolicyTree {
289 /// Start building a tree.
290 pub fn builder() -> PolicyTreeBuilder {
291 PolicyTreeBuilder::default()
292 }
293
294 /// Every evaluator this tree holds, each named once regardless of how
295 /// many policies it serves.
296 ///
297 /// A lifecycle event (froze, unfroze, deleted — see
298 /// [`Evaluator::lifecycle`]) is about an *account*, not a subject shape
299 /// or a collection, so it has no applicability to route it through the
300 /// way [`decision_candidates`](Self::decision_candidates) and
301 /// [`observation_candidates`](Self::observation_candidates) do for a
302 /// subject. A caller telling every evaluator about a lifecycle
303 /// event — see `plan/policy.md`'s "An evaluator can never unfreeze [...]
304 /// the evaluator must be told, or the state that triggered the freeze
305 /// survives the unfreeze and slams it shut again" — needs the complete
306 /// set, not a filtered one, which is what this returns.
307 pub fn evaluators(&self) -> impl Iterator<Item = &Arc<dyn Evaluator>> {
308 self.evaluators.iter()
309 }
310
311 /// The evaluator responsible for `policy`, if it has one.
312 ///
313 /// `None` for a policy whose [`Predicate`] is
314 /// [`Unusable`](Predicate::Unusable) — there is nothing to call
315 /// `observe` or `lifecycle` on. A caller walking
316 /// [`observation_candidates`](Self::observation_candidates) skips such
317 /// a policy for those two channels; [`decide`](Self::decide) already
318 /// accounts for it on the decision channel.
319 ///
320 /// Panics if `policy` did not come from this tree — the same programmer
321 /// error as indexing a `Vec` out of bounds, and treated the same way.
322 pub fn evaluator(&self, policy: PolicyId) -> Option<&dyn Evaluator> {
323 match &self.predicates[policy.index() as usize] {
324 Predicate::Evaluator(evaluator_id, _) => {
325 Some(self.evaluators[evaluator_id.0 as usize].as_ref())
326 }
327 Predicate::Unusable { .. } => None,
328 }
329 }
330
331 /// The compiled artifact `policy`'s evaluator was told to use, if it
332 /// has one — see [`CompiledId`]'s doc comment. `None` for a policy
333 /// whose predicate is [`Unusable`](Predicate::Unusable), same as
334 /// [`evaluator`](Self::evaluator): there is no artifact to name.
335 ///
336 /// Panics if `policy` did not come from this tree — see
337 /// [`evaluator`](Self::evaluator).
338 pub fn compiled_id(&self, policy: PolicyId) -> Option<CompiledId> {
339 match &self.predicates[policy.index() as usize] {
340 Predicate::Evaluator(_, compiled) => Some(*compiled),
341 Predicate::Unusable { .. } => None,
342 }
343 }
344
345 /// What `policy` decides about `subject`, handling both a working
346 /// evaluator and an [`Unusable`](Predicate::Unusable) predicate the
347 /// same way `evaluate` handles an [`EvalError`](crate::outcome::EvalError):
348 /// fail-closed, scoped to exactly this policy's applicability.
349 ///
350 /// Panics if `policy` did not come from this tree — see
351 /// [`evaluator`](Self::evaluator).
352 pub fn decide(&self, subject: &Subject<'_>, policy: PolicyId) -> Outcome {
353 self.decide_detailed(subject, policy).0
354 }
355
356 /// [`decide`](Self::decide), plus whether the outcome is the evaluator's
357 /// own decision (`false`) or a fail-closed refusal this tree substituted
358 /// because the evaluator could not answer at all (`true`): an
359 /// [`EvalError`](crate::outcome::EvalError), or a predicate that never
360 /// compiled. The gate never needs the difference — both refuse alike —
361 /// but a page explaining a refusal to an operator does: "the policy
362 /// refused this" and "the policy could not be checked" are different
363 /// claims, and only this method's caller can tell them apart.
364 ///
365 /// Panics if `policy` did not come from this tree — see
366 /// [`evaluator`](Self::evaluator).
367 pub fn decide_detailed(&self, subject: &Subject<'_>, policy: PolicyId) -> (Outcome, bool) {
368 match &self.predicates[policy.index() as usize] {
369 Predicate::Evaluator(evaluator_id, compiled) => {
370 let evaluator = self.evaluators[evaluator_id.0 as usize].as_ref();
371 match evaluator.evaluate(subject, *compiled) {
372 Ok(outcome) => (outcome, false),
373 Err(err) => (
374 Outcome::Reject {
375 // `EvalError`'s own `Display` already says what
376 // kind of failure this was ("evaluator timed
377 // out", "evaluator failed: ..."); wrapping it in
378 // another "evaluator failed:" would say it twice.
379 reason: err.to_string(),
380 },
381 true,
382 ),
383 }
384 }
385 Predicate::Unusable { reason } => (
386 Outcome::Reject {
387 reason: format!("policy predicate unusable: {reason}"),
388 },
389 true,
390 ),
391 }
392 }
393
394 /// The ordered candidate policies this subject may be decided by.
395 ///
396 /// For a write matching nothing, this costs only a tree walk bounded by
397 /// the write's own changed paths, never by how many policies are
398 /// registered — no evaluator is invoked.
399 pub fn decision_candidates(&self, subject: &Subject<'_>) -> Vec<PolicyId> {
400 self.index.decision.candidates(subject)
401 }
402
403 /// The ordered set of policies whose *observation* surface — not
404 /// necessarily their decision surface — this subject falls under.
405 ///
406 /// Independent of [`decision_candidates`](Self::decision_candidates):
407 /// a policy can watch attempts it has no say in deciding.
408 pub fn observation_candidates(&self, subject: &Subject<'_>) -> Vec<PolicyId> {
409 self.index.observation.candidates(subject)
410 }
411}
412
413/// Builds a [`PolicyTree`] from a set of evaluators and the policies that
414/// use them.
415#[derive(Default)]
416pub struct PolicyTreeBuilder {
417 evaluators: Vec<Arc<dyn Evaluator>>,
418 declarations: Vec<PolicyDeclaration>,
419}
420
421impl PolicyTreeBuilder {
422 /// Register an evaluator, returning the id later policies reference.
423 /// One evaluator may serve many policies.
424 pub fn register_evaluator(&mut self, evaluator: Arc<dyn Evaluator>) -> EvaluatorId {
425 let id = EvaluatorId(self.evaluators.len() as u32);
426 self.evaluators.push(evaluator);
427 id
428 }
429
430 /// Add a policy, returning its [`PolicyId`]. Policies are assigned ids
431 /// in the order they are added here, which becomes the tree's
432 /// presentational evaluation order.
433 ///
434 /// Panics if `declaration.predicate` names an evaluator id that was not
435 /// returned by [`register_evaluator`](Self::register_evaluator) on
436 /// this builder — a programmer error in the caller wiring up the tree,
437 /// not a runtime condition.
438 /// [`Predicate::Unusable`] needs no
439 /// such check: it names no evaluator to have registered.
440 pub fn add_policy(&mut self, declaration: PolicyDeclaration) -> PolicyId {
441 if let Predicate::Evaluator(evaluator_id, _) = &declaration.predicate {
442 assert!(
443 (evaluator_id.0 as usize) < self.evaluators.len(),
444 "policy declared for an evaluator id this builder never registered"
445 );
446 }
447 let id = PolicyId(self.declarations.len() as u32);
448 self.declarations.push(declaration);
449 id
450 }
451
452 /// Build the tree.
453 pub fn build(self) -> PolicyTree {
454 let mut predicates = Vec::with_capacity(self.declarations.len());
455 let mut index = ShapeIndex::default();
456 for (i, decl) in self.declarations.iter().enumerate() {
457 let policy = PolicyId(i as u32);
458 predicates.push(decl.predicate.clone());
459 index.decision.insert(policy, &decl.applicability);
460 index.observation.insert(policy, &decl.observes);
461 }
462 PolicyTree {
463 evaluators: self.evaluators,
464 predicates,
465 index,
466 }
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 /// The write path writes an array element as `tags[0]` and a nested
475 /// one as `labels.values[0].val`. A subscript has to be its own
476 /// segment or a declared prefix naming the field can never be an
477 /// ancestor of the element's path.
478 #[test]
479 fn a_subscript_is_its_own_segment() {
480 assert_eq!(segments("displayName"), vec!["displayName"]);
481 assert_eq!(segments("tags[0]"), vec!["tags", "[0]"]);
482 assert_eq!(
483 segments("labels.values[0].val"),
484 vec!["labels", "values", "[0]", "val"]
485 );
486 assert_eq!(segments("grid[0][1]"), vec!["grid", "[0]", "[1]"]);
487 assert_eq!(segments(""), vec![""]);
488 assert_eq!(segments("a..b"), vec!["a", "", "b"]);
489 }
490}