use async_trait::async_trait;
use gatehouse::{
DelegatingPolicy, EvalCtx, EvaluationSession, PermissionChecker, Policy, PolicyBuilder,
PolicyDomain, PolicyEvalResult,
};
use std::borrow::Cow;
use uuid::Uuid;
#[derive(Debug, Clone)]
struct DocUser {
id: Uuid,
is_admin: bool,
}
#[derive(Debug, Clone)]
struct Document {
owner_id: Uuid,
}
#[derive(Debug, Clone)]
struct EditDoc;
struct DocumentDomain;
impl PolicyDomain for DocumentDomain {
type Subject = DocUser;
type Action = EditDoc;
type Resource = Document;
type Context = ();
}
fn document_checker() -> PermissionChecker<DocumentDomain> {
let owner = PolicyBuilder::<DocumentDomain>::new("DocumentOwner")
.when(|user, _action, document, _ctx| user.id == document.owner_id)
.build();
let admin = PolicyBuilder::<DocumentDomain>::new("DocumentAdmin")
.subjects(|user| user.is_admin)
.build();
let mut checker = PermissionChecker::named("DocumentChecker");
checker.add_policy(owner);
checker.add_policy(admin);
checker
}
#[derive(Debug, Clone)]
struct Principal {
user_id: Uuid,
is_admin: bool,
}
#[derive(Debug, Clone)]
struct Comment {
author_id: Uuid,
document: Document,
}
#[derive(Debug, Clone)]
struct EditComment;
struct CommentDomain;
impl PolicyDomain for CommentDomain {
type Subject = Principal;
type Action = EditComment;
type Resource = Comment;
type Context = ();
}
struct AuthorCanEditComment;
#[async_trait]
impl Policy<CommentDomain> for AuthorCanEditComment {
async fn evaluate(&self, ctx: &EvalCtx<'_, CommentDomain>) -> PolicyEvalResult {
if ctx.subject.user_id == ctx.resource.author_id {
ctx.grant("subject is the comment author")
} else {
ctx.not_applicable("subject is not the comment author")
}
}
fn policy_type(&self) -> Cow<'static, str> {
Cow::Borrowed("AuthorCanEditComment")
}
}
fn comment_checker() -> PermissionChecker<CommentDomain> {
let inherit_from_document = DelegatingPolicy::new(
"InheritDocumentEdit",
document_checker(),
|principal: &Principal| DocUser {
id: principal.user_id,
is_admin: principal.is_admin,
},
|_action: &EditComment| EditDoc,
|_subject: &Principal, _action: &EditComment, comment: &Comment, _ctx: &()| {
comment.document.clone()
},
|_subject, _action, _ctx| (),
);
let mut checker = PermissionChecker::named("CommentChecker");
checker.add_policy(AuthorCanEditComment);
checker.add_policy(inherit_from_document);
checker
}
#[tokio::main]
async fn main() {
let owner_id = Uuid::new_v4();
let author_id = Uuid::new_v4();
let comment = Comment {
author_id,
document: Document { owner_id },
};
let author = Principal {
user_id: author_id,
is_admin: false,
};
let document_owner = Principal {
user_id: owner_id,
is_admin: false,
};
let admin = Principal {
user_id: Uuid::new_v4(),
is_admin: true,
};
let stranger = Principal {
user_id: Uuid::new_v4(),
is_admin: false,
};
let checker = comment_checker();
let session = EvaluationSession::empty();
let action = EditComment;
let context = ();
let cases = [
("author", &author),
("document owner (not author)", &document_owner),
("admin (not author/owner)", &admin),
("unrelated user", &stranger),
];
for (who, principal) in cases {
let granted = checker
.bind(&session, principal, &action, &context)
.check(&comment)
.await
.is_granted();
println!(
"{who:<28} can edit the comment? {}",
if granted { "yes" } else { "no" }
);
}
println!("\nTrace — document owner (not the author) editing the comment:");
let decision = checker
.bind(&session, &document_owner, &action, &context)
.check(&comment)
.await;
println!("{}", decision.display_trace());
assert!(checker
.bind(&session, &author, &action, &context)
.check(&comment)
.await
.is_granted());
assert!(checker
.bind(&session, &document_owner, &action, &context)
.check(&comment)
.await
.is_granted());
assert!(checker
.bind(&session, &admin, &action, &context)
.check(&comment)
.await
.is_granted());
assert!(!checker
.bind(&session, &stranger, &action, &context)
.check(&comment)
.await
.is_granted());
}