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