Skip to main content

camel_auth/
permission_policy.rs

1//! Bridge between [`SecurityPolicy`] (Exchange-level) and [`PermissionEvaluator`] (permission-level).
2//!
3//! [`PermissionPolicy`] resolves resource and action from the exchange (literal, header, or property),
4//! builds an evaluation context from configured headers/properties, and delegates to a
5//! [`PermissionEvaluator`] implementation.
6
7use 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
18/// Where to read the resource/action label for error messages.
19trait 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
33/// SecurityPolicy implementation that delegates to a [`PermissionEvaluator`].
34///
35/// Resolves resource and action from the exchange, builds an evaluation context
36/// from configured headers and properties, and translates the evaluator's decision
37/// into an [`AuthorizationDecision`].
38pub 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    // --- Mock evaluators ---
161
162    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    /// Evaluator that checks the resource field matches an expected value.
191    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    /// Evaluator that asserts specific keys are present/absent in the context.
215    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    // --- Tests ---
249
250    #[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        // X-Resource header NOT set → cannot resolve
344        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}