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
CoreResolverthat walks the authorization graph, resolvingCheckrequests 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 implementations (e.g. pgauthz).
[dependencies]
authz-core = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }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
}
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);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),
}The DSL is a superset of the OpenFGA syntax.
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.
| 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 -.
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 | 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 |
There are two categories of traits to implement:
- Tuple traits —
TupleReader/TupleWriter: read and write relationship tuples (e.g.document:42#viewer@user:alice). Used directly byCoreResolver. - Policy traits —
PolicyReader/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.
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!()
}
}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!()
}
}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.
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!() }
}Rust 1.85 or later (edition 2024).
Licensed under the Apache License, Version 2.0.