1use std::sync::Arc;
8
9use async_trait::async_trait;
10use camel_api::security_policy::{AuthContext, AuthorizationDecision, SecurityPolicy};
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(
105 &self,
106 exchange: &mut Exchange,
107 auth: &AuthContext<'_>,
108 ) -> Result<AuthorizationDecision, CamelError> {
109 let principal = auth.principal.principal().clone();
110 let resource = Self::resolve_source(&self.resource, exchange)
111 .ok_or_else(|| CamelError::Unauthorized("cannot resolve permission resource".into()))?;
112 let action = Self::resolve_source(&self.action, exchange)
113 .ok_or_else(|| CamelError::Unauthorized("cannot resolve permission action".into()))?;
114 let context = self.build_context(exchange);
115 let request = PermissionRequest {
116 principal: principal.clone(),
117 resource,
118 action,
119 requested_scopes: self.scopes.clone(),
120 context,
121 };
122 match self.evaluator.evaluate(request).await {
123 Ok(PermissionDecision::Granted) => Ok(AuthorizationDecision::Granted { principal }),
124 Ok(PermissionDecision::Denied { reason }) => Ok(AuthorizationDecision::Denied {
125 reason,
126 required: vec![format!("{}:{}", self.resource_label(), self.action_label())],
127 actual: vec![],
128 }),
129 Err(e) => Err(e.into()),
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::permission::{
138 PermissionContextConfig, PermissionDecision, PermissionEvaluator, PermissionRequest,
139 PermissionValueSource,
140 };
141 use crate::types::AuthError;
142 use camel_api::Message;
143 use camel_api::security_policy::{AuthContext, AuthPrincipal, Principal, TransportId};
144 use serde_json::json;
145
146 fn test_principal() -> Principal {
147 Principal {
148 subject: "alice".into(),
149 issuer: "https://keycloak.example.com/realms/test".into(),
150 audience: vec!["camel-api".into()],
151 roles: vec!["admin".into()],
152 scopes: vec!["read".into()],
153 claims: json!({}),
154 }
155 }
156
157 struct TestPrincipal(Principal);
158
159 impl AuthPrincipal for TestPrincipal {
160 fn principal(&self) -> &Principal {
161 &self.0
162 }
163 fn provider_id(&self) -> &str {
164 "test"
165 }
166 }
167
168 fn auth_ctx<'a>(principal: &'a TestPrincipal) -> AuthContext<'a> {
169 AuthContext {
170 principal,
171 transport: TransportId::Http,
172 }
173 }
174
175 struct GrantEvaluator;
178
179 #[async_trait]
180 impl PermissionEvaluator for GrantEvaluator {
181 async fn evaluate(
182 &self,
183 _request: PermissionRequest,
184 ) -> Result<PermissionDecision, AuthError> {
185 Ok(PermissionDecision::Granted)
186 }
187 }
188
189 struct DenyEvaluator {
190 reason: String,
191 }
192
193 #[async_trait]
194 impl PermissionEvaluator for DenyEvaluator {
195 async fn evaluate(
196 &self,
197 _request: PermissionRequest,
198 ) -> Result<PermissionDecision, AuthError> {
199 Ok(PermissionDecision::Denied {
200 reason: self.reason.clone(),
201 })
202 }
203 }
204
205 struct CheckEvaluator {
207 expected_resource: String,
208 }
209
210 #[async_trait]
211 impl PermissionEvaluator for CheckEvaluator {
212 async fn evaluate(
213 &self,
214 request: PermissionRequest,
215 ) -> Result<PermissionDecision, AuthError> {
216 if request.resource == self.expected_resource {
217 Ok(PermissionDecision::Granted)
218 } else {
219 Ok(PermissionDecision::Denied {
220 reason: format!(
221 "expected resource '{}', got '{}'",
222 self.expected_resource, request.resource
223 ),
224 })
225 }
226 }
227 }
228
229 struct ContextCheckEvaluator {
231 must_have: String,
232 must_not_have: String,
233 }
234
235 #[async_trait]
236 impl PermissionEvaluator for ContextCheckEvaluator {
237 async fn evaluate(
238 &self,
239 request: PermissionRequest,
240 ) -> Result<PermissionDecision, AuthError> {
241 let ctx = request
242 .context
243 .as_object()
244 .expect("context should be an object");
245 if !ctx.contains_key(&self.must_have) {
246 return Ok(PermissionDecision::Denied {
247 reason: format!("context missing required key '{}'", self.must_have),
248 });
249 }
250 if ctx.contains_key(&self.must_not_have) {
251 return Ok(PermissionDecision::Denied {
252 reason: format!("context should not contain key '{}'", self.must_not_have),
253 });
254 }
255 Ok(PermissionDecision::Granted)
256 }
257 }
258
259 fn default_context_config() -> PermissionContextConfig {
260 PermissionContextConfig::default()
261 }
262
263 #[tokio::test]
266 async fn grants_when_evaluator_grants() {
267 let principal = TestPrincipal(test_principal());
268 let policy = PermissionPolicy::new(
269 Arc::new(GrantEvaluator),
270 PermissionValueSource::Literal("/orders".into()),
271 PermissionValueSource::Literal("read".into()),
272 vec![],
273 default_context_config(),
274 );
275 let mut ex = Exchange::new(Message::default());
276 let auth = auth_ctx(&principal);
277 let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
278 match decision {
279 AuthorizationDecision::Granted { principal: p } => {
280 assert_eq!(p.subject, "alice");
281 }
282 AuthorizationDecision::Denied { .. } => panic!("expected Granted, got Denied"),
283 _ => panic!("unexpected AuthorizationDecision variant"),
284 }
285 }
286
287 #[tokio::test]
288 async fn denies_when_evaluator_denies() {
289 let principal = TestPrincipal(test_principal());
290 let policy = PermissionPolicy::new(
291 Arc::new(DenyEvaluator {
292 reason: "insufficient scope".into(),
293 }),
294 PermissionValueSource::Literal("/orders".into()),
295 PermissionValueSource::Literal("write".into()),
296 vec![],
297 default_context_config(),
298 );
299 let mut ex = Exchange::new(Message::default());
300 let auth = auth_ctx(&principal);
301 let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
302 match decision {
303 AuthorizationDecision::Denied { reason, .. } => {
304 assert_eq!(reason, "insufficient scope");
305 }
306 AuthorizationDecision::Granted { .. } => panic!("expected Denied, got Granted"),
307 _ => panic!("unexpected AuthorizationDecision variant"),
308 }
309 }
310
311 #[tokio::test]
312 async fn resolves_header_source() {
313 let principal = TestPrincipal(test_principal());
314 let policy = PermissionPolicy::new(
315 Arc::new(CheckEvaluator {
316 expected_resource: "res-from-header".into(),
317 }),
318 PermissionValueSource::Header("X-Resource".into()),
319 PermissionValueSource::Literal("read".into()),
320 vec![],
321 default_context_config(),
322 );
323 let mut ex = Exchange::new(Message::default());
324 ex.input.set_header("X-Resource", "res-from-header");
325 let auth = auth_ctx(&principal);
326 let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
327 assert!(
328 matches!(decision, AuthorizationDecision::Granted { .. }),
329 "expected Granted, got {:?}",
330 decision
331 );
332 }
333
334 #[tokio::test]
335 async fn unauthorized_when_resource_cannot_be_resolved() {
336 let principal = TestPrincipal(test_principal());
337 let policy = PermissionPolicy::new(
338 Arc::new(GrantEvaluator),
339 PermissionValueSource::Header("X-Resource".into()),
340 PermissionValueSource::Literal("read".into()),
341 vec![],
342 default_context_config(),
343 );
344 let mut ex = Exchange::new(Message::default());
345 let auth = auth_ctx(&principal);
347 let result = policy.evaluate(&mut ex, &auth).await;
348 assert!(
349 matches!(result, Err(CamelError::Unauthorized(ref msg)) if msg.contains("cannot resolve permission resource")),
350 "expected Unauthorized error for unresolved resource, got {:?}",
351 result
352 );
353 }
354
355 #[tokio::test]
356 async fn context_includes_only_configured_fields() {
357 let principal = TestPrincipal(test_principal());
358 let context_config = PermissionContextConfig {
359 include_headers: vec!["X-Tenant".into()],
360 include_properties: vec![],
361 };
362 let policy = PermissionPolicy::new(
363 Arc::new(ContextCheckEvaluator {
364 must_have: "X-Tenant".into(),
365 must_not_have: "X-Other".into(),
366 }),
367 PermissionValueSource::Literal("/orders".into()),
368 PermissionValueSource::Literal("read".into()),
369 vec![],
370 context_config,
371 );
372 let mut ex = Exchange::new(Message::default());
373 ex.input.set_header("X-Tenant", "acme");
374 ex.input.set_header("X-Other", "should-not-appear");
375 let auth = auth_ctx(&principal);
376 let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
377 assert!(
378 matches!(decision, AuthorizationDecision::Granted { .. }),
379 "expected Granted, got {:?}",
380 decision
381 );
382 }
383}