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 { principal: Principal },
60 Denied {
61 reason: String,
62 required: Vec<String>,
63 actual: Vec<String>,
64 },
65}
66
67impl std::fmt::Display for AuthorizationDecision {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 Self::Granted { principal } => {
71 write!(f, "Access granted for {}", principal.subject)
72 }
73 Self::Denied { reason, .. } => write!(f, "Access denied: {reason}"),
74 }
75 }
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
87pub enum TransportId {
88 Http,
89 Ws,
90 Grpc,
91 Mcp,
92 Wasm,
93}
94
95pub trait AuthPrincipal: Send + Sync {
101 fn principal(&self) -> &Principal;
103 fn provider_id(&self) -> &str;
105}
106
107pub struct AuthContext<'a> {
113 pub principal: &'a dyn AuthPrincipal,
114 pub transport: TransportId,
115}
116
117#[async_trait]
118pub trait SecurityPolicy: Send + Sync {
119 async fn evaluate(
120 &self,
121 exchange: &mut Exchange,
122 auth: &AuthContext<'_>,
123 ) -> Result<AuthorizationDecision, CamelError>;
124}
125
126pub const CAMEL_HTTP_QUERY_HEADER: &str = "CamelHttpQuery";
133
134#[derive(Clone, PartialEq, Eq)]
140pub enum CredentialSource {
141 AuthorizationHeader,
143 QueryParam { param: String },
145 Cookie { name: String },
147 Header { name: String },
152}
153
154impl CredentialSource {
155 pub fn variant_name(&self) -> &'static str {
157 match self {
158 CredentialSource::AuthorizationHeader => "AuthorizationHeader",
159 CredentialSource::QueryParam { .. } => "QueryParam",
160 CredentialSource::Cookie { .. } => "Cookie",
161 CredentialSource::Header { .. } => "Header",
162 }
163 }
164}
165
166impl std::fmt::Debug for CredentialSource {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 match self {
169 CredentialSource::AuthorizationHeader => f.write_str("AuthorizationHeader"),
170 CredentialSource::QueryParam { param } => {
171 write!(f, "QueryParam {{ param: {:?} }}", param) }
173 CredentialSource::Cookie { name } => {
174 write!(f, "Cookie {{ name: {:?} }}", name) }
176 CredentialSource::Header { name } => {
177 write!(f, "Header {{ name: {:?} }}", name) }
179 }
180 }
181}
182
183pub struct SecurityPolicyConfig {
184 pub policy: Arc<dyn SecurityPolicy>,
185 pub credential_sources: Vec<CredentialSource>,
188}
189
190impl SecurityPolicyConfig {
191 pub fn new(policy: impl SecurityPolicy + 'static) -> Self {
192 Self {
193 policy: Arc::new(policy),
194 credential_sources: vec![CredentialSource::AuthorizationHeader],
195 }
196 }
197
198 pub fn from_arc(policy: Arc<dyn SecurityPolicy>) -> Self {
199 Self {
200 policy,
201 credential_sources: vec![CredentialSource::AuthorizationHeader],
202 }
203 }
204
205 pub fn with_credential_sources(mut self, sources: Vec<CredentialSource>) -> Self {
206 self.credential_sources = sources;
207 self
208 }
209}
210
211impl Clone for SecurityPolicyConfig {
212 fn clone(&self) -> Self {
213 Self {
214 policy: Arc::clone(&self.policy),
215 credential_sources: self.credential_sources.clone(),
216 }
217 }
218}
219
220impl std::fmt::Debug for SecurityPolicyConfig {
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 f.debug_struct("SecurityPolicyConfig")
223 .field("policy", &"<SecurityPolicy>")
224 .field("credential_sources", &self.credential_sources)
225 .finish()
226 }
227}
228
229pub enum AccessMode {
238 Public,
239 Authenticated,
240 Authorized(Arc<dyn SecurityPolicy>),
241}
242
243impl Clone for AccessMode {
244 fn clone(&self) -> Self {
245 match self {
246 Self::Public => Self::Public,
247 Self::Authenticated => Self::Authenticated,
248 Self::Authorized(policy) => Self::Authorized(Arc::clone(policy)),
249 }
250 }
251}
252
253impl std::fmt::Debug for AccessMode {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 match self {
256 Self::Public => f.write_str("Public"),
257 Self::Authenticated => f.write_str("Authenticated"),
258 Self::Authorized(_) => f.write_str("Authorized(<SecurityPolicy>)"),
259 }
260 }
261}
262
263#[derive(Clone, Debug, PartialEq)]
268pub struct AudienceBinding {
269 pub issuers: Vec<String>,
270 pub audiences: Vec<String>,
271}
272
273pub struct RouteSecurityPlan {
279 pub access_mode: AccessMode,
280 pub provider_ref: Option<String>,
281 pub transport: TransportId,
282 pub credential_sources: Vec<CredentialSource>,
283 pub audience_binding: Option<AudienceBinding>,
284}
285
286impl Clone for RouteSecurityPlan {
287 fn clone(&self) -> Self {
288 Self {
289 access_mode: self.access_mode.clone(),
290 provider_ref: self.provider_ref.clone(),
291 transport: self.transport,
292 credential_sources: self.credential_sources.clone(),
293 audience_binding: self.audience_binding.clone(),
294 }
295 }
296}
297
298impl std::fmt::Debug for RouteSecurityPlan {
299 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
300 f.debug_struct("RouteSecurityPlan")
301 .field("access_mode", &self.access_mode)
302 .field("provider_ref", &self.provider_ref)
303 .field("transport", &self.transport)
304 .field("credential_sources", &self.credential_sources)
305 .field("audience_binding", &self.audience_binding)
306 .finish()
307 }
308}
309
310pub const PRINCIPAL_SUBJECT_KEY: &str = "camel.auth.subject";
314pub const PRINCIPAL_ROLES_KEY: &str = "camel.auth.roles";
316pub const PRINCIPAL_SCOPES_KEY: &str = "camel.auth.scopes";
318pub const PRINCIPAL_ISSUER_KEY: &str = "camel.auth.issuer";
320pub const PRINCIPAL_CLAIMS_KEY: &str = "camel.auth.claims";
322pub const PRINCIPAL_AUDIENCE_KEY: &str = "camel.auth.audience";
324pub const PRINCIPAL_KEY: &str = "camel.auth.principal";
326
327pub fn store_principal_properties(exchange: &mut Exchange, principal: &Principal) {
329 exchange.set_property(PRINCIPAL_SUBJECT_KEY, principal.subject.clone());
330 exchange.set_property(
331 PRINCIPAL_ROLES_KEY,
332 serde_json::to_string(&principal.roles).unwrap_or_default(),
333 );
334 exchange.set_property(
335 PRINCIPAL_SCOPES_KEY,
336 serde_json::to_string(&principal.scopes).unwrap_or_default(),
337 );
338 exchange.set_property(PRINCIPAL_ISSUER_KEY, principal.issuer.clone());
339 exchange.set_property(
340 PRINCIPAL_CLAIMS_KEY,
341 serde_json::to_string(&principal.claims).unwrap_or_default(),
342 );
343 exchange.set_property(
344 PRINCIPAL_AUDIENCE_KEY,
345 serde_json::to_string(&principal.audience).unwrap_or_default(),
346 );
347 exchange.set_property(
348 PRINCIPAL_KEY,
349 serde_json::to_string(principal).unwrap_or_default(),
350 );
351}
352
353pub fn principal_from_exchange(exchange: &Exchange) -> Option<Principal> {
354 exchange
355 .property(PRINCIPAL_KEY)
356 .and_then(|v| v.as_str())
357 .and_then(|s| serde_json::from_str(s).ok())
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use crate::Body;
364
365 fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
366 Principal {
367 subject: "user1".into(),
368 issuer: "test".into(),
369 audience: vec![],
370 scopes: scopes.into_iter().map(String::from).collect(),
371 roles: roles.into_iter().map(String::from).collect(),
372 claims: serde_json::Value::Null,
373 }
374 }
375
376 fn grant_policy() -> impl SecurityPolicy + 'static {
378 struct GrantPolicy;
379
380 #[async_trait]
381 impl SecurityPolicy for GrantPolicy {
382 async fn evaluate(
383 &self,
384 _exchange: &mut Exchange,
385 _auth: &AuthContext<'_>,
386 ) -> Result<AuthorizationDecision, CamelError> {
387 Ok(AuthorizationDecision::Granted {
388 principal: test_principal(vec![], vec![]),
389 })
390 }
391 }
392
393 GrantPolicy
394 }
395
396 #[test]
397 fn principal_has_role_is_case_sensitive() {
398 let p = test_principal(vec!["Admin", "User"], vec![]);
399 assert!(!p.has_role("admin"));
400 assert!(!p.has_role("ADMIN"));
401 assert!(p.has_role("User"));
402 assert!(!p.has_role("guest"));
403 }
404
405 #[test]
406 fn principal_has_scope() {
407 let p = test_principal(vec![], vec!["read", "write"]);
408 assert!(p.has_scope("read"));
409 assert!(!p.has_scope("delete"));
410 }
411
412 #[test]
413 fn authorization_decision_granted_display() {
414 let p = test_principal(vec![], vec![]);
415 let d = AuthorizationDecision::Granted { principal: p };
416 assert!(format!("{d}").contains("user1"));
417 }
418
419 #[test]
420 fn authorization_decision_denied_display() {
421 let d = AuthorizationDecision::Denied {
422 reason: "missing role".into(),
423 required: vec!["admin".into()],
424 actual: vec![],
425 };
426 assert!(format!("{d}").contains("missing role"));
427 }
428
429 #[test]
430 fn security_policy_config_debug_redacts_policy() {
431 let config = SecurityPolicyConfig::new(grant_policy());
432 let debug = format!("{config:?}");
433 assert!(debug.contains("SecurityPolicyConfig"));
434 assert!(debug.contains("<SecurityPolicy>"));
435 }
436
437 #[test]
438 fn security_policy_config_new_is_header_only() {
439 let config = SecurityPolicyConfig::new(grant_policy());
440 assert_eq!(
441 config.credential_sources,
442 vec![CredentialSource::AuthorizationHeader]
443 );
444 }
445
446 #[test]
447 fn store_principal_properties_populates_all_keys() {
448 let principal = Principal {
449 subject: "alice".into(),
450 issuer: "keycloak".into(),
451 audience: vec!["api".into()],
452 scopes: vec!["read".into(), "write".into()],
453 roles: vec!["admin".into()],
454 claims: serde_json::json!({"sub": "alice", "custom": true}),
455 };
456 let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
457 store_principal_properties(&mut exchange, &principal);
458
459 assert_eq!(
460 exchange.property(PRINCIPAL_SUBJECT_KEY).unwrap(),
461 &serde_json::Value::String("alice".into())
462 );
463 assert_eq!(
464 exchange.property(PRINCIPAL_ISSUER_KEY).unwrap(),
465 &serde_json::Value::String("keycloak".into())
466 );
467 let roles: Vec<String> = serde_json::from_str(
468 exchange
469 .property(PRINCIPAL_ROLES_KEY)
470 .unwrap()
471 .as_str()
472 .unwrap(),
473 )
474 .unwrap();
475 assert_eq!(roles, vec!["admin"]);
476 let scopes: Vec<String> = serde_json::from_str(
477 exchange
478 .property(PRINCIPAL_SCOPES_KEY)
479 .unwrap()
480 .as_str()
481 .unwrap(),
482 )
483 .unwrap();
484 assert_eq!(scopes, vec!["read", "write"]);
485 let audience: Vec<String> = serde_json::from_str(
486 exchange
487 .property(PRINCIPAL_AUDIENCE_KEY)
488 .unwrap()
489 .as_str()
490 .unwrap(),
491 )
492 .unwrap();
493 assert_eq!(audience, vec!["api"]);
494 let claims: serde_json::Value = serde_json::from_str(
495 exchange
496 .property(PRINCIPAL_CLAIMS_KEY)
497 .unwrap()
498 .as_str()
499 .unwrap(),
500 )
501 .unwrap();
502 assert!(claims.as_object().unwrap().contains_key("custom"));
503 let full: serde_json::Value =
504 serde_json::from_str(exchange.property(PRINCIPAL_KEY).unwrap().as_str().unwrap())
505 .unwrap();
506 assert_eq!(full["subject"], "alice");
507 }
508
509 #[test]
510 fn security_policy_config_clone() {
511 let config = SecurityPolicyConfig::new(grant_policy());
512 let cloned = config.clone();
513 assert!(Arc::ptr_eq(&config.policy, &cloned.policy));
515 }
516
517 #[test]
518 fn test_principal_from_exchange_round_trip() {
519 let principal = Principal {
520 subject: "bob".into(),
521 issuer: "keycloak".into(),
522 audience: vec!["api".into()],
523 scopes: vec!["read".into()],
524 roles: vec!["user".into()],
525 claims: serde_json::json!({"sub": "bob"}),
526 };
527 let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
528 store_principal_properties(&mut exchange, &principal);
529
530 let recovered = principal_from_exchange(&exchange).expect("principal should be recovered");
531 assert_eq!(recovered.subject, "bob");
532 assert_eq!(recovered.issuer, "keycloak");
533 assert_eq!(recovered.audience, vec!["api"]);
534 assert_eq!(recovered.scopes, vec!["read"]);
535 assert_eq!(recovered.roles, vec!["user"]);
536 }
537
538 #[test]
539 fn principal_debug_redacts_claims_compact() {
540 let principal = Principal {
541 subject: "subj-1".into(),
542 issuer: "iss".into(),
543 audience: vec!["a1".into()],
544 scopes: vec!["s1".into()],
545 roles: vec!["r1".into()],
546 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
547 };
548 let s = format!("{principal:?}");
549 assert!(
550 s.contains("claims: \"[REDACTED]\""),
551 "compact debug should show [REDACTED] for claims"
552 );
553 assert!(
554 !s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
555 "compact debug should NOT contain raw claim value"
556 );
557 assert!(s.contains("subj-1"), "compact debug should contain subject");
558 assert!(s.contains("iss"), "compact debug should contain issuer");
559 assert!(s.contains("a1"), "compact debug should contain audience");
560 assert!(s.contains("s1"), "compact debug should contain scopes");
561 assert!(s.contains("r1"), "compact debug should contain roles");
562 }
563
564 #[test]
565 fn principal_debug_redacts_claims_pretty() {
566 let principal = Principal {
567 subject: "subj-1".into(),
568 issuer: "iss".into(),
569 audience: vec!["a1".into()],
570 scopes: vec!["s1".into()],
571 roles: vec!["r1".into()],
572 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
573 };
574 let s = format!("{principal:#?}");
575 assert!(
576 s.contains("[REDACTED]"),
577 "pretty debug should show [REDACTED]"
578 );
579 assert!(
580 !s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
581 "pretty debug should NOT contain raw claim value"
582 );
583 }
584
585 #[test]
586 fn principal_serialize_preserves_claims() {
587 let principal = Principal {
588 subject: "subj-1".into(),
589 issuer: "iss".into(),
590 audience: vec!["a1".into()],
591 scopes: vec!["s1".into()],
592 roles: vec!["r1".into()],
593 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
594 };
595 let s = serde_json::to_string(&principal).unwrap();
596 assert!(
597 s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
598 "serialization should preserve raw claim value"
599 );
600 }
601
602 #[test]
603 fn access_mode_debug_redacts_policy() {
604 let mode = AccessMode::Authorized(Arc::new(grant_policy()));
605 let debug = format!("{mode:?}");
606 assert!(debug.contains("<SecurityPolicy>"));
607 assert!(!debug.contains("GrantPolicy"));
608 }
609
610 #[test]
611 fn transport_id_derives_all() {
612 fn name(t: TransportId) -> &'static str {
613 match t {
614 TransportId::Http => "http",
615 TransportId::Ws => "ws",
616 TransportId::Grpc => "grpc",
617 TransportId::Mcp => "mcp",
618 TransportId::Wasm => "wasm",
619 }
620 }
621 assert_eq!(name(TransportId::Http), "http");
622 assert_eq!(name(TransportId::Ws), "ws");
623 assert_eq!(name(TransportId::Grpc), "grpc");
624 assert_eq!(name(TransportId::Mcp), "mcp");
625 assert_eq!(name(TransportId::Wasm), "wasm");
626 }
627
628 #[test]
629 fn route_security_plan_clone_debug() {
630 let plan = RouteSecurityPlan {
631 access_mode: AccessMode::Authenticated,
632 provider_ref: Some("idp-a".to_string()),
633 transport: TransportId::Http,
634 credential_sources: vec![CredentialSource::AuthorizationHeader],
635 audience_binding: None,
636 };
637 let cloned = plan.clone();
638 assert_eq!(cloned.provider_ref, plan.provider_ref);
639 assert_eq!(cloned.transport, plan.transport);
640 assert_eq!(cloned.credential_sources, plan.credential_sources);
641 assert!(matches!(cloned.access_mode, AccessMode::Authenticated));
642 assert!(cloned.audience_binding.is_none());
643
644 let debug = format!("{plan:?}");
645 assert!(debug.contains(r#"provider_ref: Some("idp-a")"#));
646 assert!(!debug.contains("GrantPolicy"));
647 }
648}