Skip to main content

simple_zanzibar/
api.rs

1//! Public request/response engine API.
2
3use std::{
4    borrow::Borrow,
5    collections::HashMap,
6    fmt,
7    num::NonZeroUsize,
8    path::Path,
9    str::FromStr,
10    sync::{
11        Arc, Mutex,
12        atomic::{AtomicBool, Ordering},
13        mpsc::{self, SyncSender},
14    },
15    thread::{self, JoinHandle},
16};
17
18use arc_swap::{ArcSwap, ArcSwapOption};
19use thiserror::Error;
20
21use crate::{
22    WriterState,
23    domain::{DomainError, ObjectType, RelationName},
24    error::ZanzibarError,
25    eval::{self, EvaluationError, EvaluationLimits},
26    model::{
27        CheckRequest, CheckResponse, ExpandRequest, ExpandResponse, ExpandedUserset,
28        LookupObjectPermissions, LookupObjectPermissionsRequest, LookupPermissions,
29        LookupPermissionsRequest, LookupResources, LookupResourcesRequest, LookupSubjects,
30        LookupSubjectsRequest, NamespaceConfig, Object, PermissionSubjects, Relation,
31        RelationTuple, User,
32    },
33    policy::{self, PolicyIoError, PolicyText},
34    relationship::{Precondition, RelationshipMutation, StoreError},
35    revision::{Consistency, ConsistencyError, ConsistencyToken, default_retained_snapshots},
36    runtime::{EngineState, SharedEngineState},
37    schema::{SchemaError, SchemaSource},
38    snapshot::{IndexProfile, SnapshotIoError, SnapshotLoadOptions, SnapshotSaveOptions},
39};
40
41const DEFAULT_WRITER_QUEUE_CAPACITY: usize = 1024;
42const MAX_TENANT_ID_BYTES: usize = 128;
43
44macro_rules! enter_api_span {
45    ($operation:literal) => {
46        #[cfg(feature = "tracing")]
47        let _span_guard = tracing::debug_span!("zanzibar.engine", operation = $operation).entered();
48    };
49}
50
51/// Public local Zanzibar engine.
52///
53/// Reads clone immutable published snapshots through an atomic pointer and do not acquire a
54/// service-level lock. Writes are submitted to one bounded writer actor that owns the mutable
55/// schema, relationship store, and revision history for this engine instance.
56#[derive(Debug)]
57pub struct ZanzibarEngine {
58    state: SharedEngineState,
59    writer: WriterActor,
60}
61
62impl ZanzibarEngine {
63    /// Creates a builder for a local engine.
64    #[must_use]
65    pub fn builder() -> ZanzibarEngineBuilder {
66        ZanzibarEngineBuilder::new()
67    }
68
69    /// Checks whether a subject has a relation or permission on an object.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`EngineError`] when request validation, consistency, store access, or evaluation
74    /// fails.
75    pub fn check(&self, request: CheckRequest) -> Result<CheckResponse, EngineError> {
76        enter_api_span!("check");
77        request.validate()?;
78        let (snapshot, limits) = self.snapshot_for_consistency(request.consistency)?;
79        let object_type = ObjectType::try_from(request.object.namespace.as_str())?;
80        let relation_name = RelationName::try_from(request.relation.0.as_str())?;
81        let relation_definition = snapshot
82            .schema()
83            .resolver()
84            .relation(&object_type, &relation_name)?;
85        let allowed = eval::check_prepared_with_snapshot(
86            &snapshot,
87            &request.object,
88            &request.relation,
89            &request.user,
90            relation_definition,
91            limits,
92        )?
93        .is_allowed();
94        Ok(CheckResponse { allowed })
95    }
96
97    /// Checks a relation or permission using latest consistency.
98    ///
99    /// # Errors
100    ///
101    /// Returns [`EngineError`] when request validation, consistency, store access, or evaluation
102    /// fails.
103    pub fn check_relation(
104        &self,
105        object: &Object,
106        relation: &Relation,
107        user: &User,
108    ) -> Result<bool, EngineError> {
109        self.check_relation_with_consistency(object, relation, user, Consistency::Latest)
110    }
111
112    /// Checks a relation or permission at the requested consistency.
113    ///
114    /// # Errors
115    ///
116    /// Returns [`EngineError`] when request validation, consistency, store access, or evaluation
117    /// fails.
118    pub fn check_relation_with_consistency(
119        &self,
120        object: &Object,
121        relation: &Relation,
122        user: &User,
123        consistency: Consistency,
124    ) -> Result<bool, EngineError> {
125        Ok(self
126            .check(CheckRequest::new(
127                object.clone(),
128                relation.clone(),
129                user.clone(),
130                consistency,
131            ))?
132            .allowed)
133    }
134
135    /// Checks a relation or permission at the requested consistency.
136    ///
137    /// This convenience method is equivalent to [`Self::check_relation_with_consistency`].
138    ///
139    /// # Errors
140    ///
141    /// Returns [`EngineError`] when request validation, consistency, store access, or evaluation
142    /// fails.
143    pub fn check_with_consistency(
144        &self,
145        object: &Object,
146        relation: &Relation,
147        user: &User,
148        consistency: Consistency,
149    ) -> Result<bool, EngineError> {
150        self.check_relation_with_consistency(object, relation, user, consistency)
151    }
152
153    /// Expands the effective userset for an object relation or permission.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`EngineError`] when request validation, consistency, store access, or evaluation
158    /// fails.
159    pub fn expand(&self, request: ExpandRequest) -> Result<ExpandResponse, EngineError> {
160        enter_api_span!("expand");
161        request.validate()?;
162        let (snapshot, limits) = self.snapshot_for_consistency(request.consistency)?;
163        let object_type = ObjectType::try_from(request.object.namespace.as_str())?;
164        let relation_name = RelationName::try_from(request.relation.0.as_str())?;
165        snapshot
166            .schema()
167            .resolver()
168            .relation(&object_type, &relation_name)?;
169        let expanded =
170            eval::expand_with_snapshot(&snapshot, &request.object, &request.relation, limits)?;
171        Ok(ExpandResponse { expanded })
172    }
173
174    /// Expands a relation or permission using latest consistency.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`EngineError`] when request validation, consistency, store access, or evaluation
179    /// fails.
180    pub fn expand_relation(
181        &self,
182        object: &Object,
183        relation: &Relation,
184    ) -> Result<ExpandedUserset, EngineError> {
185        Ok(self
186            .expand(ExpandRequest::new(
187                object.clone(),
188                relation.clone(),
189                Consistency::Latest,
190            ))?
191            .expanded)
192    }
193
194    /// Looks up resources of a type that a subject can access at latest consistency.
195    ///
196    /// # Errors
197    ///
198    /// Returns [`EngineError`] when request validation, store access, or evaluation fails.
199    pub fn lookup_resources(
200        &self,
201        request: impl Borrow<LookupResourcesRequest>,
202    ) -> Result<LookupResources, EngineError> {
203        self.lookup_resources_with_consistency(request, Consistency::Latest)
204    }
205
206    /// Looks up resources of a type at the requested consistency.
207    ///
208    /// # Errors
209    ///
210    /// Returns [`EngineError`] when request validation, consistency, store access, or evaluation
211    /// fails.
212    pub fn lookup_resources_with_consistency(
213        &self,
214        request: impl Borrow<LookupResourcesRequest>,
215        consistency: Consistency,
216    ) -> Result<LookupResources, EngineError> {
217        enter_api_span!("lookup_resources");
218        request.borrow().validate()?;
219        let (snapshot, limits) = self.snapshot_for_consistency(consistency)?;
220        Self::ensure_subject_reverse_lookup_supported(&snapshot, "lookup_resources")?;
221        let request = request.borrow();
222        Ok(eval::lookup_resources_with_snapshot(
223            &snapshot, request, limits,
224        )?)
225    }
226
227    /// Looks up subjects of a type that can access a resource at latest consistency.
228    ///
229    /// # Errors
230    ///
231    /// Returns [`EngineError`] when request validation, store access, or evaluation fails.
232    pub fn lookup_subjects(
233        &self,
234        request: impl Borrow<LookupSubjectsRequest>,
235    ) -> Result<LookupSubjects, EngineError> {
236        self.lookup_subjects_with_consistency(request, Consistency::Latest)
237    }
238
239    /// Looks up subjects of a type at the requested consistency.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`EngineError`] when request validation, consistency, store access, or evaluation
244    /// fails.
245    pub fn lookup_subjects_with_consistency(
246        &self,
247        request: impl Borrow<LookupSubjectsRequest>,
248        consistency: Consistency,
249    ) -> Result<LookupSubjects, EngineError> {
250        enter_api_span!("lookup_subjects");
251        request.borrow().validate()?;
252        let (snapshot, limits) = self.snapshot_for_consistency(consistency)?;
253        let request = request.borrow();
254        Ok(eval::lookup_subjects_with_snapshot(
255            &snapshot, request, limits,
256        )?)
257    }
258
259    /// Returns benchmark-only delta stats for the selected snapshot.
260    ///
261    /// # Errors
262    ///
263    /// Returns [`EngineError`] when the requested consistency cannot be resolved.
264    #[cfg(feature = "bench-internals")]
265    pub fn bench_relationship_delta_stats(
266        &self,
267        consistency: Consistency,
268    ) -> Result<crate::relationship::StoreViewDeltaStats, EngineError> {
269        let (snapshot, _) = self.snapshot_for_consistency(consistency)?;
270        Ok(snapshot.relationships().delta_stats())
271    }
272
273    /// Returns benchmark-only posting histograms for the selected snapshot.
274    ///
275    /// # Errors
276    ///
277    /// Returns [`EngineError`] when the requested consistency cannot be resolved.
278    #[cfg(feature = "bench-internals")]
279    pub fn bench_relationship_posting_histograms(
280        &self,
281        consistency: Consistency,
282    ) -> Result<crate::relationship::StorePostingHistograms, EngineError> {
283        let (snapshot, _) = self.snapshot_for_consistency(consistency)?;
284        Ok(snapshot.relationships().posting_histograms())
285    }
286
287    /// Applies relationship mutations and publishes a new revision.
288    ///
289    /// # Errors
290    ///
291    /// Returns [`EngineError`] when no schema is loaded, validation fails, or mutation semantics
292    /// are invalid.
293    pub fn write_relationships(
294        &self,
295        mutations: impl IntoIterator<Item = RelationshipMutation>,
296    ) -> Result<ConsistencyToken, EngineError> {
297        enter_api_span!("write_relationships");
298        self.write_relationships_with_preconditions(mutations, [])
299    }
300
301    /// Applies relationship mutations with optional preconditions.
302    ///
303    /// This is an alias for [`Self::write_relationships_with_preconditions`] kept as the explicit
304    /// batch mutation verb in the public API.
305    ///
306    /// # Errors
307    ///
308    /// Returns [`EngineError`] when no schema is loaded, validation fails, preconditions fail, or
309    /// mutation semantics are invalid.
310    pub fn apply_relationship_mutations(
311        &self,
312        mutations: impl IntoIterator<Item = RelationshipMutation>,
313        preconditions: impl IntoIterator<Item = Precondition>,
314    ) -> Result<ConsistencyToken, EngineError> {
315        self.write_relationships_with_preconditions(mutations, preconditions)
316    }
317
318    /// Writes one legacy tuple as an idempotent relationship touch.
319    ///
320    /// Prefer [`Self::write_relationships`] for high-throughput callers because it batches many
321    /// mutations into one writer-actor turn.
322    ///
323    /// # Errors
324    ///
325    /// Returns [`EngineError`] when the tuple is invalid, no schema is loaded, or validation fails.
326    pub fn write_tuple(&self, tuple: impl Borrow<RelationTuple>) -> Result<(), EngineError> {
327        self.write_tuple_with_token(tuple.borrow()).map(drop)
328    }
329
330    /// Writes one legacy tuple as an idempotent relationship touch and returns the published token.
331    ///
332    /// # Errors
333    ///
334    /// Returns [`EngineError`] when the tuple is invalid, no schema is loaded, or validation fails.
335    pub fn write_tuple_with_token(
336        &self,
337        tuple: &RelationTuple,
338    ) -> Result<ConsistencyToken, EngineError> {
339        let relationship = crate::domain::Relationship::try_from(tuple)?;
340        self.write_relationships([RelationshipMutation::Touch(relationship)])
341    }
342
343    /// Deletes one legacy tuple.
344    ///
345    /// # Errors
346    ///
347    /// Returns [`EngineError`] when the tuple is invalid, no schema is loaded, validation fails, or
348    /// the tuple is absent.
349    pub fn delete_tuple(&self, tuple: &RelationTuple) -> Result<(), EngineError> {
350        let relationship = crate::domain::Relationship::try_from(tuple)?;
351        self.write_relationships([RelationshipMutation::Delete(relationship)])
352            .map(drop)
353    }
354
355    /// Deletes one legacy tuple and returns the published token.
356    ///
357    /// # Errors
358    ///
359    /// Returns [`EngineError`] when the tuple is invalid, no schema is loaded, validation fails, or
360    /// the tuple is absent.
361    pub fn delete_tuple_with_token(
362        &self,
363        tuple: &RelationTuple,
364    ) -> Result<ConsistencyToken, EngineError> {
365        let relationship = crate::domain::Relationship::try_from(tuple)?;
366        self.write_relationships([RelationshipMutation::Delete(relationship)])
367    }
368
369    /// Creates one relationship, failing if it already exists.
370    ///
371    /// # Errors
372    ///
373    /// Returns [`EngineError`] when the relationship text is invalid, no schema is loaded,
374    /// validation fails, or the relationship already exists.
375    pub fn create_relationship(&self, relationship: &str) -> Result<ConsistencyToken, EngineError> {
376        enter_api_span!("create_relationship");
377        self.write_relationships([RelationshipMutation::create(relationship)?])
378    }
379
380    /// Grants or refreshes one relationship idempotently.
381    ///
382    /// # Errors
383    ///
384    /// Returns [`EngineError`] when the relationship text is invalid, no schema is loaded, or
385    /// validation fails.
386    pub fn touch_relationship(&self, relationship: &str) -> Result<ConsistencyToken, EngineError> {
387        enter_api_span!("touch_relationship");
388        self.write_relationships([RelationshipMutation::touch(relationship)?])
389    }
390
391    /// Deletes one relationship, failing if it does not exist.
392    ///
393    /// # Errors
394    ///
395    /// Returns [`EngineError`] when the relationship text is invalid, no schema is loaded,
396    /// validation fails, or the relationship does not exist.
397    pub fn delete_relationship(&self, relationship: &str) -> Result<ConsistencyToken, EngineError> {
398        enter_api_span!("delete_relationship");
399        self.write_relationships([RelationshipMutation::delete(relationship)?])
400    }
401
402    /// Applies relationship mutations with preconditions and publishes a new revision.
403    ///
404    /// # Errors
405    ///
406    /// Returns [`EngineError`] when no schema is loaded, validation fails, preconditions fail, or
407    /// mutation semantics are invalid.
408    pub fn write_relationships_with_preconditions(
409        &self,
410        mutations: impl IntoIterator<Item = RelationshipMutation>,
411        preconditions: impl IntoIterator<Item = Precondition>,
412    ) -> Result<ConsistencyToken, EngineError> {
413        enter_api_span!("write_relationships_with_preconditions");
414        self.submit_write("write_relationships", |response| {
415            WriterCommand::WriteRelationships {
416                mutations: mutations.into_iter().collect(),
417                preconditions: preconditions.into_iter().collect(),
418                response,
419            }
420        })
421    }
422
423    /// Applies a schema document and publishes a new revision.
424    ///
425    /// # Errors
426    ///
427    /// Returns [`EngineError`] when the schema cannot be parsed or validated.
428    pub fn apply_schema(&self, source: SchemaSource<'_>) -> Result<ConsistencyToken, EngineError> {
429        enter_api_span!("apply_schema");
430        self.submit_write("apply_schema", |response| WriterCommand::ApplySchema {
431            text: source.text.to_string(),
432            response,
433        })
434    }
435
436    /// Applies a legacy DSL schema document and publishes a new revision.
437    ///
438    /// # Errors
439    ///
440    /// Returns [`EngineError`] when the DSL cannot be parsed or validated.
441    pub fn add_dsl(&self, dsl: &str) -> Result<(), EngineError> {
442        self.add_dsl_with_token(dsl).map(drop)
443    }
444
445    /// Applies a legacy DSL schema document and returns the published token.
446    ///
447    /// # Errors
448    ///
449    /// Returns [`EngineError`] when the DSL cannot be parsed or validated.
450    pub fn add_dsl_with_token(&self, dsl: &str) -> Result<ConsistencyToken, EngineError> {
451        self.apply_schema(SchemaSource {
452            name: Some("dsl"),
453            text: dsl,
454        })
455    }
456
457    /// Applies one structured namespace config and publishes a new revision.
458    ///
459    /// # Errors
460    ///
461    /// Returns [`EngineError`] when the namespace config cannot be validated.
462    pub fn apply_namespace_config(
463        &self,
464        config: NamespaceConfig,
465    ) -> Result<ConsistencyToken, EngineError> {
466        self.apply_namespace_configs([config])
467    }
468
469    /// Applies one structured namespace config and returns the published token.
470    ///
471    /// # Errors
472    ///
473    /// Returns [`EngineError`] when the namespace config cannot be validated.
474    pub fn add_config_with_token(
475        &self,
476        config: NamespaceConfig,
477    ) -> Result<ConsistencyToken, EngineError> {
478        self.apply_namespace_config(config)
479    }
480
481    /// Applies structured namespace configs and publishes a single new revision.
482    ///
483    /// # Errors
484    ///
485    /// Returns [`EngineError`] when any namespace config cannot be validated.
486    pub fn apply_namespace_configs(
487        &self,
488        configs: impl IntoIterator<Item = NamespaceConfig>,
489    ) -> Result<ConsistencyToken, EngineError> {
490        let configs = configs.into_iter().collect();
491        self.submit_write("apply_namespace_configs", |response| {
492            WriterCommand::ApplyNamespaceConfigs { configs, response }
493        })
494    }
495
496    /// Replaces the complete schema document and publishes a new revision.
497    ///
498    /// # Errors
499    ///
500    /// Returns [`EngineError`] when the schema cannot be parsed or existing relationships no longer
501    /// validate against it.
502    pub fn replace_schema(
503        &self,
504        source: SchemaSource<'_>,
505    ) -> Result<ConsistencyToken, EngineError> {
506        enter_api_span!("replace_schema");
507        self.submit_write("replace_schema", |response| WriterCommand::ReplaceSchema {
508            text: source.text.to_string(),
509            response,
510        })
511    }
512
513    /// Deletes one namespace definition.
514    ///
515    /// # Errors
516    ///
517    /// Returns [`EngineError`] when the namespace is missing or existing relationships still
518    /// reference it.
519    pub fn delete_namespace(&self, namespace: &str) -> Result<ConsistencyToken, EngineError> {
520        enter_api_span!("delete_namespace");
521        self.submit_write("delete_namespace", |response| {
522            WriterCommand::DeleteNamespace {
523                namespace: namespace.to_string(),
524                response,
525            }
526        })
527    }
528
529    /// Deletes one relation definition.
530    ///
531    /// # Errors
532    ///
533    /// Returns [`EngineError`] when the relation is missing or existing relationships still
534    /// reference it.
535    pub fn delete_relation(
536        &self,
537        namespace: &str,
538        relation: &str,
539    ) -> Result<ConsistencyToken, EngineError> {
540        enter_api_span!("delete_relation");
541        self.submit_write("delete_relation", |response| {
542            WriterCommand::DeleteRelation {
543                namespace: namespace.to_string(),
544                relation: relation.to_string(),
545                response,
546            }
547        })
548    }
549
550    /// Builds a new engine from policy text.
551    ///
552    /// # Errors
553    ///
554    /// Returns [`EngineError`] when policy text cannot be parsed or validated.
555    pub fn from_policy_text(policy: &PolicyText) -> Result<Self, EngineError> {
556        enter_api_span!("from_policy_text");
557        let engine = Self::builder().build();
558        engine.apply_policy_text(policy)?;
559        Ok(engine)
560    }
561
562    /// Replaces this engine's state with policy text.
563    ///
564    /// # Errors
565    ///
566    /// Returns [`EngineError`] when policy text cannot be parsed or validated.
567    pub fn apply_policy_text(&self, policy: &PolicyText) -> Result<ConsistencyToken, EngineError> {
568        enter_api_span!("apply_policy_text");
569        let policy = policy.clone();
570        self.submit_write("apply_policy_text", |response| {
571            WriterCommand::ApplyPolicyText { policy, response }
572        })
573    }
574
575    /// Saves a snapshot built from policy text without keeping an engine.
576    ///
577    /// # Errors
578    ///
579    /// Returns [`PolicyIoError`] when policy parsing or snapshot writing fails.
580    pub fn save_snapshot_from_policy_text(
581        path: impl AsRef<Path>,
582        policy: &PolicyText,
583        options: SnapshotSaveOptions,
584    ) -> Result<(), PolicyIoError> {
585        policy::save_snapshot_from_policy_text(path.as_ref(), policy, options)
586    }
587
588    /// Exports the latest state as deterministic policy text.
589    ///
590    /// # Errors
591    ///
592    /// Returns [`EngineError`] when no schema has been loaded.
593    pub fn export_policy_text(&self) -> Result<PolicyText, EngineError> {
594        enter_api_span!("export_policy_text");
595        let snapshot = self.latest_snapshot()?;
596        Ok(policy::export_policy_text(
597            snapshot.configs(),
598            snapshot.relationships().rows(),
599        ))
600    }
601
602    /// Exports deterministic policy files under `directory`.
603    ///
604    /// # Errors
605    ///
606    /// Returns [`PolicyIoError`] when no schema has been loaded or file output fails.
607    pub fn export_policy_files(&self, directory: impl AsRef<Path>) -> Result<(), PolicyIoError> {
608        enter_api_span!("export_policy_files");
609        let snapshot = self
610            .latest_snapshot()
611            .map_err(|_| PolicyIoError::Zanzibar {
612                source: ZanzibarError::SchemaRequired,
613            })?;
614        let policy =
615            policy::export_policy_text(snapshot.configs(), snapshot.relationships().rows());
616        policy::write_policy_files(directory.as_ref(), &policy)
617    }
618
619    /// Looks up every relation or permission a subject has on one resource.
620    ///
621    /// # Errors
622    ///
623    /// Returns [`EngineError`] when request validation or evaluation fails.
624    pub fn lookup_permissions(
625        &self,
626        request: impl Borrow<LookupPermissionsRequest>,
627    ) -> Result<LookupPermissions, EngineError> {
628        enter_api_span!("lookup_permissions");
629        let request = request.borrow();
630        request.validate()?;
631        let (snapshot, limits) = self.snapshot_for_consistency(request.consistency.clone())?;
632        let object_type = ObjectType::try_from(request.resource.namespace.as_str())?;
633        snapshot.schema().resolver().namespace(&object_type)?;
634        let mut permissions = Vec::new();
635        let mut check_context = eval::EvaluationContext::new_with_request_memo(&snapshot, limits);
636        for relation_definition in snapshot
637            .schema()
638            .resolver()
639            .sorted_relations(&object_type)?
640        {
641            let relation = Relation(relation_definition.name().as_str().to_string());
642            check_context.reset_for_reuse();
643            if check_context
644                .check_prepared(
645                    &request.resource,
646                    &relation,
647                    &request.subject,
648                    relation_definition,
649                )?
650                .is_allowed()
651            {
652                permissions.push(relation);
653            }
654        }
655        Ok(LookupPermissions { permissions })
656    }
657
658    /// Looks up subjects grouped by every relation or permission they have on one resource.
659    ///
660    /// # Errors
661    ///
662    /// Returns [`EngineError`] when request validation, store access, or evaluation fails.
663    pub fn lookup_object_permissions(
664        &self,
665        request: impl Borrow<LookupObjectPermissionsRequest>,
666    ) -> Result<LookupObjectPermissions, EngineError> {
667        enter_api_span!("lookup_object_permissions");
668        let request = request.borrow();
669        request.validate()?;
670        let (snapshot, limits) = self.snapshot_for_consistency(request.consistency.clone())?;
671        let object_type = ObjectType::try_from(request.resource.namespace.as_str())?;
672        snapshot.schema().resolver().namespace(&object_type)?;
673        let subject_type = crate::domain::SubjectType::try_from(request.subject_type.as_str())?;
674        if subject_type.as_str() != "user" {
675            let subject_object_type = ObjectType::try_from(subject_type.as_str())?;
676            snapshot
677                .schema()
678                .resolver()
679                .namespace(&subject_object_type)?;
680        }
681
682        let mut permissions = Vec::new();
683        for relation_definition in snapshot
684            .schema()
685            .resolver()
686            .sorted_relations(&object_type)?
687        {
688            let permission = Relation(relation_definition.name().as_str().to_string());
689            let subjects = eval::lookup_subjects_with_snapshot(
690                &snapshot,
691                &LookupSubjectsRequest {
692                    resource: request.resource.clone(),
693                    permission: permission.clone(),
694                    subject_type: request.subject_type.clone(),
695                },
696                limits,
697            )?
698            .subjects;
699            if !subjects.is_empty() {
700                permissions.push(PermissionSubjects {
701                    permission,
702                    subjects,
703                });
704            }
705        }
706        Ok(LookupObjectPermissions { permissions })
707    }
708
709    /// Saves the latest published snapshot to a versioned `.szsnap` artifact.
710    ///
711    /// # Errors
712    ///
713    /// Returns [`SnapshotIoError`] when no schema is loaded, the save options are unsupported, or
714    /// the artifact cannot be written.
715    pub fn save_snapshot(
716        &self,
717        path: impl AsRef<Path>,
718        options: SnapshotSaveOptions,
719    ) -> Result<(), SnapshotIoError> {
720        enter_api_span!("save_snapshot");
721        let snapshot = self.latest_snapshot().map_err(|error| match error {
722            EngineError::SchemaRequired => SnapshotIoError::Format {
723                reason: "schema snapshot is required before saving",
724            },
725            _ => SnapshotIoError::Format {
726                reason: "engine state unavailable during snapshot save",
727            },
728        })?;
729        crate::snapshot::save_snapshot_file(path.as_ref(), &snapshot, options)
730    }
731
732    /// Loads a versioned `.szsnap` artifact into a new engine.
733    ///
734    /// # Errors
735    ///
736    /// Returns [`SnapshotIoError`] when the artifact cannot be read or fails validation.
737    pub fn load_snapshot(
738        path: impl AsRef<Path>,
739        options: SnapshotLoadOptions,
740    ) -> Result<Self, SnapshotIoError> {
741        enter_api_span!("load_snapshot");
742        let state = Arc::new(ArcSwapOption::empty());
743        let writer_state =
744            WriterState::load_snapshot_with_publisher(path, options, Arc::clone(&state))?;
745        Ok(Self {
746            state,
747            writer: WriterActor::start(writer_state, default_writer_queue_capacity()),
748        })
749    }
750
751    fn submit_write(
752        &self,
753        operation: &'static str,
754        build_command: impl FnOnce(WriteResponseSender) -> WriterCommand,
755    ) -> Result<ConsistencyToken, EngineError> {
756        let (sender, receiver) = mpsc::sync_channel(1);
757        self.writer.send(build_command(sender), operation)?;
758        receiver
759            .recv()
760            .map_err(|_| EngineError::WriterUnavailable { operation })?
761            .map_err(EngineError::from)
762    }
763
764    fn current_state(&self) -> Result<Arc<EngineState>, EngineError> {
765        self.state.load_full().ok_or(EngineError::SchemaRequired)
766    }
767
768    fn latest_snapshot(&self) -> Result<Arc<crate::revision::PublishedSnapshot>, EngineError> {
769        Ok(self.current_state()?.latest_snapshot())
770    }
771
772    fn snapshot_for_consistency(
773        &self,
774        consistency: Consistency,
775    ) -> Result<(Arc<crate::revision::PublishedSnapshot>, EvaluationLimits), EngineError> {
776        let state = self.current_state()?;
777        let limits = state.evaluation_limits();
778        let snapshot = state.snapshot_for_consistency(consistency)?;
779        Ok((snapshot, limits))
780    }
781
782    fn ensure_subject_reverse_lookup_supported(
783        snapshot: &crate::revision::PublishedSnapshot,
784        operation: &'static str,
785    ) -> Result<(), EngineError> {
786        let profile = snapshot.relationships().index_profile();
787        if profile.supports_subject_reverse_lookup() {
788            return Ok(());
789        }
790        Err(EngineError::UnsupportedIndexProfile { operation, profile })
791    }
792}
793
794impl Default for ZanzibarEngine {
795    fn default() -> Self {
796        Self::builder().build()
797    }
798}
799
800/// Builder for [`ZanzibarEngine`].
801#[derive(Debug, Clone, Copy)]
802pub struct ZanzibarEngineBuilder {
803    retained_snapshots: NonZeroUsize,
804    evaluation_limits: EvaluationLimits,
805    writer_queue_capacity: NonZeroUsize,
806}
807
808impl ZanzibarEngineBuilder {
809    /// Creates a builder with production defaults.
810    #[must_use]
811    pub fn new() -> Self {
812        Self {
813            retained_snapshots: default_retained_snapshots(),
814            evaluation_limits: EvaluationLimits::default(),
815            writer_queue_capacity: default_writer_queue_capacity(),
816        }
817    }
818
819    /// Sets how many exact revision snapshots the engine retains.
820    #[must_use]
821    pub fn retained_snapshots(mut self, retained_snapshots: NonZeroUsize) -> Self {
822        self.retained_snapshots = retained_snapshots;
823        self
824    }
825
826    /// Sets evaluator recursion, fanout, and lookup-result limits.
827    #[must_use]
828    pub fn evaluation_limits(mut self, evaluation_limits: EvaluationLimits) -> Self {
829        self.evaluation_limits = evaluation_limits;
830        self
831    }
832
833    /// Sets the bounded writer actor queue capacity.
834    #[must_use]
835    pub fn writer_queue_capacity(mut self, writer_queue_capacity: NonZeroUsize) -> Self {
836        self.writer_queue_capacity = writer_queue_capacity;
837        self
838    }
839
840    /// Builds the engine.
841    #[must_use]
842    pub fn build(self) -> ZanzibarEngine {
843        let state = Arc::new(ArcSwapOption::empty());
844        let writer_state = WriterState::with_snapshot_retention_and_publisher(
845            self.retained_snapshots,
846            Arc::clone(&state),
847        )
848        .with_evaluation_limits(self.evaluation_limits);
849        ZanzibarEngine {
850            state,
851            writer: WriterActor::start(writer_state, self.writer_queue_capacity),
852        }
853    }
854}
855
856impl Default for ZanzibarEngineBuilder {
857    fn default() -> Self {
858        Self::new()
859    }
860}
861
862/// Validated tenant identifier used by [`ZanzibarTenantShards`].
863#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
864pub struct TenantId(String);
865
866impl TenantId {
867    /// Creates a tenant id after validating length and charset.
868    ///
869    /// # Errors
870    ///
871    /// Returns [`TenantIdError`] when `value` is empty, too long, or contains unsupported bytes.
872    pub fn new(value: impl Into<String>) -> Result<Self, TenantIdError> {
873        let value = value.into();
874        validate_tenant_id(&value)?;
875        Ok(Self(value))
876    }
877
878    /// Returns the tenant id as a string slice.
879    #[must_use]
880    pub fn as_str(&self) -> &str {
881        &self.0
882    }
883}
884
885impl fmt::Display for TenantId {
886    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
887        formatter.write_str(&self.0)
888    }
889}
890
891impl FromStr for TenantId {
892    type Err = TenantIdError;
893
894    fn from_str(value: &str) -> Result<Self, Self::Err> {
895        Self::new(value)
896    }
897}
898
899impl TryFrom<&str> for TenantId {
900    type Error = TenantIdError;
901
902    fn try_from(value: &str) -> Result<Self, Self::Error> {
903        Self::new(value)
904    }
905}
906
907/// Error returned by [`TenantId`] validation.
908#[derive(Debug, Clone, PartialEq, Eq, Error)]
909pub enum TenantIdError {
910    /// Tenant id was empty.
911    #[error("tenant id must not be empty")]
912    Empty,
913    /// Tenant id exceeded the maximum byte length.
914    #[error("tenant id exceeds maximum byte length {max_bytes}")]
915    TooLong {
916        /// Maximum accepted tenant id length in bytes.
917        max_bytes: usize,
918    },
919    /// Tenant id contained an unsupported byte.
920    #[error("tenant id contains invalid byte at offset {offset}")]
921    InvalidByte {
922        /// Byte offset of the invalid value.
923        offset: usize,
924    },
925}
926
927/// Tenant-sharded owner of independent [`ZanzibarEngine`] instances.
928#[derive(Debug)]
929pub struct ZanzibarTenantShards {
930    shards: Arc<ArcSwap<ShardMap>>,
931    create_gate: Mutex<()>,
932    builder: ZanzibarEngineBuilder,
933}
934
935impl ZanzibarTenantShards {
936    /// Creates an empty tenant shard set using `builder` for new tenants.
937    #[must_use]
938    pub fn new(builder: ZanzibarEngineBuilder) -> Self {
939        Self {
940            shards: Arc::new(ArcSwap::from_pointee(ShardMap::default())),
941            create_gate: Mutex::new(()),
942            builder,
943        }
944    }
945
946    /// Returns an existing tenant engine without creating it.
947    #[must_use]
948    pub fn get(&self, tenant: &TenantId) -> Option<Arc<ZanzibarEngine>> {
949        self.shards.load_full().engines.get(tenant).cloned()
950    }
951
952    /// Returns an existing tenant engine or creates one under a short creation gate.
953    #[must_use]
954    pub fn get_or_create(&self, tenant: TenantId) -> Arc<ZanzibarEngine> {
955        if let Some(engine) = self.get(&tenant) {
956            return engine;
957        }
958        let _guard = self
959            .create_gate
960            .lock()
961            .unwrap_or_else(std::sync::PoisonError::into_inner);
962        if let Some(engine) = self.get(&tenant) {
963            return engine;
964        }
965        let mut next = (*self.shards.load_full()).clone();
966        let engine = Arc::new(self.builder.build());
967        next.engines.insert(tenant, Arc::clone(&engine));
968        self.shards.store(Arc::new(next));
969        engine
970    }
971
972    /// Returns sorted tenant ids currently present in this shard set.
973    #[must_use]
974    pub fn tenants(&self) -> Vec<TenantId> {
975        let mut tenants = self
976            .shards
977            .load_full()
978            .engines
979            .keys()
980            .cloned()
981            .collect::<Vec<_>>();
982        tenants.sort();
983        tenants
984    }
985}
986
987impl Default for ZanzibarTenantShards {
988    fn default() -> Self {
989        Self::new(ZanzibarEngine::builder())
990    }
991}
992
993/// Error returned by public [`ZanzibarEngine`] methods.
994#[derive(Debug, Error, PartialEq)]
995pub enum EngineError {
996    /// Domain primitive validation failed.
997    #[error(transparent)]
998    Domain(DomainError),
999
1000    /// Schema compilation or validation failed.
1001    #[error(transparent)]
1002    Schema(SchemaError),
1003
1004    /// Relationship store access or mutation semantics failed.
1005    #[error(transparent)]
1006    Store(StoreError),
1007
1008    /// Consistency token validation failed.
1009    #[error(transparent)]
1010    Consistency(ConsistencyError),
1011
1012    /// Graph evaluation failed.
1013    #[error(transparent)]
1014    Evaluation(EvaluationError),
1015
1016    /// Requested namespace is not loaded.
1017    #[error("namespace '{namespace}' not found")]
1018    NamespaceNotFound {
1019        /// Missing namespace name.
1020        namespace: String,
1021    },
1022
1023    /// Requested relation is not loaded in the namespace.
1024    #[error("relation '{relation}' not found in namespace '{namespace}'")]
1025    RelationNotFound {
1026        /// Namespace searched.
1027        namespace: String,
1028        /// Missing relation name.
1029        relation: String,
1030    },
1031
1032    /// Schema source parsing failed.
1033    #[error("schema parse error: {message}")]
1034    ParseError {
1035        /// Parser diagnostic.
1036        message: String,
1037    },
1038
1039    /// Legacy compatibility storage failed.
1040    #[error("storage error: {message}")]
1041    StorageError {
1042        /// Storage diagnostic.
1043        message: String,
1044    },
1045
1046    /// Operation requires a loaded schema.
1047    #[error("schema must be loaded before this operation")]
1048    SchemaRequired,
1049
1050    /// The writer actor is unavailable.
1051    #[error("engine writer actor unavailable during {operation}")]
1052    WriterUnavailable {
1053        /// Operation that attempted to use the writer actor.
1054        operation: &'static str,
1055    },
1056
1057    /// The loaded index profile cannot support a requested operation.
1058    #[error("index profile {profile:?} does not support {operation}")]
1059    UnsupportedIndexProfile {
1060        /// Operation that requires an omitted index family.
1061        operation: &'static str,
1062        /// Loaded profile.
1063        profile: IndexProfile,
1064    },
1065}
1066
1067impl From<ZanzibarError> for EngineError {
1068    fn from(error: ZanzibarError) -> Self {
1069        match error {
1070            ZanzibarError::NamespaceNotFound(namespace) => Self::NamespaceNotFound { namespace },
1071            ZanzibarError::RelationNotFound(relation, namespace) => Self::RelationNotFound {
1072                namespace,
1073                relation,
1074            },
1075            ZanzibarError::ParseError(message) => Self::ParseError { message },
1076            ZanzibarError::StorageError(message) => Self::StorageError { message },
1077            ZanzibarError::SchemaRequired => Self::SchemaRequired,
1078            ZanzibarError::Domain(error) => Self::Domain(error),
1079            ZanzibarError::Schema(error) => Self::Schema(error),
1080            ZanzibarError::Store(error) => Self::Store(error),
1081            ZanzibarError::Consistency(error) => Self::Consistency(error),
1082            ZanzibarError::Evaluation(error) => Self::Evaluation(error),
1083        }
1084    }
1085}
1086
1087impl From<EngineError> for ZanzibarError {
1088    fn from(error: EngineError) -> Self {
1089        match error {
1090            EngineError::NamespaceNotFound { namespace } => Self::NamespaceNotFound(namespace),
1091            EngineError::RelationNotFound {
1092                namespace,
1093                relation,
1094            } => Self::RelationNotFound(relation, namespace),
1095            EngineError::ParseError { message } => Self::ParseError(message),
1096            EngineError::StorageError { message } => Self::StorageError(message),
1097            EngineError::SchemaRequired => Self::SchemaRequired,
1098            EngineError::Domain(error) => Self::Domain(error),
1099            EngineError::Schema(error) => Self::Schema(error),
1100            EngineError::Store(error) => Self::Store(error),
1101            EngineError::Consistency(error) => Self::Consistency(error),
1102            EngineError::Evaluation(error) => Self::Evaluation(error),
1103            EngineError::WriterUnavailable { operation } => Self::StorageError(format!(
1104                "engine writer actor unavailable during {operation}",
1105            )),
1106            EngineError::UnsupportedIndexProfile { operation, profile } => Self::StorageError(
1107                format!("index profile {profile:?} does not support {operation}"),
1108            ),
1109        }
1110    }
1111}
1112
1113impl From<DomainError> for EngineError {
1114    fn from(error: DomainError) -> Self {
1115        Self::Domain(error)
1116    }
1117}
1118
1119impl From<SchemaError> for EngineError {
1120    fn from(error: SchemaError) -> Self {
1121        Self::Schema(error)
1122    }
1123}
1124
1125impl From<StoreError> for EngineError {
1126    fn from(error: StoreError) -> Self {
1127        Self::Store(error)
1128    }
1129}
1130
1131impl From<ConsistencyError> for EngineError {
1132    fn from(error: ConsistencyError) -> Self {
1133        Self::Consistency(error)
1134    }
1135}
1136
1137impl From<EvaluationError> for EngineError {
1138    fn from(error: EvaluationError) -> Self {
1139        Self::Evaluation(error)
1140    }
1141}
1142
1143type WriteResponseSender = SyncSender<Result<ConsistencyToken, ZanzibarError>>;
1144
1145enum WriterCommand {
1146    WriteRelationships {
1147        mutations: Vec<RelationshipMutation>,
1148        preconditions: Vec<Precondition>,
1149        response: WriteResponseSender,
1150    },
1151    ApplySchema {
1152        text: String,
1153        response: WriteResponseSender,
1154    },
1155    ApplyNamespaceConfigs {
1156        configs: Vec<NamespaceConfig>,
1157        response: WriteResponseSender,
1158    },
1159    ReplaceSchema {
1160        text: String,
1161        response: WriteResponseSender,
1162    },
1163    DeleteNamespace {
1164        namespace: String,
1165        response: WriteResponseSender,
1166    },
1167    DeleteRelation {
1168        namespace: String,
1169        relation: String,
1170        response: WriteResponseSender,
1171    },
1172    ApplyPolicyText {
1173        policy: PolicyText,
1174        response: WriteResponseSender,
1175    },
1176    Shutdown,
1177}
1178
1179struct WriterActor {
1180    sender: SyncSender<WriterCommand>,
1181    handle: Mutex<Option<JoinHandle<()>>>,
1182    shutdown: AtomicBool,
1183}
1184
1185impl WriterActor {
1186    fn start(mut state: WriterState, queue_capacity: NonZeroUsize) -> Self {
1187        let (sender, receiver) = mpsc::sync_channel(queue_capacity.get());
1188        let handle = thread::spawn(move || {
1189            while let Ok(command) = receiver.recv() {
1190                match command {
1191                    WriterCommand::WriteRelationships {
1192                        mutations,
1193                        preconditions,
1194                        response,
1195                    } => {
1196                        drop(
1197                            response
1198                                .send(state.apply_relationship_mutations(mutations, preconditions)),
1199                        );
1200                    }
1201                    WriterCommand::ApplySchema { text, response } => {
1202                        drop(response.send(state.add_dsl_with_token(&text)));
1203                    }
1204                    WriterCommand::ApplyNamespaceConfigs { configs, response } => {
1205                        drop(response.send(state.apply_namespace_configs(configs)));
1206                    }
1207                    WriterCommand::ReplaceSchema { text, response } => {
1208                        drop(response.send(state.replace_dsl_with_token(&text)));
1209                    }
1210                    WriterCommand::DeleteNamespace {
1211                        namespace,
1212                        response,
1213                    } => {
1214                        drop(response.send(state.delete_namespace(&namespace)));
1215                    }
1216                    WriterCommand::DeleteRelation {
1217                        namespace,
1218                        relation,
1219                        response,
1220                    } => {
1221                        drop(response.send(state.delete_relation(&namespace, &relation)));
1222                    }
1223                    WriterCommand::ApplyPolicyText { policy, response } => {
1224                        drop(response.send(state.apply_policy_text(&policy)));
1225                    }
1226                    WriterCommand::Shutdown => break,
1227                }
1228            }
1229        });
1230        Self {
1231            sender,
1232            handle: Mutex::new(Some(handle)),
1233            shutdown: AtomicBool::new(false),
1234        }
1235    }
1236
1237    fn send(&self, command: WriterCommand, operation: &'static str) -> Result<(), EngineError> {
1238        if self.shutdown.load(Ordering::Acquire) {
1239            return Err(EngineError::WriterUnavailable { operation });
1240        }
1241        self.sender
1242            .send(command)
1243            .map_err(|_| EngineError::WriterUnavailable { operation })
1244    }
1245}
1246
1247impl fmt::Debug for WriterActor {
1248    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1249        formatter
1250            .debug_struct("WriterActor")
1251            .field("sender", &"<sync sender>")
1252            .field("handle", &"<join handle>")
1253            .finish()
1254    }
1255}
1256
1257impl Drop for WriterActor {
1258    fn drop(&mut self) {
1259        if !self.shutdown.swap(true, Ordering::AcqRel) {
1260            drop(self.sender.try_send(WriterCommand::Shutdown));
1261        }
1262        if let Ok(mut handle) = self.handle.lock()
1263            && let Some(handle) = handle.take()
1264        {
1265            drop(handle.join());
1266        }
1267    }
1268}
1269
1270#[derive(Debug, Clone, Default)]
1271struct ShardMap {
1272    engines: HashMap<TenantId, Arc<ZanzibarEngine>>,
1273}
1274
1275fn default_writer_queue_capacity() -> NonZeroUsize {
1276    match NonZeroUsize::new(DEFAULT_WRITER_QUEUE_CAPACITY) {
1277        Some(value) => value,
1278        None => NonZeroUsize::MIN,
1279    }
1280}
1281
1282fn validate_tenant_id(value: &str) -> Result<(), TenantIdError> {
1283    if value.is_empty() {
1284        return Err(TenantIdError::Empty);
1285    }
1286    if value.len() > MAX_TENANT_ID_BYTES {
1287        return Err(TenantIdError::TooLong {
1288            max_bytes: MAX_TENANT_ID_BYTES,
1289        });
1290    }
1291    for (offset, byte) in value.bytes().enumerate() {
1292        if !(byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')) {
1293            return Err(TenantIdError::InvalidByte { offset });
1294        }
1295    }
1296    Ok(())
1297}