use axum::extract::FromRequestParts;
use axum::http::request::Parts;
use serde::{Deserialize, Serialize};
use super::{AuthError, AuthenticatedCaller};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum Scope {
Read,
Write,
Admin,
}
impl Scope {
pub fn is_satisfied_by(self, granted: &[Scope]) -> bool {
granted.iter().any(|s| *s >= self)
}
}
impl std::fmt::Display for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Scope::Read => write!(f, "read"),
Scope::Write => write!(f, "write"),
Scope::Admin => write!(f, "admin"),
}
}
}
pub struct RequireScope(pub AuthenticatedCaller);
impl RequireScope {
fn check(caller: &AuthenticatedCaller, required: Scope) -> Result<(), AuthError> {
if required.is_satisfied_by(&caller.scopes) {
Ok(())
} else {
Err(AuthError::InsufficientScope { required })
}
}
}
pub struct RequireRead(pub AuthenticatedCaller);
impl<S> FromRequestParts<S> for RequireRead
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let caller = AuthenticatedCaller::from_request_parts(parts, state).await?;
RequireScope::check(&caller, Scope::Read)?;
Ok(Self(caller))
}
}
pub struct RequireWrite(pub AuthenticatedCaller);
impl<S> FromRequestParts<S> for RequireWrite
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let caller = AuthenticatedCaller::from_request_parts(parts, state).await?;
RequireScope::check(&caller, Scope::Write)?;
Ok(Self(caller))
}
}
pub struct RequireAdmin(pub AuthenticatedCaller);
impl<S> FromRequestParts<S> for RequireAdmin
where
S: Send + Sync,
{
type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let caller = AuthenticatedCaller::from_request_parts(parts, state).await?;
RequireScope::check(&caller, Scope::Admin)?;
Ok(Self(caller))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scope_ordering() {
assert!(Scope::Admin > Scope::Write);
assert!(Scope::Write > Scope::Read);
assert!(Scope::Admin > Scope::Read);
}
#[test]
fn test_scope_contains_same_level() {
assert!(Scope::Write.is_satisfied_by(&[Scope::Write]));
assert!(Scope::Read.is_satisfied_by(&[Scope::Read]));
assert!(Scope::Admin.is_satisfied_by(&[Scope::Admin]));
}
#[test]
fn test_scope_contains_higher_level() {
assert!(Scope::Write.is_satisfied_by(&[Scope::Admin]));
assert!(Scope::Read.is_satisfied_by(&[Scope::Admin]));
assert!(Scope::Read.is_satisfied_by(&[Scope::Write]));
}
#[test]
fn test_scope_rejects_lower_level() {
assert!(!Scope::Write.is_satisfied_by(&[Scope::Read]));
assert!(!Scope::Admin.is_satisfied_by(&[Scope::Write]));
assert!(!Scope::Admin.is_satisfied_by(&[Scope::Read]));
}
#[test]
fn test_scope_empty_grants_rejects_all() {
assert!(!Scope::Read.is_satisfied_by(&[]));
assert!(!Scope::Write.is_satisfied_by(&[]));
assert!(!Scope::Admin.is_satisfied_by(&[]));
}
#[test]
fn test_scope_check_with_caller() {
let caller = AuthenticatedCaller {
key_id: "test".to_string(),
scopes: vec![Scope::Read, Scope::Write],
tenant: crate::Tenant::default(),
};
assert!(RequireScope::check(&caller, Scope::Read).is_ok());
assert!(RequireScope::check(&caller, Scope::Write).is_ok());
assert!(RequireScope::check(&caller, Scope::Admin).is_err());
}
#[test]
fn scope_display_renders_each_level() {
assert_eq!(Scope::Read.to_string(), "read");
assert_eq!(Scope::Write.to_string(), "write");
assert_eq!(Scope::Admin.to_string(), "admin");
}
}