use axum::http::StatusCode;
use crate::api::errors::{api_error, ApiErrorCode};
use crate::auth::principal::Principal;
use crate::auth::scopes::{Scope, ScopeSet};
type GuardError = (StatusCode, axum::Json<serde_json::Value>);
pub fn require_scope(principal: &Principal, scope: Scope) -> Result<(), GuardError> {
if principal.has_scopes(ScopeSet::from_iter([scope])) {
Ok(())
} else {
Err(api_error(
StatusCode::FORBIDDEN,
ApiErrorCode::InsufficientScope,
format!("token does not hold required scope: {}", scope.as_str()),
))
}
}
pub fn resolve_namespace(
principal: &Principal,
query_namespace: Option<&str>,
) -> Result<String, GuardError> {
match (&principal.tenant_id, query_namespace) {
(None, Some(q)) => Ok(q.to_string()),
(None, None) => Err(api_error(
StatusCode::BAD_REQUEST,
ApiErrorCode::InvalidQueryParameter,
"namespace is required for cluster-wide tokens",
)),
(Some(ns), None) => Ok(ns.clone()),
(Some(ns), Some(q)) if q == ns => Ok(ns.clone()),
(Some(_ns), Some(q)) => Err(api_error(
StatusCode::FORBIDDEN,
ApiErrorCode::NamespaceNotFound,
format!("namespace not found: {q}"),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::principal::Principal;
use crate::auth::scopes::{Scope, ScopeSet};
fn principal_with(scopes: impl IntoIterator<Item = Scope>, tenant: Option<&str>) -> Principal {
let mut p = Principal::new("test-token").with_scopes(ScopeSet::from_iter(scopes));
if let Some(t) = tenant {
p = p.with_tenant(t);
}
p
}
fn body_field<'a>(err: &'a GuardError, field: &str) -> &'a serde_json::Value {
&err.1 .0["error"][field]
}
#[test]
fn require_scope_passes_when_principal_has_scope() {
let p = principal_with([Scope::Read], None);
assert!(require_scope(&p, Scope::Read).is_ok());
}
#[test]
fn require_scope_fails_403_when_missing() {
let p = principal_with([Scope::Read], None);
let err = require_scope(&p, Scope::Admin).expect_err("must reject");
assert_eq!(err.0, StatusCode::FORBIDDEN);
assert_eq!(body_field(&err, "code"), "insufficient_scope");
}
#[test]
fn require_scope_fails_for_empty_principal() {
let p = principal_with([], None);
let err = require_scope(&p, Scope::Read).expect_err("must reject");
assert_eq!(err.0, StatusCode::FORBIDDEN);
}
#[test]
fn resolve_namespace_returns_tenant_when_no_query_param() {
let p = principal_with([Scope::Read], Some("default"));
assert_eq!(resolve_namespace(&p, None).unwrap(), "default");
}
#[test]
fn resolve_namespace_accepts_matching_query_param() {
let p = principal_with([Scope::Read], Some("default"));
assert_eq!(resolve_namespace(&p, Some("default")).unwrap(), "default");
}
#[test]
fn resolve_namespace_rejects_403_when_query_differs() {
let p = principal_with([Scope::Read], Some("default"));
let err = resolve_namespace(&p, Some("private")).expect_err("must reject");
assert_eq!(err.0, StatusCode::FORBIDDEN);
assert_eq!(body_field(&err, "code"), "namespace_not_found");
}
#[test]
fn resolve_namespace_cluster_wide_with_query_returns_query() {
let p = principal_with([Scope::Admin], None);
assert_eq!(resolve_namespace(&p, Some("anything")).unwrap(), "anything");
assert_eq!(resolve_namespace(&p, Some("private")).unwrap(), "private");
}
#[test]
fn resolve_namespace_cluster_wide_without_query_requires_explicit() {
let p = principal_with([Scope::Admin], None);
let err = resolve_namespace(&p, None).expect_err("must reject");
assert_eq!(err.0, StatusCode::BAD_REQUEST);
assert_eq!(body_field(&err, "code"), "invalid_query_parameter");
}
#[test]
fn resolve_namespace_pinned_cannot_broaden_via_admin_query() {
let p = principal_with([Scope::Read], Some("user_abc"));
let err = resolve_namespace(&p, Some("admin")).expect_err("must reject");
assert_eq!(err.0, StatusCode::FORBIDDEN);
assert_eq!(body_field(&err, "code"), "namespace_not_found");
assert_eq!(resolve_namespace(&p, Some("user_abc")).unwrap(), "user_abc");
}
}