1use std::sync::Arc;
8
9use async_trait::async_trait;
10use camel_api::security_policy::{AuthorizationDecision, SecurityPolicy, principal_from_exchange};
11use camel_api::{CamelError, Exchange};
12
13use crate::permission::{
14 PermissionContextConfig, PermissionDecision, PermissionEvaluator, PermissionRequest,
15 PermissionValueSource,
16};
17
18trait LabelSource {
20 fn label(&self) -> String;
21}
22
23impl LabelSource for PermissionValueSource {
24 fn label(&self) -> String {
25 match self {
26 PermissionValueSource::Literal(s) => s.clone(),
27 PermissionValueSource::Header(name) => format!("header:{name}"),
28 PermissionValueSource::Property(name) => format!("property:{name}"),
29 }
30 }
31}
32
33pub struct PermissionPolicy {
39 evaluator: Arc<dyn PermissionEvaluator>,
40 resource: PermissionValueSource,
41 action: PermissionValueSource,
42 scopes: Vec<String>,
43 context: PermissionContextConfig,
44}
45
46impl PermissionPolicy {
47 pub fn new(
48 evaluator: Arc<dyn PermissionEvaluator>,
49 resource: PermissionValueSource,
50 action: PermissionValueSource,
51 scopes: Vec<String>,
52 context: PermissionContextConfig,
53 ) -> Self {
54 Self {
55 evaluator,
56 resource,
57 action,
58 scopes,
59 context,
60 }
61 }
62
63 fn resolve_source(source: &PermissionValueSource, exchange: &Exchange) -> Option<String> {
64 match source {
65 PermissionValueSource::Literal(s) => Some(s.clone()),
66 PermissionValueSource::Header(name) => exchange
67 .input
68 .header(name)
69 .and_then(|v| v.as_str())
70 .map(String::from),
71 PermissionValueSource::Property(name) => exchange
72 .property(name)
73 .and_then(|v| v.as_str())
74 .map(String::from),
75 }
76 }
77
78 fn build_context(&self, exchange: &Exchange) -> serde_json::Value {
79 let mut map = serde_json::Map::new();
80 for name in &self.context.include_headers {
81 if let Some(v) = exchange.input.header(name) {
82 map.insert(name.clone(), v.clone());
83 }
84 }
85 for name in &self.context.include_properties {
86 if let Some(v) = exchange.property(name) {
87 map.insert(name.clone(), v.clone());
88 }
89 }
90 serde_json::Value::Object(map)
91 }
92
93 fn resource_label(&self) -> String {
94 self.resource.label()
95 }
96
97 fn action_label(&self) -> String {
98 self.action.label()
99 }
100}
101
102#[async_trait]
103impl SecurityPolicy for PermissionPolicy {
104 async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError> {
105 let principal = principal_from_exchange(exchange)
106 .ok_or_else(|| CamelError::Unauthenticated("no principal in exchange".into()))?;
107 let resource = Self::resolve_source(&self.resource, exchange)
108 .ok_or_else(|| CamelError::Unauthorized("cannot resolve permission resource".into()))?;
109 let action = Self::resolve_source(&self.action, exchange)
110 .ok_or_else(|| CamelError::Unauthorized("cannot resolve permission action".into()))?;
111 let context = self.build_context(exchange);
112 let request = PermissionRequest {
113 principal: principal.clone(),
114 resource,
115 action,
116 requested_scopes: self.scopes.clone(),
117 context,
118 };
119 match self.evaluator.evaluate(request).await {
120 Ok(PermissionDecision::Granted) => Ok(AuthorizationDecision::Granted { principal }),
121 Ok(PermissionDecision::Denied { reason }) => Ok(AuthorizationDecision::Denied {
122 reason,
123 required: vec![format!("{}:{}", self.resource_label(), self.action_label())],
124 actual: vec![],
125 }),
126 Err(e) => Err(e.into()),
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use crate::permission::{
135 PermissionContextConfig, PermissionDecision, PermissionEvaluator, PermissionRequest,
136 PermissionValueSource,
137 };
138 use crate::types::AuthError;
139 use camel_api::Message;
140 use camel_api::security_policy::{Principal, store_principal_properties};
141 use serde_json::json;
142
143 fn test_principal() -> Principal {
144 Principal {
145 subject: "alice".into(),
146 issuer: "https://keycloak.example.com/realms/test".into(),
147 audience: vec!["camel-api".into()],
148 roles: vec!["admin".into()],
149 scopes: vec!["read".into()],
150 claims: json!({}),
151 }
152 }
153
154 fn exchange_with_principal(principal: &Principal) -> Exchange {
155 let mut ex = Exchange::new(Message::default());
156 store_principal_properties(&mut ex, principal);
157 ex
158 }
159
160 struct GrantEvaluator;
163
164 #[async_trait]
165 impl PermissionEvaluator for GrantEvaluator {
166 async fn evaluate(
167 &self,
168 _request: PermissionRequest,
169 ) -> Result<PermissionDecision, AuthError> {
170 Ok(PermissionDecision::Granted)
171 }
172 }
173
174 struct DenyEvaluator {
175 reason: String,
176 }
177
178 #[async_trait]
179 impl PermissionEvaluator for DenyEvaluator {
180 async fn evaluate(
181 &self,
182 _request: PermissionRequest,
183 ) -> Result<PermissionDecision, AuthError> {
184 Ok(PermissionDecision::Denied {
185 reason: self.reason.clone(),
186 })
187 }
188 }
189
190 struct CheckEvaluator {
192 expected_resource: String,
193 }
194
195 #[async_trait]
196 impl PermissionEvaluator for CheckEvaluator {
197 async fn evaluate(
198 &self,
199 request: PermissionRequest,
200 ) -> Result<PermissionDecision, AuthError> {
201 if request.resource == self.expected_resource {
202 Ok(PermissionDecision::Granted)
203 } else {
204 Ok(PermissionDecision::Denied {
205 reason: format!(
206 "expected resource '{}', got '{}'",
207 self.expected_resource, request.resource
208 ),
209 })
210 }
211 }
212 }
213
214 struct ContextCheckEvaluator {
216 must_have: String,
217 must_not_have: String,
218 }
219
220 #[async_trait]
221 impl PermissionEvaluator for ContextCheckEvaluator {
222 async fn evaluate(
223 &self,
224 request: PermissionRequest,
225 ) -> Result<PermissionDecision, AuthError> {
226 let ctx = request
227 .context
228 .as_object()
229 .expect("context should be an object");
230 if !ctx.contains_key(&self.must_have) {
231 return Ok(PermissionDecision::Denied {
232 reason: format!("context missing required key '{}'", self.must_have),
233 });
234 }
235 if ctx.contains_key(&self.must_not_have) {
236 return Ok(PermissionDecision::Denied {
237 reason: format!("context should not contain key '{}'", self.must_not_have),
238 });
239 }
240 Ok(PermissionDecision::Granted)
241 }
242 }
243
244 fn default_context_config() -> PermissionContextConfig {
245 PermissionContextConfig::default()
246 }
247
248 #[tokio::test]
251 async fn grants_when_evaluator_grants() {
252 let principal = test_principal();
253 let policy = PermissionPolicy::new(
254 Arc::new(GrantEvaluator),
255 PermissionValueSource::Literal("/orders".into()),
256 PermissionValueSource::Literal("read".into()),
257 vec![],
258 default_context_config(),
259 );
260 let mut ex = exchange_with_principal(&principal);
261 let decision = policy.evaluate(&mut ex).await.unwrap();
262 match decision {
263 AuthorizationDecision::Granted { principal: p } => {
264 assert_eq!(p.subject, "alice");
265 }
266 AuthorizationDecision::Denied { .. } => panic!("expected Granted, got Denied"),
267 }
268 }
269
270 #[tokio::test]
271 async fn denies_when_evaluator_denies() {
272 let principal = test_principal();
273 let policy = PermissionPolicy::new(
274 Arc::new(DenyEvaluator {
275 reason: "insufficient scope".into(),
276 }),
277 PermissionValueSource::Literal("/orders".into()),
278 PermissionValueSource::Literal("write".into()),
279 vec![],
280 default_context_config(),
281 );
282 let mut ex = exchange_with_principal(&principal);
283 let decision = policy.evaluate(&mut ex).await.unwrap();
284 match decision {
285 AuthorizationDecision::Denied { reason, .. } => {
286 assert_eq!(reason, "insufficient scope");
287 }
288 AuthorizationDecision::Granted { .. } => panic!("expected Denied, got Granted"),
289 }
290 }
291
292 #[tokio::test]
293 async fn unauthenticated_when_no_principal() {
294 let policy = PermissionPolicy::new(
295 Arc::new(GrantEvaluator),
296 PermissionValueSource::Literal("/orders".into()),
297 PermissionValueSource::Literal("read".into()),
298 vec![],
299 default_context_config(),
300 );
301 let mut ex = Exchange::new(Message::default());
302 let result = policy.evaluate(&mut ex).await;
303 assert!(
304 matches!(result, Err(CamelError::Unauthenticated(ref msg)) if msg.contains("no principal")),
305 "expected Unauthenticated error, got {:?}",
306 result
307 );
308 }
309
310 #[tokio::test]
311 async fn resolves_header_source() {
312 let principal = test_principal();
313 let policy = PermissionPolicy::new(
314 Arc::new(CheckEvaluator {
315 expected_resource: "res-from-header".into(),
316 }),
317 PermissionValueSource::Header("X-Resource".into()),
318 PermissionValueSource::Literal("read".into()),
319 vec![],
320 default_context_config(),
321 );
322 let mut ex = exchange_with_principal(&principal);
323 ex.input.set_header("X-Resource", "res-from-header");
324 let decision = policy.evaluate(&mut ex).await.unwrap();
325 assert!(
326 matches!(decision, AuthorizationDecision::Granted { .. }),
327 "expected Granted, got {:?}",
328 decision
329 );
330 }
331
332 #[tokio::test]
333 async fn unauthorized_when_resource_cannot_be_resolved() {
334 let principal = test_principal();
335 let policy = PermissionPolicy::new(
336 Arc::new(GrantEvaluator),
337 PermissionValueSource::Header("X-Resource".into()),
338 PermissionValueSource::Literal("read".into()),
339 vec![],
340 default_context_config(),
341 );
342 let mut ex = exchange_with_principal(&principal);
343 let result = policy.evaluate(&mut ex).await;
345 assert!(
346 matches!(result, Err(CamelError::Unauthorized(ref msg)) if msg.contains("cannot resolve permission resource")),
347 "expected Unauthorized error for unresolved resource, got {:?}",
348 result
349 );
350 }
351
352 #[tokio::test]
353 async fn context_includes_only_configured_fields() {
354 let principal = test_principal();
355 let context_config = PermissionContextConfig {
356 include_headers: vec!["X-Tenant".into()],
357 include_properties: vec![],
358 };
359 let policy = PermissionPolicy::new(
360 Arc::new(ContextCheckEvaluator {
361 must_have: "X-Tenant".into(),
362 must_not_have: "X-Other".into(),
363 }),
364 PermissionValueSource::Literal("/orders".into()),
365 PermissionValueSource::Literal("read".into()),
366 vec![],
367 context_config,
368 );
369 let mut ex = exchange_with_principal(&principal);
370 ex.input.set_header("X-Tenant", "acme");
371 ex.input.set_header("X-Other", "should-not-appear");
372 let decision = policy.evaluate(&mut ex).await.unwrap();
373 assert!(
374 matches!(decision, AuthorizationDecision::Granted { .. }),
375 "expected Granted, got {:?}",
376 decision
377 );
378 }
379}