use http::StatusCode;
use crate::{extract::Extractor, utils::State, Request};
pub trait HasRoles {
fn has_role(&self, role: &str) -> bool;
}
pub trait RoleExtractor {
const ROLE: &'static str;
}
#[skyzen::error(status = StatusCode::FORBIDDEN)]
pub enum AuthorizationError {
#[error("User not authenticated", status = StatusCode::UNAUTHORIZED)]
NotAuthenticated,
#[error("Insufficient permissions")]
Forbidden,
}
#[derive(Debug, Clone)]
pub struct Admin<U>(pub U);
impl_deref!(Admin);
impl<U> RoleExtractor for Admin<U> {
const ROLE: &'static str = "admin";
}
impl<U> Extractor for Admin<U>
where
U: HasRoles + Clone + Send + Sync + 'static,
{
type Error = AuthorizationError;
async fn extract(request: &mut Request) -> Result<Self, Self::Error> {
let user = request
.extensions()
.get::<State<U>>()
.ok_or(AuthorizationError::NotAuthenticated)?
.0
.clone();
if user.has_role(Self::ROLE) {
Ok(Self(user))
} else {
Err(AuthorizationError::Forbidden)
}
}
}
#[macro_export]
macro_rules! define_role_guard {
($name:ident, $role:literal, $doc:literal) => {
#[doc = $doc]
#[derive(Debug, Clone)]
pub struct $name<U>(pub U);
impl<U: Send + Sync> std::ops::Deref for $name<U> {
type Target = U;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<U: Send + Sync> std::ops::DerefMut for $name<U> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<U> $crate::auth::guard::RoleExtractor for $name<U> {
const ROLE: &'static str = $role;
}
impl<U> $crate::extract::Extractor for $name<U>
where
U: $crate::auth::guard::HasRoles + Clone + Send + Sync + 'static,
{
type Error = $crate::auth::guard::AuthorizationError;
async fn extract(request: &mut $crate::Request) -> Result<Self, Self::Error> {
use $crate::auth::guard::RoleExtractor;
let user = request
.extensions()
.get::<$crate::utils::State<U>>()
.ok_or($crate::auth::guard::AuthorizationError::NotAuthenticated)?
.0
.clone();
if user.has_role(Self::ROLE) {
Ok(Self(user))
} else {
Err($crate::auth::guard::AuthorizationError::Forbidden)
}
}
}
};
}
#[cfg(test)]
mod tests {
use skyzen_core::Extractor;
use super::{Admin, AuthorizationError, HasRoles};
use crate::utils::State;
#[derive(Clone, Debug)]
struct TestUser {
name: String,
roles: Vec<String>,
}
impl HasRoles for TestUser {
fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
}
#[tokio::test]
async fn test_admin_guard_success() {
let user = TestUser {
name: "Alice".to_owned(),
roles: vec!["admin".to_owned(), "user".to_owned()],
};
let mut request = http::Request::builder()
.body(http_kit::Body::empty())
.unwrap();
request.extensions_mut().insert(State(user.clone()));
let result = Admin::<TestUser>::extract(&mut request).await;
assert!(result.is_ok());
assert_eq!(result.unwrap().0.name, "Alice");
}
#[tokio::test]
async fn test_admin_guard_forbidden() {
let user = TestUser {
name: "Bob".to_owned(),
roles: vec!["user".to_owned()],
};
let mut request = http::Request::builder()
.body(http_kit::Body::empty())
.unwrap();
request.extensions_mut().insert(State(user));
let result = Admin::<TestUser>::extract(&mut request).await;
assert!(matches!(result, Err(AuthorizationError::Forbidden)));
}
#[tokio::test]
async fn test_admin_guard_not_authenticated() {
let mut request = http::Request::builder()
.body(http_kit::Body::empty())
.unwrap();
let result = Admin::<TestUser>::extract(&mut request).await;
assert!(matches!(result, Err(AuthorizationError::NotAuthenticated)));
}
}