use ironflow_auth::extractor::{AuthMethod, Authenticated};
use ironflow_store::entities::ApiKeyScope;
use crate::error::ApiError;
pub trait AuthorizeExt {
fn require(&self, scope: ApiKeyScope, admin_only: bool) -> Result<(), ApiError>;
}
impl AuthorizeExt for Authenticated {
fn require(&self, scope: ApiKeyScope, admin_only: bool) -> Result<(), ApiError> {
match &self.method {
AuthMethod::Jwt { is_admin, .. } => {
if admin_only && !is_admin {
return Err(ApiError::Forbidden);
}
}
AuthMethod::ApiKey {
scopes,
owner_is_admin,
..
} => {
if !ApiKeyScope::has_permission(scopes, &scope) {
return Err(ApiError::InsufficientScope);
}
if admin_only && !owner_is_admin {
return Err(ApiError::Forbidden);
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use uuid::Uuid;
use super::*;
fn jwt(is_admin: bool) -> Authenticated {
Authenticated {
user_id: Uuid::now_v7(),
method: AuthMethod::Jwt {
username: "user".to_string(),
is_admin,
},
}
}
fn key(scopes: Vec<ApiKeyScope>, owner_is_admin: bool) -> Authenticated {
Authenticated {
user_id: Uuid::now_v7(),
method: AuthMethod::ApiKey {
key_id: Uuid::now_v7(),
key_name: "key".to_string(),
scopes,
owner_is_admin,
},
}
}
#[test]
fn scope_jwt_member_on_admin_only_route_is_forbidden() {
let result = jwt(false).require(ApiKeyScope::RunsManage, true);
assert!(matches!(result, Err(ApiError::Forbidden)));
}
#[test]
fn scope_jwt_member_on_regular_route_passes() {
assert!(jwt(false).require(ApiKeyScope::RunsRead, false).is_ok());
}
#[test]
fn scope_jwt_admin_passes_admin_only_route() {
assert!(jwt(true).require(ApiKeyScope::Admin, true).is_ok());
}
#[test]
fn scope_key_with_another_scope_is_insufficient() {
let auth = key(vec![ApiKeyScope::StatsRead], true);
let result = auth.require(ApiKeyScope::RunsRead, false);
assert!(matches!(result, Err(ApiError::InsufficientScope)));
}
#[test]
fn scope_key_with_the_exact_scope_passes() {
let auth = key(vec![ApiKeyScope::RunsRead], false);
assert!(auth.require(ApiKeyScope::RunsRead, false).is_ok());
}
#[test]
fn scope_admin_key_satisfies_any_scope() {
let auth = key(vec![ApiKeyScope::Admin], true);
for scope in ApiKeyScope::all_non_admin() {
assert!(auth.require(scope, false).is_ok());
}
assert!(auth.require(ApiKeyScope::Admin, true).is_ok());
}
#[test]
fn scope_key_of_member_on_admin_only_route_is_forbidden() {
let auth = key(vec![ApiKeyScope::RunsManage], false);
let result = auth.require(ApiKeyScope::RunsManage, true);
assert!(matches!(result, Err(ApiError::Forbidden)));
}
#[test]
fn scope_is_checked_before_the_owner_admin_flag() {
let auth = key(vec![ApiKeyScope::StatsRead], false);
let result = auth.require(ApiKeyScope::RunsManage, true);
assert!(matches!(result, Err(ApiError::InsufficientScope)));
}
#[test]
fn scope_admin_key_of_member_is_forbidden_on_admin_only_route() {
let auth = key(vec![ApiKeyScope::Admin], false);
let result = auth.require(ApiKeyScope::Admin, true);
assert!(matches!(result, Err(ApiError::Forbidden)));
}
}