use super::response::AuthResponse;
use crate::auth::Authenticatable;
pub trait Policy<M>: Send + Sync {
type User: Authenticatable;
fn before(&self, _user: &Self::User, _ability: &str) -> Option<bool> {
None
}
fn view_any(&self, _user: &Self::User) -> AuthResponse {
AuthResponse::deny_silent()
}
fn view(&self, _user: &Self::User, _model: &M) -> AuthResponse {
AuthResponse::deny_silent()
}
fn create(&self, _user: &Self::User) -> AuthResponse {
AuthResponse::deny_silent()
}
fn update(&self, _user: &Self::User, _model: &M) -> AuthResponse {
AuthResponse::deny_silent()
}
fn delete(&self, _user: &Self::User, _model: &M) -> AuthResponse {
AuthResponse::deny_silent()
}
fn restore(&self, _user: &Self::User, _model: &M) -> AuthResponse {
AuthResponse::deny_silent()
}
fn force_delete(&self, _user: &Self::User, _model: &M) -> AuthResponse {
AuthResponse::deny_silent()
}
fn check(&self, user: &Self::User, ability: &str, model: Option<&M>) -> AuthResponse {
if let Some(result) = self.before(user, ability) {
return result.into();
}
match ability {
"viewAny" | "view_any" => self.view_any(user),
"view" => model
.map(|m| self.view(user, m))
.unwrap_or_else(AuthResponse::deny_silent),
"create" => self.create(user),
"update" => model
.map(|m| self.update(user, m))
.unwrap_or_else(AuthResponse::deny_silent),
"delete" => model
.map(|m| self.delete(user, m))
.unwrap_or_else(AuthResponse::deny_silent),
"restore" => model
.map(|m| self.restore(user, m))
.unwrap_or_else(AuthResponse::deny_silent),
"forceDelete" | "force_delete" => model
.map(|m| self.force_delete(user, m))
.unwrap_or_else(AuthResponse::deny_silent),
_ => AuthResponse::deny_silent(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::any::Any;
#[derive(Debug, Clone)]
struct TestUser {
id: i64,
is_admin: bool,
}
impl Authenticatable for TestUser {
fn auth_identifier(&self) -> i64 {
self.id
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[derive(Debug)]
struct TestPost {
#[allow(dead_code)]
id: i64,
user_id: i64,
}
struct TestPostPolicy;
impl Policy<TestPost> for TestPostPolicy {
type User = TestUser;
fn before(&self, user: &Self::User, _ability: &str) -> Option<bool> {
if user.is_admin {
return Some(true);
}
None
}
fn view(&self, _user: &Self::User, _model: &TestPost) -> AuthResponse {
AuthResponse::allow()
}
fn update(&self, user: &Self::User, post: &TestPost) -> AuthResponse {
(user.id == post.user_id).into()
}
fn delete(&self, user: &Self::User, post: &TestPost) -> AuthResponse {
self.update(user, post)
}
}
#[test]
fn test_policy_allows_view() {
let policy = TestPostPolicy;
let user = TestUser {
id: 1,
is_admin: false,
};
let post = TestPost { id: 1, user_id: 2 };
let response = policy.view(&user, &post);
assert!(response.allowed());
}
#[test]
fn test_policy_owner_can_update() {
let policy = TestPostPolicy;
let user = TestUser {
id: 1,
is_admin: false,
};
let post = TestPost { id: 1, user_id: 1 };
let response = policy.update(&user, &post);
assert!(response.allowed());
}
#[test]
fn test_policy_non_owner_cannot_update() {
let policy = TestPostPolicy;
let user = TestUser {
id: 1,
is_admin: false,
};
let post = TestPost { id: 1, user_id: 2 };
let response = policy.update(&user, &post);
assert!(response.denied());
}
#[test]
fn test_policy_admin_bypass() {
let policy = TestPostPolicy;
let admin = TestUser {
id: 1,
is_admin: true,
};
let post = TestPost {
id: 1,
user_id: 999,
};
let response = policy.check(&admin, "update", Some(&post));
assert!(response.allowed());
}
#[test]
fn test_policy_check_method() {
let policy = TestPostPolicy;
let user = TestUser {
id: 1,
is_admin: false,
};
let post = TestPost { id: 1, user_id: 1 };
let response = policy.check(&user, "update", Some(&post));
assert!(response.allowed());
let other_post = TestPost { id: 2, user_id: 2 };
let response = policy.check(&user, "update", Some(&other_post));
assert!(response.denied());
}
}