Skip to main content

simple_zanzibar/
eval.rs

1//! Core evaluation logic for `check` and `expand` requests.
2
3#[cfg(feature = "bench-internals")]
4use std::cell::Cell;
5#[cfg(feature = "bench-internals")]
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::{
8    collections::{HashMap, HashSet, VecDeque},
9    num::{NonZeroU32, NonZeroUsize},
10};
11
12use thiserror::Error;
13
14use crate::{
15    domain::{ObjectRef as DomainObjectRef, ObjectType, RelationName, SubjectId, SubjectType},
16    error::ZanzibarError,
17    model::{
18        ExpandedUserset, LookupResources, LookupResourcesRequest, LookupSubjects,
19        LookupSubjectsRequest, Object, Relation, User,
20    },
21    relationship::{QueryLimit, StoreCheckKey, SubjectFilter},
22    revision::PublishedSnapshot,
23    schema::{
24        CompiledUsersetExpression, RelationDefinition as SchemaRelationDefinition, SchemaRelationId,
25    },
26};
27
28const DEFAULT_MAX_DEPTH: u32 = 50;
29const DEFAULT_MAX_FANOUT_PER_STEP: u32 = 1_000;
30const DEFAULT_MAX_LOOKUP_RESULTS: u32 = 1_000;
31const ACTIVE_INDEX_THRESHOLD: usize = 8;
32const LOOKUP_PLANNER_SAMPLE_RELATIONSHIPS: u32 = 64;
33const LOOKUP_PLANNER_MIN_PRUNE_BPS: u32 = 500;
34#[cfg(feature = "bench-internals")]
35thread_local! {
36    static EVALUATION_READ_COUNTERS_ENABLED: Cell<bool> = const { Cell::new(false) };
37}
38#[cfg(feature = "bench-internals")]
39static CHECK_EVALUATIONS: AtomicU64 = AtomicU64::new(0);
40#[cfg(feature = "bench-internals")]
41static CHECK_MEMO_HIT_OPPORTUNITIES: AtomicU64 = AtomicU64::new(0);
42#[cfg(feature = "bench-internals")]
43static CHECK_MEMO_HITS: AtomicU64 = AtomicU64::new(0);
44#[cfg(feature = "bench-internals")]
45static CHECK_MEMO_MISSES: AtomicU64 = AtomicU64::new(0);
46#[cfg(feature = "bench-internals")]
47static CHECK_MEMO_INSERTS: AtomicU64 = AtomicU64::new(0);
48#[cfg(feature = "bench-internals")]
49static CHECK_MEMO_CAPACITY_SKIPS: AtomicU64 = AtomicU64::new(0);
50#[cfg(feature = "bench-internals")]
51static CHECK_MEMO_ACTIVE_CYCLE_SKIPS: AtomicU64 = AtomicU64::new(0);
52#[cfg(feature = "bench-internals")]
53static CHECK_MEMO_DEPTH_INSUFFICIENT_SKIPS: AtomicU64 = AtomicU64::new(0);
54#[cfg(feature = "bench-internals")]
55static CHECK_COMPLETED_RESULTS: AtomicU64 = AtomicU64::new(0);
56#[cfg(feature = "bench-internals")]
57static CHECK_ACTIVE_CYCLE_DENIALS: AtomicU64 = AtomicU64::new(0);
58#[cfg(feature = "bench-internals")]
59static LOOKUP_RESOURCES_FRONTIER_SUBJECTS: AtomicU64 = AtomicU64::new(0);
60#[cfg(feature = "bench-internals")]
61static LOOKUP_RESOURCES_FRONTIER_RELATIONSHIPS: AtomicU64 = AtomicU64::new(0);
62#[cfg(feature = "bench-internals")]
63static LOOKUP_RESOURCES_CANDIDATE_RESOURCES: AtomicU64 = AtomicU64::new(0);
64#[cfg(feature = "bench-internals")]
65static LOOKUP_RESOURCES_SCHEMA_PRUNED: AtomicU64 = AtomicU64::new(0);
66#[cfg(feature = "bench-internals")]
67static LOOKUP_RESOURCES_PLANNER_FALLBACKS: AtomicU64 = AtomicU64::new(0);
68#[cfg(feature = "bench-internals")]
69static LOOKUP_RESOURCES_FULL_ROOT_CHECKS: AtomicU64 = AtomicU64::new(0);
70#[cfg(feature = "bench-internals")]
71static LOOKUP_RESOURCES_RESIDUAL_CHECKS: AtomicU64 = AtomicU64::new(0);
72#[cfg(feature = "bench-internals")]
73static LOOKUP_RESOURCES_PROVEN_WITHOUT_CHECK: AtomicU64 = AtomicU64::new(0);
74#[cfg(feature = "bench-internals")]
75static LOOKUP_RESOURCES_TUPLE_FALLBACKS: AtomicU64 = AtomicU64::new(0);
76#[cfg(feature = "bench-internals")]
77static LOOKUP_RESOURCES_RETURNED: AtomicU64 = AtomicU64::new(0);
78#[cfg(feature = "bench-internals")]
79static LOOKUP_RESOURCES_RESULT_LIMIT_EXITS: AtomicU64 = AtomicU64::new(0);
80#[cfg(feature = "bench-internals")]
81static LOOKUP_SUBJECTS_CANDIDATE_SUBJECTS: AtomicU64 = AtomicU64::new(0);
82#[cfg(feature = "bench-internals")]
83static LOOKUP_SUBJECTS_CANDIDATE_USERSETS: AtomicU64 = AtomicU64::new(0);
84#[cfg(feature = "bench-internals")]
85static LOOKUP_SUBJECTS_FULL_ROOT_CHECKS: AtomicU64 = AtomicU64::new(0);
86#[cfg(feature = "bench-internals")]
87static LOOKUP_SUBJECTS_RETURNED: AtomicU64 = AtomicU64::new(0);
88#[cfg(feature = "bench-internals")]
89static LOOKUP_SUBJECTS_RESULT_LIMIT_EXITS: AtomicU64 = AtomicU64::new(0);
90
91/// Benchmark-only evaluator counters for read-optimization follow-up work.
92#[cfg(feature = "bench-internals")]
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct EvaluationReadCounters {
96    /// Number of memo-eligible check evaluations that reached the non-active path.
97    pub check_evaluations: u64,
98    /// Number of completed check keys observed again inside the same request context.
99    pub check_memo_hit_opportunities: u64,
100    /// Number of request-local memo hits returned without re-evaluation.
101    pub check_memo_hits: u64,
102    /// Number of request-local memo misses that proceeded to evaluation.
103    pub check_memo_misses: u64,
104    /// Number of completed check results inserted into the request-local memo.
105    pub check_memo_inserts: u64,
106    /// Number of completed check results skipped because the request-local memo was full.
107    pub check_memo_capacity_skips: u64,
108    /// Number of active-cycle denials that intentionally bypassed request-local memo lookup.
109    pub check_memo_active_cycle_skips: u64,
110    /// Number of memo entries skipped because the caller had insufficient remaining depth.
111    pub check_memo_depth_insufficient_skips: u64,
112    /// Number of memo-eligible successful check results tracked for request-local memoization.
113    pub check_completed_results: u64,
114    /// Number of active-recursion cycle denials.
115    pub check_active_cycle_denials: u64,
116    /// Number of subject frontier entries popped by `lookup_resources`.
117    pub lookup_resources_frontier_subjects: u64,
118    /// Number of reverse relationships scanned by `lookup_resources`.
119    pub lookup_resources_frontier_relationships: u64,
120    /// Number of resource candidates sent to root verification.
121    pub lookup_resources_candidate_resources: u64,
122    /// Number of root-resource reverse relationships pruned by schema producer analysis.
123    pub lookup_resources_schema_pruned: u64,
124    /// Number of lookup-resource requests that fell back to the broad producer planner.
125    pub lookup_resources_planner_fallbacks: u64,
126    /// Number of full-root checks run by `lookup_resources`.
127    pub lookup_resources_full_root_checks: u64,
128    /// Number of residual guard checks run by `lookup_resources`.
129    pub lookup_resources_residual_checks: u64,
130    /// Number of exact-proof resources returned by `lookup_resources` without a root check.
131    pub lookup_resources_proven_without_check: u64,
132    /// Number of tuple-to-userset candidate paths intentionally verified by full-root fallback.
133    pub lookup_resources_tuple_fallbacks: u64,
134    /// Number of resources returned by `lookup_resources`.
135    pub lookup_resources_returned: u64,
136    /// Number of `lookup_resources` exits caused by `max_lookup_results`.
137    pub lookup_resources_result_limit_exits: u64,
138    /// Number of direct subject candidates seen by `lookup_subjects`.
139    pub lookup_subjects_candidate_subjects: u64,
140    /// Number of userset candidates seen by `lookup_subjects`.
141    pub lookup_subjects_candidate_usersets: u64,
142    /// Number of full-root checks run by `lookup_subjects`.
143    pub lookup_subjects_full_root_checks: u64,
144    /// Number of subjects returned by `lookup_subjects`.
145    pub lookup_subjects_returned: u64,
146    /// Number of `lookup_subjects` exits caused by `max_lookup_results`.
147    pub lookup_subjects_result_limit_exits: u64,
148}
149
150/// Resets and enables benchmark-only evaluator counters.
151#[cfg(feature = "bench-internals")]
152pub fn reset_evaluation_read_counters() {
153    set_evaluation_read_counters_enabled(true);
154    CHECK_EVALUATIONS.store(0, Ordering::Relaxed);
155    CHECK_MEMO_HIT_OPPORTUNITIES.store(0, Ordering::Relaxed);
156    CHECK_MEMO_HITS.store(0, Ordering::Relaxed);
157    CHECK_MEMO_MISSES.store(0, Ordering::Relaxed);
158    CHECK_MEMO_INSERTS.store(0, Ordering::Relaxed);
159    CHECK_MEMO_CAPACITY_SKIPS.store(0, Ordering::Relaxed);
160    CHECK_MEMO_ACTIVE_CYCLE_SKIPS.store(0, Ordering::Relaxed);
161    CHECK_MEMO_DEPTH_INSUFFICIENT_SKIPS.store(0, Ordering::Relaxed);
162    CHECK_COMPLETED_RESULTS.store(0, Ordering::Relaxed);
163    CHECK_ACTIVE_CYCLE_DENIALS.store(0, Ordering::Relaxed);
164    LOOKUP_RESOURCES_FRONTIER_SUBJECTS.store(0, Ordering::Relaxed);
165    LOOKUP_RESOURCES_FRONTIER_RELATIONSHIPS.store(0, Ordering::Relaxed);
166    LOOKUP_RESOURCES_CANDIDATE_RESOURCES.store(0, Ordering::Relaxed);
167    LOOKUP_RESOURCES_SCHEMA_PRUNED.store(0, Ordering::Relaxed);
168    LOOKUP_RESOURCES_PLANNER_FALLBACKS.store(0, Ordering::Relaxed);
169    LOOKUP_RESOURCES_FULL_ROOT_CHECKS.store(0, Ordering::Relaxed);
170    LOOKUP_RESOURCES_RESIDUAL_CHECKS.store(0, Ordering::Relaxed);
171    LOOKUP_RESOURCES_PROVEN_WITHOUT_CHECK.store(0, Ordering::Relaxed);
172    LOOKUP_RESOURCES_TUPLE_FALLBACKS.store(0, Ordering::Relaxed);
173    LOOKUP_RESOURCES_RETURNED.store(0, Ordering::Relaxed);
174    LOOKUP_RESOURCES_RESULT_LIMIT_EXITS.store(0, Ordering::Relaxed);
175    LOOKUP_SUBJECTS_CANDIDATE_SUBJECTS.store(0, Ordering::Relaxed);
176    LOOKUP_SUBJECTS_CANDIDATE_USERSETS.store(0, Ordering::Relaxed);
177    LOOKUP_SUBJECTS_FULL_ROOT_CHECKS.store(0, Ordering::Relaxed);
178    LOOKUP_SUBJECTS_RETURNED.store(0, Ordering::Relaxed);
179    LOOKUP_SUBJECTS_RESULT_LIMIT_EXITS.store(0, Ordering::Relaxed);
180}
181
182/// Enables or disables benchmark-only evaluator counters.
183#[cfg(feature = "bench-internals")]
184pub fn set_evaluation_read_counters_enabled(enabled: bool) {
185    EVALUATION_READ_COUNTERS_ENABLED.with(|flag| flag.set(enabled));
186}
187
188#[cfg(feature = "bench-internals")]
189fn evaluation_read_counters_enabled() -> bool {
190    EVALUATION_READ_COUNTERS_ENABLED.with(Cell::get)
191}
192
193#[cfg(feature = "bench-internals")]
194fn record_counter(counter: &AtomicU64) {
195    if evaluation_read_counters_enabled() {
196        counter.fetch_add(1, Ordering::Relaxed);
197    }
198}
199
200/// Returns benchmark-only evaluator counters.
201#[cfg(feature = "bench-internals")]
202#[must_use]
203pub fn evaluation_read_counters() -> EvaluationReadCounters {
204    EvaluationReadCounters {
205        check_evaluations: CHECK_EVALUATIONS.load(Ordering::Relaxed),
206        check_memo_hit_opportunities: CHECK_MEMO_HIT_OPPORTUNITIES.load(Ordering::Relaxed),
207        check_memo_hits: CHECK_MEMO_HITS.load(Ordering::Relaxed),
208        check_memo_misses: CHECK_MEMO_MISSES.load(Ordering::Relaxed),
209        check_memo_inserts: CHECK_MEMO_INSERTS.load(Ordering::Relaxed),
210        check_memo_capacity_skips: CHECK_MEMO_CAPACITY_SKIPS.load(Ordering::Relaxed),
211        check_memo_active_cycle_skips: CHECK_MEMO_ACTIVE_CYCLE_SKIPS.load(Ordering::Relaxed),
212        check_memo_depth_insufficient_skips: CHECK_MEMO_DEPTH_INSUFFICIENT_SKIPS
213            .load(Ordering::Relaxed),
214        check_completed_results: CHECK_COMPLETED_RESULTS.load(Ordering::Relaxed),
215        check_active_cycle_denials: CHECK_ACTIVE_CYCLE_DENIALS.load(Ordering::Relaxed),
216        lookup_resources_frontier_subjects: LOOKUP_RESOURCES_FRONTIER_SUBJECTS
217            .load(Ordering::Relaxed),
218        lookup_resources_frontier_relationships: LOOKUP_RESOURCES_FRONTIER_RELATIONSHIPS
219            .load(Ordering::Relaxed),
220        lookup_resources_candidate_resources: LOOKUP_RESOURCES_CANDIDATE_RESOURCES
221            .load(Ordering::Relaxed),
222        lookup_resources_schema_pruned: LOOKUP_RESOURCES_SCHEMA_PRUNED.load(Ordering::Relaxed),
223        lookup_resources_planner_fallbacks: LOOKUP_RESOURCES_PLANNER_FALLBACKS
224            .load(Ordering::Relaxed),
225        lookup_resources_full_root_checks: LOOKUP_RESOURCES_FULL_ROOT_CHECKS
226            .load(Ordering::Relaxed),
227        lookup_resources_residual_checks: LOOKUP_RESOURCES_RESIDUAL_CHECKS.load(Ordering::Relaxed),
228        lookup_resources_proven_without_check: LOOKUP_RESOURCES_PROVEN_WITHOUT_CHECK
229            .load(Ordering::Relaxed),
230        lookup_resources_tuple_fallbacks: LOOKUP_RESOURCES_TUPLE_FALLBACKS.load(Ordering::Relaxed),
231        lookup_resources_returned: LOOKUP_RESOURCES_RETURNED.load(Ordering::Relaxed),
232        lookup_resources_result_limit_exits: LOOKUP_RESOURCES_RESULT_LIMIT_EXITS
233            .load(Ordering::Relaxed),
234        lookup_subjects_candidate_subjects: LOOKUP_SUBJECTS_CANDIDATE_SUBJECTS
235            .load(Ordering::Relaxed),
236        lookup_subjects_candidate_usersets: LOOKUP_SUBJECTS_CANDIDATE_USERSETS
237            .load(Ordering::Relaxed),
238        lookup_subjects_full_root_checks: LOOKUP_SUBJECTS_FULL_ROOT_CHECKS.load(Ordering::Relaxed),
239        lookup_subjects_returned: LOOKUP_SUBJECTS_RETURNED.load(Ordering::Relaxed),
240        lookup_subjects_result_limit_exits: LOOKUP_SUBJECTS_RESULT_LIMIT_EXITS
241            .load(Ordering::Relaxed),
242    }
243}
244
245#[cfg(feature = "bench-internals")]
246fn record_active_cycle_denial() {
247    record_counter(&CHECK_ACTIVE_CYCLE_DENIALS);
248}
249
250#[cfg(not(feature = "bench-internals"))]
251fn record_active_cycle_denial() {}
252
253#[cfg(feature = "bench-internals")]
254fn record_check_memo_hit() {
255    record_counter(&CHECK_MEMO_HITS);
256}
257
258#[cfg(not(feature = "bench-internals"))]
259fn record_check_memo_hit() {}
260
261#[cfg(feature = "bench-internals")]
262fn record_check_memo_miss() {
263    record_counter(&CHECK_MEMO_MISSES);
264}
265
266#[cfg(not(feature = "bench-internals"))]
267fn record_check_memo_miss() {}
268
269#[cfg(feature = "bench-internals")]
270fn record_check_memo_insert() {
271    record_counter(&CHECK_MEMO_INSERTS);
272}
273
274#[cfg(not(feature = "bench-internals"))]
275fn record_check_memo_insert() {}
276
277#[cfg(feature = "bench-internals")]
278fn record_check_memo_capacity_skip() {
279    record_counter(&CHECK_MEMO_CAPACITY_SKIPS);
280}
281
282#[cfg(not(feature = "bench-internals"))]
283fn record_check_memo_capacity_skip() {}
284
285#[cfg(feature = "bench-internals")]
286fn record_check_memo_active_cycle_skip() {
287    record_counter(&CHECK_MEMO_ACTIVE_CYCLE_SKIPS);
288}
289
290#[cfg(not(feature = "bench-internals"))]
291fn record_check_memo_active_cycle_skip() {}
292
293#[cfg(feature = "bench-internals")]
294fn record_check_memo_depth_insufficient_skip() {
295    record_counter(&CHECK_MEMO_DEPTH_INSUFFICIENT_SKIPS);
296}
297
298#[cfg(not(feature = "bench-internals"))]
299fn record_check_memo_depth_insufficient_skip() {}
300
301#[cfg(feature = "bench-internals")]
302fn record_lookup_resources_frontier_subject() {
303    record_counter(&LOOKUP_RESOURCES_FRONTIER_SUBJECTS);
304}
305
306#[cfg(not(feature = "bench-internals"))]
307fn record_lookup_resources_frontier_subject() {}
308
309#[cfg(feature = "bench-internals")]
310fn record_lookup_resources_frontier_relationship() {
311    record_counter(&LOOKUP_RESOURCES_FRONTIER_RELATIONSHIPS);
312}
313
314#[cfg(not(feature = "bench-internals"))]
315fn record_lookup_resources_frontier_relationship() {}
316
317#[cfg(feature = "bench-internals")]
318fn record_lookup_resources_candidate_resource() {
319    record_counter(&LOOKUP_RESOURCES_CANDIDATE_RESOURCES);
320}
321
322#[cfg(not(feature = "bench-internals"))]
323fn record_lookup_resources_candidate_resource() {}
324
325#[cfg(feature = "bench-internals")]
326fn record_lookup_resources_schema_pruned() {
327    record_counter(&LOOKUP_RESOURCES_SCHEMA_PRUNED);
328}
329
330#[cfg(not(feature = "bench-internals"))]
331fn record_lookup_resources_schema_pruned() {}
332
333#[cfg(feature = "bench-internals")]
334fn record_lookup_resources_planner_fallback() {
335    record_counter(&LOOKUP_RESOURCES_PLANNER_FALLBACKS);
336}
337
338#[cfg(not(feature = "bench-internals"))]
339fn record_lookup_resources_planner_fallback() {}
340
341#[cfg(feature = "bench-internals")]
342fn record_lookup_resources_full_root_check() {
343    record_counter(&LOOKUP_RESOURCES_FULL_ROOT_CHECKS);
344}
345
346#[cfg(not(feature = "bench-internals"))]
347fn record_lookup_resources_full_root_check() {}
348
349#[cfg(feature = "bench-internals")]
350fn record_lookup_resources_residual_check() {
351    record_counter(&LOOKUP_RESOURCES_RESIDUAL_CHECKS);
352}
353
354#[cfg(not(feature = "bench-internals"))]
355fn record_lookup_resources_residual_check() {}
356
357#[cfg(feature = "bench-internals")]
358fn record_lookup_resources_proven_without_check() {
359    record_counter(&LOOKUP_RESOURCES_PROVEN_WITHOUT_CHECK);
360}
361
362#[cfg(not(feature = "bench-internals"))]
363fn record_lookup_resources_proven_without_check() {}
364
365#[cfg(feature = "bench-internals")]
366fn record_lookup_resources_tuple_fallback() {
367    record_counter(&LOOKUP_RESOURCES_TUPLE_FALLBACKS);
368}
369
370#[cfg(not(feature = "bench-internals"))]
371fn record_lookup_resources_tuple_fallback() {}
372
373#[cfg(feature = "bench-internals")]
374fn record_lookup_resources_returned() {
375    record_counter(&LOOKUP_RESOURCES_RETURNED);
376}
377
378#[cfg(not(feature = "bench-internals"))]
379fn record_lookup_resources_returned() {}
380
381#[cfg(feature = "bench-internals")]
382fn record_lookup_resources_result_limit_exit() {
383    record_counter(&LOOKUP_RESOURCES_RESULT_LIMIT_EXITS);
384}
385
386#[cfg(not(feature = "bench-internals"))]
387fn record_lookup_resources_result_limit_exit() {}
388
389#[cfg(feature = "bench-internals")]
390fn record_lookup_subjects_candidate_subject() {
391    record_counter(&LOOKUP_SUBJECTS_CANDIDATE_SUBJECTS);
392}
393
394#[cfg(not(feature = "bench-internals"))]
395fn record_lookup_subjects_candidate_subject() {}
396
397#[cfg(feature = "bench-internals")]
398fn record_lookup_subjects_candidate_userset() {
399    record_counter(&LOOKUP_SUBJECTS_CANDIDATE_USERSETS);
400}
401
402#[cfg(not(feature = "bench-internals"))]
403fn record_lookup_subjects_candidate_userset() {}
404
405#[cfg(feature = "bench-internals")]
406fn record_lookup_subjects_full_root_check() {
407    record_counter(&LOOKUP_SUBJECTS_FULL_ROOT_CHECKS);
408}
409
410#[cfg(not(feature = "bench-internals"))]
411fn record_lookup_subjects_full_root_check() {}
412
413#[cfg(feature = "bench-internals")]
414fn record_lookup_subjects_returned() {
415    record_counter(&LOOKUP_SUBJECTS_RETURNED);
416}
417
418#[cfg(not(feature = "bench-internals"))]
419fn record_lookup_subjects_returned() {}
420
421#[cfg(feature = "bench-internals")]
422fn record_lookup_subjects_result_limit_exit() {
423    record_counter(&LOOKUP_SUBJECTS_RESULT_LIMIT_EXITS);
424}
425
426#[cfg(not(feature = "bench-internals"))]
427fn record_lookup_subjects_result_limit_exit() {}
428
429/// Errors produced by the shared evaluation engine.
430#[derive(Debug, Clone, PartialEq, Eq, Error)]
431pub enum EvaluationError {
432    /// A recursive evaluation exceeded the configured depth limit.
433    #[error("evaluation depth exceeded at {key:?}")]
434    DepthExceeded {
435        /// Evaluation key that exceeded the limit.
436        key: Box<EvaluationKey>,
437    },
438
439    /// A single evaluator step exceeded the configured fanout limit.
440    #[error("evaluation fanout exceeded limit {limit}")]
441    FanoutExceeded {
442        /// Configured fanout limit.
443        limit: NonZeroU32,
444    },
445}
446
447/// Immutable key for evaluator depth errors.
448#[derive(Debug, Clone, PartialEq, Eq)]
449pub enum EvaluationKey {
450    /// A check key.
451    Check(CheckKey),
452    /// An expand key.
453    Expand(ExpandKey),
454}
455
456/// Immutable key for recursion tracking.
457#[derive(Debug, Clone, PartialEq, Eq, Hash)]
458pub enum CheckKey {
459    /// Store-native identifier key for relationships already interned in the compact snapshot.
460    Store(StoreCheckKey),
461    /// Validated public object/relation-name key used when a segment-native compact key is not
462    /// available, such as checkpoint-plus-delta views.
463    PublicName {
464        /// Object being checked.
465        object: Object,
466        /// Validated relation or permission name being checked.
467        relation: RelationName,
468        /// Subject being checked.
469        user: User,
470    },
471    /// Owned public-model fallback when at least one identifier is absent from the snapshot.
472    Public {
473        /// Object being checked.
474        object: Object,
475        /// Relation or permission being checked.
476        relation: Relation,
477        /// Subject being checked.
478        user: User,
479    },
480}
481
482impl CheckKey {
483    fn new(
484        snapshot: &PublishedSnapshot,
485        object: &Object,
486        relation: &Relation,
487        user: &User,
488    ) -> Self {
489        snapshot
490            .relationships()
491            .store_check_key(object, relation, user)
492            .map_or_else(
493                || Self::Public {
494                    object: object.clone(),
495                    relation: relation.clone(),
496                    user: user.clone(),
497                },
498                Self::Store,
499            )
500    }
501
502    fn from_relation_name(
503        snapshot: &PublishedSnapshot,
504        object: &Object,
505        relation_name: &RelationName,
506        user: &User,
507    ) -> Self {
508        snapshot
509            .relationships()
510            .store_check_key_for_relation_name(object, relation_name, user)
511            .map_or_else(
512                || Self::PublicName {
513                    object: object.clone(),
514                    relation: relation_name.clone(),
515                    user: user.clone(),
516                },
517                Self::Store,
518            )
519    }
520}
521
522/// Immutable key for expand recursion tracking.
523#[derive(Debug, Clone, PartialEq, Eq, Hash)]
524pub struct ExpandKey {
525    object: Object,
526    relation: RelationName,
527}
528
529impl ExpandKey {
530    fn new(object: &Object, relation: &RelationName) -> Self {
531        Self {
532            object: object.clone(),
533            relation: relation.clone(),
534        }
535    }
536}
537
538/// Evaluator resource limits.
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub struct EvaluationLimits {
541    /// Maximum recursive check depth.
542    pub max_depth: NonZeroU32,
543    /// Maximum userset fanout per evaluator step.
544    pub max_fanout_per_step: NonZeroU32,
545    /// Maximum lookup results for lookup APIs.
546    pub max_lookup_results: NonZeroU32,
547}
548
549impl Default for EvaluationLimits {
550    fn default() -> Self {
551        Self {
552            max_depth: non_zero_u32(DEFAULT_MAX_DEPTH),
553            max_fanout_per_step: non_zero_u32(DEFAULT_MAX_FANOUT_PER_STEP),
554            max_lookup_results: non_zero_u32(DEFAULT_MAX_LOOKUP_RESULTS),
555        }
556    }
557}
558
559/// Membership algebra result.
560#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum Membership {
562    /// The subject is a member.
563    Allowed,
564    /// The subject is not a member.
565    Denied,
566    /// Reserved for future caveat/condition support.
567    Conditional,
568}
569
570impl Membership {
571    /// Returns true when membership is allowed.
572    #[must_use]
573    pub const fn is_allowed(self) -> bool {
574        matches!(self, Self::Allowed)
575    }
576
577    /// Returns the union of two membership results.
578    #[must_use]
579    pub const fn union(self, other: Self) -> Self {
580        match (self, other) {
581            (Self::Allowed, _) | (_, Self::Allowed) => Self::Allowed,
582            (Self::Conditional, _) | (_, Self::Conditional) => Self::Conditional,
583            (Self::Denied, Self::Denied) => Self::Denied,
584        }
585    }
586
587    /// Returns the intersection of two membership results.
588    #[must_use]
589    pub const fn intersection(self, other: Self) -> Self {
590        match (self, other) {
591            (Self::Denied, _) | (_, Self::Denied) => Self::Denied,
592            (Self::Conditional, _) | (_, Self::Conditional) => Self::Conditional,
593            (Self::Allowed, Self::Allowed) => Self::Allowed,
594        }
595    }
596
597    /// Returns membership for `self - other`.
598    #[must_use]
599    pub const fn exclusion(self, other: Self) -> Self {
600        match (self, other) {
601            (Self::Denied, _) | (_, Self::Allowed) => Self::Denied,
602            (Self::Conditional, _) | (_, Self::Conditional) => Self::Conditional,
603            (Self::Allowed, Self::Denied) => Self::Allowed,
604        }
605    }
606
607    const fn is_memo_cacheable(self) -> bool {
608        matches!(self, Self::Allowed | Self::Denied)
609    }
610}
611
612#[derive(Debug, Clone, PartialEq, Eq, Hash)]
613enum CheckMemoKey {
614    Store(StoreCheckKey),
615    Public {
616        object: Object,
617        relation: RelationName,
618        user: User,
619    },
620}
621
622impl CheckMemoKey {
623    fn from_check_key(key: &CheckKey) -> Result<Self, ZanzibarError> {
624        match key {
625            CheckKey::Store(key) => Ok(Self::Store(*key)),
626            CheckKey::PublicName {
627                object,
628                relation,
629                user,
630            } => Ok(Self::Public {
631                object: object.clone(),
632                relation: relation.clone(),
633                user: user.clone(),
634            }),
635            CheckKey::Public {
636                object,
637                relation,
638                user,
639            } => Ok(Self::Public {
640                object: object.clone(),
641                relation: RelationName::try_from(relation)?,
642                user: user.clone(),
643            }),
644        }
645    }
646}
647
648#[derive(Debug, Clone, Copy, PartialEq, Eq)]
649struct CheckMemoEntry {
650    membership: Membership,
651    depth_required: NonZeroU32,
652}
653
654#[derive(Debug)]
655struct RequestCheckMemo {
656    entries: HashMap<CheckMemoKey, CheckMemoEntry>,
657    capacity: usize,
658}
659
660impl RequestCheckMemo {
661    fn new(limits: EvaluationLimits) -> Self {
662        Self {
663            entries: HashMap::new(),
664            capacity: memo_capacity(limits),
665        }
666    }
667
668    fn get(&self, key: &CheckMemoKey, remaining_depth: u32) -> MemoLookup {
669        match self.entries.get(key) {
670            Some(entry) if entry.depth_required.get() <= remaining_depth => {
671                MemoLookup::Hit(entry.membership)
672            }
673            Some(_) => MemoLookup::DepthInsufficient,
674            None => MemoLookup::Miss,
675        }
676    }
677
678    fn insert(&mut self, key: CheckMemoKey, membership: Membership, depth_required: NonZeroU32) {
679        if let Some(entry) = self.entries.get_mut(&key) {
680            if depth_required.get() <= entry.depth_required.get() {
681                *entry = CheckMemoEntry {
682                    membership,
683                    depth_required,
684                };
685            }
686            return;
687        }
688        if self.entries.len() >= self.capacity {
689            record_check_memo_capacity_skip();
690            return;
691        }
692        self.entries.insert(
693            key,
694            CheckMemoEntry {
695                membership,
696                depth_required,
697            },
698        );
699        record_check_memo_insert();
700    }
701}
702
703#[derive(Debug, Clone, Copy, PartialEq, Eq)]
704enum MemoLookup {
705    Hit(Membership),
706    Miss,
707    DepthInsufficient,
708}
709
710#[derive(Debug, Clone, Copy, PartialEq, Eq)]
711struct CheckFrame {
712    entry_remaining_depth: u32,
713    minimum_remaining_depth: u32,
714}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
717struct LookupProducerPlan {
718    relations: Vec<RelationName>,
719    verification_rules: Vec<LookupVerificationRule>,
720}
721
722impl LookupProducerPlan {
723    fn allows_relationship(&self, relationship: crate::relationship::RelationshipRef<'_>) -> bool {
724        self.relations
725            .iter()
726            .any(|relation| relationship.relation_name_eq(relation))
727    }
728
729    fn verification_for_relationship(
730        &self,
731        relationship: crate::relationship::RelationshipRef<'_>,
732        proof: LookupSubjectProof,
733        limits: EvaluationLimits,
734    ) -> LookupCandidateVerification {
735        if !proof.is_exact() || proof.direct_depth >= limits.max_depth.get() {
736            return LookupCandidateVerification::FullRoot;
737        }
738        self.verification_rules
739            .iter()
740            .find(|rule| relationship.relation_name_eq(&rule.relation))
741            .map_or(LookupCandidateVerification::FullRoot, |rule| {
742                rule.verification.clone()
743            })
744    }
745}
746
747#[derive(Debug, Clone, PartialEq, Eq)]
748struct LookupVerificationRule {
749    relation: RelationName,
750    verification: LookupCandidateVerification,
751}
752
753#[derive(Debug, Clone, PartialEq, Eq)]
754enum LookupCandidateVerification {
755    FullRoot,
756    ProvenWithoutCheck,
757    Residual(Vec<LookupResidualCheck>),
758}
759
760impl LookupCandidateVerification {
761    fn with_required_allowed(
762        self,
763        relation_context: RelationName,
764        expression: CompiledUsersetExpression,
765    ) -> Self {
766        self.with_residual(LookupResidualCheck {
767            relation_context,
768            expression,
769            expected: LookupResidualExpected::Allowed,
770        })
771    }
772
773    fn with_required_denied(
774        self,
775        relation_context: RelationName,
776        expression: CompiledUsersetExpression,
777    ) -> Self {
778        self.with_residual(LookupResidualCheck {
779            relation_context,
780            expression,
781            expected: LookupResidualExpected::Denied,
782        })
783    }
784
785    fn with_residual(self, residual: LookupResidualCheck) -> Self {
786        match self {
787            Self::FullRoot => Self::FullRoot,
788            Self::ProvenWithoutCheck => Self::Residual(vec![residual]),
789            Self::Residual(mut residuals) => {
790                residuals.push(residual);
791                Self::Residual(residuals)
792            }
793        }
794    }
795
796    fn union_merge(self, other: Self) -> Self {
797        match (self, other) {
798            (Self::ProvenWithoutCheck, _) | (_, Self::ProvenWithoutCheck) => {
799                Self::ProvenWithoutCheck
800            }
801            (Self::Residual(left), Self::Residual(right)) if left == right => Self::Residual(left),
802            _ => Self::FullRoot,
803        }
804    }
805}
806
807#[derive(Debug, Clone, PartialEq, Eq)]
808struct LookupResidualCheck {
809    relation_context: RelationName,
810    expression: CompiledUsersetExpression,
811    expected: LookupResidualExpected,
812}
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
815enum LookupResidualExpected {
816    Allowed,
817    Denied,
818}
819
820#[derive(Debug, Clone, Copy, PartialEq, Eq)]
821struct LookupSubjectProof {
822    direct_depth: u32,
823    exact: bool,
824}
825
826impl LookupSubjectProof {
827    const fn exact_root() -> Self {
828        Self {
829            direct_depth: 0,
830            exact: true,
831        }
832    }
833
834    const fn inexact() -> Self {
835        Self {
836            direct_depth: 0,
837            exact: false,
838        }
839    }
840
841    const fn is_exact(self) -> bool {
842        self.exact
843    }
844
845    fn next_exact(self) -> Self {
846        if self.exact {
847            Self {
848                direct_depth: self.direct_depth.saturating_add(1),
849                exact: true,
850            }
851        } else {
852            Self::inexact()
853        }
854    }
855}
856
857#[derive(Debug, Clone, PartialEq, Eq)]
858struct LookupFrontierEntry {
859    subject: User,
860    proof: LookupSubjectProof,
861}
862
863impl LookupFrontierEntry {
864    fn new(subject: User, proof: LookupSubjectProof) -> Self {
865        Self { subject, proof }
866    }
867}
868
869#[derive(Debug, Clone, Copy, PartialEq, Eq)]
870struct LookupProducerRuntime {
871    enabled: bool,
872    sampled_relationships: u32,
873    pruned_relationships: u32,
874}
875
876impl LookupProducerRuntime {
877    const fn new(enabled: bool) -> Self {
878        Self {
879            enabled,
880            sampled_relationships: 0,
881            pruned_relationships: 0,
882        }
883    }
884
885    fn should_prune(
886        &mut self,
887        plan: &LookupProducerPlan,
888        relationship: crate::relationship::RelationshipRef<'_>,
889    ) -> bool {
890        if !self.enabled {
891            return false;
892        }
893        let should_prune = !plan.allows_relationship(relationship);
894        self.record_sample(should_prune);
895        should_prune
896    }
897
898    fn record_sample(&mut self, should_prune: bool) {
899        if self.sampled_relationships >= LOOKUP_PLANNER_SAMPLE_RELATIONSHIPS {
900            return;
901        }
902        self.sampled_relationships = self.sampled_relationships.saturating_add(1);
903        if should_prune {
904            self.pruned_relationships = self.pruned_relationships.saturating_add(1);
905        }
906        if self.sampled_relationships == LOOKUP_PLANNER_SAMPLE_RELATIONSHIPS
907            && self.pruned_relationships.saturating_mul(10_000)
908                < LOOKUP_PLANNER_MIN_PRUNE_BPS.saturating_mul(self.sampled_relationships)
909        {
910            self.enabled = false;
911        }
912    }
913}
914
915#[derive(Debug)]
916struct SameObjectRelationExpansion {
917    object_type: ObjectType,
918    source_relation: RelationName,
919    target_relations: Vec<RelationName>,
920}
921
922#[derive(Debug)]
923struct TupleToUsersetRelationExpansion {
924    object_type: ObjectType,
925    source_relation: RelationName,
926    targets: Vec<TupleToUsersetExpansionTarget>,
927}
928
929#[derive(Debug, Clone, PartialEq, Eq)]
930struct TupleToUsersetExpansionTarget {
931    resource_type: ObjectType,
932    tupleset_relation: RelationName,
933    computed_relation: RelationName,
934    target_relation: RelationName,
935}
936
937/// Evaluation context over one immutable snapshot.
938#[derive(Debug)]
939pub struct EvaluationContext<'a> {
940    snapshot: &'a PublishedSnapshot,
941    limits: EvaluationLimits,
942    generation: NonZeroU32,
943    use_active_indexes: bool,
944    remaining_depth: u32,
945    active_checks: HashMap<CheckKey, VisitMark>,
946    active_expands: HashMap<ExpandKey, VisitMark>,
947    check_stack: Vec<CheckKey>,
948    expand_stack: Vec<ExpandKey>,
949    check_frames: Vec<CheckFrame>,
950    check_memo: Option<RequestCheckMemo>,
951    #[cfg(feature = "bench-internals")]
952    completed_check_keys: HashSet<CheckKey>,
953}
954
955#[derive(Debug, Clone, Copy, PartialEq, Eq)]
956struct VisitMark {
957    generation: NonZeroU32,
958    active: bool,
959}
960
961impl<'a> EvaluationContext<'a> {
962    /// Creates an evaluation context for a snapshot.
963    #[must_use]
964    pub fn new(snapshot: &'a PublishedSnapshot, limits: EvaluationLimits) -> Self {
965        Self {
966            snapshot,
967            limits,
968            generation: NonZeroU32::MIN,
969            use_active_indexes: false,
970            remaining_depth: limits.max_depth.get(),
971            active_checks: HashMap::new(),
972            active_expands: HashMap::new(),
973            check_stack: Vec::new(),
974            expand_stack: Vec::new(),
975            check_frames: Vec::new(),
976            check_memo: None,
977            #[cfg(feature = "bench-internals")]
978            completed_check_keys: HashSet::new(),
979        }
980    }
981
982    #[must_use]
983    pub(crate) fn new_with_request_memo(
984        snapshot: &'a PublishedSnapshot,
985        limits: EvaluationLimits,
986    ) -> Self {
987        let mut context = Self::new(snapshot, limits);
988        context.check_memo = Some(RequestCheckMemo::new(limits));
989        context
990    }
991
992    pub(crate) fn reset_for_reuse(&mut self) {
993        self.remaining_depth = self.limits.max_depth.get();
994        self.use_active_indexes = true;
995        self.check_stack.clear();
996        self.expand_stack.clear();
997        self.check_frames.clear();
998        if let Some(generation) = self
999            .generation
1000            .get()
1001            .checked_add(1)
1002            .and_then(NonZeroU32::new)
1003        {
1004            self.generation = generation;
1005        } else {
1006            self.generation = NonZeroU32::MIN;
1007            self.active_checks.clear();
1008            self.active_expands.clear();
1009        }
1010    }
1011
1012    #[cfg(feature = "bench-internals")]
1013    fn record_check_started(&self, key: &CheckKey, memo_eligible: bool) {
1014        if !memo_eligible || !evaluation_read_counters_enabled() {
1015            return;
1016        }
1017        CHECK_EVALUATIONS.fetch_add(1, Ordering::Relaxed);
1018        if self.completed_check_keys.contains(key) {
1019            CHECK_MEMO_HIT_OPPORTUNITIES.fetch_add(1, Ordering::Relaxed);
1020        }
1021    }
1022
1023    #[cfg(feature = "bench-internals")]
1024    fn record_check_completed(
1025        &mut self,
1026        key: &CheckKey,
1027        result: &Result<Membership, ZanzibarError>,
1028        memo_eligible: bool,
1029    ) {
1030        if !memo_eligible || !evaluation_read_counters_enabled() {
1031            return;
1032        }
1033        if result.is_ok() && self.completed_check_keys.insert(key.clone()) {
1034            CHECK_COMPLETED_RESULTS.fetch_add(1, Ordering::Relaxed);
1035        }
1036    }
1037
1038    fn evaluate_check_key(
1039        &mut self,
1040        key: &CheckKey,
1041        evaluate: impl FnOnce(&mut Self) -> Result<Membership, ZanzibarError>,
1042    ) -> Result<Membership, ZanzibarError> {
1043        if self.is_check_active(key) {
1044            record_active_cycle_denial();
1045            if self.check_memo.is_some() {
1046                record_check_memo_active_cycle_skip();
1047            }
1048            return Ok(Membership::Denied);
1049        }
1050        let memo_eligible = self.check_memo.is_some() && !self.check_stack.is_empty();
1051        #[cfg(feature = "bench-internals")]
1052        self.record_check_started(key, memo_eligible);
1053        if memo_eligible && let Some(membership) = self.memo_lookup(key)? {
1054            return Ok(membership);
1055        }
1056
1057        let entry_remaining_depth = self.remaining_depth;
1058        self.enter(EvaluationKey::Check(key.clone()))?;
1059        if memo_eligible {
1060            self.push_check_frame(entry_remaining_depth);
1061        }
1062        self.push_check_key(key.clone());
1063
1064        let result = evaluate(self);
1065        self.pop_check_key();
1066        let depth_required = if memo_eligible {
1067            self.pop_check_frame_depth_required()
1068        } else {
1069            NonZeroU32::MIN
1070        };
1071        self.leave();
1072        if memo_eligible
1073            && let Ok(membership) = &result
1074            && membership.is_memo_cacheable()
1075        {
1076            self.memo_insert(key, *membership, depth_required);
1077        }
1078        #[cfg(feature = "bench-internals")]
1079        self.record_check_completed(key, &result, memo_eligible);
1080        result
1081    }
1082
1083    fn memo_lookup(&self, key: &CheckKey) -> Result<Option<Membership>, ZanzibarError> {
1084        let Some(memo) = &self.check_memo else {
1085            return Ok(None);
1086        };
1087        let memo_key = CheckMemoKey::from_check_key(key)?;
1088        match memo.get(&memo_key, self.remaining_depth) {
1089            MemoLookup::Hit(membership) => {
1090                record_check_memo_hit();
1091                Ok(Some(membership))
1092            }
1093            MemoLookup::Miss => {
1094                record_check_memo_miss();
1095                Ok(None)
1096            }
1097            MemoLookup::DepthInsufficient => {
1098                record_check_memo_depth_insufficient_skip();
1099                Ok(None)
1100            }
1101        }
1102    }
1103
1104    fn memo_insert(&mut self, key: &CheckKey, membership: Membership, depth_required: NonZeroU32) {
1105        let Some(memo) = &mut self.check_memo else {
1106            return;
1107        };
1108        if let Ok(memo_key) = CheckMemoKey::from_check_key(key) {
1109            memo.insert(memo_key, membership, depth_required);
1110        }
1111    }
1112
1113    /// Evaluates a check request and returns membership algebra.
1114    ///
1115    /// # Errors
1116    ///
1117    /// Returns [`ZanzibarError`] when validation, store access, or evaluator limits fail.
1118    pub fn check(
1119        &mut self,
1120        object: &Object,
1121        relation: &Relation,
1122        user: &User,
1123    ) -> Result<Membership, ZanzibarError> {
1124        let key = CheckKey::new(self.snapshot, object, relation, user);
1125        self.evaluate_check_key(&key, |context| {
1126            context.check_entered(object, relation, user)
1127        })
1128    }
1129
1130    pub(crate) fn check_prepared(
1131        &mut self,
1132        object: &Object,
1133        relation: &Relation,
1134        user: &User,
1135        relation_definition: &SchemaRelationDefinition,
1136    ) -> Result<Membership, ZanzibarError> {
1137        let key = CheckKey::new(self.snapshot, object, relation, user);
1138        self.evaluate_check_key(&key, |context| {
1139            match relation_definition.compiled_userset_rewrite() {
1140                Some(expression) => context.eval_compiled_schema_expression(
1141                    object,
1142                    relation_definition.name(),
1143                    user,
1144                    expression,
1145                ),
1146                None => context.eval_this(object, relation_definition.name(), user),
1147            }
1148        })
1149    }
1150
1151    fn verify_lookup_residuals(
1152        &mut self,
1153        object: &Object,
1154        root_relation: &RelationName,
1155        user: &User,
1156        residuals: &[LookupResidualCheck],
1157    ) -> Result<Membership, ZanzibarError> {
1158        let key = CheckKey::from_relation_name(self.snapshot, object, root_relation, user);
1159        self.evaluate_check_key(&key, |context| {
1160            context.verify_lookup_residuals_entered(object, user, residuals)
1161        })
1162    }
1163
1164    fn verify_lookup_residuals_entered(
1165        &mut self,
1166        object: &Object,
1167        user: &User,
1168        residuals: &[LookupResidualCheck],
1169    ) -> Result<Membership, ZanzibarError> {
1170        for residual in residuals {
1171            record_lookup_resources_residual_check();
1172            let result = self.eval_compiled_schema_expression(
1173                object,
1174                &residual.relation_context,
1175                user,
1176                &residual.expression,
1177            )?;
1178            match residual.expected {
1179                LookupResidualExpected::Allowed if result != Membership::Allowed => {
1180                    return Ok(Membership::Denied);
1181                }
1182                LookupResidualExpected::Denied if result == Membership::Allowed => {
1183                    return Ok(Membership::Denied);
1184                }
1185                LookupResidualExpected::Allowed | LookupResidualExpected::Denied => {}
1186            }
1187        }
1188        Ok(Membership::Allowed)
1189    }
1190
1191    fn check_relation_name(
1192        &mut self,
1193        object: &Object,
1194        relation_name: &RelationName,
1195        user: &User,
1196    ) -> Result<Membership, ZanzibarError> {
1197        let key = CheckKey::from_relation_name(self.snapshot, object, relation_name, user);
1198        self.evaluate_check_key(&key, |context| {
1199            context.check_relation_name_entered(object, relation_name, user)
1200        })
1201    }
1202
1203    fn check_relation_id(
1204        &mut self,
1205        object: &Object,
1206        relation_name: &RelationName,
1207        relation_id: SchemaRelationId,
1208        user: &User,
1209    ) -> Result<Membership, ZanzibarError> {
1210        let key = CheckKey::from_relation_name(self.snapshot, object, relation_name, user);
1211        self.evaluate_check_key(&key, |context| {
1212            context.check_relation_id_entered(object, relation_name, relation_id, user)
1213        })
1214    }
1215
1216    /// Expands a userset using the same snapshot and recursion limits.
1217    ///
1218    /// # Errors
1219    ///
1220    /// Returns [`ZanzibarError`] when validation, store access, or evaluator limits fail.
1221    pub fn expand(
1222        &mut self,
1223        object: &Object,
1224        relation: &Relation,
1225    ) -> Result<ExpandedUserset, ZanzibarError> {
1226        let relation_name = RelationName::try_from(relation)?;
1227        self.expand_relation_name(object, &relation_name)
1228    }
1229
1230    fn expand_relation_name(
1231        &mut self,
1232        object: &Object,
1233        relation_name: &RelationName,
1234    ) -> Result<ExpandedUserset, ZanzibarError> {
1235        let key = ExpandKey::new(object, relation_name);
1236        if self.is_expand_active(&key) {
1237            return Ok(ExpandedUserset::Union(Vec::new()));
1238        }
1239        self.enter(EvaluationKey::Expand(key.clone()))?;
1240        self.push_expand_key(key);
1241
1242        let result = self.expand_relation_name_entered(object, relation_name);
1243        self.pop_expand_key();
1244        self.leave();
1245        result
1246    }
1247
1248    fn expand_relation_name_entered(
1249        &mut self,
1250        object: &Object,
1251        relation_name: &RelationName,
1252    ) -> Result<ExpandedUserset, ZanzibarError> {
1253        let object_type = ObjectType::try_from(object.namespace.as_str())?;
1254        let relation_definition = self
1255            .snapshot
1256            .schema()
1257            .resolver()
1258            .relation(&object_type, relation_name)?;
1259        match relation_definition.compiled_userset_rewrite() {
1260            Some(expression) => {
1261                self.expand_compiled_schema_expression(object, relation_name, expression)
1262            }
1263            None => self.expand_this(object, relation_name),
1264        }
1265    }
1266
1267    fn check_entered(
1268        &mut self,
1269        object: &Object,
1270        relation: &Relation,
1271        user: &User,
1272    ) -> Result<Membership, ZanzibarError> {
1273        let object_type = ObjectType::try_from(object.namespace.as_str())?;
1274        let relation_name = RelationName::try_from(relation)?;
1275        self.check_relation_name_entered_with_type(object, &object_type, &relation_name, user)
1276    }
1277
1278    fn check_relation_name_entered(
1279        &mut self,
1280        object: &Object,
1281        relation_name: &RelationName,
1282        user: &User,
1283    ) -> Result<Membership, ZanzibarError> {
1284        let object_type = ObjectType::try_from(object.namespace.as_str())?;
1285        self.check_relation_name_entered_with_type(object, &object_type, relation_name, user)
1286    }
1287
1288    fn check_relation_name_entered_with_type(
1289        &mut self,
1290        object: &Object,
1291        object_type: &ObjectType,
1292        relation_name: &RelationName,
1293        user: &User,
1294    ) -> Result<Membership, ZanzibarError> {
1295        let relation_definition = self
1296            .snapshot
1297            .schema()
1298            .resolver()
1299            .relation(object_type, relation_name)?;
1300        match relation_definition.compiled_userset_rewrite() {
1301            Some(expression) => {
1302                self.eval_compiled_schema_expression(object, relation_name, user, expression)
1303            }
1304            None => self.eval_this(object, relation_name, user),
1305        }
1306    }
1307
1308    fn check_relation_id_entered(
1309        &mut self,
1310        object: &Object,
1311        relation_name: &RelationName,
1312        relation_id: SchemaRelationId,
1313        user: &User,
1314    ) -> Result<Membership, ZanzibarError> {
1315        let relation_definition = self
1316            .snapshot
1317            .schema()
1318            .resolver()
1319            .relation_by_id(relation_id)
1320            .ok_or_else(compiled_schema_invariant_error)?;
1321        match relation_definition.compiled_userset_rewrite() {
1322            Some(expression) => {
1323                self.eval_compiled_schema_expression(object, relation_name, user, expression)
1324            }
1325            None => self.eval_this(object, relation_name, user),
1326        }
1327    }
1328
1329    fn eval_compiled_schema_expression(
1330        &mut self,
1331        object: &Object,
1332        relation_name: &RelationName,
1333        user: &User,
1334        expression: &CompiledUsersetExpression,
1335    ) -> Result<Membership, ZanzibarError> {
1336        match expression {
1337            CompiledUsersetExpression::This => self.eval_this(object, relation_name, user),
1338            CompiledUsersetExpression::ComputedUserset {
1339                relation,
1340                relation_id,
1341                target_has_rewrite,
1342            } => {
1343                if !target_has_rewrite {
1344                    return self.eval_plain_computed_userset(object, relation, user);
1345                }
1346                self.check_relation_id(object, relation, *relation_id, user)
1347            }
1348            CompiledUsersetExpression::TupleToUserset {
1349                tupleset_relation: _,
1350                tupleset_relation_id,
1351                computed_userset_relation,
1352            } => {
1353                let tupleset_relation = self
1354                    .snapshot
1355                    .schema()
1356                    .resolver()
1357                    .relation_by_id(*tupleset_relation_id)
1358                    .ok_or_else(compiled_schema_invariant_error)?
1359                    .name()
1360                    .clone();
1361                self.eval_tuple_to_userset(
1362                    object,
1363                    user,
1364                    &tupleset_relation,
1365                    computed_userset_relation,
1366                )
1367            }
1368            CompiledUsersetExpression::Union(expressions) => {
1369                self.eval_compiled_schema_union(object, relation_name, user, expressions)
1370            }
1371            CompiledUsersetExpression::Intersection(expressions) => {
1372                self.eval_compiled_schema_intersection(object, relation_name, user, expressions)
1373            }
1374            CompiledUsersetExpression::Exclusion { base, exclude } => {
1375                self.eval_compiled_schema_exclusion(object, relation_name, user, base, exclude)
1376            }
1377        }
1378    }
1379
1380    fn eval_plain_computed_userset(
1381        &mut self,
1382        object: &Object,
1383        relation_name: &RelationName,
1384        user: &User,
1385    ) -> Result<Membership, ZanzibarError> {
1386        let key = CheckKey::from_relation_name(self.snapshot, object, relation_name, user);
1387        self.evaluate_check_key(&key, |context| {
1388            context.eval_this(object, relation_name, user)
1389        })
1390    }
1391
1392    fn eval_this(
1393        &mut self,
1394        object: &Object,
1395        relation_name: &RelationName,
1396        user: &User,
1397    ) -> Result<Membership, ZanzibarError> {
1398        let resource = DomainObjectRef::try_from(object)?;
1399        let subject = SubjectFilter::try_from(user)?;
1400        if self.snapshot.relationships().any_resource_relation_subject(
1401            &resource,
1402            relation_name,
1403            &subject,
1404        ) {
1405            return Ok(Membership::Allowed);
1406        }
1407
1408        let mut fanout = 0_u32;
1409        for relationship in self.snapshot.relationships().resource_relation(
1410            &resource,
1411            relation_name,
1412            unbounded_query_limit(),
1413        ) {
1414            if let Some((nested_object, nested_relation)) =
1415                relationship.subject_userset_relation_name()?
1416            {
1417                self.increment_fanout(&mut fanout)?;
1418                if self
1419                    .check_relation_name(&nested_object, &nested_relation, user)?
1420                    .is_allowed()
1421                {
1422                    return Ok(Membership::Allowed);
1423                }
1424            }
1425        }
1426
1427        Ok(Membership::Denied)
1428    }
1429
1430    fn eval_tuple_to_userset(
1431        &mut self,
1432        object: &Object,
1433        user: &User,
1434        tupleset_relation: &RelationName,
1435        computed_userset_relation: &RelationName,
1436    ) -> Result<Membership, ZanzibarError> {
1437        let mut fanout = 0_u32;
1438        let resource = DomainObjectRef::try_from(object)?;
1439        for relationship in self.snapshot.relationships().resource_relation(
1440            &resource,
1441            tupleset_relation,
1442            unbounded_query_limit(),
1443        ) {
1444            if let Some((intermediate_object, _)) = relationship.subject_userset_relation_name()? {
1445                self.increment_fanout(&mut fanout)?;
1446                if self
1447                    .check_relation_name(&intermediate_object, computed_userset_relation, user)?
1448                    .is_allowed()
1449                {
1450                    return Ok(Membership::Allowed);
1451                }
1452            }
1453        }
1454        Ok(Membership::Denied)
1455    }
1456
1457    fn eval_compiled_schema_union(
1458        &mut self,
1459        object: &Object,
1460        relation_name: &RelationName,
1461        user: &User,
1462        expressions: &[CompiledUsersetExpression],
1463    ) -> Result<Membership, ZanzibarError> {
1464        let mut result = Membership::Denied;
1465        for expression in expressions {
1466            result = result.union(self.eval_compiled_schema_expression(
1467                object,
1468                relation_name,
1469                user,
1470                expression,
1471            )?);
1472            if result == Membership::Allowed {
1473                return Ok(result);
1474            }
1475        }
1476        Ok(result)
1477    }
1478
1479    fn eval_compiled_schema_intersection(
1480        &mut self,
1481        object: &Object,
1482        relation_name: &RelationName,
1483        user: &User,
1484        expressions: &[CompiledUsersetExpression],
1485    ) -> Result<Membership, ZanzibarError> {
1486        let mut result = Membership::Allowed;
1487        for expression in expressions {
1488            result = result.intersection(self.eval_compiled_schema_expression(
1489                object,
1490                relation_name,
1491                user,
1492                expression,
1493            )?);
1494            if result == Membership::Denied {
1495                return Ok(result);
1496            }
1497        }
1498        Ok(result)
1499    }
1500
1501    fn eval_compiled_schema_exclusion(
1502        &mut self,
1503        object: &Object,
1504        relation_name: &RelationName,
1505        user: &User,
1506        base: &CompiledUsersetExpression,
1507        exclude: &CompiledUsersetExpression,
1508    ) -> Result<Membership, ZanzibarError> {
1509        if Self::should_eval_exclude_first(exclude) {
1510            let exclude_result =
1511                self.eval_compiled_schema_expression(object, relation_name, user, exclude)?;
1512            if exclude_result == Membership::Allowed {
1513                return Ok(Membership::Denied);
1514            }
1515            let base_result =
1516                self.eval_compiled_schema_expression(object, relation_name, user, base)?;
1517            return Ok(base_result.exclusion(exclude_result));
1518        }
1519
1520        let base = self.eval_compiled_schema_expression(object, relation_name, user, base)?;
1521        if base == Membership::Denied {
1522            return Ok(Membership::Denied);
1523        }
1524        let exclude = self.eval_compiled_schema_expression(object, relation_name, user, exclude)?;
1525        Ok(base.exclusion(exclude))
1526    }
1527
1528    fn should_eval_exclude_first(exclude: &CompiledUsersetExpression) -> bool {
1529        match exclude {
1530            CompiledUsersetExpression::This => true,
1531            CompiledUsersetExpression::ComputedUserset {
1532                target_has_rewrite, ..
1533            } => !target_has_rewrite,
1534            CompiledUsersetExpression::TupleToUserset { .. }
1535            | CompiledUsersetExpression::Union(_)
1536            | CompiledUsersetExpression::Intersection(_)
1537            | CompiledUsersetExpression::Exclusion { .. } => false,
1538        }
1539    }
1540
1541    fn expand_compiled_schema_expression(
1542        &mut self,
1543        object: &Object,
1544        relation_name: &RelationName,
1545        expression: &CompiledUsersetExpression,
1546    ) -> Result<ExpandedUserset, ZanzibarError> {
1547        match expression {
1548            CompiledUsersetExpression::This => self.expand_this(object, relation_name),
1549            CompiledUsersetExpression::ComputedUserset { relation, .. } => {
1550                self.expand_relation_name(object, relation)
1551            }
1552            CompiledUsersetExpression::TupleToUserset {
1553                tupleset_relation: _,
1554                tupleset_relation_id,
1555                computed_userset_relation,
1556            } => {
1557                let mut users = Vec::new();
1558                let mut fanout = 0_u32;
1559                let resource = DomainObjectRef::try_from(object)?;
1560                let tupleset_relation = self
1561                    .snapshot
1562                    .schema()
1563                    .resolver()
1564                    .relation_by_id(*tupleset_relation_id)
1565                    .ok_or_else(compiled_schema_invariant_error)?
1566                    .name()
1567                    .clone();
1568                for relationship in self.snapshot.relationships().resource_relation(
1569                    &resource,
1570                    &tupleset_relation,
1571                    unbounded_query_limit(),
1572                ) {
1573                    if let Some((intermediate_object, _)) =
1574                        relationship.subject_userset_relation_name()?
1575                    {
1576                        self.increment_fanout(&mut fanout)?;
1577                        users.push(self.expand_relation_name(
1578                            &intermediate_object,
1579                            computed_userset_relation,
1580                        )?);
1581                    }
1582                }
1583                Ok(ExpandedUserset::Union(users))
1584            }
1585            CompiledUsersetExpression::Union(expressions) => {
1586                let mut users = Vec::with_capacity(expressions.len());
1587                for expression in expressions {
1588                    users.push(self.expand_compiled_schema_expression(
1589                        object,
1590                        relation_name,
1591                        expression,
1592                    )?);
1593                }
1594                Ok(ExpandedUserset::Union(users))
1595            }
1596            CompiledUsersetExpression::Intersection(expressions) => {
1597                let mut users = Vec::with_capacity(expressions.len());
1598                for expression in expressions {
1599                    users.push(self.expand_compiled_schema_expression(
1600                        object,
1601                        relation_name,
1602                        expression,
1603                    )?);
1604                }
1605                Ok(ExpandedUserset::Intersection(users))
1606            }
1607            CompiledUsersetExpression::Exclusion { base, exclude } => {
1608                let base = self.expand_compiled_schema_expression(object, relation_name, base)?;
1609                let exclude =
1610                    self.expand_compiled_schema_expression(object, relation_name, exclude)?;
1611                Ok(ExpandedUserset::Exclusion {
1612                    base: Box::new(base),
1613                    exclude: Box::new(exclude),
1614                })
1615            }
1616        }
1617    }
1618
1619    fn expand_this(
1620        &mut self,
1621        object: &Object,
1622        relation_name: &RelationName,
1623    ) -> Result<ExpandedUserset, ZanzibarError> {
1624        let mut users = Vec::new();
1625        let mut fanout = 0_u32;
1626        let resource = DomainObjectRef::try_from(object)?;
1627        for relationship in self.snapshot.relationships().resource_relation(
1628            &resource,
1629            relation_name,
1630            self.fanout_query_limit(),
1631        ) {
1632            self.increment_fanout(&mut fanout)?;
1633            users.push(relationship.expanded_subject()?);
1634        }
1635        Ok(ExpandedUserset::Union(users))
1636    }
1637
1638    fn stream_lookup_subjects_relation(
1639        &mut self,
1640        collector: &mut LookupSubjectCollector<'_, '_>,
1641        object: &Object,
1642        relation_name: &RelationName,
1643        inherited_verify_candidates: bool,
1644    ) -> Result<(), ZanzibarError> {
1645        if collector.result_limit_reached() {
1646            record_lookup_subjects_result_limit_exit();
1647            return Ok(());
1648        }
1649        let key = ExpandKey::new(object, relation_name);
1650        if self.is_expand_active(&key) {
1651            return Ok(());
1652        }
1653        self.enter(EvaluationKey::Expand(key.clone()))?;
1654        self.push_expand_key(key);
1655
1656        let result = self.stream_lookup_subjects_relation_entered(
1657            collector,
1658            object,
1659            relation_name,
1660            inherited_verify_candidates,
1661        );
1662        self.pop_expand_key();
1663        self.leave();
1664        result
1665    }
1666
1667    fn stream_lookup_subjects_relation_entered(
1668        &mut self,
1669        collector: &mut LookupSubjectCollector<'_, '_>,
1670        object: &Object,
1671        relation_name: &RelationName,
1672        inherited_verify_candidates: bool,
1673    ) -> Result<(), ZanzibarError> {
1674        let object_type = ObjectType::try_from(object.namespace.as_str())?;
1675        let relation_definition = self
1676            .snapshot
1677            .schema()
1678            .resolver()
1679            .relation(&object_type, relation_name)?;
1680        let verify_candidates = inherited_verify_candidates
1681            || relation_definition_requires_lookup_subject_verification(
1682                self.snapshot,
1683                relation_definition,
1684            )?;
1685        match relation_definition.compiled_userset_rewrite() {
1686            Some(expression) => self.stream_lookup_subjects_expression(
1687                collector,
1688                object,
1689                relation_name,
1690                expression,
1691                verify_candidates,
1692            ),
1693            None => self.stream_lookup_subjects_this(
1694                collector,
1695                object,
1696                relation_name,
1697                verify_candidates,
1698            ),
1699        }
1700    }
1701
1702    fn stream_lookup_subjects_expression(
1703        &mut self,
1704        collector: &mut LookupSubjectCollector<'_, '_>,
1705        object: &Object,
1706        relation_name: &RelationName,
1707        expression: &CompiledUsersetExpression,
1708        verify_candidates: bool,
1709    ) -> Result<(), ZanzibarError> {
1710        if collector.result_limit_reached() {
1711            record_lookup_subjects_result_limit_exit();
1712            return Ok(());
1713        }
1714        match expression {
1715            CompiledUsersetExpression::This => self.stream_lookup_subjects_this(
1716                collector,
1717                object,
1718                relation_name,
1719                verify_candidates,
1720            ),
1721            CompiledUsersetExpression::ComputedUserset { relation, .. } => {
1722                self.stream_lookup_subjects_relation(collector, object, relation, verify_candidates)
1723            }
1724            CompiledUsersetExpression::TupleToUserset {
1725                tupleset_relation: _,
1726                tupleset_relation_id,
1727                computed_userset_relation,
1728            } => {
1729                let mut fanout = 0_u32;
1730                let resource = DomainObjectRef::try_from(object)?;
1731                let tupleset_relation = self
1732                    .snapshot
1733                    .schema()
1734                    .resolver()
1735                    .relation_by_id(*tupleset_relation_id)
1736                    .ok_or_else(compiled_schema_invariant_error)?
1737                    .name()
1738                    .clone();
1739                for relationship in self.snapshot.relationships().resource_relation(
1740                    &resource,
1741                    &tupleset_relation,
1742                    unbounded_query_limit(),
1743                ) {
1744                    if collector.result_limit_reached() {
1745                        record_lookup_subjects_result_limit_exit();
1746                        return Ok(());
1747                    }
1748                    if let Some((intermediate_object, _)) =
1749                        relationship.subject_userset_relation_name()?
1750                    {
1751                        self.increment_fanout(&mut fanout)?;
1752                        self.stream_lookup_subjects_relation(
1753                            collector,
1754                            &intermediate_object,
1755                            computed_userset_relation,
1756                            true,
1757                        )?;
1758                    }
1759                }
1760                Ok(())
1761            }
1762            CompiledUsersetExpression::Union(expressions)
1763            | CompiledUsersetExpression::Intersection(expressions) => {
1764                for expression in expressions {
1765                    self.stream_lookup_subjects_expression(
1766                        collector,
1767                        object,
1768                        relation_name,
1769                        expression,
1770                        verify_candidates,
1771                    )?;
1772                    if collector.result_limit_reached() {
1773                        record_lookup_subjects_result_limit_exit();
1774                        return Ok(());
1775                    }
1776                }
1777                Ok(())
1778            }
1779            CompiledUsersetExpression::Exclusion { base, .. } => {
1780                self.stream_lookup_subjects_expression(collector, object, relation_name, base, true)
1781            }
1782        }
1783    }
1784
1785    fn stream_lookup_subjects_this(
1786        &mut self,
1787        collector: &mut LookupSubjectCollector<'_, '_>,
1788        object: &Object,
1789        relation_name: &RelationName,
1790        verify_candidates: bool,
1791    ) -> Result<(), ZanzibarError> {
1792        let mut fanout = 0_u32;
1793        let resource = DomainObjectRef::try_from(object)?;
1794        for relationship in self.snapshot.relationships().resource_relation(
1795            &resource,
1796            relation_name,
1797            self.fanout_query_limit(),
1798        ) {
1799            if collector.result_limit_reached() {
1800                record_lookup_subjects_result_limit_exit();
1801                return Ok(());
1802            }
1803            self.increment_fanout(&mut fanout)?;
1804            if let Some((nested_object, nested_relation)) =
1805                relationship.subject_userset_relation_name()?
1806            {
1807                if collector.collect_userset_candidate(
1808                    &nested_object,
1809                    &nested_relation,
1810                    verify_candidates,
1811                )? {
1812                    self.stream_lookup_subjects_relation(
1813                        collector,
1814                        &nested_object,
1815                        &nested_relation,
1816                        verify_candidates,
1817                    )?;
1818                }
1819            } else if let Some(user_id) = relationship.direct_user_subject_id() {
1820                collector.collect_user_candidate(user_id, verify_candidates)?;
1821            } else {
1822                let _ = relationship.expanded_subject()?;
1823            }
1824        }
1825        Ok(())
1826    }
1827
1828    fn enter(&mut self, key: EvaluationKey) -> Result<(), ZanzibarError> {
1829        if self.remaining_depth == 0 {
1830            return Err(EvaluationError::DepthExceeded { key: Box::new(key) }.into());
1831        }
1832        self.remaining_depth = self.remaining_depth.saturating_sub(1);
1833        self.record_check_frame_depth(self.remaining_depth);
1834        Ok(())
1835    }
1836
1837    fn leave(&mut self) {
1838        self.remaining_depth = self.remaining_depth.saturating_add(1);
1839    }
1840
1841    fn increment_fanout(&self, current: &mut u32) -> Result<(), ZanzibarError> {
1842        *current = current.saturating_add(1);
1843        if *current > self.limits.max_fanout_per_step.get() {
1844            return Err(EvaluationError::FanoutExceeded {
1845                limit: self.limits.max_fanout_per_step,
1846            }
1847            .into());
1848        }
1849        Ok(())
1850    }
1851
1852    fn fanout_query_limit(&self) -> QueryLimit {
1853        let requested = u64::from(self.limits.max_fanout_per_step.get()) + 1;
1854        let limit = match usize::try_from(requested).ok().and_then(NonZeroUsize::new) {
1855            Some(limit) => limit,
1856            None => NonZeroUsize::MAX,
1857        };
1858        QueryLimit::new(limit)
1859    }
1860
1861    fn push_check_frame(&mut self, entry_remaining_depth: u32) {
1862        self.check_frames.push(CheckFrame {
1863            entry_remaining_depth,
1864            minimum_remaining_depth: self.remaining_depth,
1865        });
1866    }
1867
1868    fn pop_check_frame_depth_required(&mut self) -> NonZeroU32 {
1869        let Some(frame) = self.check_frames.pop() else {
1870            return NonZeroU32::MIN;
1871        };
1872        self.record_check_frame_depth(frame.minimum_remaining_depth);
1873        non_zero_u32(
1874            frame
1875                .entry_remaining_depth
1876                .saturating_sub(frame.minimum_remaining_depth),
1877        )
1878    }
1879
1880    fn record_check_frame_depth(&mut self, remaining_depth: u32) {
1881        if let Some(frame) = self.check_frames.last_mut() {
1882            frame.minimum_remaining_depth = frame.minimum_remaining_depth.min(remaining_depth);
1883        }
1884    }
1885
1886    fn is_check_active(&self, key: &CheckKey) -> bool {
1887        if !self.use_active_indexes || self.check_stack.len() < ACTIVE_INDEX_THRESHOLD {
1888            return self.check_stack.contains(key);
1889        }
1890        self.active_checks
1891            .get(key)
1892            .is_some_and(|mark| mark.generation == self.generation && mark.active)
1893    }
1894
1895    fn push_check_key(&mut self, key: CheckKey) {
1896        if self.use_active_indexes && self.check_stack.len() + 1 >= ACTIVE_INDEX_THRESHOLD {
1897            if self.check_stack.len() + 1 == ACTIVE_INDEX_THRESHOLD {
1898                self.rebuild_active_checks();
1899            }
1900            self.active_checks.insert(
1901                key.clone(),
1902                VisitMark {
1903                    generation: self.generation,
1904                    active: true,
1905                },
1906            );
1907        }
1908        self.check_stack.push(key);
1909    }
1910
1911    fn pop_check_key(&mut self) {
1912        if let Some(key) = self.check_stack.pop() {
1913            self.deactivate_check_key(&key);
1914        }
1915    }
1916
1917    fn deactivate_check_key(&mut self, key: &CheckKey) {
1918        if let Some(mark) = self.active_checks.get_mut(key)
1919            && mark.generation == self.generation
1920        {
1921            mark.active = false;
1922        }
1923    }
1924
1925    fn rebuild_active_checks(&mut self) {
1926        for key in &self.check_stack {
1927            self.active_checks.insert(
1928                key.clone(),
1929                VisitMark {
1930                    generation: self.generation,
1931                    active: true,
1932                },
1933            );
1934        }
1935    }
1936
1937    fn is_expand_active(&self, key: &ExpandKey) -> bool {
1938        if !self.use_active_indexes || self.expand_stack.len() < ACTIVE_INDEX_THRESHOLD {
1939            return self.expand_stack.contains(key);
1940        }
1941        self.active_expands
1942            .get(key)
1943            .is_some_and(|mark| mark.generation == self.generation && mark.active)
1944    }
1945
1946    fn push_expand_key(&mut self, key: ExpandKey) {
1947        if self.use_active_indexes && self.expand_stack.len() + 1 >= ACTIVE_INDEX_THRESHOLD {
1948            if self.expand_stack.len() + 1 == ACTIVE_INDEX_THRESHOLD {
1949                self.rebuild_active_expands();
1950            }
1951            self.active_expands.insert(
1952                key.clone(),
1953                VisitMark {
1954                    generation: self.generation,
1955                    active: true,
1956                },
1957            );
1958        }
1959        self.expand_stack.push(key);
1960    }
1961
1962    fn pop_expand_key(&mut self) {
1963        if let Some(key) = self.expand_stack.pop() {
1964            self.deactivate_expand_key(&key);
1965        }
1966    }
1967
1968    fn deactivate_expand_key(&mut self, key: &ExpandKey) {
1969        if let Some(mark) = self.active_expands.get_mut(key)
1970            && mark.generation == self.generation
1971        {
1972            mark.active = false;
1973        }
1974    }
1975
1976    fn rebuild_active_expands(&mut self) {
1977        for key in &self.expand_stack {
1978            self.active_expands.insert(
1979                key.clone(),
1980                VisitMark {
1981                    generation: self.generation,
1982                    active: true,
1983                },
1984            );
1985        }
1986    }
1987}
1988
1989/// Evaluates a snapshot-backed check request.
1990///
1991/// # Errors
1992///
1993/// Returns [`ZanzibarError`] when validation, store access, or evaluator limits fail.
1994pub fn check_with_snapshot(
1995    snapshot: &PublishedSnapshot,
1996    object: &Object,
1997    relation: &Relation,
1998    user: &User,
1999    limits: EvaluationLimits,
2000) -> Result<Membership, ZanzibarError> {
2001    EvaluationContext::new(snapshot, limits).check(object, relation, user)
2002}
2003
2004pub(crate) fn check_prepared_with_snapshot(
2005    snapshot: &PublishedSnapshot,
2006    object: &Object,
2007    relation: &Relation,
2008    user: &User,
2009    relation_definition: &SchemaRelationDefinition,
2010    limits: EvaluationLimits,
2011) -> Result<Membership, ZanzibarError> {
2012    EvaluationContext::new(snapshot, limits).check_prepared(
2013        object,
2014        relation,
2015        user,
2016        relation_definition,
2017    )
2018}
2019
2020/// Evaluates a snapshot-backed expand request.
2021///
2022/// # Errors
2023///
2024/// Returns [`ZanzibarError`] when validation, store access, or evaluator limits fail.
2025pub fn expand_with_snapshot(
2026    snapshot: &PublishedSnapshot,
2027    object: &Object,
2028    relation: &Relation,
2029    limits: EvaluationLimits,
2030) -> Result<ExpandedUserset, ZanzibarError> {
2031    EvaluationContext::new(snapshot, limits).expand(object, relation)
2032}
2033
2034/// Looks up resources of the requested type that pass the shared snapshot-backed check evaluator.
2035///
2036/// # Errors
2037///
2038/// Returns [`ZanzibarError`] when request validation, store access, or evaluation fails.
2039pub fn lookup_resources_with_snapshot(
2040    snapshot: &PublishedSnapshot,
2041    request: &LookupResourcesRequest,
2042    limits: EvaluationLimits,
2043) -> Result<LookupResources, ZanzibarError> {
2044    let resource_type = ObjectType::try_from(request.resource_type.as_str())?;
2045    let permission = RelationName::try_from(&request.permission)?;
2046    snapshot
2047        .schema()
2048        .resolver()
2049        .relation(&resource_type, &permission)?;
2050    let producer_plan = lookup_producer_plan(snapshot, &resource_type, &permission)?;
2051    if producer_plan.is_none() {
2052        record_lookup_resources_planner_fallback();
2053    }
2054    let mut producer_runtime = LookupProducerRuntime::new(producer_plan.is_some());
2055    let resource_subject_type = SubjectType::try_from(resource_type.as_str())?;
2056    let mut pruned_relation_has_downstream = Vec::new();
2057    let mut relation_expansions = Vec::new();
2058    let mut tuple_relation_expansions = Vec::new();
2059
2060    let mut frontier = VecDeque::from([LookupFrontierEntry::new(
2061        request.subject.clone(),
2062        LookupSubjectProof::exact_root(),
2063    )]);
2064    let mut visited_subjects = HashSet::from([request.subject.clone()]);
2065    let mut seen = HashSet::new();
2066    let mut resources = Vec::new();
2067    let mut check_context = EvaluationContext::new_with_request_memo(snapshot, limits);
2068
2069    while let Some(frontier_entry) = frontier.pop_front() {
2070        record_lookup_resources_frontier_subject();
2071        enqueue_same_object_relation_expansions(
2072            snapshot,
2073            &frontier_entry.subject,
2074            &mut frontier,
2075            &mut visited_subjects,
2076            &mut relation_expansions,
2077        )?;
2078        let subject_filter = SubjectFilter::try_from(&frontier_entry.subject)?;
2079        for relationship in snapshot
2080            .relationships()
2081            .reverse_query_compact_relationships(&subject_filter)
2082        {
2083            record_lookup_resources_frontier_relationship();
2084            if process_lookup_resources_relationship(
2085                snapshot,
2086                relationship,
2087                &frontier_entry,
2088                request,
2089                limits,
2090                &resource_type,
2091                &resource_subject_type,
2092                producer_plan.as_ref(),
2093                &mut producer_runtime,
2094                &mut pruned_relation_has_downstream,
2095                &mut frontier,
2096                &mut visited_subjects,
2097                &mut seen,
2098                &mut resources,
2099                &mut check_context,
2100            )? {
2101                return Ok(LookupResources { resources });
2102            }
2103        }
2104        if process_tuple_to_userset_ignored_relation_edges(
2105            snapshot,
2106            &frontier_entry.subject,
2107            request,
2108            limits,
2109            &resource_type,
2110            &mut frontier,
2111            &mut visited_subjects,
2112            &mut seen,
2113            &mut resources,
2114            &mut check_context,
2115            &mut tuple_relation_expansions,
2116        )? {
2117            return Ok(LookupResources { resources });
2118        }
2119    }
2120
2121    Ok(LookupResources { resources })
2122}
2123
2124#[allow(
2125    clippy::too_many_arguments,
2126    reason = "relationship traversal updates shared lookup_resources scratch without heap boxing"
2127)]
2128fn process_lookup_resources_relationship(
2129    snapshot: &PublishedSnapshot,
2130    relationship: crate::relationship::RelationshipRef<'_>,
2131    frontier_entry: &LookupFrontierEntry,
2132    request: &LookupResourcesRequest,
2133    limits: EvaluationLimits,
2134    resource_type: &ObjectType,
2135    resource_subject_type: &SubjectType,
2136    producer_plan: Option<&LookupProducerPlan>,
2137    producer_runtime: &mut LookupProducerRuntime,
2138    pruned_relation_has_downstream: &mut Vec<(RelationName, bool)>,
2139    frontier: &mut VecDeque<LookupFrontierEntry>,
2140    visited_subjects: &mut HashSet<User>,
2141    seen: &mut HashSet<Object>,
2142    resources: &mut Vec<Object>,
2143    check_context: &mut EvaluationContext<'_>,
2144) -> Result<bool, ZanzibarError> {
2145    let resource_type_matches = relationship.resource_type_eq(resource_type);
2146    let should_prune = if resource_type_matches {
2147        producer_plan.is_some_and(|plan| producer_runtime.should_prune(plan, relationship))
2148    } else {
2149        false
2150    };
2151    if should_prune {
2152        record_lookup_resources_schema_pruned();
2153        if !pruned_relation_may_have_downstream(
2154            snapshot,
2155            resource_subject_type,
2156            relationship.relation_name_str(),
2157            pruned_relation_has_downstream,
2158        )? {
2159            return Ok(false);
2160        }
2161    }
2162
2163    let object = relationship.resource_object_legacy();
2164    let relationship_proof =
2165        lookup_relationship_proof(snapshot, relationship, frontier_entry.proof, &object)?;
2166    if resource_type_matches && !should_prune && seen.insert(object.clone()) {
2167        record_lookup_resources_candidate_resource();
2168        let verification = producer_plan.map_or(LookupCandidateVerification::FullRoot, |plan| {
2169            plan.verification_for_relationship(relationship, relationship_proof, limits)
2170        });
2171        if verify_lookup_resource_candidate(check_context, &object, request, &verification)? {
2172            resources.push(object.clone());
2173            record_lookup_resources_returned();
2174            if lookup_result_limit_reached(resources.len(), limits) {
2175                record_lookup_resources_result_limit_exit();
2176                return Ok(true);
2177            }
2178        }
2179    }
2180
2181    let userset_subject = User::Userset(
2182        object,
2183        Relation(relationship.relation_name_str().to_string()),
2184    );
2185    let should_follow_userset = if should_prune {
2186        let userset_filter = SubjectFilter::try_from(&userset_subject)?;
2187        snapshot
2188            .relationships()
2189            .has_reverse_subject_candidates(&userset_filter)
2190    } else {
2191        true
2192    };
2193    if should_follow_userset && visited_subjects.insert(userset_subject.clone()) {
2194        frontier.push_back(LookupFrontierEntry::new(
2195            userset_subject,
2196            relationship_proof,
2197        ));
2198    }
2199    Ok(false)
2200}
2201
2202fn lookup_relationship_proof(
2203    snapshot: &PublishedSnapshot,
2204    relationship: crate::relationship::RelationshipRef<'_>,
2205    frontier_proof: LookupSubjectProof,
2206    object: &Object,
2207) -> Result<LookupSubjectProof, ZanzibarError> {
2208    if !frontier_proof.is_exact() || frontier_proof.direct_depth > 0 {
2209        return Ok(LookupSubjectProof::inexact());
2210    }
2211    let object_type = ObjectType::try_from(object.namespace.as_str())?;
2212    let relation = RelationName::try_from(relationship.relation_name_str())?;
2213    if relation_direct_row_is_exact_proof(snapshot, &object_type, &relation)? {
2214        Ok(frontier_proof.next_exact())
2215    } else {
2216        Ok(LookupSubjectProof::inexact())
2217    }
2218}
2219
2220fn relation_direct_row_is_exact_proof(
2221    snapshot: &PublishedSnapshot,
2222    object_type: &ObjectType,
2223    relation: &RelationName,
2224) -> Result<bool, ZanzibarError> {
2225    let relation_definition = snapshot
2226        .schema()
2227        .resolver()
2228        .relation(object_type, relation)?;
2229    Ok(matches!(
2230        relation_definition.compiled_userset_rewrite(),
2231        None | Some(CompiledUsersetExpression::This)
2232    ))
2233}
2234
2235fn verify_lookup_resource_candidate(
2236    check_context: &mut EvaluationContext<'_>,
2237    object: &Object,
2238    request: &LookupResourcesRequest,
2239    verification: &LookupCandidateVerification,
2240) -> Result<bool, ZanzibarError> {
2241    match verification {
2242        LookupCandidateVerification::FullRoot => {
2243            check_context.reset_for_reuse();
2244            record_lookup_resources_full_root_check();
2245            Ok(check_context
2246                .check(object, &request.permission, &request.subject)?
2247                .is_allowed())
2248        }
2249        LookupCandidateVerification::ProvenWithoutCheck => {
2250            record_lookup_resources_proven_without_check();
2251            Ok(true)
2252        }
2253        LookupCandidateVerification::Residual(residuals) => {
2254            check_context.reset_for_reuse();
2255            let root_relation = RelationName::try_from(&request.permission)?;
2256            Ok(check_context
2257                .verify_lookup_residuals(object, &root_relation, &request.subject, residuals)?
2258                .is_allowed())
2259        }
2260    }
2261}
2262
2263fn enqueue_same_object_relation_expansions(
2264    snapshot: &PublishedSnapshot,
2265    subject: &User,
2266    frontier: &mut VecDeque<LookupFrontierEntry>,
2267    visited_subjects: &mut HashSet<User>,
2268    relation_expansions: &mut Vec<SameObjectRelationExpansion>,
2269) -> Result<(), ZanzibarError> {
2270    let User::Userset(object, relation) = subject else {
2271        return Ok(());
2272    };
2273    let object_type = ObjectType::try_from(object.namespace.as_str())?;
2274    let source_relation = RelationName::try_from(relation.0.as_str())?;
2275    let target_relations = same_object_relation_expansions(
2276        snapshot,
2277        &object_type,
2278        &source_relation,
2279        relation_expansions,
2280    )?;
2281    for target_relation in target_relations {
2282        let expanded_subject = User::Userset(
2283            object.clone(),
2284            Relation(target_relation.as_str().to_string()),
2285        );
2286        let subject_filter = SubjectFilter::try_from(&expanded_subject)?;
2287        if snapshot
2288            .relationships()
2289            .has_reverse_subject_candidates(&subject_filter)
2290            && visited_subjects.insert(expanded_subject.clone())
2291        {
2292            frontier.push_back(LookupFrontierEntry::new(
2293                expanded_subject,
2294                LookupSubjectProof::inexact(),
2295            ));
2296        }
2297    }
2298    Ok(())
2299}
2300
2301fn same_object_relation_expansions<'a>(
2302    snapshot: &PublishedSnapshot,
2303    object_type: &ObjectType,
2304    source_relation: &RelationName,
2305    relation_expansions: &'a mut Vec<SameObjectRelationExpansion>,
2306) -> Result<&'a [RelationName], ZanzibarError> {
2307    if let Some(index) = relation_expansions.iter().position(|expansion| {
2308        &expansion.object_type == object_type && &expansion.source_relation == source_relation
2309    }) {
2310        return Ok(&relation_expansions[index].target_relations);
2311    }
2312
2313    let target_relations =
2314        compute_same_object_relation_expansions(snapshot, object_type, source_relation)?;
2315    relation_expansions.push(SameObjectRelationExpansion {
2316        object_type: object_type.clone(),
2317        source_relation: source_relation.clone(),
2318        target_relations,
2319    });
2320    let index = relation_expansions.len().saturating_sub(1);
2321    Ok(&relation_expansions[index].target_relations)
2322}
2323
2324fn compute_same_object_relation_expansions(
2325    snapshot: &PublishedSnapshot,
2326    object_type: &ObjectType,
2327    source_relation: &RelationName,
2328) -> Result<Vec<RelationName>, ZanzibarError> {
2329    let resolver = snapshot.schema().resolver();
2330    let mut expansions = Vec::new();
2331    for relation_definition in resolver.sorted_relations(object_type)? {
2332        if relation_definition.name() == source_relation {
2333            continue;
2334        }
2335        let relation_id = resolver.relation_id(object_type, relation_definition.name())?;
2336        let mut visiting = HashSet::new();
2337        if relation_id_can_include_same_object_relation(
2338            snapshot,
2339            relation_id,
2340            source_relation,
2341            &mut visiting,
2342        )? {
2343            expansions.push(relation_definition.name().clone());
2344        }
2345    }
2346    Ok(expansions)
2347}
2348
2349fn relation_id_can_include_same_object_relation(
2350    snapshot: &PublishedSnapshot,
2351    relation_id: SchemaRelationId,
2352    source_relation: &RelationName,
2353    visiting: &mut HashSet<SchemaRelationId>,
2354) -> Result<bool, ZanzibarError> {
2355    if !visiting.insert(relation_id) {
2356        return Ok(false);
2357    }
2358    let relation_definition = snapshot
2359        .schema()
2360        .resolver()
2361        .relation_by_id(relation_id)
2362        .ok_or_else(compiled_schema_invariant_error)?;
2363    let includes = relation_definition_can_include_same_object_relation(
2364        snapshot,
2365        relation_definition,
2366        source_relation,
2367        visiting,
2368    )?;
2369    visiting.remove(&relation_id);
2370    Ok(includes)
2371}
2372
2373fn relation_definition_can_include_same_object_relation(
2374    snapshot: &PublishedSnapshot,
2375    relation_definition: &SchemaRelationDefinition,
2376    source_relation: &RelationName,
2377    visiting: &mut HashSet<SchemaRelationId>,
2378) -> Result<bool, ZanzibarError> {
2379    let Some(expression) = relation_definition.compiled_userset_rewrite() else {
2380        return Ok(false);
2381    };
2382    expression_can_include_same_object_relation(
2383        snapshot,
2384        relation_definition.name(),
2385        expression,
2386        source_relation,
2387        visiting,
2388    )
2389}
2390
2391fn expression_can_include_same_object_relation(
2392    snapshot: &PublishedSnapshot,
2393    current_relation: &RelationName,
2394    expression: &CompiledUsersetExpression,
2395    source_relation: &RelationName,
2396    visiting: &mut HashSet<SchemaRelationId>,
2397) -> Result<bool, ZanzibarError> {
2398    match expression {
2399        CompiledUsersetExpression::This => Ok(current_relation == source_relation),
2400        CompiledUsersetExpression::ComputedUserset {
2401            relation,
2402            relation_id,
2403            target_has_rewrite,
2404        } => {
2405            if relation == source_relation {
2406                return Ok(true);
2407            }
2408            if !target_has_rewrite {
2409                return Ok(false);
2410            }
2411            relation_id_can_include_same_object_relation(
2412                snapshot,
2413                *relation_id,
2414                source_relation,
2415                visiting,
2416            )
2417        }
2418        CompiledUsersetExpression::Union(expressions)
2419        | CompiledUsersetExpression::Intersection(expressions) => {
2420            for expression in expressions {
2421                if expression_can_include_same_object_relation(
2422                    snapshot,
2423                    current_relation,
2424                    expression,
2425                    source_relation,
2426                    visiting,
2427                )? {
2428                    return Ok(true);
2429                }
2430            }
2431            Ok(false)
2432        }
2433        CompiledUsersetExpression::Exclusion { base, .. } => {
2434            expression_can_include_same_object_relation(
2435                snapshot,
2436                current_relation,
2437                base,
2438                source_relation,
2439                visiting,
2440            )
2441        }
2442        CompiledUsersetExpression::TupleToUserset { .. } => Ok(false),
2443    }
2444}
2445
2446#[allow(
2447    clippy::too_many_arguments,
2448    reason = "lookup_resources keeps traversal-owned scratch in locals to avoid per-request heap \
2449              indirection"
2450)]
2451fn process_tuple_to_userset_ignored_relation_edges(
2452    snapshot: &PublishedSnapshot,
2453    subject: &User,
2454    request: &LookupResourcesRequest,
2455    limits: EvaluationLimits,
2456    resource_type: &ObjectType,
2457    frontier: &mut VecDeque<LookupFrontierEntry>,
2458    visited_subjects: &mut HashSet<User>,
2459    seen: &mut HashSet<Object>,
2460    resources: &mut Vec<Object>,
2461    check_context: &mut EvaluationContext<'_>,
2462    tuple_relation_expansions: &mut Vec<TupleToUsersetRelationExpansion>,
2463) -> Result<bool, ZanzibarError> {
2464    let User::Userset(object, relation) = subject else {
2465        return Ok(false);
2466    };
2467    let object_type = ObjectType::try_from(object.namespace.as_str())?;
2468    let source_relation = RelationName::try_from(relation.0.as_str())?;
2469    let targets = tuple_to_userset_relation_expansions(
2470        snapshot,
2471        &object_type,
2472        &source_relation,
2473        tuple_relation_expansions,
2474    )?;
2475    let subject_type = SubjectType::try_from(object.namespace.as_str())?;
2476    let subject_id = SubjectId::try_from(object.id.as_str())?;
2477    let broad_targets =
2478        tuple_to_userset_broad_targets(snapshot, &subject_type, &subject_id, targets);
2479    if broad_targets.is_empty() {
2480        return Ok(false);
2481    }
2482
2483    let subject_filter = SubjectFilter::exact(subject_type, subject_id, None);
2484    for relationship in snapshot
2485        .relationships()
2486        .reverse_query_compact_relationships(&subject_filter)
2487    {
2488        record_lookup_resources_frontier_relationship();
2489        for target in broad_targets.iter().copied().filter(|target| {
2490            relationship.resource_type_eq(&target.resource_type)
2491                && relationship.relation_name_eq(&target.tupleset_relation)
2492        }) {
2493            let candidate = relationship.resource_object_legacy();
2494            if relationship.resource_type_eq(resource_type) && seen.insert(candidate.clone()) {
2495                record_lookup_resources_candidate_resource();
2496                record_lookup_resources_tuple_fallback();
2497                if verify_lookup_resource_candidate(
2498                    check_context,
2499                    &candidate,
2500                    request,
2501                    &LookupCandidateVerification::FullRoot,
2502                )? {
2503                    resources.push(candidate.clone());
2504                    record_lookup_resources_returned();
2505                    if lookup_result_limit_reached(resources.len(), limits) {
2506                        record_lookup_resources_result_limit_exit();
2507                        return Ok(true);
2508                    }
2509                }
2510            }
2511
2512            let target_subject = User::Userset(
2513                candidate,
2514                Relation(target.target_relation.as_str().to_string()),
2515            );
2516            let target_filter = SubjectFilter::try_from(&target_subject)?;
2517            if snapshot
2518                .relationships()
2519                .has_reverse_subject_candidates(&target_filter)
2520                && visited_subjects.insert(target_subject.clone())
2521            {
2522                frontier.push_back(LookupFrontierEntry::new(
2523                    target_subject,
2524                    LookupSubjectProof::inexact(),
2525                ));
2526            }
2527        }
2528    }
2529    Ok(false)
2530}
2531
2532fn tuple_to_userset_broad_targets<'a>(
2533    snapshot: &PublishedSnapshot,
2534    subject_type: &SubjectType,
2535    subject_id: &SubjectId,
2536    targets: &'a [TupleToUsersetExpansionTarget],
2537) -> Vec<&'a TupleToUsersetExpansionTarget> {
2538    let mut broad_targets = Vec::new();
2539    for target in targets {
2540        let exact_filter = SubjectFilter::exact(
2541            subject_type.clone(),
2542            subject_id.clone(),
2543            Some(target.computed_relation.clone()),
2544        );
2545        if !snapshot
2546            .relationships()
2547            .has_reverse_subject_candidates(&exact_filter)
2548        {
2549            broad_targets.push(target);
2550        }
2551    }
2552    broad_targets
2553}
2554
2555fn tuple_to_userset_relation_expansions<'a>(
2556    snapshot: &PublishedSnapshot,
2557    object_type: &ObjectType,
2558    source_relation: &RelationName,
2559    tuple_relation_expansions: &'a mut Vec<TupleToUsersetRelationExpansion>,
2560) -> Result<&'a [TupleToUsersetExpansionTarget], ZanzibarError> {
2561    if let Some(index) = tuple_relation_expansions.iter().position(|expansion| {
2562        &expansion.object_type == object_type && &expansion.source_relation == source_relation
2563    }) {
2564        return Ok(&tuple_relation_expansions[index].targets);
2565    }
2566
2567    let targets =
2568        compute_tuple_to_userset_relation_expansions(snapshot, object_type, source_relation)?;
2569    tuple_relation_expansions.push(TupleToUsersetRelationExpansion {
2570        object_type: object_type.clone(),
2571        source_relation: source_relation.clone(),
2572        targets,
2573    });
2574    let index = tuple_relation_expansions.len().saturating_sub(1);
2575    Ok(&tuple_relation_expansions[index].targets)
2576}
2577
2578fn compute_tuple_to_userset_relation_expansions(
2579    snapshot: &PublishedSnapshot,
2580    object_type: &ObjectType,
2581    source_relation: &RelationName,
2582) -> Result<Vec<TupleToUsersetExpansionTarget>, ZanzibarError> {
2583    let mut targets = Vec::new();
2584    for namespace in snapshot.schema().definitions() {
2585        for relation_definition in namespace.relations() {
2586            let mut visiting = HashSet::new();
2587            collect_tuple_to_userset_relation_expansions(
2588                snapshot,
2589                namespace.name(),
2590                relation_definition.name(),
2591                relation_definition,
2592                object_type,
2593                source_relation,
2594                &mut visiting,
2595                &mut targets,
2596            )?;
2597        }
2598    }
2599    Ok(targets)
2600}
2601
2602#[allow(
2603    clippy::too_many_arguments,
2604    reason = "schema tuple expansion needs both owner and intermediate relation context"
2605)]
2606fn collect_tuple_to_userset_relation_expansions(
2607    snapshot: &PublishedSnapshot,
2608    owner_object_type: &ObjectType,
2609    owner_relation: &RelationName,
2610    relation_definition: &SchemaRelationDefinition,
2611    intermediate_type: &ObjectType,
2612    source_relation: &RelationName,
2613    visiting: &mut HashSet<SchemaRelationId>,
2614    targets: &mut Vec<TupleToUsersetExpansionTarget>,
2615) -> Result<(), ZanzibarError> {
2616    let Some(expression) = relation_definition.compiled_userset_rewrite() else {
2617        return Ok(());
2618    };
2619    collect_tuple_to_userset_expression_expansions(
2620        snapshot,
2621        owner_object_type,
2622        owner_relation,
2623        expression,
2624        intermediate_type,
2625        source_relation,
2626        visiting,
2627        targets,
2628    )
2629}
2630
2631#[allow(
2632    clippy::too_many_arguments,
2633    reason = "schema tuple expansion needs both owner and intermediate expression context"
2634)]
2635fn collect_tuple_to_userset_expression_expansions(
2636    snapshot: &PublishedSnapshot,
2637    owner_object_type: &ObjectType,
2638    owner_relation: &RelationName,
2639    expression: &CompiledUsersetExpression,
2640    intermediate_type: &ObjectType,
2641    source_relation: &RelationName,
2642    visiting: &mut HashSet<SchemaRelationId>,
2643    targets: &mut Vec<TupleToUsersetExpansionTarget>,
2644) -> Result<(), ZanzibarError> {
2645    match expression {
2646        CompiledUsersetExpression::This => Ok(()),
2647        CompiledUsersetExpression::ComputedUserset {
2648            relation_id,
2649            target_has_rewrite,
2650            ..
2651        } => {
2652            if !target_has_rewrite || !visiting.insert(*relation_id) {
2653                return Ok(());
2654            }
2655            let relation_definition = snapshot
2656                .schema()
2657                .resolver()
2658                .relation_by_id(*relation_id)
2659                .ok_or_else(compiled_schema_invariant_error)?;
2660            collect_tuple_to_userset_relation_expansions(
2661                snapshot,
2662                owner_object_type,
2663                owner_relation,
2664                relation_definition,
2665                intermediate_type,
2666                source_relation,
2667                visiting,
2668                targets,
2669            )?;
2670            visiting.remove(relation_id);
2671            Ok(())
2672        }
2673        CompiledUsersetExpression::TupleToUserset {
2674            tupleset_relation,
2675            computed_userset_relation,
2676            ..
2677        } => {
2678            if relation_name_can_include_same_object_relation(
2679                snapshot,
2680                intermediate_type,
2681                computed_userset_relation,
2682                source_relation,
2683            )? {
2684                targets.push(TupleToUsersetExpansionTarget {
2685                    resource_type: owner_object_type.clone(),
2686                    tupleset_relation: tupleset_relation.clone(),
2687                    computed_relation: computed_userset_relation.clone(),
2688                    target_relation: owner_relation.clone(),
2689                });
2690            }
2691            Ok(())
2692        }
2693        CompiledUsersetExpression::Union(expressions)
2694        | CompiledUsersetExpression::Intersection(expressions) => {
2695            for expression in expressions {
2696                collect_tuple_to_userset_expression_expansions(
2697                    snapshot,
2698                    owner_object_type,
2699                    owner_relation,
2700                    expression,
2701                    intermediate_type,
2702                    source_relation,
2703                    visiting,
2704                    targets,
2705                )?;
2706            }
2707            Ok(())
2708        }
2709        CompiledUsersetExpression::Exclusion { base, .. } => {
2710            collect_tuple_to_userset_expression_expansions(
2711                snapshot,
2712                owner_object_type,
2713                owner_relation,
2714                base,
2715                intermediate_type,
2716                source_relation,
2717                visiting,
2718                targets,
2719            )
2720        }
2721    }
2722}
2723
2724fn relation_name_can_include_same_object_relation(
2725    snapshot: &PublishedSnapshot,
2726    object_type: &ObjectType,
2727    relation: &RelationName,
2728    source_relation: &RelationName,
2729) -> Result<bool, ZanzibarError> {
2730    if relation == source_relation {
2731        return Ok(true);
2732    }
2733    let Ok(relation_id) = snapshot
2734        .schema()
2735        .resolver()
2736        .relation_id(object_type, relation)
2737    else {
2738        return Ok(false);
2739    };
2740    let mut visiting = HashSet::new();
2741    relation_id_can_include_same_object_relation(
2742        snapshot,
2743        relation_id,
2744        source_relation,
2745        &mut visiting,
2746    )
2747}
2748
2749fn pruned_relation_may_have_downstream(
2750    snapshot: &PublishedSnapshot,
2751    resource_subject_type: &SubjectType,
2752    relation_name: &str,
2753    pruned_relation_has_downstream: &mut Vec<(RelationName, bool)>,
2754) -> Result<bool, ZanzibarError> {
2755    if let Some((_, has_downstream)) = pruned_relation_has_downstream
2756        .iter()
2757        .find(|(known_relation, _)| known_relation.as_str() == relation_name)
2758    {
2759        return Ok(*has_downstream);
2760    }
2761    let relation_name = RelationName::try_from(relation_name)?;
2762    let relation_filter = SubjectFilter::new(
2763        resource_subject_type.clone(),
2764        None,
2765        Some(relation_name.clone()),
2766    );
2767    let has_downstream = snapshot
2768        .relationships()
2769        .has_reverse_subject_candidates(&relation_filter);
2770    pruned_relation_has_downstream.push((relation_name, has_downstream));
2771    Ok(has_downstream)
2772}
2773
2774/// Looks up subjects of the requested type that pass the shared snapshot-backed check evaluator.
2775///
2776/// # Errors
2777///
2778/// Returns [`ZanzibarError`] when request validation, store access, or evaluation fails.
2779pub fn lookup_subjects_with_snapshot(
2780    snapshot: &PublishedSnapshot,
2781    request: &LookupSubjectsRequest,
2782    limits: EvaluationLimits,
2783) -> Result<LookupSubjects, ZanzibarError> {
2784    let resource = DomainObjectRef::try_from(&request.resource)?;
2785    let permission = RelationName::try_from(&request.permission)?;
2786    snapshot
2787        .schema()
2788        .resolver()
2789        .relation(resource.object_type(), &permission)?;
2790
2791    let subject_type = SubjectType::try_from(request.subject_type.as_str())?;
2792    if subject_type.as_str() != "user" {
2793        let object_type = ObjectType::try_from(subject_type.as_str())?;
2794        snapshot.schema().resolver().namespace(&object_type)?;
2795    }
2796
2797    let mut seen = HashSet::new();
2798    let mut seen_usersets = HashSet::new();
2799    let mut subjects = Vec::new();
2800    let mut expand_context = EvaluationContext::new(snapshot, limits);
2801    let mut check_context = EvaluationContext::new(snapshot, limits);
2802    let mut collector = LookupSubjectCollector {
2803        resource: &request.resource,
2804        permission: &request.permission,
2805        subject_type: &subject_type,
2806        limits,
2807        seen_subjects: &mut seen,
2808        seen_usersets: &mut seen_usersets,
2809        subjects: &mut subjects,
2810        check_context: &mut check_context,
2811    };
2812    expand_context.stream_lookup_subjects_relation(
2813        &mut collector,
2814        &request.resource,
2815        &permission,
2816        false,
2817    )?;
2818
2819    Ok(LookupSubjects { subjects })
2820}
2821
2822fn compiled_schema_invariant_error() -> ZanzibarError {
2823    ZanzibarError::StorageError("compiled schema relation id is out of bounds".to_string())
2824}
2825
2826fn lookup_producer_plan(
2827    snapshot: &PublishedSnapshot,
2828    resource_type: &ObjectType,
2829    permission: &RelationName,
2830) -> Result<Option<LookupProducerPlan>, ZanzibarError> {
2831    let resolver = snapshot.schema().resolver();
2832    let relation_id = resolver.relation_id(resource_type, permission)?;
2833    let relation_definition = resolver
2834        .relation_by_id(relation_id)
2835        .ok_or_else(compiled_schema_invariant_error)?;
2836    let mut visiting = HashSet::new();
2837    let Some(verification_rules) = collect_relation_verification_rules(
2838        snapshot,
2839        relation_id,
2840        relation_definition,
2841        &mut visiting,
2842    )?
2843    else {
2844        return Ok(None);
2845    };
2846    if verification_rules.is_empty() {
2847        Ok(None)
2848    } else {
2849        let mut relations = Vec::new();
2850        for rule in &verification_rules {
2851            if !relations.contains(&rule.relation) {
2852                relations.push(rule.relation.clone());
2853            }
2854        }
2855        Ok(Some(LookupProducerPlan {
2856            relations,
2857            verification_rules,
2858        }))
2859    }
2860}
2861
2862fn collect_relation_verification_rules(
2863    snapshot: &PublishedSnapshot,
2864    relation_id: SchemaRelationId,
2865    relation_definition: &SchemaRelationDefinition,
2866    visiting: &mut HashSet<SchemaRelationId>,
2867) -> Result<Option<Vec<LookupVerificationRule>>, ZanzibarError> {
2868    if !visiting.insert(relation_id) {
2869        return Ok(None);
2870    }
2871    let rules = if let Some(expression) = relation_definition.compiled_userset_rewrite() {
2872        collect_expression_verification_rules(
2873            snapshot,
2874            relation_definition.name(),
2875            expression,
2876            visiting,
2877        )?
2878    } else {
2879        Some(vec![LookupVerificationRule {
2880            relation: relation_definition.name().clone(),
2881            verification: LookupCandidateVerification::ProvenWithoutCheck,
2882        }])
2883    };
2884    visiting.remove(&relation_id);
2885    Ok(rules.map(merge_lookup_verification_rules))
2886}
2887
2888fn collect_expression_verification_rules(
2889    snapshot: &PublishedSnapshot,
2890    current_relation: &RelationName,
2891    expression: &CompiledUsersetExpression,
2892    visiting: &mut HashSet<SchemaRelationId>,
2893) -> Result<Option<Vec<LookupVerificationRule>>, ZanzibarError> {
2894    match expression {
2895        CompiledUsersetExpression::This => Ok(Some(vec![LookupVerificationRule {
2896            relation: current_relation.clone(),
2897            verification: LookupCandidateVerification::ProvenWithoutCheck,
2898        }])),
2899        CompiledUsersetExpression::ComputedUserset {
2900            relation,
2901            relation_id,
2902            target_has_rewrite,
2903        } => {
2904            if !target_has_rewrite {
2905                return Ok(Some(vec![LookupVerificationRule {
2906                    relation: relation.clone(),
2907                    verification: LookupCandidateVerification::ProvenWithoutCheck,
2908                }]));
2909            }
2910            let relation_definition = snapshot
2911                .schema()
2912                .resolver()
2913                .relation_by_id(*relation_id)
2914                .ok_or_else(compiled_schema_invariant_error)?;
2915            collect_relation_verification_rules(
2916                snapshot,
2917                *relation_id,
2918                relation_definition,
2919                visiting,
2920            )
2921        }
2922        CompiledUsersetExpression::Union(expressions) => {
2923            let mut rules = Vec::new();
2924            for expression in expressions {
2925                let Some(child_rules) = collect_expression_verification_rules(
2926                    snapshot,
2927                    current_relation,
2928                    expression,
2929                    visiting,
2930                )?
2931                else {
2932                    return Ok(None);
2933                };
2934                rules.extend(child_rules);
2935            }
2936            Ok(Some(rules))
2937        }
2938        CompiledUsersetExpression::Exclusion { base, exclude } => {
2939            let Some(base_rules) =
2940                collect_expression_verification_rules(snapshot, current_relation, base, visiting)?
2941            else {
2942                return Ok(None);
2943            };
2944            Ok(Some(
2945                base_rules
2946                    .into_iter()
2947                    .map(|rule| LookupVerificationRule {
2948                        relation: rule.relation,
2949                        verification: rule.verification.with_required_denied(
2950                            current_relation.clone(),
2951                            exclude.as_ref().clone(),
2952                        ),
2953                    })
2954                    .collect(),
2955            ))
2956        }
2957        CompiledUsersetExpression::TupleToUserset {
2958            tupleset_relation, ..
2959        } => Ok(Some(vec![LookupVerificationRule {
2960            relation: tupleset_relation.clone(),
2961            verification: LookupCandidateVerification::FullRoot,
2962        }])),
2963        CompiledUsersetExpression::Intersection(expressions) => {
2964            collect_intersection_verification_rules(
2965                snapshot,
2966                current_relation,
2967                expressions,
2968                visiting,
2969            )
2970        }
2971    }
2972}
2973
2974fn collect_intersection_verification_rules(
2975    snapshot: &PublishedSnapshot,
2976    current_relation: &RelationName,
2977    expressions: &[CompiledUsersetExpression],
2978    visiting: &mut HashSet<SchemaRelationId>,
2979) -> Result<Option<Vec<LookupVerificationRule>>, ZanzibarError> {
2980    for (seed_index, seed_expression) in expressions.iter().enumerate() {
2981        let Some(seed_rules) = collect_expression_verification_rules(
2982            snapshot,
2983            current_relation,
2984            seed_expression,
2985            visiting,
2986        )?
2987        else {
2988            continue;
2989        };
2990        if seed_rules
2991            .iter()
2992            .any(|rule| matches!(rule.verification, LookupCandidateVerification::FullRoot))
2993        {
2994            continue;
2995        }
2996        let mut guarded_rules = seed_rules;
2997        for (guard_index, guard_expression) in expressions.iter().enumerate() {
2998            if guard_index == seed_index {
2999                continue;
3000            }
3001            for rule in &mut guarded_rules {
3002                rule.verification = rule
3003                    .verification
3004                    .clone()
3005                    .with_required_allowed(current_relation.clone(), guard_expression.clone());
3006            }
3007        }
3008        return Ok(Some(guarded_rules));
3009    }
3010    Ok(None)
3011}
3012
3013fn merge_lookup_verification_rules(
3014    rules: Vec<LookupVerificationRule>,
3015) -> Vec<LookupVerificationRule> {
3016    let mut merged: Vec<LookupVerificationRule> = Vec::new();
3017    for rule in rules {
3018        if let Some(existing) = merged
3019            .iter_mut()
3020            .find(|existing| existing.relation == rule.relation)
3021        {
3022            existing.verification = existing.verification.clone().union_merge(rule.verification);
3023        } else {
3024            merged.push(rule);
3025        }
3026    }
3027    merged
3028}
3029
3030fn relation_definition_requires_lookup_subject_verification(
3031    snapshot: &PublishedSnapshot,
3032    relation_definition: &SchemaRelationDefinition,
3033) -> Result<bool, ZanzibarError> {
3034    let Some(expression) = relation_definition.compiled_userset_rewrite() else {
3035        return Ok(false);
3036    };
3037    let mut visiting = HashSet::new();
3038    expression_requires_lookup_subject_verification(snapshot, expression, &mut visiting)
3039}
3040
3041fn expression_requires_lookup_subject_verification(
3042    snapshot: &PublishedSnapshot,
3043    expression: &CompiledUsersetExpression,
3044    visiting: &mut HashSet<SchemaRelationId>,
3045) -> Result<bool, ZanzibarError> {
3046    match expression {
3047        CompiledUsersetExpression::This => Ok(false),
3048        CompiledUsersetExpression::ComputedUserset {
3049            relation_id,
3050            target_has_rewrite,
3051            ..
3052        } => {
3053            if !target_has_rewrite {
3054                return Ok(false);
3055            }
3056            if !visiting.insert(*relation_id) {
3057                return Ok(true);
3058            }
3059            let relation_definition = snapshot
3060                .schema()
3061                .resolver()
3062                .relation_by_id(*relation_id)
3063                .ok_or_else(compiled_schema_invariant_error)?;
3064            let requires = match relation_definition.compiled_userset_rewrite() {
3065                Some(expression) => {
3066                    expression_requires_lookup_subject_verification(snapshot, expression, visiting)?
3067                }
3068                None => false,
3069            };
3070            visiting.remove(relation_id);
3071            Ok(requires)
3072        }
3073        CompiledUsersetExpression::Union(expressions) => {
3074            for expression in expressions {
3075                if expression_requires_lookup_subject_verification(snapshot, expression, visiting)?
3076                {
3077                    return Ok(true);
3078                }
3079            }
3080            Ok(false)
3081        }
3082        CompiledUsersetExpression::TupleToUserset { .. }
3083        | CompiledUsersetExpression::Intersection(_)
3084        | CompiledUsersetExpression::Exclusion { .. } => Ok(true),
3085    }
3086}
3087
3088struct LookupSubjectCollector<'a, 'ctx> {
3089    resource: &'a Object,
3090    permission: &'a Relation,
3091    subject_type: &'a SubjectType,
3092    limits: EvaluationLimits,
3093    seen_subjects: &'ctx mut HashSet<User>,
3094    seen_usersets: &'ctx mut HashSet<(Object, RelationName)>,
3095    subjects: &'ctx mut Vec<User>,
3096    check_context: &'ctx mut EvaluationContext<'a>,
3097}
3098
3099impl LookupSubjectCollector<'_, '_> {
3100    fn result_limit_reached(&self) -> bool {
3101        lookup_result_limit_reached(self.subjects.len(), self.limits)
3102    }
3103
3104    fn collect_user_candidate(
3105        &mut self,
3106        id: &str,
3107        verify_candidate: bool,
3108    ) -> Result<(), ZanzibarError> {
3109        if self.subject_type.as_str() != "user" {
3110            return Ok(());
3111        }
3112        let subject = User::UserId(id.to_string());
3113        record_lookup_subjects_candidate_subject();
3114        if self.seen_subjects.insert(subject.clone()) {
3115            if !verify_candidate {
3116                self.push_verified_subject(subject);
3117                return Ok(());
3118            }
3119            self.check_context.reset_for_reuse();
3120            record_lookup_subjects_full_root_check();
3121            if self
3122                .check_context
3123                .check(self.resource, self.permission, &subject)?
3124                .is_allowed()
3125            {
3126                self.push_verified_subject(subject);
3127            }
3128        }
3129        Ok(())
3130    }
3131
3132    fn collect_userset_candidate(
3133        &mut self,
3134        object: &Object,
3135        relation_name: &RelationName,
3136        verify_candidate: bool,
3137    ) -> Result<bool, ZanzibarError> {
3138        record_lookup_subjects_candidate_userset();
3139        if object.namespace == self.subject_type.as_str() {
3140            let relation = Relation(relation_name.as_str().to_string());
3141            let userset = User::Userset(object.clone(), relation);
3142            if self.seen_subjects.insert(userset.clone()) {
3143                if !verify_candidate {
3144                    return Ok(self.push_verified_subject(userset));
3145                }
3146                self.check_context.reset_for_reuse();
3147                record_lookup_subjects_full_root_check();
3148                if self
3149                    .check_context
3150                    .check(self.resource, self.permission, &userset)?
3151                    .is_allowed()
3152                    && !self.push_verified_subject(userset)
3153                {
3154                    return Ok(false);
3155                }
3156            }
3157        }
3158        Ok(self
3159            .seen_usersets
3160            .insert((object.clone(), relation_name.clone())))
3161    }
3162
3163    fn push_verified_subject(&mut self, subject: User) -> bool {
3164        self.subjects.push(subject);
3165        record_lookup_subjects_returned();
3166        if lookup_result_limit_reached(self.subjects.len(), self.limits) {
3167            record_lookup_subjects_result_limit_exit();
3168            return false;
3169        }
3170        true
3171    }
3172}
3173
3174fn non_zero_u32(value: u32) -> NonZeroU32 {
3175    match NonZeroU32::new(value) {
3176        Some(value) => value,
3177        None => NonZeroU32::MIN,
3178    }
3179}
3180
3181fn memo_capacity(limits: EvaluationLimits) -> usize {
3182    let scaled = limits.max_lookup_results.get().saturating_mul(4);
3183    let clamped = scaled.clamp(128, 16_384);
3184    usize::try_from(clamped).map_or(16_384, |value| value)
3185}
3186
3187fn unbounded_query_limit() -> QueryLimit {
3188    QueryLimit::new(NonZeroUsize::MAX)
3189}
3190
3191fn lookup_result_limit_reached(current_len: usize, limits: EvaluationLimits) -> bool {
3192    let limit = match usize::try_from(limits.max_lookup_results.get()) {
3193        Ok(limit) => limit,
3194        Err(_) => usize::MAX,
3195    };
3196    current_len >= limit
3197}
3198
3199#[cfg(test)]
3200mod tests {
3201    use super::*;
3202
3203    #[test]
3204    fn test_should_skip_request_memo_hit_when_depth_is_insufficient()
3205    -> Result<(), Box<dyn std::error::Error>> {
3206        let mut memo = RequestCheckMemo::new(EvaluationLimits::default());
3207        let key = CheckMemoKey::Public {
3208            object: Object::new("doc", "one"),
3209            relation: RelationName::try_from("viewer")?,
3210            user: User::user_id("alice"),
3211        };
3212
3213        memo.insert(key.clone(), Membership::Allowed, non_zero_u32(3));
3214
3215        assert_eq!(memo.get(&key, 2), MemoLookup::DepthInsufficient);
3216        assert_eq!(memo.get(&key, 3), MemoLookup::Hit(Membership::Allowed));
3217        Ok(())
3218    }
3219
3220    #[test]
3221    fn test_should_disable_lookup_producer_runtime_when_pruning_ratio_is_low() {
3222        let mut runtime = LookupProducerRuntime::new(true);
3223        runtime.record_sample(true);
3224        for _ in 1..LOOKUP_PLANNER_SAMPLE_RELATIONSHIPS {
3225            runtime.record_sample(false);
3226        }
3227
3228        assert!(!runtime.enabled);
3229    }
3230
3231    #[test]
3232    fn test_should_keep_lookup_producer_runtime_when_pruning_ratio_is_high() {
3233        let mut runtime = LookupProducerRuntime::new(true);
3234        for _ in 0..LOOKUP_PLANNER_SAMPLE_RELATIONSHIPS {
3235            runtime.record_sample(true);
3236        }
3237
3238        assert!(runtime.enabled);
3239    }
3240}