Skip to main content

didbot_policy/
policy.rs

1//! What a policy declares about itself, statically — the data the
2//! applicability index is built from.
3//!
4//! `plan/policy.md`: "Split what a policy declares from what it computes.
5//! Applicability is static, declarative data — subject kinds, collections,
6//! path prefixes, action kinds — and it is what goes in the tree. Evaluation
7//! is opaque [...] The core never needs to understand an evaluator's
8//! internals, only its declaration."
9
10use crate::outcome::Staleness;
11use crate::subject::{AccountKind, SubjectKind, WriteAction};
12
13/// A policy, identified by its position in a [`PolicyTree`](crate::index::PolicyTree).
14///
15/// Assigned in registration order, which doubles as this server's
16/// presentational evaluation order — `plan/policy.md`'s "the order is part
17/// of this server's version of the tree, so that the reason reported for a
18/// denial is stable."
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct PolicyId(pub(crate) u32);
21
22impl PolicyId {
23    /// This policy's position in registration order.
24    pub fn index(self) -> u32 {
25        self.0
26    }
27}
28
29/// An evaluator, registered once and referenced by any number of policies.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub struct EvaluatorId(pub(crate) u32);
32
33/// A compiled predicate, as an evaluator itself names it.
34///
35/// Minted by [`crate::outcome::Evaluator::compile`] and opaque to this
36/// crate beyond equality: not an index, not something this crate can
37/// allocate or interpret, because the compiled artifact it names belongs to
38/// the evaluator, not the caller. That ownership split is deliberate --
39/// see `compile`'s doc comment -- because an evaluator may run out of
40/// process, possibly in another language, where a caller-owned handle
41/// could only ever be an identifier for state the caller does not hold.
42/// [`PolicyId`] is this crate's own bookkeeping, assigned by
43/// [`PolicyTreeBuilder`](crate::index::PolicyTreeBuilder) at registration
44/// time; `CompiledId` is the evaluator's, assigned at `compile` time, and
45/// the two are not interchangeable -- a multi-policy evaluator learns which
46/// artifact to use for a subject from `CompiledId`, never from `PolicyId`,
47/// which it never sees.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct CompiledId(pub u64);
50
51/// Whether a declarative dimension is unconstrained, or limited to a
52/// specific set of values.
53///
54/// `Any` is not "an empty set of values" — it is the wildcard, matching
55/// every value including one the index has never seen. Kept generic rather
56/// than repeated per field, since applicability has several such
57/// dimensions.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Scope<T> {
60    /// Every value matches.
61    Any,
62    /// Only these values match.
63    Only(Vec<T>),
64}
65
66impl<T: PartialEq> Scope<T> {
67    /// Whether `value` falls inside this scope.
68    pub fn matches(&self, value: &T) -> bool {
69        match self {
70            Scope::Any => true,
71            Scope::Only(values) => values.contains(value),
72        }
73    }
74}
75
76/// Which [`WriteAction`]s a write-shaped applicability declaration covers.
77///
78/// A small closed set, so a bitset rather than a `Vec` — cheap to copy and
79/// cheap to test membership in on the hot path.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct ActionSet(u8);
82
83const CREATE_BIT: u8 = 0b001;
84const UPDATE_BIT: u8 = 0b010;
85const DELETE_BIT: u8 = 0b100;
86
87impl ActionSet {
88    /// Create, update and delete all match.
89    pub const ALL: ActionSet = ActionSet(CREATE_BIT | UPDATE_BIT | DELETE_BIT);
90
91    /// Only the given actions match. `plan/policy.md` takes no position on
92    /// what a delete means, so a policy that wants nothing to do with
93    /// deletes states that here, explicitly, rather than the core inferring
94    /// it from an empty diff.
95    pub fn of(actions: &[WriteAction]) -> Self {
96        let mut bits = 0u8;
97        for action in actions {
98            bits |= bit(*action);
99        }
100        ActionSet(bits)
101    }
102
103    /// Whether `action` is in this set.
104    pub fn contains(&self, action: WriteAction) -> bool {
105        self.0 & bit(action) != 0
106    }
107}
108
109fn bit(action: WriteAction) -> u8 {
110    match action {
111        WriteAction::Create => CREATE_BIT,
112        WriteAction::Update => UPDATE_BIT,
113        WriteAction::Delete => DELETE_BIT,
114    }
115}
116
117/// Which account kinds a write-shaped applicability declaration covers.
118///
119/// `plan/policy.md` lists "subject kinds" among applicability's declarative
120/// dimensions alongside collections, path prefixes and action kinds; this
121/// is that dimension. Filtering here rather than inside every evaluator
122/// means a policy that only makes sense for agents routes a host's write
123/// past it entirely — the tree walk, not an `Allow` an evaluator had to
124/// compute — which keeps the empty-match property doing real work for the
125/// kinds a policy does not cover, not just for collections it does not
126/// cover.
127///
128/// # Naming specific kinds does not cover an unrecognized one
129///
130/// `bot.did.registration`'s `actor` union is open, so a subject can carry a
131/// [`SubjectKind::Unknown`] this
132/// build has never heard of. [`of`](Self::of) builds a set from named
133/// [`AccountKind`]s, and such a set **never** matches an unknown kind, no
134/// matter which kinds it names — not even by naming all four.
135///
136/// This is deliberate, and the conservative-sounding instinct is the
137/// opposite: a rule declared over `[Agent]` made a claim about agents. In a
138/// deny-only system, silently stretching that claim to cover a kind its
139/// author had never seen would deny writes on a premise nobody established
140/// — the same shape of mistake as inventing what a delete means instead of
141/// routing on an explicit [`WriteAction`]. [`ANY`](Self::ANY) is how an
142/// author opts a policy into kinds invented after it was written, and it is
143/// the only way to.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct KindSet(Repr);
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq)]
148enum Repr {
149    /// Every kind matches, known or not.
150    Any,
151    /// Only these named, known kinds match.
152    Known(u8),
153}
154
155const AGENT_BIT: u8 = 0b0001;
156const PIPELINE_BIT: u8 = 0b0010;
157const HOST_BIT: u8 = 0b0100;
158const SERVICE_BIT: u8 = 0b1000;
159const ALL_KNOWN_BITS: u8 = AGENT_BIT | PIPELINE_BIT | HOST_BIT | SERVICE_BIT;
160
161impl KindSet {
162    /// All four kinds this build recognizes by name — but, per this type's
163    /// doc comment, *not* a kind it does not. For that, use [`ANY`](Self::ANY).
164    pub const ALL: KindSet = KindSet(Repr::Known(ALL_KNOWN_BITS));
165
166    /// Every kind matches, including one this build has never heard of. The
167    /// explicit opt-in a policy names to cover kinds invented after it was
168    /// written.
169    pub const ANY: KindSet = KindSet(Repr::Any);
170
171    /// Only the given known kinds match — never an unrecognized one, no
172    /// matter which kinds are named.
173    pub fn of(kinds: &[AccountKind]) -> Self {
174        let mut bits = 0u8;
175        for kind in kinds {
176            bits |= kind_bit(*kind);
177        }
178        KindSet(Repr::Known(bits))
179    }
180
181    /// Whether `kind` is in this set.
182    pub fn contains(&self, kind: SubjectKind<'_>) -> bool {
183        match self.0 {
184            Repr::Any => true,
185            Repr::Known(bits) => match kind {
186                SubjectKind::Known(known) => bits & kind_bit(known) != 0,
187                // A specific-kind set matches only kinds it named, and it
188                // never named this one — it could not have, this build
189                // has never seen it.
190                SubjectKind::Unknown(_) => false,
191            },
192        }
193    }
194}
195
196fn kind_bit(kind: AccountKind) -> u8 {
197    match kind {
198        AccountKind::Agent => AGENT_BIT,
199        AccountKind::Pipeline => PIPELINE_BIT,
200        AccountKind::Host => HOST_BIT,
201        AccountKind::Service => SERVICE_BIT,
202    }
203}
204
205/// What a policy statically applies to — the data the tree indexes on.
206///
207/// One variant per subject shape, matching [`Subject`](crate::subject::Subject).
208/// This is used twice per policy: once as its decision surface (what
209/// [`evaluate`](crate::evaluate::evaluate) may ask it about) and once,
210/// independently, as its observation surface (what
211/// [`Evaluator::observe`](crate::outcome::Evaluator::observe) is told
212/// about) — see [`PolicyDeclaration`].
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub enum Applicability {
215    /// Applies to writes.
216    Write {
217        /// Which account kinds. A policy that only makes sense for agents
218        /// declares that here, and a host's or a service's write never
219        /// reaches its evaluator. See [`KindSet`].
220        kinds: KindSet,
221        /// Which collections (NSIDs). `Any` for a policy that is not
222        /// collection-specific — an application denial, for instance.
223        collections: Scope<String>,
224        /// Which actions. A policy declaring only [`WriteAction::Delete`]
225        /// never sees a create or update, and vice versa.
226        actions: ActionSet,
227        /// Which changed-path prefixes, matched by splitting on `.` — this
228        /// crate's own routing convention, kept separate from whatever
229        /// segmentation meaning an evaluator's engine gives
230        /// [`Change::path`](crate::subject::Change::path) itself; see that
231        /// field's doc comment. `Any` matches the write regardless of its
232        /// diff, including one with no changed paths at all.
233        path_prefixes: Scope<String>,
234    },
235    /// Applies to blob uploads.
236    Blob,
237    /// Applies to pushed authorization requests.
238    Grant {
239        /// Which applications, by client id. `Any` for a policy that
240        /// applies to every requester.
241        client_ids: Scope<String>,
242    },
243    /// Applies to tokens about to be issued: a code exchanged, a refresh
244    /// rotated.
245    Token {
246        /// Which applications, by client id. `Any` for a policy that
247        /// applies to every client.
248        client_ids: Scope<String>,
249    },
250    /// Applies to where a new account lands.
251    Create,
252}
253
254/// What decides this policy's outcome, once its applicability is already
255/// established.
256///
257/// Split from `Applicability` deliberately, because the two fail
258/// differently and only one of the failures is a *policy's* failure.
259/// `Applicability` is simple declarative data — collections, path
260/// prefixes, kinds, actions — that the core itself validates at the
261/// boundary, never something an engine interprets. A record whose
262/// applicability does not parse is therefore not a malformed policy; it is
263/// a malformed record that never becomes a [`PolicyDeclaration`] at all.
264/// It enforces nothing, it is surfaced to the operator, and nothing about
265/// it is represented anywhere in this crate's types — there is no third
266/// state here for it, and there should not be one.
267///
268/// What *is* real, because a `PolicyDeclaration` can still hold it: the
269/// predicate — a regular expression, a hand-rolled matcher, anything an
270/// engine actually interprets — fails to parse or load, while its already
271/// known-good applicability says exactly what the policy covers. That
272/// failure can be scoped the same way an unavailable evaluator's is,
273/// denying only the writes it would have judged rather than everything or
274/// nothing. [`Unusable`](Self::Unusable) is that state, named rather than
275/// left implicit, because a reader needs to tell "this policy denies
276/// things because it is broken" apart from "this policy denies things
277/// because it says so."
278#[derive(Debug, Clone, PartialEq, Eq)]
279pub enum Predicate {
280    /// Applicability and predicate both parsed and compiled; this
281    /// evaluator judges it, consulting the artifact named by `CompiledId`
282    /// -- see [`CompiledId`]'s doc comment for why that, not [`PolicyId`],
283    /// is what an evaluator is handed back at decision time.
284    Evaluator(EvaluatorId, CompiledId),
285    /// Applicability parsed; the predicate did not. Denies every subject
286    /// within this policy's applicability — fail-closed, scoped by the
287    /// tree exactly like an [`EvalError`](crate::outcome::EvalError) from
288    /// a live evaluator, because from the tree's perspective the two are
289    /// the same event at different times: a predicate that cannot be
290    /// asked.
291    Unusable {
292        /// Why the predicate could not be used: bad syntax, an engine this
293        /// build does not have, or one that itself failed to load.
294        reason: String,
295    },
296}
297
298/// A policy record's own revision, for detecting a stale observation.
299///
300/// Policies are independent records and a poll only ever sees a random
301/// point of that set — never a snapshot with a version of its own — so a
302/// later poll can observe an *older* copy of one particular policy than a
303/// previous poll already applied (a slow replica, a retried request
304/// racing a fresher one, in any order the transport permits). Applying
305/// that older copy over the newer one would silently revert an operator's
306/// edit, and in a deny-only system a reversion is a *widening* — the one
307/// direction every other property in this crate exists to prevent.
308///
309/// This crate does not perform that refusal itself: comparing two
310/// observations of what is nominally the same policy over *time* is
311/// exactly the kind of state this crate has no I/O and no polling to hold
312/// — see the crate root's "What is deliberately not here". `Revision` is
313/// here because the type that carries one policy's declaration is where
314/// the value naturally lives; a loader reading two observations of a
315/// policy compares their `Revision`s and refuses to apply the lesser one.
316#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
317pub struct Revision(pub u64);
318
319/// One policy: its applicability, its observation surface, its staleness
320/// mode, its predicate, and its own revision.
321///
322/// A policy is not its wrapper — see `plan/policy.md`'s "A policy is not its
323/// wrapper". This is the policy itself, already stripped of the
324/// transport-shaped metadata (source, format version) that got it here.
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct PolicyDeclaration {
327    /// What this policy may be asked to decide. Feeds the decision tree that
328    /// [`PolicyTree::decision_candidates`](crate::index::PolicyTree::decision_candidates)
329    /// walks. Simple declarative data, deliberately never delegated to an
330    /// engine — see [`Predicate`]'s doc comment for why a
331    /// `PolicyDeclaration` is never reached at all when this cannot be
332    /// made sense of, and holds only the one real failure: a
333    /// known-good applicability paired with a predicate that is not.
334    pub applicability: Applicability,
335    /// What this policy is told about, whether or not it is the one
336    /// deciding. Feeds a second, independent tree — see
337    /// [`Evaluator::observe`](crate::outcome::Evaluator::observe)'s doc
338    /// comment for why this has to be separate from `applicability`.
339    pub observes: Applicability,
340    /// How stale this policy's evaluator's view may be. Duplicated per
341    /// policy rather than looked up from the evaluator, even though one
342    /// evaluator answers the same for all of its policies today, because a
343    /// future evaluator serving policies of genuinely different staleness
344    /// should not require restructuring this type.
345    pub staleness: Staleness,
346    /// What decides this policy — a working evaluator, or a named
347    /// [`Predicate::Unusable`].
348    pub predicate: Predicate,
349    /// This policy record's own revision. See [`Revision`].
350    pub revision: Revision,
351}