use async_trait::async_trait;
use camel_api::security_policy::{AuthContext, AuthorizationDecision, SecurityPolicy};
use camel_api::{CamelError, Exchange};
pub use camel_api::security_policy::PRINCIPAL_KEY;
pub struct RolePolicy {
required_roles: Vec<String>,
all_required: bool,
}
impl RolePolicy {
pub fn new(required_roles: Vec<String>, all_required: bool) -> Self {
Self {
required_roles,
all_required,
}
}
}
#[async_trait]
impl SecurityPolicy for RolePolicy {
async fn evaluate(
&self,
_exchange: &mut Exchange,
auth: &AuthContext<'_>,
) -> Result<AuthorizationDecision, CamelError> {
let principal = auth.principal.principal();
let missing: Vec<String> = self
.required_roles
.iter()
.filter(|r| !principal.has_role(r))
.cloned()
.collect();
let granted = if self.all_required {
missing.is_empty()
} else {
self.required_roles.is_empty() || missing.len() < self.required_roles.len()
};
if granted {
Ok(AuthorizationDecision::Granted {
principal: principal.clone(),
})
} else {
let actual = principal.roles.clone();
Ok(AuthorizationDecision::Denied {
reason: format!("missing required role(s): {}", missing.join(", ")), required: self.required_roles.clone(),
actual,
})
}
}
}
pub struct ScopePolicy {
required_scopes: Vec<String>,
all_required: bool,
}
impl ScopePolicy {
pub fn new(required_scopes: Vec<String>, all_required: bool) -> Self {
Self {
required_scopes,
all_required,
}
}
}
#[async_trait]
impl SecurityPolicy for ScopePolicy {
async fn evaluate(
&self,
_exchange: &mut Exchange,
auth: &AuthContext<'_>,
) -> Result<AuthorizationDecision, CamelError> {
let principal = auth.principal.principal();
let missing: Vec<String> = self
.required_scopes
.iter()
.filter(|s| !principal.has_scope(s))
.cloned()
.collect();
let granted = if self.all_required {
missing.is_empty()
} else {
self.required_scopes.is_empty() || missing.len() < self.required_scopes.len()
};
if granted {
Ok(AuthorizationDecision::Granted {
principal: principal.clone(),
})
} else {
let actual = principal.scopes.clone();
Ok(AuthorizationDecision::Denied {
reason: format!("missing required scope(s): {}", missing.join(", ")),
required: self.required_scopes.clone(),
actual,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use camel_api::Message;
use camel_api::security_policy::{
AuthPrincipal, Principal, TransportId, store_principal_properties,
};
fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
Principal {
subject: "test-user".into(),
issuer: "test".into(),
audience: vec![],
roles: roles.iter().map(|s| s.to_string()).collect(),
scopes: scopes.iter().map(|s| s.to_string()).collect(),
claims: serde_json::Value::Null,
}
}
struct TestPrincipal(Principal);
impl AuthPrincipal for TestPrincipal {
fn principal(&self) -> &Principal {
&self.0
}
fn provider_id(&self) -> &str {
"test"
}
}
fn auth_ctx<'a>(principal: &'a TestPrincipal) -> AuthContext<'a> {
AuthContext {
principal,
transport: TransportId::Http,
}
}
fn empty_exchange() -> Exchange {
Exchange::new(Message::default())
}
#[tokio::test]
async fn role_policy_reads_typed_principal_roles() {
let principal = TestPrincipal(test_principal(vec!["admin"], vec![]));
let policy = RolePolicy::new(vec!["admin".into()], true);
let mut ex = empty_exchange();
let auth = auth_ctx(&principal);
let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn property_only_evidence_denies() {
let spoofed = test_principal(vec!["admin"], vec![]);
let mut ex = empty_exchange();
store_principal_properties(&mut ex, &spoofed);
let typed = TestPrincipal(test_principal(vec!["user"], vec![]));
let policy = RolePolicy::new(vec!["admin".into()], true);
let auth = auth_ctx(&typed);
let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
}
#[tokio::test]
async fn role_policy_grants_when_role_present() {
let principal = TestPrincipal(test_principal(vec!["admin"], vec![]));
let policy = RolePolicy::new(vec!["admin".into()], true);
let mut ex = empty_exchange();
let auth = auth_ctx(&principal);
let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn role_policy_denies_when_role_missing() {
let principal = TestPrincipal(test_principal(vec!["user"], vec![]));
let policy = RolePolicy::new(vec!["admin".into()], true);
let mut ex = empty_exchange();
let auth = auth_ctx(&principal);
let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
}
#[tokio::test]
async fn role_policy_any_required() {
let principal = TestPrincipal(test_principal(vec!["user"], vec![]));
let policy = RolePolicy::new(vec!["admin".into(), "user".into()], false);
let mut ex = empty_exchange();
let auth = auth_ctx(&principal);
let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn scope_policy_grants() {
let principal = TestPrincipal(test_principal(vec![], vec!["read"]));
let policy = ScopePolicy::new(vec!["read".into()], true);
let mut ex = empty_exchange();
let auth = auth_ctx(&principal);
let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn scope_policy_denies_when_scope_missing() {
let principal = TestPrincipal(test_principal(vec![], vec!["read"]));
let policy = ScopePolicy::new(vec!["write".into()], true);
let mut ex = empty_exchange();
let auth = auth_ctx(&principal);
let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
}
#[tokio::test]
async fn scope_policy_any_required() {
let principal = TestPrincipal(test_principal(vec![], vec!["read"]));
let policy = ScopePolicy::new(vec!["write".into(), "read".into()], false);
let mut ex = empty_exchange();
let auth = auth_ctx(&principal);
let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
}