Skip to main content

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}