use crate::error::{Error, Result};
use crate::http::IHttpContext;
use std::collections::HashMap;
pub trait IClaims: Send + Sync {
fn subject(&self) -> &str;
fn roles(&self) -> &[String];
fn permissions(&self) -> &[String];
fn claims(&self) -> &HashMap<String, String>;
fn clone_box(&self) -> Box<dyn IClaims>;
fn has_role(&self, role: &str) -> bool {
self.roles().iter().any(|r| r == role)
}
fn get_userid(&self) -> &str {
self.subject()
}
fn get_username(&self) -> Option<&str> {
self.claims().get("name").map(|s| s.as_str())
}
fn get_tenantid(&self) -> Option<&str> {
self.claims()
.get("tenant_id")
.or_else(|| self.claims().get("tenant"))
.map(|s| s.as_str())
}
fn get_email(&self) -> Option<&str> {
self.claims().get("email").map(|s| s.as_str())
}
}
impl Clone for Box<dyn IClaims> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[async_trait::async_trait]
pub trait IAuthenticationHandler: Send + Sync {
async fn authenticate(&self, ctx: &mut dyn IHttpContext) -> Result<Option<Box<dyn IClaims>>>;
}
#[async_trait::async_trait]
pub trait IAuthorizationPolicy: Send + Sync {
async fn authorize(&self, claims: &dyn IClaims, resource_key: &str, method: &str)
-> Result<()>;
}
#[async_trait::async_trait]
pub trait IDynamicAuthorizer: Send + Sync {
async fn authorize(
&self,
claims: &dyn IClaims,
route_pattern: &str,
method: &str,
) -> Result<()>;
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct IdentityUser {
pub id: String,
pub name: String,
pub email: String,
pub tenant_id: String,
pub roles: Vec<String>,
pub permissions: Vec<String>,
}
impl IdentityUser {
pub fn from_claims(claims: &dyn IClaims) -> Self {
Self {
id: claims.subject().to_string(),
name: claims.get_username().unwrap_or_default().to_string(),
email: claims.get_email().unwrap_or_default().to_string(),
tenant_id: claims.get_tenantid().unwrap_or_default().to_string(),
roles: claims.roles().to_vec(),
permissions: claims.permissions().to_vec(),
}
}
pub fn tenant(&self) -> Option<&str> {
if self.tenant_id.is_empty() {
None
} else {
Some(self.tenant_id.as_str())
}
}
pub fn require_tenant(&self) -> Result<&str> {
self.tenant().ok_or_else(|| {
Error::Forbidden("the authenticated principal has no tenant claim".to_string())
})
}
pub fn is_in_role(&self, role: &str) -> bool {
self.roles.iter().any(|assigned| assigned == role)
}
pub fn has_permission(&self, permission: &str) -> bool {
self.permissions
.iter()
.any(|assigned| assigned == permission)
}
pub fn display_name(&self) -> &str {
if self.name.is_empty() {
&self.id
} else {
&self.name
}
}
}
pub trait RequestUser {
fn current_user(&self) -> Option<&IdentityUser>;
fn user_id(&self) -> Option<&str> {
self.current_user().map(|user| user.id.as_str())
}
fn user_name(&self) -> Option<&str> {
self.current_user().map(|user| user.name.as_str())
}
fn user_email(&self) -> Option<&str> {
self.current_user().map(|user| user.email.as_str())
}
fn tenant_id(&self) -> Option<&str> {
self.current_user().map(|user| user.tenant_id.as_str())
}
fn roles(&self) -> &[String] {
match self.current_user() {
Some(user) => &user.roles,
None => &[],
}
}
fn permissions(&self) -> &[String] {
match self.current_user() {
Some(user) => &user.permissions,
None => &[],
}
}
fn is_in_role(&self, role: &str) -> bool {
self.current_user()
.map(|user| user.is_in_role(role))
.unwrap_or(false)
}
fn has_permission(&self, permission: &str) -> bool {
self.current_user()
.map(|user| user.has_permission(permission))
.unwrap_or(false)
}
fn require_user(&self) -> Result<&IdentityUser> {
self.current_user()
.ok_or_else(|| Error::Unauthorized("authentication required".to_string()))
}
fn require_role(&self, role: &str) -> Result<()> {
let user = self.require_user()?;
if user.is_in_role(role) {
Ok(())
} else {
Err(Error::Forbidden(format!("{role} role required")))
}
}
fn require_tenant(&self) -> Result<&str> {
self.require_user()?.require_tenant()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestRequest<'a>(Option<&'a IdentityUser>);
impl RequestUser for TestRequest<'_> {
fn current_user(&self) -> Option<&IdentityUser> {
self.0
}
}
fn multi_tenant_admin() -> IdentityUser {
IdentityUser {
id: "user-1".into(),
name: "Ada".into(),
roles: vec!["admin".into(), "user".into()],
permissions: vec!["orders:read".into()],
tenant_id: "acme".into(),
..Default::default()
}
}
fn single_tenant_user() -> IdentityUser {
IdentityUser {
id: "user-2".into(),
roles: Vec::new(),
permissions: Vec::new(),
tenant_id: String::new(),
..Default::default()
}
}
#[test]
fn identity_tenant_is_none_when_claim_is_empty() {
assert_eq!(multi_tenant_admin().tenant(), Some("acme"));
assert_eq!(
single_tenant_user().tenant(),
None,
"empty tenant_id means 'no tenant claim', not a tenant named \"\""
);
}
#[test]
fn identity_require_tenant_is_forbidden_without_claim() {
let user = single_tenant_user();
let err = user.require_tenant().unwrap_err();
assert!(
matches!(&err, Error::Forbidden(detail) if detail.contains("no tenant claim")),
"expected Forbidden(no tenant claim), got {err:?}"
);
assert_eq!(err.status_code(), 403, "missing tenant scope is not 401");
assert_eq!(multi_tenant_admin().require_tenant().unwrap(), "acme");
}
#[test]
fn anonymous_request_user_is_empty_and_unauthorized() {
let anonymous = TestRequest(None);
assert!(anonymous.permissions().is_empty());
assert!(!anonymous.has_permission("orders:read"));
assert!(!anonymous.is_in_role("admin"));
assert!(anonymous.current_user().is_none());
let role_err = anonymous.require_role("admin").unwrap_err();
assert!(
matches!(&role_err, Error::Unauthorized(detail) if detail == "authentication required"),
"anonymous must be 401, got {role_err:?}"
);
assert_eq!(role_err.status_code(), 401);
let tenant_err = anonymous.require_tenant().unwrap_err();
assert_eq!(tenant_err.status_code(), 401);
}
#[test]
fn authenticated_request_user_exposes_roles_permissions_and_tenant() {
let user = multi_tenant_admin();
let request = TestRequest(Some(&user));
assert_eq!(request.permissions().len(), 1);
assert_eq!(request.permissions()[0], "orders:read");
assert!(request.has_permission("orders:read"));
assert!(!request.has_permission("orders:write"));
assert!(request.is_in_role("admin"));
assert!(request.require_role("admin").is_ok());
assert_eq!(request.require_tenant().unwrap(), "acme");
}
#[test]
fn require_role_is_forbidden_when_role_is_missing() {
let user = multi_tenant_admin();
let request = TestRequest(Some(&user));
let err = request.require_role("auditor").unwrap_err();
assert!(
matches!(&err, Error::Forbidden(detail) if detail == "auditor role required"),
"expected Forbidden(auditor role required), got {err:?}"
);
assert_eq!(err.status_code(), 403);
}
#[test]
fn require_tenant_is_forbidden_for_authenticated_principal_without_claim() {
let user = single_tenant_user();
let request = TestRequest(Some(&user));
assert!(request.current_user().is_some());
let err = request.require_tenant().unwrap_err();
assert!(matches!(&err, Error::Forbidden(_)), "got {err:?}");
assert_eq!(err.status_code(), 403);
}
}