authz-core 0.1.0

Zanzibar-style authorization engine — model parser, resolver, policy traits, and CEL condition evaluation
Documentation

authz-core

Crates.io Docs.rs License

A Zanzibar-inspired Fine-Grained Authorization engine for Rust, based on Google's globally consistent authorization system.

authz-core is the database- and transport-agnostic heart of an authorization system. It provides:

  • A model DSL for declaring types, relations, and permissions
  • A type system for validating tuples against the parsed model
  • Datastore traits (TupleReader, TupleWriter, PolicyReader, PolicyWriter) that you implement against any backend
  • A CoreResolver that walks the authorization graph, resolving Check requests via depth-first or breadth-first traversal with configurable cycle detection
  • CEL condition evaluation for attribute-based access control (ABAC)
  • A cache abstraction (AuthzCache) for pluggable caching backends
  • A dispatcher abstraction for fan-out to multiple resolvers

No database or transport dependencies are included — those live in downstream crates (e.g. pgauthz).


Quick start

[dependencies]
authz-core = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

1. Define your model

type user {}

type organization {
    relations
        define member: [user]
        define admin:  [user]
}

type document {
    relations
        define owner:  [user]
        define editor: [user | organization#member]
        define viewer: [user | organization#member]
    permissions
        define can_view = viewer + editor + owner
        define can_edit = editor + owner
}

2. Parse and build a TypeSystem

use authz_core::model_parser::parse_dsl;
use authz_core::type_system::TypeSystem;

let model = parse_dsl(include_str!("model.fga")).expect("invalid model");
let type_system = TypeSystem::new(model);

3. Wire up the resolver

use authz_core::core_resolver::CoreResolver;
use authz_core::policy_provider::StaticPolicyProvider;
use authz_core::resolver::{CheckResolver, CheckResult, ResolveCheckRequest};

let provider = StaticPolicyProvider::new(type_system);
let resolver = CoreResolver::new(
    my_tuple_store,  // impl TupleReader + Clone
    provider,
);

let req = ResolveCheckRequest::new(
    "document".into(), "doc-42".into(), "can_view".into(),
    "user".into(),     "alice".into(),
);

match resolver.resolve_check(req).await? {
    CheckResult::Allowed                   => println!("access granted"),
    CheckResult::Denied                    => println!("access denied"),
    CheckResult::ConditionRequired(params) => println!("need context: {:?}", params),
}

Model DSL

The DSL is a superset of the OpenFGA syntax.

Types and relations

type <name> {
    relations
        define <relation>: <expression>
    permissions
        define <permission> = <expression>
}

Relations are stored as tuples in the datastore. Permissions are derived — they cannot be the subject of a write tuple.

Relation expressions

Syntax Meaning
[user] Direct assignment — only user subjects
[user | group#member] Union of direct types and usersets
[user:*] Public / wildcard — any user subject
[user with ip_check] Conditional on a named CEL condition
editor Computed userset — inherit from another relation
parent->viewer Tuple-to-userset — traverse a relation, then check viewer on the target
a + b Union
a & b Intersection
a - b Exclusion (a minus b)

Operator precedence: + (union) binds tighter than &, which binds tighter than -.

Conditions (ABAC)

condition ip_check(allowed_cidrs: list<string>, request_ip: string) {
    request_ip in allowed_cidrs
}

Supported parameter types: string, int, bool, list<string>, list<int>, list<bool>, map<string, string>.

The condition expression is a CEL expression evaluated at check time when the context map is populated in ResolveCheckRequest.


Module overview

Module Contents
model_ast AST types: ModelFile, TypeDef, RelationDef, RelationExpr, ConditionDef
model_parser parse_dsl(src) — parses the DSL into a ModelFile
model_validator Post-parse semantic validation
type_system TypeSystem — query types/relations, validate tuples
traits Tuple, TupleFilter, TupleReader, TupleWriter, PolicyReader, PolicyWriter, RevisionReader
resolver CheckResolver trait, ResolveCheckRequest, CheckResult, RecursionConfig
core_resolver CoreResolver — the actual graph-walking engine
policy_provider PolicyProvider trait + StaticPolicyProvider
dispatcher Dispatcher trait + LocalDispatcher
cache AuthzCache<V> trait + NoopCache + noop_cache() helper
cel CEL expression compilation and evaluation
tenant_schema ChangelogReader trait for the Watch API
error AuthzError — all error variants

Implementing the datastore traits

There are two categories of traits to implement:

  • Tuple traitsTupleReader / TupleWriter: read and write relationship tuples (e.g. document:42#viewer@user:alice). Used directly by CoreResolver.
  • Policy traitsPolicyReader / PolicyWriter: read and write the authorization policy (the DSL source stored in the datastore). Used by your service layer to load and persist model definitions.

TupleReader

use async_trait::async_trait;
use authz_core::traits::{Tuple, TupleFilter, TupleReader};
use authz_core::error::AuthzError;

struct MyStore { /* db pool */ }

#[async_trait]
impl TupleReader for MyStore {
    async fn read_tuples(&self, filter: &TupleFilter) -> Result<Vec<Tuple>, AuthzError> {
        // SELECT * FROM tuples WHERE ...
        todo!()
    }

    async fn read_user_tuple(
        &self,
        object_type: &str, object_id: &str, relation: &str,
        subject_type: &str, subject_id: &str,
    ) -> Result<Option<Tuple>, AuthzError> {
        todo!()
    }

    async fn read_userset_tuples(
        &self,
        object_type: &str, object_id: &str, relation: &str,
    ) -> Result<Vec<Tuple>, AuthzError> {
        todo!()
    }

    async fn read_starting_with_user(
        &self,
        subject_type: &str, subject_id: &str,
    ) -> Result<Vec<Tuple>, AuthzError> {
        todo!()
    }

    async fn read_user_tuple_batch(
        &self,
        object_type: &str, object_id: &str, relations: &[String],
        subject_type: &str, subject_id: &str,
    ) -> Result<Option<Tuple>, AuthzError> {
        todo!()
    }
}

PolicyReader

use async_trait::async_trait;
use authz_core::traits::{AuthorizationPolicy, Pagination, PolicyReader};
use authz_core::error::AuthzError;

#[async_trait]
impl PolicyReader for MyStore {
    async fn read_authorization_policy(&self, id: &str) -> Result<Option<AuthorizationPolicy>, AuthzError> {
        // SELECT * FROM policies WHERE id = $1
        todo!()
    }

    async fn read_latest_authorization_policy(&self) -> Result<Option<AuthorizationPolicy>, AuthzError> {
        // SELECT * FROM policies ORDER BY created_at DESC LIMIT 1
        todo!()
    }

    async fn list_authorization_policies(&self, pagination: &Pagination) -> Result<Vec<AuthorizationPolicy>, AuthzError> {
        todo!()
    }
}

Recursion and performance

ResolveCheckRequest accepts a RecursionConfig to tune resolution behaviour:

use authz_core::resolver::{RecursionConfig, RecursionStrategy};

// Depth-first (default) — fast, memory-efficient
let cfg = RecursionConfig::depth_first().max_depth(30);

// Breadth-first — finds shortest path, higher memory
let cfg = RecursionConfig::breadth_first().cycle_detection(true);

ResolverMetadata (shared via Arc<AtomicU32>) tracks dispatch count, datastore queries, cache hits, and max depth reached across recursive calls — useful for observability.


Plugging in a cache

Implement AuthzCache<CheckResult> and pass it to CoreResolver:

use authz_core::cache::AuthzCache;
use authz_core::resolver::CheckResult;

struct MokaCache(moka::sync::Cache<String, CheckResult>);

impl AuthzCache<CheckResult> for MokaCache {
    fn get(&self, key: &str)               -> Option<CheckResult> { self.0.get(key) }
    fn insert(&self, key: &str, v: CheckResult)                    { self.0.insert(key.to_string(), v); }
    fn invalidate(&self, key: &str)                                { self.0.invalidate(key); }
    fn invalidate_all(&self)                                       { self.0.invalidate_all(); }
    fn metrics(&self) -> Box<dyn authz_core::cache::CacheMetrics>  { todo!() }
}

MSRV

Rust 1.85 or later (edition 2024).


License

Licensed under the Apache License, Version 2.0.