A flexible authorization library that combines role-based (RBAC), attribute-based (ABAC), and relationship-based (ReBAC) access control policies.
- Multi-paradigm Authorization: Support for RBAC, ABAC, and ReBAC patterns
- Policy Composition: Combine policies with logical operators (
AND,OR,NOT) - Detailed Evaluation Tracing: Decision trace for the policies and branches that were actually evaluated
- Fluent Builder API: Construct custom policies with a PolicyBuilder.
- Type Safety: Strongly typed resources/actions/contexts
- Async Ready: Built with async/await support
use gatehouse::*;
#[derive(Debug, Clone)]
struct User {
id: u64,
roles: Vec<String>,
}
#[derive(Debug, Clone)]
struct Document {
owner_id: u64,
}
#[derive(Debug, Clone)]
struct Action;
#[derive(Debug, Clone)]
struct Context;
let admin_policy = PolicyBuilder::<User, Document, Action, Context>::new("AdminOnly")
.subjects(|user| user.roles.iter().any(|role| role == "admin"))
.build();
let owner_policy = PolicyBuilder::<User, Document, Action, Context>::new("OwnerOnly")
.when(|user, _action, resource, _ctx| resource.owner_id == user.id)
.build();
let mut checker = PermissionChecker::new();
checker.add_policy(admin_policy);
checker.add_policy(owner_policy);
# tokio_test::block_on(async {
let user = User {
id: 1,
roles: vec!["admin".to_string()],
};
let document = Document { owner_id: 1 };
let evaluation = checker
.evaluate_access(&user, &Action, &document, &Context)
.await;
assert!(evaluation.is_granted());
println!("{}", evaluation.display_trace());
let outcome: Result<(), String> = evaluation.to_result(|reason| reason.to_string());
assert!(outcome.is_ok());
# });PermissionCheckerevaluates policies sequentially withORsemantics and short-circuits on the first grant.- An empty
PermissionCheckeralways denies with the reason"No policies configured". AndPolicyshort-circuits on the first denial;OrPolicyshort-circuits on the first grant.NotPolicyinverts the result of its inner policy.PolicyBuildercombines all configured predicates withANDlogic.PolicyBuilder::effect(Effect::Deny)changes a matching policy result from allow to deny; a non-match is still treated as denied/non-applicable. It does not create global "deny overrides allow" behavior when used insidePermissionChecker.AccessEvaluation::Denied.reasonis a summary string such as"All policies denied access". Inspect the trace tree for individual policy reasons.- Evaluation traces only contain policies and branches that were actually evaluated before short-circuiting.
The foundation of the authorization system:
use async_trait::async_trait;
use gatehouse::{PolicyEvalResult, SecurityRuleMetadata};
#[async_trait]
trait Policy<Subject, Resource, Action, Context>: Send + Sync {
async fn evaluate_access(
&self,
subject: &Subject,
action: &Action,
resource: &Resource,
context: &Context,
) -> PolicyEvalResult;
fn policy_type(&self) -> String;
fn security_rule(&self) -> SecurityRuleMetadata {
SecurityRuleMetadata::default()
}
}Aggregates multiple policies (e.g. RBAC, ABAC) with OR logic by default: if any policy grants access, permission is granted. The returned AccessEvaluation contains both the final decision and a trace tree of the evaluated policies.
let mut checker = PermissionChecker::new();
checker.add_policy(rbac_policy);
checker.add_policy(owner_policy);
// Check if access is granted
let evaluation = checker.evaluate_access(&user, &action, &resource, &context).await;
if evaluation.is_granted() {
// Access allowed
} else {
// Access denied
}
println!("{}", evaluation.display_trace());The PolicyBuilder provides a fluent API to construct custom policies by chaining predicate functions for
subjects, actions, resources, and context. Every configured predicate must pass for the built policy to grant access. Once built, the policy can be added to a PermissionChecker.
Use PolicyBuilder for synchronous predicate logic. If your policy needs async I/O or external lookups, implement Policy directly.
let custom_policy = PolicyBuilder::<MySubject, MyResource, MyAction, MyContext>::new("CustomPolicy")
.subjects(|s| /* ... */)
.actions(|a| /* ... */)
.resources(|r| /* ... */)
.context(|c| /* ... */)
.when(|s, a, r, c| /* ... */)
.build();RbacPolicy: Role-based access control. Grants when at least one required role for(resource, action)is present in the subject's roles.AbacPolicy: Attribute-based access control. Grants when its boolean condition closure returnstrue.RebacPolicy: Relationship-based access control. Grants when itsRelationshipResolverreturnstruefor the configured relationship.
RelationshipResolver returns bool, so resolver errors and timeouts need to be handled by the resolver implementation and mapped to false or to your own surrounding telemetry/logging strategy.
AndPolicy: Grants access only if all inner policies allow access. Must be created with at least one policy.OrPolicy: Grants access if any inner policy allows access. Must be created with at least one policy.NotPolicy: Inverts the decision of an inner policy.
When trace-level events are enabled, PermissionChecker::evaluate_access creates an instrumented span and every evaluated policy records a trace! event on the gatehouse::security target.
Emitted fields:
security_rule.namesecurity_rule.categorysecurity_rule.descriptionsecurity_rule.referencesecurity_rule.ruleset.namesecurity_rule.uuidsecurity_rule.versionsecurity_rule.licenseevent.outcomepolicy.typepolicy.result.reason
Fallback behavior when security_rule() is not overridden:
security_rule.namefalls back topolicy_type()security_rule.categoryfalls back to"Access Control"security_rule.ruleset.namefalls back to"PermissionChecker"
See the examples directory for complete demonstrations of:
- Role-based access control (
rbac_policy) - Attribute-style custom policies with
PolicyBuilder(policy_builder) - Relationship-based access control (
rebac_policy) - Group authorization with trace output (
groups_policy) - Policy combinators (
combinator_policy) - Axum integration with shared policies (
axum) - Actix Web integration with shared policies (
actix_web)
Run with:
cargo run --example rbac_policyCriterion benchmarks in benches/permission_checker.rs exercise PermissionChecker::evaluate_access across
several policy stack sizes. Run them with cargo bench to track changes in evaluation latency as you evolve
your policy definitions.