use crate::Authenticatable;
use larust_core::AppError;
pub trait Policy<U: Authenticatable> {
fn view_any(user: &U) -> bool;
fn view(&self, user: &U) -> bool;
fn create(user: &U) -> bool;
fn update(&self, user: &U) -> bool;
fn delete(&self, user: &U) -> bool;
fn authorize_view_any(user: &U) -> Result<(), AppError> {
crate::authorize(Self::view_any(user))
}
fn authorize_view(&self, user: &U) -> Result<(), AppError> {
crate::authorize(self.view(user))
}
fn authorize_create(user: &U) -> Result<(), AppError> {
crate::authorize(Self::create(user))
}
fn authorize_update(&self, user: &U) -> Result<(), AppError> {
crate::authorize(self.update(user))
}
fn authorize_delete(&self, user: &U) -> Result<(), AppError> {
crate::authorize(self.delete(user))
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
struct TestUser {
id: i64,
}
impl Authenticatable for TestUser {
fn auth_id(&self) -> i64 {
self.id
}
async fn find_for_auth(_id: i64) -> Result<Option<Self>, AppError> {
unimplemented!("not exercised by these pure-logic tests")
}
}
struct TestResource {
owner_id: i64,
}
impl Policy<TestUser> for TestResource {
fn view_any(_user: &TestUser) -> bool {
true
}
fn view(&self, _user: &TestUser) -> bool {
true
}
fn create(_user: &TestUser) -> bool {
true
}
fn update(&self, user: &TestUser) -> bool {
self.owner_id == user.id
}
fn delete(&self, user: &TestUser) -> bool {
self.owner_id == user.id
}
}
fn assert_forbidden(result: Result<(), AppError>) {
let err = result.unwrap_err();
assert!(matches!(
err,
AppError::Http {
status: StatusCode::FORBIDDEN,
..
}
));
}
#[test]
fn class_level_abilities_are_callable_as_associated_functions() {
let user = TestUser { id: 1 };
assert!(TestResource::view_any(&user));
assert!(TestResource::create(&user));
assert!(TestResource::authorize_view_any(&user).is_ok());
assert!(TestResource::authorize_create(&user).is_ok());
}
#[test]
fn instance_level_abilities_are_callable_as_methods() {
let owner = TestUser { id: 1 };
let stranger = TestUser { id: 2 };
let resource = TestResource { owner_id: 1 };
assert!(resource.view(&owner));
assert!(resource.update(&owner));
assert!(!resource.update(&stranger));
assert!(resource.delete(&owner));
assert!(!resource.delete(&stranger));
}
#[test]
fn authorize_update_allows_the_owner() {
let owner = TestUser { id: 1 };
let resource = TestResource { owner_id: 1 };
assert!(resource.authorize_update(&owner).is_ok());
}
#[test]
fn authorize_update_forbids_a_non_owner() {
let stranger = TestUser { id: 2 };
let resource = TestResource { owner_id: 1 };
assert_forbidden(resource.authorize_update(&stranger));
}
#[test]
fn authorize_delete_forbids_a_non_owner() {
let stranger = TestUser { id: 2 };
let resource = TestResource { owner_id: 1 };
assert_forbidden(resource.authorize_delete(&stranger));
}
}