Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 138 additions & 12 deletions examples/rebac_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use async_trait::async_trait;
use gatehouse::*;
use std::fmt;
use std::time::Duration;
use uuid::Uuid;

Expand Down Expand Up @@ -66,8 +67,13 @@ impl ProjectRelationshipResolver {
}

#[async_trait]
impl RelationshipResolver<User, Project> for ProjectRelationshipResolver {
async fn has_relationship(&self, user: &User, project: &Project, relationship: &str) -> bool {
impl RelationshipResolver<User, Project, String> for ProjectRelationshipResolver {
async fn has_relationship(
&self,
user: &User,
project: &Project,
relationship: &String,
) -> bool {
println!(
"Checking if user {} has '{}' relationship with project {}",
user.name, relationship, project.name
Expand Down Expand Up @@ -156,18 +162,20 @@ async fn main() {
let normal_resolver = ProjectRelationshipResolver::new(relationships.clone());

// Create ReBAC policies for different relationships
let owner_policy = RebacPolicy::<User, Project, EditAction, EmptyContext, _>::new(
"owner",
let owner_policy = RebacPolicy::<User, Project, EditAction, EmptyContext, _, _>::new(
"owner".to_string(),
normal_resolver.clone(),
);

let contributor_policy = RebacPolicy::<User, Project, EditAction, EmptyContext, _>::new(
"contributor",
let contributor_policy = RebacPolicy::<User, Project, EditAction, EmptyContext, _, _>::new(
"contributor".to_string(),
normal_resolver.clone(),
);

let _viewer_policy =
RebacPolicy::<User, Project, EditAction, EmptyContext, _>::new("viewer", normal_resolver);
let _viewer_policy = RebacPolicy::<User, Project, EditAction, EmptyContext, String, _>::new(
"viewer".to_string(),
normal_resolver,
);

// Create a permission checker with multiple policies
// Only owners and contributors can edit, not viewers
Expand All @@ -186,8 +194,10 @@ async fn main() {

// Create a resolver that simulates a database error
let error_resolver = ProjectRelationshipResolver::new(relationships.clone()).with_error();
let error_policy =
RebacPolicy::<User, Project, EditAction, EmptyContext, _>::new("owner", error_resolver);
let error_policy = RebacPolicy::<User, Project, EditAction, EmptyContext, _, _>::new(
"owner".to_string(),
error_resolver,
);

let mut error_checker = PermissionChecker::<User, Project, EditAction, EmptyContext>::new();
error_checker.add_policy(error_policy);
Expand All @@ -199,14 +209,19 @@ async fn main() {

// Create a resolver that simulates a timeout
let timeout_resolver = ProjectRelationshipResolver::new(relationships).with_timeout();
let timeout_policy =
RebacPolicy::<User, Project, EditAction, EmptyContext, _>::new("owner", timeout_resolver);
let timeout_policy = RebacPolicy::<User, Project, EditAction, EmptyContext, _, _>::new(
"owner".to_string(),
timeout_resolver,
);

let mut timeout_checker = PermissionChecker::<User, Project, EditAction, EmptyContext>::new();
timeout_checker.add_policy(timeout_policy);

println!("Testing with database timeout:");
test_access(&timeout_checker, &owner, &project).await;

// Demonstrate enum-based relationships (type-safe alternative to strings)
enum_relationship_example().await;
}

async fn test_access(
Expand Down Expand Up @@ -240,3 +255,114 @@ async fn test_access(
}
);
}

// --- Enum-based relationship example ---
//
// The relationship type parameter is generic, so you can use enums instead
// of strings for compile-time safety. A typo like "contibutor" becomes a
Comment thread
hardbyte marked this conversation as resolved.
// compile error rather than a silent permission failure.

#[derive(Debug, Clone, PartialEq)]
enum Relation {
Owner,
Contributor,
Viewer,
}

impl fmt::Display for Relation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Relation::Owner => write!(f, "owner"),
Relation::Contributor => write!(f, "contributor"),
Relation::Viewer => write!(f, "viewer"),
}
}
}

struct EnumRelationshipResolver {
relationships: Vec<(Uuid, Uuid, Relation)>,
}

#[async_trait]
impl RelationshipResolver<User, Project, Relation> for EnumRelationshipResolver {
async fn has_relationship(
&self,
user: &User,
project: &Project,
relationship: &Relation,
) -> bool {
self.relationships
.iter()
.any(|(uid, pid, rel)| *uid == user.id && *pid == project.id && rel == relationship)
}
}

async fn enum_relationship_example() {
println!("\n=== Enum-Based Relationship Types ===\n");

let alice = User {
id: Uuid::new_v4(),
name: "Alice".to_string(),
};
let bob = User {
id: Uuid::new_v4(),
name: "Bob".to_string(),
};

let project = Project {
id: Uuid::new_v4(),
name: "Typed Project".to_string(),
};

let charlie = User {
id: Uuid::new_v4(),
name: "Charlie".to_string(),
};

let resolver = EnumRelationshipResolver {
relationships: vec![
(alice.id, project.id, Relation::Owner),
(bob.id, project.id, Relation::Contributor),
(charlie.id, project.id, Relation::Viewer),
],
};

// Only owners can edit — using an enum variant instead of a string.
let owner_policy = RebacPolicy::<User, Project, EditAction, EmptyContext, _, _>::new(
Relation::Owner,
resolver,
);

let mut checker = PermissionChecker::new();
checker.add_policy(owner_policy);
Comment thread
hardbyte marked this conversation as resolved.

let context = EmptyContext;
let action = EditAction;

for (user, expected_granted, role) in [
(&alice, true, "owner"),
(&bob, false, "contributor"),
(&charlie, false, "viewer"),
] {
let result = checker
.evaluate_access(user, &action, &project, &context)
.await;
let status = if result.is_granted() {
"GRANTED ✓"
} else {
"DENIED ✗"
};
println!(
"{} ({}) edit access: {} (expected: {})",
user.name,
role,
status,
if expected_granted {
"granted"
} else {
"denied"
}
);
assert_eq!(result.is_granted(), expected_granted);
}
}
Loading