1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6use crate::{CamelError, Exchange};
7
8#[derive(Debug, 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
36#[derive(Debug, Clone, PartialEq)]
37pub enum AuthorizationDecision {
38 Granted {
39 principal: Principal,
40 },
41 Denied {
42 reason: String,
43 required: Vec<String>,
44 actual: Vec<String>,
45 },
46}
47
48impl std::fmt::Display for AuthorizationDecision {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 match self {
51 Self::Granted { principal } => {
52 write!(f, "Access granted for {}", principal.subject)
53 }
54 Self::Denied { reason, .. } => write!(f, "Access denied: {reason}"),
55 }
56 }
57}
58
59#[async_trait]
60pub trait SecurityPolicy: Send + Sync {
61 async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError>;
62}
63
64pub struct SecurityPolicyConfig {
65 pub policy: Arc<dyn SecurityPolicy>,
66}
67
68impl SecurityPolicyConfig {
69 pub fn new(policy: impl SecurityPolicy + 'static) -> Self {
70 Self {
71 policy: Arc::new(policy),
72 }
73 }
74
75 pub fn from_arc(policy: Arc<dyn SecurityPolicy>) -> Self {
76 Self { policy }
77 }
78}
79
80impl Clone for SecurityPolicyConfig {
81 fn clone(&self) -> Self {
82 Self {
83 policy: Arc::clone(&self.policy),
84 }
85 }
86}
87
88impl std::fmt::Debug for SecurityPolicyConfig {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_struct("SecurityPolicyConfig")
91 .field("policy", &"<SecurityPolicy>")
92 .finish()
93 }
94}
95
96pub const PRINCIPAL_SUBJECT_KEY: &str = "camel.auth.subject";
100pub const PRINCIPAL_ROLES_KEY: &str = "camel.auth.roles";
102pub const PRINCIPAL_SCOPES_KEY: &str = "camel.auth.scopes";
104pub const PRINCIPAL_ISSUER_KEY: &str = "camel.auth.issuer";
106pub const PRINCIPAL_CLAIMS_KEY: &str = "camel.auth.claims";
108pub const PRINCIPAL_AUDIENCE_KEY: &str = "camel.auth.audience";
110pub const PRINCIPAL_KEY: &str = "camel.auth.principal";
112
113pub fn store_principal_properties(exchange: &mut Exchange, principal: &Principal) {
115 exchange.set_property(PRINCIPAL_SUBJECT_KEY, principal.subject.clone());
116 exchange.set_property(
117 PRINCIPAL_ROLES_KEY,
118 serde_json::to_string(&principal.roles).unwrap_or_default(),
119 );
120 exchange.set_property(
121 PRINCIPAL_SCOPES_KEY,
122 serde_json::to_string(&principal.scopes).unwrap_or_default(),
123 );
124 exchange.set_property(PRINCIPAL_ISSUER_KEY, principal.issuer.clone());
125 exchange.set_property(
126 PRINCIPAL_CLAIMS_KEY,
127 serde_json::to_string(&principal.claims).unwrap_or_default(),
128 );
129 exchange.set_property(
130 PRINCIPAL_AUDIENCE_KEY,
131 serde_json::to_string(&principal.audience).unwrap_or_default(),
132 );
133 exchange.set_property(
134 PRINCIPAL_KEY,
135 serde_json::to_string(principal).unwrap_or_default(),
136 );
137}
138
139pub fn principal_from_exchange(exchange: &Exchange) -> Option<Principal> {
140 exchange
141 .property(PRINCIPAL_KEY)
142 .and_then(|v| v.as_str())
143 .and_then(|s| serde_json::from_str(s).ok())
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::Body;
150
151 fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
152 Principal {
153 subject: "user1".into(),
154 issuer: "test".into(),
155 audience: vec![],
156 scopes: scopes.into_iter().map(String::from).collect(),
157 roles: roles.into_iter().map(String::from).collect(),
158 claims: serde_json::Value::Null,
159 }
160 }
161
162 #[test]
163 fn principal_has_role_is_case_sensitive() {
164 let p = test_principal(vec!["Admin", "User"], vec![]);
165 assert!(!p.has_role("admin"));
166 assert!(!p.has_role("ADMIN"));
167 assert!(p.has_role("User"));
168 assert!(!p.has_role("guest"));
169 }
170
171 #[test]
172 fn principal_has_scope() {
173 let p = test_principal(vec![], vec!["read", "write"]);
174 assert!(p.has_scope("read"));
175 assert!(!p.has_scope("delete"));
176 }
177
178 #[test]
179 fn authorization_decision_granted_display() {
180 let p = test_principal(vec![], vec![]);
181 let d = AuthorizationDecision::Granted { principal: p };
182 assert!(format!("{d}").contains("user1"));
183 }
184
185 #[test]
186 fn authorization_decision_denied_display() {
187 let d = AuthorizationDecision::Denied {
188 reason: "missing role".into(),
189 required: vec!["admin".into()],
190 actual: vec![],
191 };
192 assert!(format!("{d}").contains("missing role"));
193 }
194
195 #[test]
196 fn security_policy_config_debug_redacts_policy() {
197 struct DummyPolicy;
198
199 #[async_trait]
200 impl SecurityPolicy for DummyPolicy {
201 async fn evaluate(
202 &self,
203 _exchange: &mut Exchange,
204 ) -> Result<AuthorizationDecision, CamelError> {
205 Ok(AuthorizationDecision::Granted {
206 principal: test_principal(vec![], vec![]),
207 })
208 }
209 }
210
211 let config = SecurityPolicyConfig::new(DummyPolicy);
212 let debug = format!("{config:?}");
213 assert!(debug.contains("SecurityPolicyConfig"));
214 assert!(debug.contains("<SecurityPolicy>"));
215 }
216
217 #[test]
218 fn store_principal_properties_populates_all_keys() {
219 let principal = Principal {
220 subject: "alice".into(),
221 issuer: "keycloak".into(),
222 audience: vec!["api".into()],
223 scopes: vec!["read".into(), "write".into()],
224 roles: vec!["admin".into()],
225 claims: serde_json::json!({"sub": "alice", "custom": true}),
226 };
227 let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
228 store_principal_properties(&mut exchange, &principal);
229
230 assert_eq!(
231 exchange.property(PRINCIPAL_SUBJECT_KEY).unwrap(),
232 &serde_json::Value::String("alice".into())
233 );
234 assert_eq!(
235 exchange.property(PRINCIPAL_ISSUER_KEY).unwrap(),
236 &serde_json::Value::String("keycloak".into())
237 );
238 let roles: Vec<String> = serde_json::from_str(
239 exchange
240 .property(PRINCIPAL_ROLES_KEY)
241 .unwrap()
242 .as_str()
243 .unwrap(),
244 )
245 .unwrap();
246 assert_eq!(roles, vec!["admin"]);
247 let scopes: Vec<String> = serde_json::from_str(
248 exchange
249 .property(PRINCIPAL_SCOPES_KEY)
250 .unwrap()
251 .as_str()
252 .unwrap(),
253 )
254 .unwrap();
255 assert_eq!(scopes, vec!["read", "write"]);
256 let audience: Vec<String> = serde_json::from_str(
257 exchange
258 .property(PRINCIPAL_AUDIENCE_KEY)
259 .unwrap()
260 .as_str()
261 .unwrap(),
262 )
263 .unwrap();
264 assert_eq!(audience, vec!["api"]);
265 let claims: serde_json::Value = serde_json::from_str(
266 exchange
267 .property(PRINCIPAL_CLAIMS_KEY)
268 .unwrap()
269 .as_str()
270 .unwrap(),
271 )
272 .unwrap();
273 assert!(claims.as_object().unwrap().contains_key("custom"));
274 let full: serde_json::Value =
275 serde_json::from_str(exchange.property(PRINCIPAL_KEY).unwrap().as_str().unwrap())
276 .unwrap();
277 assert_eq!(full["subject"], "alice");
278 }
279
280 #[test]
281 fn security_policy_config_clone() {
282 struct DummyPolicy;
283
284 #[async_trait]
285 impl SecurityPolicy for DummyPolicy {
286 async fn evaluate(
287 &self,
288 _exchange: &mut Exchange,
289 ) -> Result<AuthorizationDecision, CamelError> {
290 Ok(AuthorizationDecision::Granted {
291 principal: test_principal(vec![], vec![]),
292 })
293 }
294 }
295
296 let config = SecurityPolicyConfig::new(DummyPolicy);
297 let cloned = config.clone();
298 assert!(Arc::ptr_eq(&config.policy, &cloned.policy));
300 }
301
302 #[test]
303 fn test_principal_from_exchange_round_trip() {
304 let principal = Principal {
305 subject: "bob".into(),
306 issuer: "keycloak".into(),
307 audience: vec!["api".into()],
308 scopes: vec!["read".into()],
309 roles: vec!["user".into()],
310 claims: serde_json::json!({"sub": "bob"}),
311 };
312 let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
313 store_principal_properties(&mut exchange, &principal);
314
315 let recovered = principal_from_exchange(&exchange).expect("principal should be recovered");
316 assert_eq!(recovered.subject, "bob");
317 assert_eq!(recovered.issuer, "keycloak");
318 assert_eq!(recovered.audience, vec!["api"]);
319 assert_eq!(recovered.scopes, vec!["read"]);
320 assert_eq!(recovered.roles, vec!["user"]);
321 }
322}