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}