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 struct SecurityPolicyConfig {
81 pub policy: Arc<dyn SecurityPolicy>,
82}
83
84impl SecurityPolicyConfig {
85 pub fn new(policy: impl SecurityPolicy + 'static) -> Self {
86 Self {
87 policy: Arc::new(policy),
88 }
89 }
90
91 pub fn from_arc(policy: Arc<dyn SecurityPolicy>) -> Self {
92 Self { policy }
93 }
94}
95
96impl Clone for SecurityPolicyConfig {
97 fn clone(&self) -> Self {
98 Self {
99 policy: Arc::clone(&self.policy),
100 }
101 }
102}
103
104impl std::fmt::Debug for SecurityPolicyConfig {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 f.debug_struct("SecurityPolicyConfig")
107 .field("policy", &"<SecurityPolicy>")
108 .finish()
109 }
110}
111
112pub const PRINCIPAL_SUBJECT_KEY: &str = "camel.auth.subject";
116pub const PRINCIPAL_ROLES_KEY: &str = "camel.auth.roles";
118pub const PRINCIPAL_SCOPES_KEY: &str = "camel.auth.scopes";
120pub const PRINCIPAL_ISSUER_KEY: &str = "camel.auth.issuer";
122pub const PRINCIPAL_CLAIMS_KEY: &str = "camel.auth.claims";
124pub const PRINCIPAL_AUDIENCE_KEY: &str = "camel.auth.audience";
126pub const PRINCIPAL_KEY: &str = "camel.auth.principal";
128
129pub fn store_principal_properties(exchange: &mut Exchange, principal: &Principal) {
131 exchange.set_property(PRINCIPAL_SUBJECT_KEY, principal.subject.clone());
132 exchange.set_property(
133 PRINCIPAL_ROLES_KEY,
134 serde_json::to_string(&principal.roles).unwrap_or_default(),
135 );
136 exchange.set_property(
137 PRINCIPAL_SCOPES_KEY,
138 serde_json::to_string(&principal.scopes).unwrap_or_default(),
139 );
140 exchange.set_property(PRINCIPAL_ISSUER_KEY, principal.issuer.clone());
141 exchange.set_property(
142 PRINCIPAL_CLAIMS_KEY,
143 serde_json::to_string(&principal.claims).unwrap_or_default(),
144 );
145 exchange.set_property(
146 PRINCIPAL_AUDIENCE_KEY,
147 serde_json::to_string(&principal.audience).unwrap_or_default(),
148 );
149 exchange.set_property(
150 PRINCIPAL_KEY,
151 serde_json::to_string(principal).unwrap_or_default(),
152 );
153}
154
155pub fn principal_from_exchange(exchange: &Exchange) -> Option<Principal> {
156 exchange
157 .property(PRINCIPAL_KEY)
158 .and_then(|v| v.as_str())
159 .and_then(|s| serde_json::from_str(s).ok())
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::Body;
166
167 fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
168 Principal {
169 subject: "user1".into(),
170 issuer: "test".into(),
171 audience: vec![],
172 scopes: scopes.into_iter().map(String::from).collect(),
173 roles: roles.into_iter().map(String::from).collect(),
174 claims: serde_json::Value::Null,
175 }
176 }
177
178 #[test]
179 fn principal_has_role_is_case_sensitive() {
180 let p = test_principal(vec!["Admin", "User"], vec![]);
181 assert!(!p.has_role("admin"));
182 assert!(!p.has_role("ADMIN"));
183 assert!(p.has_role("User"));
184 assert!(!p.has_role("guest"));
185 }
186
187 #[test]
188 fn principal_has_scope() {
189 let p = test_principal(vec![], vec!["read", "write"]);
190 assert!(p.has_scope("read"));
191 assert!(!p.has_scope("delete"));
192 }
193
194 #[test]
195 fn authorization_decision_granted_display() {
196 let p = test_principal(vec![], vec![]);
197 let d = AuthorizationDecision::Granted { principal: p };
198 assert!(format!("{d}").contains("user1"));
199 }
200
201 #[test]
202 fn authorization_decision_denied_display() {
203 let d = AuthorizationDecision::Denied {
204 reason: "missing role".into(),
205 required: vec!["admin".into()],
206 actual: vec![],
207 };
208 assert!(format!("{d}").contains("missing role"));
209 }
210
211 #[test]
212 fn security_policy_config_debug_redacts_policy() {
213 struct DummyPolicy;
214
215 #[async_trait]
216 impl SecurityPolicy for DummyPolicy {
217 async fn evaluate(
218 &self,
219 _exchange: &mut Exchange,
220 ) -> Result<AuthorizationDecision, CamelError> {
221 Ok(AuthorizationDecision::Granted {
222 principal: test_principal(vec![], vec![]),
223 })
224 }
225 }
226
227 let config = SecurityPolicyConfig::new(DummyPolicy);
228 let debug = format!("{config:?}");
229 assert!(debug.contains("SecurityPolicyConfig"));
230 assert!(debug.contains("<SecurityPolicy>"));
231 }
232
233 #[test]
234 fn store_principal_properties_populates_all_keys() {
235 let principal = Principal {
236 subject: "alice".into(),
237 issuer: "keycloak".into(),
238 audience: vec!["api".into()],
239 scopes: vec!["read".into(), "write".into()],
240 roles: vec!["admin".into()],
241 claims: serde_json::json!({"sub": "alice", "custom": true}),
242 };
243 let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
244 store_principal_properties(&mut exchange, &principal);
245
246 assert_eq!(
247 exchange.property(PRINCIPAL_SUBJECT_KEY).unwrap(),
248 &serde_json::Value::String("alice".into())
249 );
250 assert_eq!(
251 exchange.property(PRINCIPAL_ISSUER_KEY).unwrap(),
252 &serde_json::Value::String("keycloak".into())
253 );
254 let roles: Vec<String> = serde_json::from_str(
255 exchange
256 .property(PRINCIPAL_ROLES_KEY)
257 .unwrap()
258 .as_str()
259 .unwrap(),
260 )
261 .unwrap();
262 assert_eq!(roles, vec!["admin"]);
263 let scopes: Vec<String> = serde_json::from_str(
264 exchange
265 .property(PRINCIPAL_SCOPES_KEY)
266 .unwrap()
267 .as_str()
268 .unwrap(),
269 )
270 .unwrap();
271 assert_eq!(scopes, vec!["read", "write"]);
272 let audience: Vec<String> = serde_json::from_str(
273 exchange
274 .property(PRINCIPAL_AUDIENCE_KEY)
275 .unwrap()
276 .as_str()
277 .unwrap(),
278 )
279 .unwrap();
280 assert_eq!(audience, vec!["api"]);
281 let claims: serde_json::Value = serde_json::from_str(
282 exchange
283 .property(PRINCIPAL_CLAIMS_KEY)
284 .unwrap()
285 .as_str()
286 .unwrap(),
287 )
288 .unwrap();
289 assert!(claims.as_object().unwrap().contains_key("custom"));
290 let full: serde_json::Value =
291 serde_json::from_str(exchange.property(PRINCIPAL_KEY).unwrap().as_str().unwrap())
292 .unwrap();
293 assert_eq!(full["subject"], "alice");
294 }
295
296 #[test]
297 fn security_policy_config_clone() {
298 struct DummyPolicy;
299
300 #[async_trait]
301 impl SecurityPolicy for DummyPolicy {
302 async fn evaluate(
303 &self,
304 _exchange: &mut Exchange,
305 ) -> Result<AuthorizationDecision, CamelError> {
306 Ok(AuthorizationDecision::Granted {
307 principal: test_principal(vec![], vec![]),
308 })
309 }
310 }
311
312 let config = SecurityPolicyConfig::new(DummyPolicy);
313 let cloned = config.clone();
314 assert!(Arc::ptr_eq(&config.policy, &cloned.policy));
316 }
317
318 #[test]
319 fn test_principal_from_exchange_round_trip() {
320 let principal = Principal {
321 subject: "bob".into(),
322 issuer: "keycloak".into(),
323 audience: vec!["api".into()],
324 scopes: vec!["read".into()],
325 roles: vec!["user".into()],
326 claims: serde_json::json!({"sub": "bob"}),
327 };
328 let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
329 store_principal_properties(&mut exchange, &principal);
330
331 let recovered = principal_from_exchange(&exchange).expect("principal should be recovered");
332 assert_eq!(recovered.subject, "bob");
333 assert_eq!(recovered.issuer, "keycloak");
334 assert_eq!(recovered.audience, vec!["api"]);
335 assert_eq!(recovered.scopes, vec!["read"]);
336 assert_eq!(recovered.roles, vec!["user"]);
337 }
338
339 #[test]
340 fn principal_debug_redacts_claims_compact() {
341 let principal = Principal {
342 subject: "subj-1".into(),
343 issuer: "iss".into(),
344 audience: vec!["a1".into()],
345 scopes: vec!["s1".into()],
346 roles: vec!["r1".into()],
347 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
348 };
349 let s = format!("{principal:?}");
350 assert!(
351 s.contains("claims: \"[REDACTED]\""),
352 "compact debug should show [REDACTED] for claims"
353 );
354 assert!(
355 !s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
356 "compact debug should NOT contain raw claim value"
357 );
358 assert!(s.contains("subj-1"), "compact debug should contain subject");
359 assert!(s.contains("iss"), "compact debug should contain issuer");
360 assert!(s.contains("a1"), "compact debug should contain audience");
361 assert!(s.contains("s1"), "compact debug should contain scopes");
362 assert!(s.contains("r1"), "compact debug should contain roles");
363 }
364
365 #[test]
366 fn principal_debug_redacts_claims_pretty() {
367 let principal = Principal {
368 subject: "subj-1".into(),
369 issuer: "iss".into(),
370 audience: vec!["a1".into()],
371 scopes: vec!["s1".into()],
372 roles: vec!["r1".into()],
373 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
374 };
375 let s = format!("{principal:#?}");
376 assert!(
377 s.contains("[REDACTED]"),
378 "pretty debug should show [REDACTED]"
379 );
380 assert!(
381 !s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
382 "pretty debug should NOT contain raw claim value"
383 );
384 }
385
386 #[test]
387 fn principal_serialize_preserves_claims() {
388 let principal = Principal {
389 subject: "subj-1".into(),
390 issuer: "iss".into(),
391 audience: vec!["a1".into()],
392 scopes: vec!["s1".into()],
393 roles: vec!["r1".into()],
394 claims: serde_json::json!({"piid": "SENTINEL_CLAIM_VALUE_9kq2"}),
395 };
396 let s = serde_json::to_string(&principal).unwrap();
397 assert!(
398 s.contains("SENTINEL_CLAIM_VALUE_9kq2"),
399 "serialization should preserve raw claim value"
400 );
401 }
402}