Expand description
An in-process authorization engine for Rust.
Gatehouse composes role-based (RBAC), attribute-based (ABAC), and
relationship-based (ReBAC) policies while keeping authorization logic in
Rust. Relationship facts are loaded through request-scoped
EvaluationSession values, so list endpoints can batch, deduplicate, and
coalesce backend calls without moving policy logic into the data layer.
§Overview
A Policy is an asynchronous decision unit that checks if a given subject may
perform an action on a resource within a given context. Policies implement the
Policy trait. A PermissionChecker aggregates multiple policies and uses OR
logic by default (i.e. if any policy grants access, then access is allowed).
The PolicyBuilder offers a builder pattern for creating custom policies.
Custom Policy implementations must provide both Policy::evaluate
and Policy::policy_type.
§Decision Semantics
gatehouse deliberately keeps decision semantics simple and explicit:
PermissionCheckerevaluates policies sequentially with OR semantics and short-circuits on the first grant.- An empty
PermissionCheckerdenies access with the reason"No policies configured". AndPolicyshort-circuits on the first denial.OrPolicyshort-circuits on the first grant.NotPolicyinverts the decision of its inner policy.PolicyBuildercombines all configured predicates with AND logic.PolicyBuilder::effectchanges the result returned by that specific built policy when its combined predicate matches. A non-match is still treated as denied/non-applicable, and the effect does not create global deny-overrides-allow semantics when used insidePermissionChecker.
Denials from AccessEvaluation are intentionally summary-level. For example,
a failed PermissionChecker returns the top-level reason
"All policies denied access". Use the attached EvalTrace to inspect the
individual policy reasons that led to that outcome.
§Trace Semantics
EvalTrace records the policies and combinator branches that were actually
evaluated. Because PermissionChecker, AndPolicy, and OrPolicy
short-circuit, the trace tree does not include policies that were never run.
§When to populate the Context type
Context carries request-scoped inputs the decision depends on but
that don’t belong on the subject or resource and aren’t fact-loadable
relationships. The current wall-clock time, the MFA freshness on
the auth session, the caller’s network zone, the request’s tenant
config — all properties of the call, not of the user or the thing
being authorized. A few shapes show up repeatedly:
-
Time-of-day / business hours. “Finance approvers can issue refunds between 09:00 and 17:00 in the company timezone, except admins.” The current wall-clock time isn’t a property of the user or the invoice — it’s a property of the call. Put a
current_time: SystemTime(orOffsetDateTime, or aClocktrait object for testability) onContextand have the policy compare against the resource’s business-hours window. Theexamples/actix_web.rsRequestContextuses exactly this shape for the draft-recency policy. -
Authentication / MFA freshness. “Approving a payment over $10k requires an MFA assertion within the last 5 minutes.” MFA freshness lives on the request (the session token records when MFA was last reasserted), not on the user record. A
mfa_verified_at: Option<SystemTime>onContextlets the high-value policy short- circuit deny when freshness has lapsed without forcing every policy to plumb the auth-session through their own arguments. Seeexamples/mfa_freshness_context.rsfor the full end-to-end shape. -
Device / network trust posture. “Production database access requires the request to come from a managed device on the corporate VPN.”
device_trust_score: u8,network_zone: NetworkZone, orclient_ip: IpAddronContextare the typical shape. Policies that don’t care about posture simply ignore the field. -
Request-wide parameters shared across actions. When the same per-request input shapes the decision for many different actions —
export_destination: ExportDestination,purpose: AccessPurpose,client_app_version: SemVer—Contextis the right home for it. Per-action attributes that only one action cares about belong on the action enum instead. -
Tenant / feature-flag overrides. A
tenant_config: &'a TenantPolicyConfigreference lets policies read tenant-level toggles (“this tenant has BYOK enabled”, “this tenant requires approval for refunds over $X”) without each policy looking the tenant up itself. Distinct fromFactSource-loaded facts: those are looked up by key during evaluation; the tenant config is already resolved at request entry.
Context = () is the right call when every decision boils down to
“does the subject have role X” — pure RBAC, no time, no posture,
no per-request flags. Reach for a real Context struct as soon as
a policy needs to compare against something time-varying or
per-request that isn’t a property of the subject, the resource, or
a fact-loadable relationship.
Context is not the place for relationship data (“who has
viewer access on this document”). That lives behind a
FactSource and gets loaded through the EvaluationSession so
batch evaluation can deduplicate and coalesce.
§Fact-Loaded Authorization
Gatehouse treats non-trivial authorization as computation over facts loaded
for one request. FactSource::load_many receives unique fact keys and
returns exactly one result per key; EvaluationSession expands duplicate
caller inputs, preserves caller order, caches results for the request, and
joins concurrent in-flight loads for the same key.
FactSource is Gatehouse’s request-scoped DataLoader-style primitive:
the session deduplicates and caches keys before calling the source, then
chunks the unique key set according to
FactSource::max_batch_size — so the source may receive one or more
batched calls per request, each over a slice of the unique keys. If your
application already uses a DataLoader implementation (for example
async_graphql::dataloader from the async-graphql crate, or the
ultra-batch crate), call it directly from inside
FactSource::load_many — gatehouse does not need its own batching
layer for the data fetch, only for the per-request fact graph. The same
composition pattern applies to the Hydrator used by lookup-style
listings (Hydrator::hydrate).
RebacPolicy is the first built-in policy backed by this model. It
extracts flat subject/resource IDs, builds RelationshipQuery keys, and
asks the session for relationship facts.
§Quick Start
The fastest way to define a policy is with PolicyBuilder:
#[derive(Debug, Clone)]
struct User { roles: Vec<String> }
#[derive(Debug, Clone)]
struct Document;
#[derive(Debug, Clone)]
struct ReadAction;
#[derive(Debug, Clone)]
struct AppContext;
let policy = PolicyBuilder::<User, Document, ReadAction, AppContext>::new("AdminOnly")
.subjects(|user: &User| user.roles.iter().any(|r| r == "admin"))
.build();
let mut checker = PermissionChecker::new();
checker.add_policy(policy);
let admin = User { roles: vec!["admin".into()] };
assert!(checker.check(&admin, &ReadAction, &Document, &AppContext).await.is_granted());
let guest = User { roles: vec!["guest".into()] };
assert!(!checker.check(&guest, &ReadAction, &Document, &AppContext).await.is_granted());check is the everyday entry point for policies that don’t need
FactSource-loaded relationship data. For fact-backed checkers
(RBAC/ABAC alongside RebacPolicy, or any policy reading from an
EvaluationSession), use PermissionChecker::evaluate_in_session
and pass a session loaded for the request.
§Built-in Policies
The library provides several built-in policies:
RbacPolicy: A role-based access control policy.AbacPolicy: An attribute-based access control policy.RebacPolicy: A relationship-based access control policy backed byFactSourceandEvaluationSession.
§Custom Policies
For full control, implement the Policy trait directly. Below we define a simple
system where a user may read a document if they are an admin (via a role-based policy)
or if they are the owner of the document (via an attribute-based policy).
// Define our core types.
#[derive(Debug, Clone)]
pub struct User {
pub id: Uuid,
pub roles: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct Document {
pub id: Uuid,
pub owner_id: Uuid,
}
#[derive(Debug, Clone)]
pub struct ReadAction;
#[derive(Debug, Clone)]
pub struct EmptyContext;
// A simple RBAC policy: grant access if the user has the "admin" role.
struct AdminPolicy;
#[async_trait]
impl Policy<User, Document, ReadAction, EmptyContext> for AdminPolicy {
async fn evaluate(&self, ctx: &EvalCtx<'_, User, Document, ReadAction, EmptyContext>) -> PolicyEvalResult {
if ctx.subject.roles.contains(&"admin".to_string()) {
ctx.grant("User is admin")
} else {
ctx.deny("User is not admin")
}
}
fn policy_type(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("AdminPolicy")
}
}
// An ABAC policy: grant access if the user is the owner of the document.
struct OwnerPolicy;
#[async_trait]
impl Policy<User, Document, ReadAction, EmptyContext> for OwnerPolicy {
async fn evaluate(&self, ctx: &EvalCtx<'_, User, Document, ReadAction, EmptyContext>) -> PolicyEvalResult {
if ctx.subject.id == ctx.resource.owner_id {
ctx.grant("User is the owner")
} else {
ctx.deny("User is not the owner")
}
}
fn policy_type(&self) -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("OwnerPolicy")
}
}
// Create a PermissionChecker (which uses OR semantics by default) and add both policies.
fn create_document_checker() -> PermissionChecker<User, Document, ReadAction, EmptyContext> {
let mut checker = PermissionChecker::new();
checker.add_policy(AdminPolicy);
checker.add_policy(OwnerPolicy);
checker
}
let admin_user = User {
id: Uuid::new_v4(),
roles: vec!["admin".into()],
};
let owner_user = User {
id: Uuid::new_v4(),
roles: vec!["user".into()],
};
let document = Document {
id: Uuid::new_v4(),
owner_id: owner_user.id,
};
let checker = create_document_checker();
// An admin should have access.
assert!(checker.check(&admin_user, &ReadAction, &document, &EmptyContext).await.is_granted());
// The owner should have access.
assert!(checker.check(&owner_user, &ReadAction, &document, &EmptyContext).await.is_granted());
// A random user should be denied access.
let random_user = User {
id: Uuid::new_v4(),
roles: vec!["user".into()],
};
assert!(!checker.check(&random_user, &ReadAction, &document, &EmptyContext).await.is_granted());§Evaluation Tracing
The permission system provides detailed tracing of policy decisions, see AccessEvaluation
for an example.
§Tracing And Telemetry
When trace-level events are enabled, PermissionChecker::evaluate_in_session
creates an instrumented span and each evaluated policy records a trace!
event on the gatehouse::security target. Batch evaluation records
aggregate item counts on PermissionChecker::evaluate_batch_in_session_by
and per-policy counts on nested gatehouse.batch_policy spans.
Emitted fields:
security_rule.namesecurity_rule.categorysecurity_rule.descriptionsecurity_rule.referencesecurity_rule.ruleset.namesecurity_rule.uuidsecurity_rule.versionsecurity_rule.licenseevent.outcomepolicy.typepolicy.result.reason
When Policy::security_rule is not overridden, tracing falls back to:
security_rule.name = policy_type()security_rule.category = "Access Control"security_rule.ruleset.name = "PermissionChecker"
§Combinators
Sometimes you may want to require that several policies pass (AND), require that
at least one passes (OR), or even invert a policy (NOT). gatehouse provides
combinators for this purpose:
AndPolicy: Grants access only if all inner policies allow access. Otherwise, returns a combined error.OrPolicy: Grants access if any inner policy allows access; otherwise returns a combined error.NotPolicy: Inverts the decision of an inner policy.DelegatingPolicy: Maps the current inputs into another authorization domain and delegates to a childPermissionCheckerwhile preserving batching.
Structs§
- Abac
Policy - An attribute-based access control policy.
Define a
conditionclosure that determines whether a subject is allowed to perform an action on a resource, given the additional context. If it returns true, access is granted. Otherwise, access is denied. - AndPolicy
- Batch
Eval Ctx - Batch policy evaluation context.
- Delegating
Policy - A policy that delegates its decision to another
PermissionChecker. - Empty
Policies Error - Error returned when no policies are provided to a combinator policy.
- EvalCtx
- Per-item policy evaluation context.
- Eval
Trace - A tree of
PolicyEvalResultnodes capturing every policy decision made during an access evaluation. - Evaluation
Session - Request-scoped fact loading and caching state.
- Evaluation
Session Builder - Builder for declaring all fact sources needed by a request-scoped
EvaluationSession. - Fact
Provenance - A record that a policy consulted a fact while reaching its decision.
- Lookup
Authorized Page - One page of authorized resources, paired with the next candidate-page cursor.
- Lookup
Page - One page of enumerated candidate IDs.
- NotPolicy
- NotPolicy
- OrPolicy
- OrPolicy
- Permission
Checker - A container for multiple policies, applied in an “OR” fashion. (If any policy returns Ok, access is granted)
- Policy
Batch Item - A borrowed resource/context pair passed to batch policy evaluators.
- Policy
Builder - A builder API for creating custom policies.
- Rbac
Policy - A role-based access control policy.
- Rebac
Policy - ReBAC Policy
- Relationship
Query - Canonical fact key for relationship (ReBAC) lookups.
- Security
Rule Metadata - Metadata describing the security rule associated with a
crate::Policy.
Enums§
- Access
Evaluation - The complete result of a permission evaluation. Contains both the final decision and a detailed trace for debugging.
- Combine
Op - The type of boolean combining operation a policy might represent.
- Effect
- Represents the intended effect of a policy.
- Fact
Load Error - Error raised while loading a fact.
- Fact
Load Result - Result of loading one fact.
- Fact
Outcome - How a fact load that informed a policy decision resolved.
- Fact
Source Registration Error - Error returned by non-panicking fact-source registration helpers.
- Lookup
Authorized Error - Failure modes for
PermissionChecker::lookup_authorizedandPermissionChecker::lookup_authorized_page. - Policy
Eval Result - The result of evaluating a single policy (or a combination).
Traits§
- FactKey
- A typed fact key that can be loaded through an
crate::EvaluationSession. - Fact
Source - A batched source for one fact key type.
- Hydrator
- Resolves enumerated IDs to caller-owned resources.
- Lookup
Source - Enumerates a candidate superset of resources for a subject.
- Policy
- A generic async trait representing a single authorization policy. A policy determines if a subject is allowed to perform an action on a resource within a given context.