1#[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#[cfg(feature = "bench-internals")]
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct EvaluationReadCounters {
96 pub check_evaluations: u64,
98 pub check_memo_hit_opportunities: u64,
100 pub check_memo_hits: u64,
102 pub check_memo_misses: u64,
104 pub check_memo_inserts: u64,
106 pub check_memo_capacity_skips: u64,
108 pub check_memo_active_cycle_skips: u64,
110 pub check_memo_depth_insufficient_skips: u64,
112 pub check_completed_results: u64,
114 pub check_active_cycle_denials: u64,
116 pub lookup_resources_frontier_subjects: u64,
118 pub lookup_resources_frontier_relationships: u64,
120 pub lookup_resources_candidate_resources: u64,
122 pub lookup_resources_schema_pruned: u64,
124 pub lookup_resources_planner_fallbacks: u64,
126 pub lookup_resources_full_root_checks: u64,
128 pub lookup_resources_residual_checks: u64,
130 pub lookup_resources_proven_without_check: u64,
132 pub lookup_resources_tuple_fallbacks: u64,
134 pub lookup_resources_returned: u64,
136 pub lookup_resources_result_limit_exits: u64,
138 pub lookup_subjects_candidate_subjects: u64,
140 pub lookup_subjects_candidate_usersets: u64,
142 pub lookup_subjects_full_root_checks: u64,
144 pub lookup_subjects_returned: u64,
146 pub lookup_subjects_result_limit_exits: u64,
148}
149
150#[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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Error)]
431pub enum EvaluationError {
432 #[error("evaluation depth exceeded at {key:?}")]
434 DepthExceeded {
435 key: Box<EvaluationKey>,
437 },
438
439 #[error("evaluation fanout exceeded limit {limit}")]
441 FanoutExceeded {
442 limit: NonZeroU32,
444 },
445}
446
447#[derive(Debug, Clone, PartialEq, Eq)]
449pub enum EvaluationKey {
450 Check(CheckKey),
452 Expand(ExpandKey),
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Hash)]
458pub enum CheckKey {
459 Store(StoreCheckKey),
461 PublicName {
464 object: Object,
466 relation: RelationName,
468 user: User,
470 },
471 Public {
473 object: Object,
475 relation: Relation,
477 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub struct EvaluationLimits {
541 pub max_depth: NonZeroU32,
543 pub max_fanout_per_step: NonZeroU32,
545 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
561pub enum Membership {
562 Allowed,
564 Denied,
566 Conditional,
568}
569
570impl Membership {
571 #[must_use]
573 pub const fn is_allowed(self) -> bool {
574 matches!(self, Self::Allowed)
575 }
576
577 #[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 #[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 #[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#[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 #[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 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 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
1989pub 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
2020pub 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
2034pub 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
2774pub 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}