Skip to main content

camel_auth/
built_in.rs

1use async_trait::async_trait;
2use std::sync::Arc;
3
4use camel_api::security_policy::{
5    AuthorizationDecision, CredentialSource, Principal, SecurityPolicy, principal_from_exchange,
6    store_principal_properties,
7};
8use camel_api::{CamelError, Exchange};
9
10use crate::credential_source::extract_token_from_exchange;
11use crate::token_authenticator::TokenAuthenticator;
12
13/// Property key used to store the authenticated principal in the exchange.
14///
15/// Re-exported from the camel-api contract so `camel_auth::built_in::PRINCIPAL_KEY`
16/// and `camel_api::security_policy::PRINCIPAL_KEY` are the same constant.
17pub use camel_api::security_policy::PRINCIPAL_KEY;
18
19/// Extracts and validates a credential from the route-declared sources.
20///
21/// If a source yields a token, validates it via the supplied [`TokenAuthenticator`] and stores
22/// the resulting [`Principal`] in `PRINCIPAL_KEY` for downstream processors.
23///
24/// If no source yields a token, the behavior depends on
25/// `trust_upstream_principal`:
26/// - `true`: falls back to an already-populated principal in the exchange
27///   (e.g. set by an upstream authentication filter). **Spoofable** unless
28///   the route topology guarantees property integrity.
29/// - `false` (default): returns `Unauthenticated` — fail-closed. Use this
30///   unless the deployment explicitly trusts an upstream producer to
31///   authenticate and stamp the principal property.
32async fn authenticate(
33    exchange: &mut Exchange,
34    authenticator: &dyn TokenAuthenticator,
35    trust_upstream_principal: bool,
36    sources: &[CredentialSource],
37) -> Result<Principal, CamelError> {
38    // Extraction returns an owned token, so the borrow on `exchange` ends before the mut borrow.
39    let token = extract_token_from_exchange(exchange, sources).map(|extracted| extracted.token);
40
41    if let Some(token) = token {
42        let principal = authenticator.authenticate_bearer(&token).await?;
43        // Store for downstream processors
44        store_principal_properties(exchange, &principal);
45        return Ok(principal);
46    }
47
48    if trust_upstream_principal {
49        extract_principal_from_exchange(exchange)
50    } else {
51        Err(CamelError::Unauthenticated(
52            "no Bearer token and trust_upstream_principal is false".into(),
53        ))
54    }
55}
56
57/// Extract a `Principal` from exchange properties, returning `Unauthenticated` if absent.
58///
59/// Delegates to the canonical `principal_from_exchange` reader so the trust
60/// branch consumes the same JSON-string format that `store_principal_properties`
61/// writes (a `from_value` read of a `to_string`-stored value silently missed the
62/// principal and turned every `trust_upstream_principal` grant into a 500).
63fn extract_principal_from_exchange(exchange: &Exchange) -> Result<Principal, CamelError> {
64    principal_from_exchange(exchange)
65        .ok_or_else(|| CamelError::Unauthenticated("no principal in exchange".into()))
66}
67
68/// Role-based access control policy.
69///
70/// Validates the incoming request via a token authenticator (Bearer token) and evaluates whether
71/// the principal holds the required roles.
72/// When `all_required` is true, every listed role must be present.
73/// When `all_required` is false, at least one listed role must be present.
74pub struct RolePolicy {
75    required_roles: Vec<String>,
76    all_required: bool,
77    /// When `true`, fall back to the `camel.auth.principal` exchange property
78    /// if no Bearer token is present. Default `false` (fail-closed) — see
79    /// H1 in `docs/superpowers/specs/v1-sec-stabilization-spec.md`.
80    trust_upstream_principal: bool,
81    authenticator: Arc<dyn TokenAuthenticator>,
82    credential_sources: Vec<CredentialSource>,
83}
84
85impl RolePolicy {
86    pub fn new(
87        required_roles: Vec<String>,
88        all_required: bool,
89        trust_upstream_principal: bool,
90        authenticator: Arc<dyn TokenAuthenticator>,
91        credential_sources: Vec<CredentialSource>,
92    ) -> Self {
93        Self {
94            required_roles,
95            all_required,
96            trust_upstream_principal,
97            authenticator,
98            credential_sources,
99        }
100    }
101
102    /// Credential sources the policy extracts tokens from, in declared order.
103    pub fn credential_sources(&self) -> &[CredentialSource] {
104        &self.credential_sources
105    }
106}
107
108#[async_trait]
109impl SecurityPolicy for RolePolicy {
110    async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError> {
111        let principal = authenticate(
112            exchange,
113            &*self.authenticator,
114            self.trust_upstream_principal,
115            &self.credential_sources,
116        )
117        .await?;
118
119        let missing: Vec<String> = self
120            .required_roles
121            .iter()
122            .filter(|r| !principal.has_role(r))
123            .cloned()
124            .collect();
125
126        let granted = if self.all_required {
127            missing.is_empty()
128        } else {
129            self.required_roles.is_empty() || missing.len() < self.required_roles.len()
130        };
131
132        if granted {
133            Ok(AuthorizationDecision::Granted { principal })
134        } else {
135            let actual = principal.roles.clone();
136            Ok(AuthorizationDecision::Denied {
137                reason: format!("missing required role(s): {}", missing.join(", ")), // allow-secret
138                required: self.required_roles.clone(),
139                actual,
140            })
141        }
142    }
143}
144
145/// Scope-based access control policy.
146///
147/// Validates the incoming request via a token authenticator (Bearer token) and evaluates whether
148/// the principal holds the required scopes.
149/// When `all_required` is true, every listed scope must be present.
150/// When `all_required` is false, at least one listed scope must be present.
151pub struct ScopePolicy {
152    required_scopes: Vec<String>,
153    all_required: bool,
154    /// When `true`, fall back to the `camel.auth.principal` exchange property
155    /// if no Bearer token is present. Default `false` (fail-closed) — see
156    /// H1 in `docs/superpowers/specs/v1-sec-stabilization-spec.md`.
157    trust_upstream_principal: bool,
158    authenticator: Arc<dyn TokenAuthenticator>,
159    credential_sources: Vec<CredentialSource>,
160}
161
162impl ScopePolicy {
163    pub fn new(
164        required_scopes: Vec<String>,
165        all_required: bool,
166        trust_upstream_principal: bool,
167        authenticator: Arc<dyn TokenAuthenticator>,
168        credential_sources: Vec<CredentialSource>,
169    ) -> Self {
170        Self {
171            required_scopes,
172            all_required,
173            trust_upstream_principal,
174            authenticator,
175            credential_sources,
176        }
177    }
178
179    /// Credential sources the policy extracts tokens from, in declared order.
180    pub fn credential_sources(&self) -> &[CredentialSource] {
181        &self.credential_sources
182    }
183}
184
185#[async_trait]
186impl SecurityPolicy for ScopePolicy {
187    async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError> {
188        let principal = authenticate(
189            exchange,
190            &*self.authenticator,
191            self.trust_upstream_principal,
192            &self.credential_sources,
193        )
194        .await?;
195
196        let missing: Vec<String> = self
197            .required_scopes
198            .iter()
199            .filter(|s| !principal.has_scope(s))
200            .cloned()
201            .collect();
202
203        let granted = if self.all_required {
204            missing.is_empty()
205        } else {
206            self.required_scopes.is_empty() || missing.len() < self.required_scopes.len()
207        };
208
209        if granted {
210            Ok(AuthorizationDecision::Granted { principal })
211        } else {
212            let actual = principal.scopes.clone();
213            Ok(AuthorizationDecision::Denied {
214                reason: format!("missing required scope(s): {}", missing.join(", ")),
215                required: self.required_scopes.clone(),
216                actual,
217            })
218        }
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use crate::jwt::JwtValidator;
226    use crate::native_auth::{
227        NativeCredential, NativeCredentialSecret, NativeCredentialStore, StaticTokenAuthenticator,
228    };
229    use crate::types::AuthError;
230    use camel_api::Message;
231
232    fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
233        Principal {
234            subject: "test-user".into(),
235            issuer: "test".into(),
236            audience: vec![],
237            roles: roles.iter().map(|s| s.to_string()).collect(),
238            scopes: scopes.iter().map(|s| s.to_string()).collect(),
239            claims: serde_json::Value::Null,
240        }
241    }
242
243    /// Mock validator that returns a fixed principal regardless of token content.
244    struct MockJwtValidator {
245        principal: Principal,
246    }
247
248    #[async_trait]
249    impl JwtValidator for MockJwtValidator {
250        async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
251            Ok(self.principal.clone())
252        }
253    }
254
255    fn mock_validator(principal: Principal) -> Arc<dyn TokenAuthenticator> {
256        Arc::new(MockJwtValidator { principal })
257    }
258
259    /// Build a static authenticator over a native store seeded with `credential`.
260    fn store_seeded_authenticator(
261        credential: &str,
262        principal: Principal,
263    ) -> Arc<dyn TokenAuthenticator> {
264        let store = NativeCredentialStore::try_new(vec![NativeCredential {
265            secret: NativeCredentialSecret::Plaintext {
266                value: zeroize::Zeroizing::new(credential.to_string()),
267            },
268            principal,
269        }])
270        .unwrap();
271        Arc::new(StaticTokenAuthenticator::new(store))
272    }
273
274    /// Build an exchange with a Bearer token in the Authorization header.
275    fn exchange_with_bearer(principal: Principal) -> Exchange {
276        let mut msg = Message::default();
277        msg.set_header(
278            "Authorization",
279            serde_json::Value::String("Bearer mock-token".into()),
280        );
281        // Also embed principal in exchange so fallback path is testable if needed.
282        let mut ex = Exchange::new(msg);
283        store_principal_properties(&mut ex, &principal);
284        ex
285    }
286
287    /// Build an exchange with the principal in the exchange property (no Bearer header).
288    ///
289    /// Uses the canonical writer so the property format matches what the WS
290    /// component produces and what the trust branch reads.
291    fn exchange_with_principal(principal: Principal) -> Exchange {
292        let mut ex = Exchange::new(Message::default());
293        store_principal_properties(&mut ex, &principal);
294        ex
295    }
296
297    #[tokio::test]
298    async fn role_policy_grants_when_role_present() {
299        let principal = test_principal(vec!["admin"], vec![]);
300        let policy = RolePolicy::new(
301            vec!["admin".into()],
302            true,
303            false,
304            mock_validator(principal.clone()),
305            vec![CredentialSource::AuthorizationHeader],
306        );
307        let mut ex = exchange_with_bearer(principal);
308        let decision = policy.evaluate(&mut ex).await.unwrap();
309        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
310    }
311
312    #[tokio::test]
313    async fn role_policy_denies_when_role_missing() {
314        let principal = test_principal(vec!["user"], vec![]);
315        let policy = RolePolicy::new(
316            vec!["admin".into()],
317            true,
318            false,
319            mock_validator(principal.clone()),
320            vec![CredentialSource::AuthorizationHeader],
321        );
322        let mut ex = exchange_with_bearer(principal);
323        let decision = policy.evaluate(&mut ex).await.unwrap();
324        assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
325    }
326
327    #[tokio::test]
328    async fn role_policy_any_required() {
329        let principal = test_principal(vec!["user"], vec![]);
330        let policy = RolePolicy::new(
331            vec!["admin".into(), "user".into()],
332            false,
333            false,
334            mock_validator(principal.clone()),
335            vec![CredentialSource::AuthorizationHeader],
336        );
337        let mut ex = exchange_with_bearer(principal);
338        let decision = policy.evaluate(&mut ex).await.unwrap();
339        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
340    }
341
342    #[tokio::test]
343    async fn scope_policy_grants() {
344        let principal = test_principal(vec![], vec!["read"]);
345        let policy = ScopePolicy::new(
346            vec!["read".into()],
347            true,
348            false,
349            mock_validator(principal.clone()),
350            vec![CredentialSource::AuthorizationHeader],
351        );
352        let mut ex = exchange_with_bearer(principal);
353        let decision = policy.evaluate(&mut ex).await.unwrap();
354        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
355    }
356
357    #[tokio::test]
358    async fn unauthenticated_when_no_principal_and_no_header() {
359        // No Bearer header, no exchange property — validator never called
360        struct FailValidator;
361        #[async_trait]
362        impl JwtValidator for FailValidator {
363            async fn validate(&self, _token: &str) -> Result<Principal, AuthError> {
364                panic!("should not be called")
365            }
366        }
367        let policy = RolePolicy::new(
368            vec!["admin".into()],
369            true,
370            false,
371            Arc::new(FailValidator),
372            vec![CredentialSource::AuthorizationHeader],
373        );
374        let mut ex = Exchange::new(Message::default());
375        let result = policy.evaluate(&mut ex).await;
376        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
377    }
378
379    #[tokio::test]
380    async fn principal_fallback_denied_by_default() {
381        // No Bearer header, but principal pre-populated (upstream filter scenario).
382        // Without `trust_upstream_principal` opt-in, MUST be denied.
383        let principal = test_principal(vec!["admin"], vec![]);
384        let policy = RolePolicy::new(
385            vec!["admin".into()],
386            true,
387            false, // trust_upstream_principal
388            mock_validator(principal.clone()),
389            vec![CredentialSource::AuthorizationHeader],
390        );
391        let mut ex = exchange_with_principal(principal); // no Authorization header
392        let result = policy.evaluate(&mut ex).await;
393        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
394    }
395
396    #[tokio::test]
397    async fn principal_fallback_allowed_with_opt_in() {
398        // Same setup but `trust_upstream_principal=true` allows upstream-set principal.
399        let principal = test_principal(vec!["admin"], vec![]);
400        let policy = RolePolicy::new(
401            vec!["admin".into()],
402            true,
403            true, // trust_upstream_principal
404            mock_validator(principal.clone()),
405            vec![CredentialSource::AuthorizationHeader],
406        );
407        let mut ex = exchange_with_principal(principal); // no Authorization header
408        let decision = policy.evaluate(&mut ex).await.unwrap();
409        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
410    }
411
412    #[tokio::test]
413    async fn scope_policy_fallback_denied_by_default() {
414        // No Bearer header, principal pre-populated — Scopes policy also gates.
415        let principal = test_principal(vec![], vec!["read"]);
416        let policy = ScopePolicy::new(
417            vec!["read".into()],
418            true,
419            false, // trust_upstream_principal
420            mock_validator(principal.clone()),
421            vec![CredentialSource::AuthorizationHeader],
422        );
423        let mut ex = exchange_with_principal(principal);
424        let result = policy.evaluate(&mut ex).await;
425        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
426    }
427
428    #[test]
429    fn role_policy_constructor_accepts_sources() {
430        let authenticator = mock_validator(test_principal(vec![], vec![]));
431        let sources = vec![CredentialSource::Cookie { name: "s".into() }];
432        let policy = RolePolicy::new(
433            vec!["r".into()],
434            true,
435            false,
436            authenticator,
437            sources.clone(),
438        );
439        assert_eq!(policy.credential_sources(), sources.as_slice());
440    }
441
442    // --- Task 1.2: multi-source extraction over the Exchange ---
443
444    #[tokio::test]
445    async fn authenticate_default_equals_bearer_prefix_strip() {
446        // The pre-change path stripped "Bearer " from the Authorization header.
447        // A real store seeded with the bare token proves the strip: a no-strip
448        // or double-strip regression would miss the lookup and fail here.
449        let principal = test_principal(vec!["admin"], vec![]);
450        let store = NativeCredentialStore::try_new(vec![NativeCredential {
451            secret: NativeCredentialSecret::Plaintext {
452                value: zeroize::Zeroizing::new("mock-token".to_string()),
453            },
454            principal: principal.clone(),
455        }])
456        .unwrap();
457        let authenticator: Arc<dyn TokenAuthenticator> =
458            Arc::new(StaticTokenAuthenticator::new(store));
459        let policy = RolePolicy::new(
460            vec!["admin".into()],
461            true,
462            false,
463            authenticator,
464            vec![CredentialSource::AuthorizationHeader],
465        );
466        let mut ex = exchange_with_bearer(principal.clone());
467        let decision = policy.evaluate(&mut ex).await.unwrap();
468        match decision {
469            AuthorizationDecision::Granted { principal: granted } => {
470                assert_eq!(granted.subject, principal.subject);
471            }
472            other => panic!("expected Granted, got {other:?}"),
473        }
474    }
475
476    #[tokio::test]
477    async fn authenticate_header_source_reads_authorization() {
478        let principal = test_principal(vec!["admin"], vec![]);
479
480        // Header present -> authenticates.
481        let policy = RolePolicy::new(
482            vec!["admin".into()],
483            true,
484            false,
485            mock_validator(principal.clone()),
486            vec![CredentialSource::AuthorizationHeader],
487        );
488        let mut ex = exchange_with_bearer(principal.clone());
489        let decision = policy.evaluate(&mut ex).await.unwrap();
490        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
491
492        // Header absent, no other source -> Unauthenticated.
493        let policy = RolePolicy::new(
494            vec!["admin".into()],
495            true,
496            false,
497            mock_validator(principal),
498            vec![CredentialSource::AuthorizationHeader],
499        );
500        let mut ex = Exchange::new(Message::default());
501        let result = policy.evaluate(&mut ex).await;
502        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
503    }
504
505    #[tokio::test]
506    async fn cookie_parse_malformed_is_absent_not_error() {
507        let principal = test_principal(vec!["admin"], vec![]);
508        let policy = RolePolicy::new(
509            vec!["admin".into()],
510            true,
511            false,
512            mock_validator(principal),
513            vec![CredentialSource::Cookie {
514                name: "session".into(),
515            }],
516        );
517        let mut ex = Exchange::new(Message::default());
518        ex.input.set_header(
519            "Cookie",
520            serde_json::Value::String("garbage-no-equals".into()),
521        );
522        // Absent source -> Unauthenticated. Never a parse panic, never another error.
523        let result = policy.evaluate(&mut ex).await;
524        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
525    }
526
527    #[tokio::test]
528    async fn query_source_reads_camel_http_query_header() {
529        let principal = test_principal(vec!["admin"], vec![]);
530        let store = NativeCredentialStore::try_new(vec![NativeCredential {
531            secret: NativeCredentialSecret::Plaintext {
532                value: zeroize::Zeroizing::new("TOK".to_string()),
533            },
534            principal: principal.clone(),
535        }])
536        .unwrap();
537        let authenticator: Arc<dyn TokenAuthenticator> =
538            Arc::new(StaticTokenAuthenticator::new(store));
539        let policy = RolePolicy::new(
540            vec!["admin".into()],
541            true,
542            false,
543            authenticator,
544            vec![CredentialSource::QueryParam {
545                param: "token".into(),
546            }],
547        );
548        let mut ex = Exchange::new(Message::default());
549        ex.input.set_header(
550            "CamelHttpQuery",
551            serde_json::Value::String("token=TOK".into()),
552        );
553        let decision = policy.evaluate(&mut ex).await.unwrap();
554        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
555    }
556
557    #[tokio::test]
558    async fn trust_false_preloaded_principal_unauthenticated() {
559        let principal = test_principal(vec!["admin"], vec![]);
560        let policy = RolePolicy::new(
561            vec!["admin".into()],
562            true,
563            false, // trust_upstream_principal
564            mock_validator(principal.clone()),
565            vec![CredentialSource::AuthorizationHeader],
566        );
567        let mut ex = exchange_with_principal(principal); // preloaded principal, no credential
568        let result = policy.evaluate(&mut ex).await;
569        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
570    }
571
572    #[tokio::test]
573    async fn trust_true_preloaded_principal_fallback() {
574        let principal = test_principal(vec!["admin"], vec![]);
575        let policy = RolePolicy::new(
576            vec!["admin".into()],
577            true,
578            true, // trust_upstream_principal
579            mock_validator(principal.clone()),
580            vec![CredentialSource::AuthorizationHeader],
581        );
582        let mut ex = exchange_with_principal(principal); // preloaded principal, no credential
583        let decision = policy.evaluate(&mut ex).await.unwrap();
584        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
585    }
586
587    #[tokio::test]
588    async fn prefix_credential_unauthenticated() {
589        // Store holds the full credential. A truncated prefix must never match
590        // (constant-time exact compare in the shared store lookup).
591        let principal = test_principal(vec!["admin"], vec![]);
592        let store = NativeCredentialStore::try_new(vec![NativeCredential {
593            secret: NativeCredentialSecret::Plaintext {
594                value: zeroize::Zeroizing::new("SENTINEL_FULL_9kq2".to_string()),
595            },
596            principal: principal.clone(),
597        }])
598        .unwrap();
599        let authenticator: Arc<dyn TokenAuthenticator> =
600            Arc::new(StaticTokenAuthenticator::new(store));
601
602        // Authorization header.
603        let policy = RolePolicy::new(
604            vec!["admin".into()],
605            true,
606            false,
607            authenticator.clone(),
608            vec![CredentialSource::AuthorizationHeader],
609        );
610        let mut ex = Exchange::new(Message::default());
611        ex.input.set_header(
612            "Authorization",
613            serde_json::Value::String("Bearer SENTINEL_FULL".into()),
614        );
615        let result = policy.evaluate(&mut ex).await;
616        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
617
618        // Cookie via exchange headers.
619        let policy = RolePolicy::new(
620            vec!["admin".into()],
621            true,
622            false,
623            authenticator.clone(),
624            vec![CredentialSource::Cookie {
625                name: "session".into(),
626            }],
627        );
628        let mut ex = Exchange::new(Message::default());
629        ex.input.set_header(
630            "Cookie",
631            serde_json::Value::String("session=SENTINEL_FULL".into()),
632        );
633        let result = policy.evaluate(&mut ex).await;
634        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
635
636        // Query via the CamelHttpQuery header.
637        let policy = RolePolicy::new(
638            vec!["admin".into()],
639            true,
640            false,
641            authenticator,
642            vec![CredentialSource::QueryParam {
643                param: "token".into(),
644            }],
645        );
646        let mut ex = Exchange::new(Message::default());
647        ex.input.set_header(
648            "CamelHttpQuery",
649            serde_json::Value::String("token=SENTINEL_FULL".into()),
650        );
651        let result = policy.evaluate(&mut ex).await;
652        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
653    }
654
655    // --- Task 4.1: Header source ---
656
657    #[tokio::test]
658    async fn header_source_authenticates_api_key() {
659        let principal = test_principal(vec!["admin"], vec![]);
660        let store = NativeCredentialStore::try_new(vec![NativeCredential {
661            secret: NativeCredentialSecret::Plaintext {
662                value: zeroize::Zeroizing::new("SENTINEL_KEY_1".to_string()),
663            },
664            principal: principal.clone(),
665        }])
666        .unwrap();
667        let authenticator: Arc<dyn TokenAuthenticator> =
668            Arc::new(StaticTokenAuthenticator::new(store));
669        let policy = RolePolicy::new(
670            vec!["admin".into()],
671            true,
672            false,
673            authenticator,
674            vec![CredentialSource::Header {
675                name: "x-api-key".into(),
676            }],
677        );
678        let mut ex = Exchange::new(Message::default());
679        ex.input.set_header(
680            "X-API-Key",
681            serde_json::Value::String("SENTINEL_KEY_1".into()),
682        );
683        let decision = policy.evaluate(&mut ex).await.unwrap();
684        match decision {
685            AuthorizationDecision::Granted { principal: granted } => {
686                assert_eq!(granted.subject, principal.subject);
687            }
688            other => panic!("expected Granted, got {other:?}"),
689        }
690    }
691
692    #[tokio::test]
693    async fn header_source_miss_maps_401() {
694        let principal = test_principal(vec!["admin"], vec![]);
695        let policy = RolePolicy::new(
696            vec!["admin".into()],
697            true,
698            false,
699            mock_validator(principal),
700            vec![CredentialSource::Header {
701                name: "x-api-key".into(),
702            }],
703        );
704        // No X-API-Key header, no other source -> Unauthenticated.
705        let mut ex = Exchange::new(Message::default());
706        let result = policy.evaluate(&mut ex).await;
707        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
708    }
709
710    #[tokio::test]
711    async fn header_lookup_case_insensitive() {
712        let principal = test_principal(vec!["admin"], vec![]);
713        let store = NativeCredentialStore::try_new(vec![NativeCredential {
714            secret: NativeCredentialSecret::Plaintext {
715                value: zeroize::Zeroizing::new("SENTINEL_KEY_1".to_string()),
716            },
717            principal: principal.clone(),
718        }])
719        .unwrap();
720        let authenticator: Arc<dyn TokenAuthenticator> =
721            Arc::new(StaticTokenAuthenticator::new(store));
722        let policy = RolePolicy::new(
723            vec!["admin".into()],
724            true,
725            false,
726            authenticator,
727            vec![CredentialSource::Header {
728                name: "x-api-key".into(),
729            }],
730        );
731        let mut ex = Exchange::new(Message::default());
732        // Header key casing differs from the declared source name.
733        ex.input.set_header(
734            "X-API-KEY",
735            serde_json::Value::String("SENTINEL_KEY_1".into()),
736        );
737        let decision = policy.evaluate(&mut ex).await.unwrap();
738        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
739    }
740
741    // --- RFC 9110/7235 bearer scheme semantics on the unified default path ---
742
743    #[tokio::test]
744    async fn bearer_scheme_lowercase_grants() {
745        let principal = test_principal(vec!["admin"], vec![]);
746        let policy = RolePolicy::new(
747            vec!["admin".into()],
748            true,
749            false,
750            store_seeded_authenticator("TOK", principal),
751            vec![CredentialSource::AuthorizationHeader],
752        );
753        let mut ex = Exchange::new(Message::default());
754        ex.input.set_header(
755            "Authorization",
756            serde_json::Value::String("bearer TOK".into()),
757        );
758        let decision = policy.evaluate(&mut ex).await.unwrap();
759        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
760    }
761
762    #[tokio::test]
763    async fn bearer_scheme_uppercase_grants() {
764        let principal = test_principal(vec!["admin"], vec![]);
765        let policy = RolePolicy::new(
766            vec!["admin".into()],
767            true,
768            false,
769            store_seeded_authenticator("TOK", principal),
770            vec![CredentialSource::AuthorizationHeader],
771        );
772        let mut ex = Exchange::new(Message::default());
773        ex.input.set_header(
774            "Authorization",
775            serde_json::Value::String("BEARER TOK".into()),
776        );
777        let decision = policy.evaluate(&mut ex).await.unwrap();
778        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
779    }
780
781    #[tokio::test]
782    async fn bearer_leading_whitespace_grants() {
783        let principal = test_principal(vec!["admin"], vec![]);
784        let policy = RolePolicy::new(
785            vec!["admin".into()],
786            true,
787            false,
788            store_seeded_authenticator("TOK", principal),
789            vec![CredentialSource::AuthorizationHeader],
790        );
791        let mut ex = Exchange::new(Message::default());
792        ex.input.set_header(
793            "Authorization",
794            serde_json::Value::String(" Bearer TOK".into()),
795        );
796        let decision = policy.evaluate(&mut ex).await.unwrap();
797        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
798    }
799
800    #[tokio::test]
801    async fn bearer_double_space_yields_trimmed_token() {
802        let principal = test_principal(vec!["admin"], vec![]);
803        let policy = RolePolicy::new(
804            vec!["admin".into()],
805            true,
806            false,
807            store_seeded_authenticator("TOK", principal),
808            vec![CredentialSource::AuthorizationHeader],
809        );
810        let mut ex = Exchange::new(Message::default());
811        ex.input.set_header(
812            "Authorization",
813            serde_json::Value::String("Bearer  TOK".into()),
814        );
815        let decision = policy.evaluate(&mut ex).await.unwrap();
816        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
817    }
818
819    #[tokio::test]
820    async fn bearer_empty_token_falls_to_trust_branch() {
821        let principal = test_principal(vec!["admin"], vec![]);
822
823        // trust=false: empty token yields no extraction -> Unauthenticated.
824        let policy = RolePolicy::new(
825            vec!["admin".into()],
826            true,
827            false,
828            mock_validator(principal.clone()),
829            vec![CredentialSource::AuthorizationHeader],
830        );
831        let mut ex = exchange_with_principal(principal.clone());
832        ex.input
833            .set_header("Authorization", serde_json::Value::String("Bearer ".into()));
834        let result = policy.evaluate(&mut ex).await;
835        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
836
837        // trust=true: empty token yields no extraction -> preloaded principal grants.
838        let policy = RolePolicy::new(
839            vec!["admin".into()],
840            true,
841            true,
842            mock_validator(principal.clone()),
843            vec![CredentialSource::AuthorizationHeader],
844        );
845        let mut ex = exchange_with_principal(principal);
846        ex.input
847            .set_header("Authorization", serde_json::Value::String("Bearer ".into()));
848        let decision = policy.evaluate(&mut ex).await.unwrap();
849        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
850    }
851}