Skip to main content

Crate gatehouse

Crate gatehouse 

Source
Expand description

An in-process authorization engine for Rust.

Gatehouse keeps authorization logic in Rust while giving policy code a request-scoped fact session for relationship and backend-loaded data. The public API is centered on one PolicyDomain marker per authorization domain, a PermissionChecker that owns that domain’s policy stack, and a BoundEvaluator created for one request/session/subject/action/context.

§Overview

A Policy is an asynchronous decision unit for one PolicyDomain. The domain names the four Rust types involved in a decision:

  • Subject: the caller.
  • Action: the operation being attempted.
  • Resource: the target resource or scope resource.
  • Context: request-scoped inputs such as current time, MFA freshness, network zone, tenant config, or feature flags.

Relationship data and other backend-loaded authorization facts do not belong in Context; expose them as FactKey values loaded by an EvaluationSession. The session batches, deduplicates, caches, and coalesces fact loads for one request.

§Quick Start

The fastest way to define a synchronous predicate policy is PolicyBuilder:

#[derive(Debug, Clone)]
struct User {
    id: u64,
    roles: Vec<&'static str>,
}
#[derive(Debug, Clone)]
struct Document {
    owner_id: u64,
}
#[derive(Debug, Clone)]
struct ReadAction;

struct Documents;
impl PolicyDomain for Documents {
    type Subject = User;
    type Action = ReadAction;
    type Resource = Document;
    type Context = ();
}

let admin_policy = PolicyBuilder::<Documents>::new("AdminOnly")
    .subjects(|user: &User| user.roles.contains(&"admin"))
    .build();

let owner_policy = PolicyBuilder::<Documents>::new("OwnerOnly")
    .when(|user: &User, _action: &ReadAction, document: &Document, _ctx: &()| {
        user.id == document.owner_id
    })
    .build();

let mut checker = PermissionChecker::<Documents>::new();
checker.add_policy(admin_policy);
checker.add_policy(owner_policy);

let session = EvaluationSession::empty();
let document = Document { owner_id: 7 };
let admin = User { id: 1, roles: vec!["admin"] };
let owner = User { id: 7, roles: vec!["user"] };
let guest = User { id: 2, roles: vec!["user"] };

assert!(checker.bind(&session, &admin, &ReadAction, &()).check(&document).await.is_granted());
assert!(checker.bind(&session, &owner, &ReadAction, &()).check(&document).await.is_granted());
assert!(!checker.bind(&session, &guest, &ReadAction, &()).check(&document).await.is_granted());

§Core Flows

Bind request-wide inputs once, then evaluate resources through the bound evaluator:

let session = registry.session();
let bound = checker.bind(&session, &subject, &action, &request_context);

let decision = bound.check(&resource).await;
let decisions = bound.evaluate(resources.clone()).await;
let authorized = bound.filter(resources).await;
let authorized_rows = bound.filter_by(rows, |row| &row.authz_resource).await;
let page = bound.lookup_page(&lookup, &hydrator, cursor.as_deref(), limit).await?;

Use EvaluationSession::empty for fact-free decisions. Use a session from FactRegistry::session when any policy calls ctx.session.get(...), such as RebacPolicy or a custom fact-backed policy.

BoundEvaluator::evaluate preserves input order and returns one AccessEvaluation per input resource. BoundEvaluator::filter keeps only granted resources. BoundEvaluator::evaluate_by and BoundEvaluator::filter_by are for wide caller-owned rows where authorization uses a projected resource. BoundEvaluator::lookup_page is for list endpoints where the application cannot load every possible candidate first; the LookupSource enumerates candidate IDs, a Hydrator resolves them, and the full policy stack authorizes the hydrated resources.

§Decision Semantics

Gatehouse deliberately keeps combining semantics fixed:

  • PermissionChecker applies deny-overrides. Any evaluated result containing PolicyEvalResult::Forbidden denies the request and overrides grants.
  • Policies declaring Effect::Forbid or Effect::AllowOrForbid are evaluated before allow-only policies so a veto cannot be skipped by grant short-circuiting.
  • If no policy forbids, the first grant wins.
  • If nothing grants, the checker denies with "All policies denied access".
  • An empty checker denies with "No policies configured".
  • PolicyEvalResult::NotApplicable means the policy did not grant. PolicyEvalResult::Forbidden means the policy actively vetoed.
  • PolicyBuilder combines configured predicates with AND logic. PolicyBuilder::forbid makes a matching built policy forbid; a non-match remains not applicable and does not block.
  • AndPolicy and OrPolicy evaluate veto-capable children before allow-only children, then short-circuit normally. NotPolicy inverts grants and non-grants, but never turns Forbidden into a grant.
  • Forbidden propagates through AndPolicy, OrPolicy, NotPolicy, and DelegatingPolicy.
  • NotPolicy does not neutralize a veto. admin.or(blocked.not()) still denies when blocked returns Forbidden. For “grant unless blocked”, use an allow-only blocked predicate under not(), or register an explicit forbid policy when the block should be global.
  • grant.and(forbid_only) can never grant: a forbid-only child does not satisfy AND’s “all children grant” rule. Use grant.and(blocked_allow_predicate.not()) for a local exclusion.

Denials from AccessEvaluation are summary-level. Use AccessEvaluation::display_trace or the attached EvalTrace to inspect individual policy reasons and fact provenance.

§Fact-Loaded Authorization

FactSource::load_many receives unique fact keys and must return exactly one result per key in the same order. EvaluationSession expands duplicate caller inputs, preserves caller order, caches results for the request, chunks loads according to FactSource::max_batch_size, and joins concurrent in-flight loads for the same key.

RebacPolicy is the built-in fact-backed policy. It extracts flat subject/resource IDs, builds RelationshipQuery keys, and grants only when the request session loads a Found(true) relationship fact. Missing sources, missing facts, backend errors, and fact-source contract violations fail closed to denied ReBAC decisions.

§Long-Lived Streams

EvaluationSession caches are scoped to one authorization pass. For SSE, WebSocket, and other long-lived streams, do not hold one fact-backed session for the stream lifetime.

If your product contract authorizes once at stream open, create a fresh session, compute the visible ID set with BoundEvaluator::filter or BoundEvaluator::filter_by, drop the session, and only emit frames for that set. If the stream must observe mid-stream permission revocation, run periodic reauthorization with a fresh FactRegistry::session and re-bind the checker for that pass.

§Built-In Policies

Use PolicyBuilder::when for attribute-style predicates that compare subject, action, resource, and context in one synchronous closure.

§Custom Policies

Implement Policy directly when a rule needs async work, custom batching, custom telemetry metadata, or hand-written forbid behavior:

struct OwnerPolicy;

#[async_trait]
impl Policy<Documents> for OwnerPolicy {
    async fn evaluate(&self, ctx: &EvalCtx<'_, Documents>) -> PolicyEvalResult {
        if ctx.subject.id == ctx.resource.owner_id {
            ctx.grant("subject owns the document")
        } else {
            ctx.not_applicable("subject does not own the document")
        }
    }

    fn policy_type(&self) -> Cow<'static, str> {
        Cow::Borrowed("OwnerPolicy")
    }
}

§Tracing

When trace-level events are enabled, checker evaluation records spans for single-resource and batch evaluation, and each evaluated policy records a trace! event on the gatehouse::security target. Batch evaluation also records per-policy counts on nested gatehouse.batch_policy spans.

Structs§

AndPolicy
Combines multiple policies with logical AND semantics.
BatchEvalCtx
Batch policy evaluation context.
BoundEvaluator
A request-bound evaluator for one checker, subject, action, context, and evaluation session.
DelegatingPolicy
A policy that maps the current domain into a child domain and delegates to another PermissionChecker.
EmptyPoliciesError
Error returned when no policies are provided to a combinator policy.
EvalCtx
Per-item policy evaluation context.
EvalTrace
A tree of PolicyEvalResult nodes capturing every policy decision made during an access evaluation.
EvaluationSession
Request-scoped fact loading and caching state.
FactProvenance
A record that a policy consulted a fact while reaching its decision.
FactRegistry
Reusable fact-source registry for creating request-scoped sessions.
FactRegistryBuilder
Builder for declaring fact sources once at application setup.
LookupAuthorizedPage
One page of authorized resources, paired with the next candidate-page cursor.
LookupPage
One page of enumerated candidate IDs.
NotPolicy
Inverts the decision of an inner policy.
OrPolicy
Combines multiple policies with logical OR semantics.
PermissionChecker
A policy stack for one PolicyDomain.
PolicyBatchItem
A borrowed resource passed to batch policy evaluators.
PolicyBuilder
Fluent builder for synchronous predicate policies.
RbacPolicy
Role-based access control policy.
RebacPolicy
Relationship-based access control backed by request-scoped fact loading.
RelationshipQuery
Canonical fact key for relationship (ReBAC) lookups.
SecurityRuleMetadata
Metadata describing the security rule associated with a crate::Policy.

Enums§

AccessEvaluation
The complete result of a permission evaluation. Contains both the final decision and a detailed trace for debugging.
CombineOp
The type of boolean combining operation a policy might represent.
Effect
The declared effect of a policy: whether it can grant, forbid, or both.
FactLoadError
Error raised while loading a fact.
FactLoadResult
Result of loading one fact.
FactOutcome
How a fact load that informed a policy decision resolved.
LookupAuthorizedError
Failure modes for crate::BoundEvaluator::lookup_page.
PolicyEvalResult
The result of evaluating a single policy (or a combination).

Traits§

FactKey
A typed fact key that can be loaded through an crate::EvaluationSession.
FactSource
A batched source for one fact key type.
Hydrator
Resolves enumerated IDs to caller-owned resources.
LookupSource
Enumerates a candidate superset of resources for a subject.
Policy
A generic async trait representing a single authorization policy for one PolicyDomain.
PolicyDomain
Names the four Rust types that make up one authorization domain.
PolicyExt
Fluent combinator helpers for policies.