use async_trait::async_trait;
use std::sync::Arc;
use camel_api::security_policy::{
AuthorizationDecision, CredentialSource, Principal, SecurityPolicy, principal_from_exchange,
store_principal_properties,
};
use camel_api::{CamelError, Exchange};
use crate::credential_source::extract_token_from_exchange;
use crate::token_authenticator::TokenAuthenticator;
pub use camel_api::security_policy::PRINCIPAL_KEY;
async fn authenticate(
exchange: &mut Exchange,
authenticator: &dyn TokenAuthenticator,
trust_upstream_principal: bool,
sources: &[CredentialSource],
) -> Result<Principal, CamelError> {
let token = extract_token_from_exchange(exchange, sources).map(|extracted| extracted.token);
if let Some(token) = token {
let principal = authenticator.authenticate_bearer(&token).await?;
store_principal_properties(exchange, &principal);
return Ok(principal);
}
if trust_upstream_principal {
extract_principal_from_exchange(exchange)
} else {
Err(CamelError::Unauthenticated(
"no Bearer token and trust_upstream_principal is false".into(),
))
}
}
fn extract_principal_from_exchange(exchange: &Exchange) -> Result<Principal, CamelError> {
principal_from_exchange(exchange)
.ok_or_else(|| CamelError::Unauthenticated("no principal in exchange".into()))
}
pub struct RolePolicy {
required_roles: Vec<String>,
all_required: bool,
trust_upstream_principal: bool,
authenticator: Arc<dyn TokenAuthenticator>,
credential_sources: Vec<CredentialSource>,
}
impl RolePolicy {
pub fn new(
required_roles: Vec<String>,
all_required: bool,
trust_upstream_principal: bool,
authenticator: Arc<dyn TokenAuthenticator>,
credential_sources: Vec<CredentialSource>,
) -> Self {
Self {
required_roles,
all_required,
trust_upstream_principal,
authenticator,
credential_sources,
}
}
pub fn credential_sources(&self) -> &[CredentialSource] {
&self.credential_sources
}
}
#[async_trait]
impl SecurityPolicy for RolePolicy {
async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError> {
let principal = authenticate(
exchange,
&*self.authenticator,
self.trust_upstream_principal,
&self.credential_sources,
)
.await?;
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 })
} 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,
trust_upstream_principal: bool,
authenticator: Arc<dyn TokenAuthenticator>,
credential_sources: Vec<CredentialSource>,
}
impl ScopePolicy {
pub fn new(
required_scopes: Vec<String>,
all_required: bool,
trust_upstream_principal: bool,
authenticator: Arc<dyn TokenAuthenticator>,
credential_sources: Vec<CredentialSource>,
) -> Self {
Self {
required_scopes,
all_required,
trust_upstream_principal,
authenticator,
credential_sources,
}
}
pub fn credential_sources(&self) -> &[CredentialSource] {
&self.credential_sources
}
}
#[async_trait]
impl SecurityPolicy for ScopePolicy {
async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError> {
let principal = authenticate(
exchange,
&*self.authenticator,
self.trust_upstream_principal,
&self.credential_sources,
)
.await?;
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 })
} 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 crate::jwt::JwtValidator;
use crate::native_auth::{
NativeCredential, NativeCredentialSecret, NativeCredentialStore, StaticTokenAuthenticator,
};
use crate::types::AuthError;
use camel_api::Message;
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 MockJwtValidator {
principal: Principal,
}
#[async_trait]
impl JwtValidator for MockJwtValidator {
async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
Ok(self.principal.clone())
}
}
fn mock_validator(principal: Principal) -> Arc<dyn TokenAuthenticator> {
Arc::new(MockJwtValidator { principal })
}
fn store_seeded_authenticator(
credential: &str,
principal: Principal,
) -> Arc<dyn TokenAuthenticator> {
let store = NativeCredentialStore::try_new(vec![NativeCredential {
secret: NativeCredentialSecret::Plaintext {
value: zeroize::Zeroizing::new(credential.to_string()),
},
principal,
}])
.unwrap();
Arc::new(StaticTokenAuthenticator::new(store))
}
fn exchange_with_bearer(principal: Principal) -> Exchange {
let mut msg = Message::default();
msg.set_header(
"Authorization",
serde_json::Value::String("Bearer mock-token".into()),
);
let mut ex = Exchange::new(msg);
store_principal_properties(&mut ex, &principal);
ex
}
fn exchange_with_principal(principal: Principal) -> Exchange {
let mut ex = Exchange::new(Message::default());
store_principal_properties(&mut ex, &principal);
ex
}
#[tokio::test]
async fn role_policy_grants_when_role_present() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_bearer(principal);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn role_policy_denies_when_role_missing() {
let principal = test_principal(vec!["user"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_bearer(principal);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
}
#[tokio::test]
async fn role_policy_any_required() {
let principal = test_principal(vec!["user"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into(), "user".into()],
false,
false,
mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_bearer(principal);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn scope_policy_grants() {
let principal = test_principal(vec![], vec!["read"]);
let policy = ScopePolicy::new(
vec!["read".into()],
true,
false,
mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_bearer(principal);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn unauthenticated_when_no_principal_and_no_header() {
struct FailValidator;
#[async_trait]
impl JwtValidator for FailValidator {
async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
panic!("should not be called")
}
}
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
Arc::new(FailValidator),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = Exchange::new(Message::default());
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
}
#[tokio::test]
async fn principal_fallback_denied_by_default() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false, mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_principal(principal); let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
}
#[tokio::test]
async fn principal_fallback_allowed_with_opt_in() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
true, mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_principal(principal); let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn scope_policy_fallback_denied_by_default() {
let principal = test_principal(vec![], vec!["read"]);
let policy = ScopePolicy::new(
vec!["read".into()],
true,
false, mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_principal(principal);
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
}
#[test]
fn role_policy_constructor_accepts_sources() {
let authenticator = mock_validator(test_principal(vec![], vec![]));
let sources = vec![CredentialSource::Cookie { name: "s".into() }];
let policy = RolePolicy::new(
vec!["r".into()],
true,
false,
authenticator,
sources.clone(),
);
assert_eq!(policy.credential_sources(), sources.as_slice());
}
#[tokio::test]
async fn authenticate_default_equals_bearer_prefix_strip() {
let principal = test_principal(vec!["admin"], vec![]);
let store = NativeCredentialStore::try_new(vec![NativeCredential {
secret: NativeCredentialSecret::Plaintext {
value: zeroize::Zeroizing::new("mock-token".to_string()),
},
principal: principal.clone(),
}])
.unwrap();
let authenticator: Arc<dyn TokenAuthenticator> =
Arc::new(StaticTokenAuthenticator::new(store));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
authenticator,
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_bearer(principal.clone());
let decision = policy.evaluate(&mut ex).await.unwrap();
match decision {
AuthorizationDecision::Granted { principal: granted } => {
assert_eq!(granted.subject, principal.subject);
}
other => panic!("expected Granted, got {other:?}"),
}
}
#[tokio::test]
async fn authenticate_header_source_reads_authorization() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_bearer(principal.clone());
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
mock_validator(principal),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = Exchange::new(Message::default());
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
}
#[tokio::test]
async fn cookie_parse_malformed_is_absent_not_error() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
mock_validator(principal),
vec![CredentialSource::Cookie {
name: "session".into(),
}],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"Cookie",
serde_json::Value::String("garbage-no-equals".into()),
);
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
}
#[tokio::test]
async fn query_source_reads_camel_http_query_header() {
let principal = test_principal(vec!["admin"], vec![]);
let store = NativeCredentialStore::try_new(vec![NativeCredential {
secret: NativeCredentialSecret::Plaintext {
value: zeroize::Zeroizing::new("TOK".to_string()),
},
principal: principal.clone(),
}])
.unwrap();
let authenticator: Arc<dyn TokenAuthenticator> =
Arc::new(StaticTokenAuthenticator::new(store));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
authenticator,
vec![CredentialSource::QueryParam {
param: "token".into(),
}],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"CamelHttpQuery",
serde_json::Value::String("token=TOK".into()),
);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn trust_false_preloaded_principal_unauthenticated() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false, mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_principal(principal); let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
}
#[tokio::test]
async fn trust_true_preloaded_principal_fallback() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
true, mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_principal(principal); let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn prefix_credential_unauthenticated() {
let principal = test_principal(vec!["admin"], vec![]);
let store = NativeCredentialStore::try_new(vec![NativeCredential {
secret: NativeCredentialSecret::Plaintext {
value: zeroize::Zeroizing::new("SENTINEL_FULL_9kq2".to_string()),
},
principal: principal.clone(),
}])
.unwrap();
let authenticator: Arc<dyn TokenAuthenticator> =
Arc::new(StaticTokenAuthenticator::new(store));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
authenticator.clone(),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"Authorization",
serde_json::Value::String("Bearer SENTINEL_FULL".into()),
);
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
authenticator.clone(),
vec![CredentialSource::Cookie {
name: "session".into(),
}],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"Cookie",
serde_json::Value::String("session=SENTINEL_FULL".into()),
);
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
authenticator,
vec![CredentialSource::QueryParam {
param: "token".into(),
}],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"CamelHttpQuery",
serde_json::Value::String("token=SENTINEL_FULL".into()),
);
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
}
#[tokio::test]
async fn header_source_authenticates_api_key() {
let principal = test_principal(vec!["admin"], vec![]);
let store = NativeCredentialStore::try_new(vec![NativeCredential {
secret: NativeCredentialSecret::Plaintext {
value: zeroize::Zeroizing::new("SENTINEL_KEY_1".to_string()),
},
principal: principal.clone(),
}])
.unwrap();
let authenticator: Arc<dyn TokenAuthenticator> =
Arc::new(StaticTokenAuthenticator::new(store));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
authenticator,
vec![CredentialSource::Header {
name: "x-api-key".into(),
}],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"X-API-Key",
serde_json::Value::String("SENTINEL_KEY_1".into()),
);
let decision = policy.evaluate(&mut ex).await.unwrap();
match decision {
AuthorizationDecision::Granted { principal: granted } => {
assert_eq!(granted.subject, principal.subject);
}
other => panic!("expected Granted, got {other:?}"),
}
}
#[tokio::test]
async fn header_source_miss_maps_401() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
mock_validator(principal),
vec![CredentialSource::Header {
name: "x-api-key".into(),
}],
);
let mut ex = Exchange::new(Message::default());
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
}
#[tokio::test]
async fn header_lookup_case_insensitive() {
let principal = test_principal(vec!["admin"], vec![]);
let store = NativeCredentialStore::try_new(vec![NativeCredential {
secret: NativeCredentialSecret::Plaintext {
value: zeroize::Zeroizing::new("SENTINEL_KEY_1".to_string()),
},
principal: principal.clone(),
}])
.unwrap();
let authenticator: Arc<dyn TokenAuthenticator> =
Arc::new(StaticTokenAuthenticator::new(store));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
authenticator,
vec![CredentialSource::Header {
name: "x-api-key".into(),
}],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"X-API-KEY",
serde_json::Value::String("SENTINEL_KEY_1".into()),
);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn bearer_scheme_lowercase_grants() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
store_seeded_authenticator("TOK", principal),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"Authorization",
serde_json::Value::String("bearer TOK".into()),
);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn bearer_scheme_uppercase_grants() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
store_seeded_authenticator("TOK", principal),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"Authorization",
serde_json::Value::String("BEARER TOK".into()),
);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn bearer_leading_whitespace_grants() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
store_seeded_authenticator("TOK", principal),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"Authorization",
serde_json::Value::String(" Bearer TOK".into()),
);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn bearer_double_space_yields_trimmed_token() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
store_seeded_authenticator("TOK", principal),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = Exchange::new(Message::default());
ex.input.set_header(
"Authorization",
serde_json::Value::String("Bearer TOK".into()),
);
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
#[tokio::test]
async fn bearer_empty_token_falls_to_trust_branch() {
let principal = test_principal(vec!["admin"], vec![]);
let policy = RolePolicy::new(
vec!["admin".into()],
true,
false,
mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_principal(principal.clone());
ex.input
.set_header("Authorization", serde_json::Value::String("Bearer ".into()));
let result = policy.evaluate(&mut ex).await;
assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
let policy = RolePolicy::new(
vec!["admin".into()],
true,
true,
mock_validator(principal.clone()),
vec![CredentialSource::AuthorizationHeader],
);
let mut ex = exchange_with_principal(principal);
ex.input
.set_header("Authorization", serde_json::Value::String("Bearer ".into()));
let decision = policy.evaluate(&mut ex).await.unwrap();
assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
}
}