Skip to main content

camel_auth/
built_in.rs

1use async_trait::async_trait;
2use std::sync::Arc;
3
4use camel_api::security_policy::{AuthorizationDecision, Principal, SecurityPolicy};
5use camel_api::{CamelError, Exchange};
6
7use crate::token_authenticator::TokenAuthenticator;
8
9/// Property key used to store the authenticated principal in the exchange.
10pub const PRINCIPAL_KEY: &str = "camel.auth.principal";
11
12/// Extracts and validates a Bearer token from the `Authorization` header.
13///
14/// If the header is present, validates it via the supplied [`TokenAuthenticator`] and stores
15/// the resulting [`Principal`] in `PRINCIPAL_KEY` for downstream processors.
16///
17/// If no `Authorization` header is present, the behavior depends on
18/// `trust_upstream_principal`:
19/// - `true`: falls back to an already-populated principal in the exchange
20///   (e.g. set by an upstream authentication filter). **Spoofable** unless
21///   the route topology guarantees property integrity.
22/// - `false` (default): returns `Unauthenticated` — fail-closed. Use this
23///   unless the deployment explicitly trusts an upstream producer to
24///   authenticate and stamp the principal property.
25async fn authenticate(
26    exchange: &mut Exchange,
27    authenticator: &dyn TokenAuthenticator,
28    trust_upstream_principal: bool,
29) -> Result<Principal, CamelError> {
30    // Clone the token string so the borrow on exchange.input ends before the mut borrow for set_property.
31    let token = exchange
32        .input
33        .header_ic("authorization")
34        .and_then(|v| v.as_str())
35        .and_then(|s| s.strip_prefix("Bearer "))
36        .map(|s| s.to_string());
37
38    if let Some(token) = token {
39        let principal = authenticator.authenticate_bearer(&token).await?;
40        // Store for downstream processors
41        if let Ok(value) = serde_json::to_value(&principal) {
42            exchange.set_property(PRINCIPAL_KEY, value);
43        }
44        return Ok(principal);
45    }
46
47    if trust_upstream_principal {
48        extract_principal_from_exchange(exchange)
49    } else {
50        Err(CamelError::Unauthenticated(
51            "no Bearer token and trust_upstream_principal is false".into(),
52        ))
53    }
54}
55
56/// Extract a `Principal` from exchange properties, returning `Unauthenticated` if absent.
57fn extract_principal_from_exchange(exchange: &Exchange) -> Result<Principal, CamelError> {
58    exchange
59        .property(PRINCIPAL_KEY)
60        .and_then(|v| serde_json::from_value::<Principal>(v.clone()).ok())
61        .ok_or_else(|| CamelError::Unauthenticated("no principal in exchange".into()))
62}
63
64/// Role-based access control policy.
65///
66/// Validates the incoming request via a token authenticator (Bearer token) and evaluates whether
67/// the principal holds the required roles.
68/// When `all_required` is true, every listed role must be present.
69/// When `all_required` is false, at least one listed role must be present.
70pub struct RolePolicy {
71    required_roles: Vec<String>,
72    all_required: bool,
73    /// When `true`, fall back to the `camel.auth.principal` exchange property
74    /// if no Bearer token is present. Default `false` (fail-closed) — see
75    /// H1 in `docs/superpowers/specs/v1-sec-stabilization-spec.md`.
76    trust_upstream_principal: bool,
77    authenticator: Arc<dyn TokenAuthenticator>,
78}
79
80impl RolePolicy {
81    pub fn new(
82        required_roles: Vec<String>,
83        all_required: bool,
84        trust_upstream_principal: bool,
85        authenticator: Arc<dyn TokenAuthenticator>,
86    ) -> Self {
87        Self {
88            required_roles,
89            all_required,
90            trust_upstream_principal,
91            authenticator,
92        }
93    }
94}
95
96#[async_trait]
97impl SecurityPolicy for RolePolicy {
98    async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError> {
99        let principal = authenticate(
100            exchange,
101            &*self.authenticator,
102            self.trust_upstream_principal,
103        )
104        .await?;
105
106        let missing: Vec<String> = self
107            .required_roles
108            .iter()
109            .filter(|r| !principal.has_role(r))
110            .cloned()
111            .collect();
112
113        let granted = if self.all_required {
114            missing.is_empty()
115        } else {
116            self.required_roles.is_empty() || missing.len() < self.required_roles.len()
117        };
118
119        if granted {
120            Ok(AuthorizationDecision::Granted { principal })
121        } else {
122            let actual = principal.roles.clone();
123            Ok(AuthorizationDecision::Denied {
124                reason: format!("missing required role(s): {}", missing.join(", ")), // allow-secret
125                required: self.required_roles.clone(),
126                actual,
127            })
128        }
129    }
130}
131
132/// Scope-based access control policy.
133///
134/// Validates the incoming request via a token authenticator (Bearer token) and evaluates whether
135/// the principal holds the required scopes.
136/// When `all_required` is true, every listed scope must be present.
137/// When `all_required` is false, at least one listed scope must be present.
138pub struct ScopePolicy {
139    required_scopes: Vec<String>,
140    all_required: bool,
141    /// When `true`, fall back to the `camel.auth.principal` exchange property
142    /// if no Bearer token is present. Default `false` (fail-closed) — see
143    /// H1 in `docs/superpowers/specs/v1-sec-stabilization-spec.md`.
144    trust_upstream_principal: bool,
145    authenticator: Arc<dyn TokenAuthenticator>,
146}
147
148impl ScopePolicy {
149    pub fn new(
150        required_scopes: Vec<String>,
151        all_required: bool,
152        trust_upstream_principal: bool,
153        authenticator: Arc<dyn TokenAuthenticator>,
154    ) -> Self {
155        Self {
156            required_scopes,
157            all_required,
158            trust_upstream_principal,
159            authenticator,
160        }
161    }
162}
163
164#[async_trait]
165impl SecurityPolicy for ScopePolicy {
166    async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError> {
167        let principal = authenticate(
168            exchange,
169            &*self.authenticator,
170            self.trust_upstream_principal,
171        )
172        .await?;
173
174        let missing: Vec<String> = self
175            .required_scopes
176            .iter()
177            .filter(|s| !principal.has_scope(s))
178            .cloned()
179            .collect();
180
181        let granted = if self.all_required {
182            missing.is_empty()
183        } else {
184            self.required_scopes.is_empty() || missing.len() < self.required_scopes.len()
185        };
186
187        if granted {
188            Ok(AuthorizationDecision::Granted { principal })
189        } else {
190            let actual = principal.scopes.clone();
191            Ok(AuthorizationDecision::Denied {
192                reason: format!("missing required scope(s): {}", missing.join(", ")),
193                required: self.required_scopes.clone(),
194                actual,
195            })
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use crate::jwt::JwtValidator;
204    use crate::types::AuthError;
205    use camel_api::Message;
206
207    fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
208        Principal {
209            subject: "test-user".into(),
210            issuer: "test".into(),
211            audience: vec![],
212            roles: roles.iter().map(|s| s.to_string()).collect(),
213            scopes: scopes.iter().map(|s| s.to_string()).collect(),
214            claims: serde_json::Value::Null,
215        }
216    }
217
218    /// Mock validator that returns a fixed principal regardless of token content.
219    struct MockJwtValidator {
220        principal: Principal,
221    }
222
223    #[async_trait]
224    impl JwtValidator for MockJwtValidator {
225        async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
226            Ok(self.principal.clone())
227        }
228    }
229
230    fn mock_validator(principal: Principal) -> Arc<dyn TokenAuthenticator> {
231        Arc::new(MockJwtValidator { principal })
232    }
233
234    /// Build an exchange with a Bearer token in the Authorization header.
235    fn exchange_with_bearer(principal: Principal) -> Exchange {
236        let validator_principal = principal.clone();
237        let mut msg = Message::default();
238        msg.set_header(
239            "Authorization",
240            serde_json::Value::String("Bearer mock-token".into()),
241        );
242        // Also embed principal in exchange so fallback path is testable if needed
243        let mut ex = Exchange::new(msg);
244        let value = serde_json::to_value(&validator_principal).unwrap();
245        ex.set_property(PRINCIPAL_KEY, value);
246        ex
247    }
248
249    /// Build an exchange with the principal in the exchange property (no Bearer header).
250    fn exchange_with_principal(principal: Principal) -> Exchange {
251        let mut ex = Exchange::new(Message::default());
252        let value = serde_json::to_value(&principal).unwrap();
253        ex.set_property(PRINCIPAL_KEY, value);
254        ex
255    }
256
257    #[tokio::test]
258    async fn role_policy_grants_when_role_present() {
259        let principal = test_principal(vec!["admin"], vec![]);
260        let policy = RolePolicy::new(
261            vec!["admin".into()],
262            true,
263            false,
264            mock_validator(principal.clone()),
265        );
266        let mut ex = exchange_with_bearer(principal);
267        let decision = policy.evaluate(&mut ex).await.unwrap();
268        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
269    }
270
271    #[tokio::test]
272    async fn role_policy_denies_when_role_missing() {
273        let principal = test_principal(vec!["user"], vec![]);
274        let policy = RolePolicy::new(
275            vec!["admin".into()],
276            true,
277            false,
278            mock_validator(principal.clone()),
279        );
280        let mut ex = exchange_with_bearer(principal);
281        let decision = policy.evaluate(&mut ex).await.unwrap();
282        assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
283    }
284
285    #[tokio::test]
286    async fn role_policy_any_required() {
287        let principal = test_principal(vec!["user"], vec![]);
288        let policy = RolePolicy::new(
289            vec!["admin".into(), "user".into()],
290            false,
291            false,
292            mock_validator(principal.clone()),
293        );
294        let mut ex = exchange_with_bearer(principal);
295        let decision = policy.evaluate(&mut ex).await.unwrap();
296        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
297    }
298
299    #[tokio::test]
300    async fn scope_policy_grants() {
301        let principal = test_principal(vec![], vec!["read"]);
302        let policy = ScopePolicy::new(
303            vec!["read".into()],
304            true,
305            false,
306            mock_validator(principal.clone()),
307        );
308        let mut ex = exchange_with_bearer(principal);
309        let decision = policy.evaluate(&mut ex).await.unwrap();
310        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
311    }
312
313    #[tokio::test]
314    async fn unauthenticated_when_no_principal_and_no_header() {
315        // No Bearer header, no exchange property — validator never called
316        struct FailValidator;
317        #[async_trait]
318        impl JwtValidator for FailValidator {
319            async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
320                panic!("should not be called")
321            }
322        }
323        let policy = RolePolicy::new(vec!["admin".into()], true, false, Arc::new(FailValidator));
324        let mut ex = Exchange::new(Message::default());
325        let result = policy.evaluate(&mut ex).await;
326        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
327    }
328
329    #[tokio::test]
330    async fn principal_fallback_denied_by_default() {
331        // No Bearer header, but principal pre-populated (upstream filter scenario).
332        // Without `trust_upstream_principal` opt-in, MUST be denied.
333        let principal = test_principal(vec!["admin"], vec![]);
334        let policy = RolePolicy::new(
335            vec!["admin".into()],
336            true,
337            false, // trust_upstream_principal
338            mock_validator(principal.clone()),
339        );
340        let mut ex = exchange_with_principal(principal); // no Authorization header
341        let result = policy.evaluate(&mut ex).await;
342        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
343    }
344
345    #[tokio::test]
346    async fn principal_fallback_allowed_with_opt_in() {
347        // Same setup but `trust_upstream_principal=true` allows upstream-set principal.
348        let principal = test_principal(vec!["admin"], vec![]);
349        let policy = RolePolicy::new(
350            vec!["admin".into()],
351            true,
352            true, // trust_upstream_principal
353            mock_validator(principal.clone()),
354        );
355        let mut ex = exchange_with_principal(principal); // no Authorization header
356        let decision = policy.evaluate(&mut ex).await.unwrap();
357        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
358    }
359
360    #[tokio::test]
361    async fn scope_policy_fallback_denied_by_default() {
362        // No Bearer header, principal pre-populated — Scopes policy also gates.
363        let principal = test_principal(vec![], vec!["read"]);
364        let policy = ScopePolicy::new(
365            vec!["read".into()],
366            true,
367            false, // trust_upstream_principal
368            mock_validator(principal.clone()),
369        );
370        let mut ex = exchange_with_principal(principal);
371        let result = policy.evaluate(&mut ex).await;
372        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
373    }
374}