1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6use crate::{CamelError, Exchange};
7
8#[derive(Clone, PartialEq, Serialize, Deserialize)]
13pub struct Principal {
14 pub subject: String,
15 #[serde(default)]
16 pub issuer: String,
17 #[serde(default)]
18 pub audience: Vec<String>,
19 pub scopes: Vec<String>,
20 pub roles: Vec<String>,
21 pub claims: serde_json::Value,
22}
23
24impl Principal {
25 pub fn has_role(&self, role: &str) -> bool {
27 self.roles.iter().any(|r| r == role)
28 }
29
30 pub fn has_scope(&self, scope: &str) -> bool {
32 self.scopes.iter().any(|s| s == scope)
33 }
34}
35
36impl std::fmt::Debug for Principal {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct("Principal")
41 .field("subject", &self.subject)
42 .field("issuer", &self.issuer)
43 .field("audience", &self.audience)
44 .field("scopes", &self.scopes)
45 .field("roles", &self.roles)
46 .field("claims", &"[REDACTED]")
47 .finish()
48 }
49}
50
51#[derive(Debug, Clone, PartialEq)]
52#[non_exhaustive]
53pub enum AuthorizationDecision {
54 Granted {
55 principal: Principal,
56 },
57 Denied {
58 reason: String,
59 required: Vec<String>,
60 actual: Vec<String>,
61 },
62}
63
64impl std::fmt::Display for AuthorizationDecision {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 Self::Granted { principal } => {
68 write!(f, "Access granted for {}", principal.subject)
69 }
70 Self::Denied { reason, .. } => write!(f, "Access denied: {reason}"),
71 }
72 }
73}
74
75#[async_trait]
76pub trait SecurityPolicy: Send + Sync {
77 async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError>;
78}
79
80pub const CAMEL_HTTP_QUERY_HEADER: &str = "CamelHttpQuery";
87
88#[derive(Clone, PartialEq, Eq)]
94pub enum CredentialSource {
95 AuthorizationHeader,
97 QueryParam { param: String },
99 Cookie { name: String },
101 Header { name: String },
106}
107
108impl CredentialSource {
109 pub fn variant_name(&self) -> &'static str {
111 match self {
112 CredentialSource::AuthorizationHeader => "AuthorizationHeader",
113 CredentialSource::QueryParam { .. } => "QueryParam",
114 CredentialSource::Cookie { .. } => "Cookie",
115 CredentialSource::Header { .. } => "Header",
116 }
117 }
118}
119
120impl std::fmt::Debug for CredentialSource {
121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 match self {
123 CredentialSource::AuthorizationHeader => f.write_str("AuthorizationHeader"),
124 CredentialSource::QueryParam { param } => {
125 write!(f, "QueryParam {{ param: {:?} }}", param) }
127 CredentialSource::Cookie { name } => {
128 write!(f, "Cookie {{ name: {:?} }}", name) }
130 CredentialSource::Header { name } => {
131 write!(f, "Header {{ name: {:?} }}", name) }
133 }
134 }
135}
136
137pub struct SecurityPolicyConfig {
138 pub policy: Arc<dyn SecurityPolicy>,
139 pub credential_sources: Vec<CredentialSource>,
142}
143
144impl SecurityPolicyConfig {
145 pub fn new(policy: impl SecurityPolicy + 'static) -> Self {
146 Self {
147 policy: Arc::new(policy),
148 credential_sources: vec![CredentialSource::AuthorizationHeader],
149 }
150 }
151
152 pub fn from_arc(policy: Arc<dyn SecurityPolicy>) -> Self {
153 Self {
154 policy,
155 credential_sources: vec![CredentialSource::AuthorizationHeader],
156 }
157 }
158
159 pub fn with_credential_sources(mut self, sources: Vec<CredentialSource>) -> Self {
160 self.credential_sources = sources;
161 self
162 }
163}
164
165impl Clone for SecurityPolicyConfig {
166 fn clone(&self) -> Self {
167 Self {
168 policy: Arc::clone(&self.policy),
169 credential_sources: self.credential_sources.clone(),
170 }
171 }
172}
173
174impl std::fmt::Debug for SecurityPolicyConfig {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("SecurityPolicyConfig")
177 .field("policy", &"<SecurityPolicy>")
178 .field("credential_sources", &self.credential_sources)
179 .finish()
180 }
181}
182
183pub const PRINCIPAL_SUBJECT_KEY: &str = "camel.auth.subject";
187pub const PRINCIPAL_ROLES_KEY: &str = "camel.auth.roles";
189pub const PRINCIPAL_SCOPES_KEY: &str = "camel.auth.scopes";
191pub const PRINCIPAL_ISSUER_KEY: &str = "camel.auth.issuer";
193pub const PRINCIPAL_CLAIMS_KEY: &str = "camel.auth.claims";
195pub const PRINCIPAL_AUDIENCE_KEY: &str = "camel.auth.audience";
197pub const PRINCIPAL_KEY: &str = "camel.auth.principal";
199
200pub fn store_principal_properties(exchange: &mut Exchange, principal: &Principal) {
202 exchange.set_property(PRINCIPAL_SUBJECT_KEY, principal.subject.clone());
203 exchange.set_property(
204 PRINCIPAL_ROLES_KEY,
205 serde_json::to_string(&principal.roles).unwrap_or_default(),
206 );
207 exchange.set_property(
208 PRINCIPAL_SCOPES_KEY,
209 serde_json::to_string(&principal.scopes).unwrap_or_default(),
210 );
211 exchange.set_property(PRINCIPAL_ISSUER_KEY, principal.issuer.clone());
212 exchange.set_property(
213 PRINCIPAL_CLAIMS_KEY,
214 serde_json::to_string(&principal.claims).unwrap_or_default(),
215 );
216 exchange.set_property(
217 PRINCIPAL_AUDIENCE_KEY,
218 serde_json::to_string(&principal.audience).unwrap_or_default(),
219 );
220 exchange.set_property(
221 PRINCIPAL_KEY,
222 serde_json::to_string(principal).unwrap_or_default(),
223 );
224}
225
226pub fn principal_from_exchange(exchange: &Exchange) -> Option<Principal> {
227 exchange
228 .property(PRINCIPAL_KEY)
229 .and_then(|v| v.as_str())
230 .and_then(|s| serde_json::from_str(s).ok())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::Body;
237
238 fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
239 Principal {
240 subject: "user1".into(),
241 issuer: "test".into(),
242 audience: vec![],
243 scopes: scopes.into_iter().map(String::from).collect(),
244 roles: roles.into_iter().map(String::from).collect(),
245 claims: serde_json::Value::Null,
246 }
247 }
248
249 fn test_policy() -> impl SecurityPolicy + 'static {
251 struct GrantPolicy;
252
253 #[async_trait]
254 impl SecurityPolicy for GrantPolicy {
255 async fn evaluate(
256 &self,
257 _exchange: &mut Exchange,
258 ) -> Result<AuthorizationDecision, CamelError> {
259 Ok(AuthorizationDecision::Granted {
260 principal: test_principal(vec![], vec![]),
261 })
262 }
263 }
264
265 GrantPolicy
266 }
267
268 #[test]
269 fn principal_has_role_is_case_sensitive() {
270 let p = test_principal(vec!["Admin", "User"], vec![]);
271 assert!(!p.has_role("admin"));
272 assert!(!p.has_role("ADMIN"));
273 assert!(p.has_role("User"));
274 assert!(!p.has_role("guest"));
275 }
276
277 #[test]
278 fn principal_has_scope() {
279 let p = test_principal(vec![], vec!["read", "write"]);
280 assert!(p.has_scope("read"));
281 assert!(!p.has_scope("delete"));
282 }
283
284 #[test]
285 fn authorization_decision_granted_display() {
286 let p = test_principal(vec![], vec![]);
287 let d = AuthorizationDecision::Granted { principal: p };
288 assert!(format!("{d}").contains("user1"));
289 }
290
291 #[test]
292 fn authorization_decision_denied_display() {
293 let d = AuthorizationDecision::Denied {
294 reason: "missing role".into(),
295 required: vec!["admin".into()],
296 actual: vec![],
297 };
298 assert!(format!("{d}").contains("missing role"));
299 }
300
301 #[test]
302 fn security_policy_config_debug_redacts_policy() {
303 let config = SecurityPolicyConfig::new(test_policy());
304 let debug = format!("{config:?}");
305 assert!(debug.contains("SecurityPolicyConfig"));
306 assert!(debug.contains("<SecurityPolicy>"));
307 }
308
309 #[test]
310 fn security_policy_config_new_is_header_only() {
311 let config = SecurityPolicyConfig::new(test_policy());
312 assert_eq!(
313 config.credential_sources,
314 vec![CredentialSource::AuthorizationHeader]
315 );
316 }
317
318 #[test]
319 fn store_principal_properties_populates_all_keys() {
320 let principal = Principal {
321 subject: "alice".into(),
322 issuer: "keycloak".into(),
323 audience: vec!["api".into()],
324 scopes: vec!["read".into(), "write".into()],
325 roles: vec!["admin".into()],
326 claims: serde_json::json!({"sub": "alice", "custom": true}),
327 };
328 let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
329 store_principal_properties(&mut exchange, &principal);
330
331 assert_eq!(
332 exchange.property(PRINCIPAL_SUBJECT_KEY).unwrap(),
333 &serde_json::Value::String("alice".into())
334 );
335 assert_eq!(
336 exchange.property(PRINCIPAL_ISSUER_KEY).unwrap(),
337 &serde_json::Value::String("keycloak".into())
338 );
339 let roles: Vec<String> = serde_json::from_str(
340 exchange
341 .property(PRINCIPAL_ROLES_KEY)
342 .unwrap()
343 .as_str()
344 .unwrap(),
345 )
346 .unwrap();
347 assert_eq!(roles, vec!["admin"]);
348 let scopes: Vec<String> = serde_json::from_str(
349 exchange
350 .property(PRINCIPAL_SCOPES_KEY)
351 .unwrap()
352 .as_str()
353 .unwrap(),
354 )
355 .unwrap();
356 assert_eq!(scopes, vec!["read", "write"]);
357 let audience: Vec<String> = serde_json::from_str(
358 exchange
359 .property(PRINCIPAL_AUDIENCE_KEY)
360 .unwrap()
361 .as_str()
362 .unwrap(),
363 )
364 .unwrap();
365 assert_eq!(audience, vec!["api"]);
366 let claims: serde_json::Value = serde_json::from_str(
367 exchange
368 .property(PRINCIPAL_CLAIMS_KEY)
369 .unwrap()
370 .as_str()
371 .unwrap(),
372 )
373 .unwrap();
374 assert!(claims.as_object().unwrap().contains_key("custom"));
375 let full: serde_json::Value =
376 serde_json::from_str(exchange.property(PRINCIPAL_KEY).unwrap().as_str().unwrap())
377 .unwrap();
378 assert_eq!(full["subject"], "alice");
379 }
380
381 #[test]
382 fn security_policy_config_clone() {
383 let config = SecurityPolicyConfig::new(test_policy());
384 let cloned = config.clone();
385 assert!(Arc::ptr_eq(&config.policy, &cloned.policy));
387 }
388
389 #[test]
390 fn test_principal_from_exchange_round_trip() {
391 let principal = Principal {
392 subject: "bob".into(),
393 issuer: "keycloak".into(),
394 audience: vec!["api".into()],
395 scopes: vec!["read".into()],
396 roles: vec!["user".into()],
397 claims: serde_json::json!({"sub": "bob"}),
398 };
399 let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
400 store_principal_properties(&mut exchange, &principal);
401
402 let recovered = principal_from_exchange(&exchange).expect("principal should be recovered");
403 assert_eq!(recovered.subject, "bob");
404 assert_eq!(recovered.issuer, "keycloak");
405 assert_eq!(recovered.audience, vec!["api"]);
406 assert_eq!(recovered.scopes, vec!["read"]);
407 assert_eq!(recovered.roles, vec!["user"]);
408 }
409
410 #[test]
411 fn principal_debug_redacts_claims_compact() {
412 let principal = Principal {
413 subject: "subj-1".into(),
414 issuer: "iss".into(),
415 audience: vec!["a1".into()],
416 scopes: vec!["s1".into()],
417 roles: vec!["r1".into()],
418 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
419 };
420 let s = format!("{principal:?}");
421 assert!(
422 s.contains("claims: \"[REDACTED]\""),
423 "compact debug should show [REDACTED] for claims"
424 );
425 assert!(
426 !s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
427 "compact debug should NOT contain raw claim value"
428 );
429 assert!(s.contains("subj-1"), "compact debug should contain subject");
430 assert!(s.contains("iss"), "compact debug should contain issuer");
431 assert!(s.contains("a1"), "compact debug should contain audience");
432 assert!(s.contains("s1"), "compact debug should contain scopes");
433 assert!(s.contains("r1"), "compact debug should contain roles");
434 }
435
436 #[test]
437 fn principal_debug_redacts_claims_pretty() {
438 let principal = Principal {
439 subject: "subj-1".into(),
440 issuer: "iss".into(),
441 audience: vec!["a1".into()],
442 scopes: vec!["s1".into()],
443 roles: vec!["r1".into()],
444 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
445 };
446 let s = format!("{principal:#?}");
447 assert!(
448 s.contains("[REDACTED]"),
449 "pretty debug should show [REDACTED]"
450 );
451 assert!(
452 !s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
453 "pretty debug should NOT contain raw claim value"
454 );
455 }
456
457 #[test]
458 fn principal_serialize_preserves_claims() {
459 let principal = Principal {
460 subject: "subj-1".into(),
461 issuer: "iss".into(),
462 audience: vec!["a1".into()],
463 scopes: vec!["s1".into()],
464 roles: vec!["r1".into()],
465 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
466 };
467 let s = serde_json::to_string(&principal).unwrap();
468 assert!(
469 s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
470 "serialization should preserve raw claim value"
471 );
472 }
473}