Skip to main content

camel_processor/
security_policy_layer.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4use std::task::{Context, Poll};
5
6use tower::{Layer, Service};
7
8use camel_api::security_policy::{
9    AuthContext, AuthPrincipal, AuthorizationDecision, SecurityPolicy, TransportId,
10    store_principal_properties,
11};
12use camel_api::{CamelError, Exchange};
13
14/// Carrier-only authorization layer (ADR-0061 Task 2.9 strict mode).
15///
16/// The layer NEVER authenticates: transports mint the typed carrier at the
17/// request boundary (`kernel_authenticate` + `install_carrier`) and the
18/// pre-pipeline dispatch check rejects carrier-less Exchanges on non-Public
19/// routes before the pipeline runs. The layer therefore sees an Exchange
20/// that already carries the sealed [`AuthenticatedPrincipal`] and only
21/// evaluates the route policy against it — no carrier, no authorization
22/// path (fail closed). The Phase-1 legacy Bearer and anonymous-principal
23/// branches were deleted here.
24#[derive(Clone)]
25pub struct SecurityPolicyLayer {
26    policy: Arc<dyn SecurityPolicy>,
27    transport: TransportId,
28}
29
30impl SecurityPolicyLayer {
31    pub fn new(policy: Arc<dyn SecurityPolicy>, transport: TransportId) -> Self {
32        Self { policy, transport }
33    }
34}
35
36impl<S> Layer<S> for SecurityPolicyLayer {
37    type Service = SecurityPolicyService<S>;
38
39    fn layer(&self, inner: S) -> Self::Service {
40        SecurityPolicyService {
41            inner,
42            policy: Arc::clone(&self.policy),
43            transport: self.transport,
44        }
45    }
46}
47
48pub struct SecurityPolicyService<S> {
49    inner: S,
50    policy: Arc<dyn SecurityPolicy>,
51    transport: TransportId,
52}
53
54impl<S: Clone> Clone for SecurityPolicyService<S> {
55    fn clone(&self) -> Self {
56        Self {
57            inner: self.inner.clone(),
58            policy: Arc::clone(&self.policy),
59            transport: self.transport,
60        }
61    }
62}
63
64impl<S> Service<Exchange> for SecurityPolicyService<S>
65where
66    S: Service<Exchange, Response = Exchange, Error = CamelError> + Clone + Send + 'static,
67    S::Future: Send,
68{
69    type Response = Exchange;
70    type Error = CamelError;
71    type Future = Pin<Box<dyn Future<Output = Result<Exchange, CamelError>> + Send>>;
72
73    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
74        self.inner.poll_ready(cx)
75    }
76
77    fn call(&mut self, exchange: Exchange) -> Self::Future {
78        let policy = Arc::clone(&self.policy);
79        let transport = self.transport;
80        let clone = self.inner.clone();
81        let inner = std::mem::replace(&mut self.inner, clone);
82
83        Box::pin(async move {
84            // Strict mode (Task 2.9): the typed carrier is the ONLY
85            // authentication evidence. `read_carrier` clones the
86            // `AuthenticatedPrincipal` out, ending the extension borrow before
87            // `evaluate(&mut exchange, ..)` (avoids E0502). No carrier → no
88            // authorization path, fail closed.
89            let principal = camel_auth::kernel::read_carrier(&exchange).ok_or_else(|| {
90                CamelError::Unauthenticated("no authenticated principal present".to_string())
91            })?;
92            evaluate(policy, exchange, &principal, transport, inner).await
93        })
94    }
95}
96
97/// Evaluate the policy against `principal` and forward (or deny).
98///
99/// The principal is always the kernel-minted typed carrier (strict mode).
100/// On `Granted`, stores the advisory principal properties and forwards to
101/// the inner service.
102async fn evaluate<S>(
103    policy: Arc<dyn SecurityPolicy>,
104    mut exchange: Exchange,
105    principal: &dyn AuthPrincipal,
106    transport: TransportId,
107    mut inner: S,
108) -> Result<Exchange, CamelError>
109where
110    S: Service<Exchange, Response = Exchange, Error = CamelError> + Send + 'static,
111    S::Future: Send,
112{
113    let auth = AuthContext {
114        principal,
115        transport,
116    };
117    match policy.evaluate(&mut exchange, &auth).await {
118        Ok(AuthorizationDecision::Granted { principal }) => {
119            store_principal_properties(&mut exchange, &principal);
120            inner.call(exchange).await
121        }
122        Ok(AuthorizationDecision::Denied {
123            reason,
124            required,
125            actual,
126        }) => {
127            let msg =
128                format!("Access denied: {reason}. Required: {required:?}, actual: {actual:?}");
129            Err(CamelError::Unauthorized(msg))
130        }
131        Err(e) => Err(e),
132        // Future AuthorizationDecision variants fail closed.
133        _ => Err(CamelError::Unauthorized(
134            "access denied by security policy".to_string(),
135        )),
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use async_trait::async_trait;
143    use camel_api::security_policy::{
144        AccessMode, CredentialSource, PRINCIPAL_AUDIENCE_KEY, PRINCIPAL_CLAIMS_KEY,
145        PRINCIPAL_ISSUER_KEY, PRINCIPAL_KEY, PRINCIPAL_ROLES_KEY, PRINCIPAL_SCOPES_KEY,
146        PRINCIPAL_SUBJECT_KEY, Principal, RouteSecurityPlan,
147    };
148    use camel_api::{BoxProcessor, BoxProcessorExt, Message};
149    use camel_auth::TokenAuthenticator;
150    use camel_auth::credential_source::ExtractedToken;
151    use camel_auth::kernel::{KERNEL_PRINCIPAL_KEY, install_carrier, kernel_authenticate};
152    use camel_auth::native_auth::{
153        NativeCredential, NativeCredentialSecret, NativeCredentialStore,
154    };
155    use camel_auth::{ProviderEntry, ProviderRegistry, StaticTokenAuthenticator};
156    use std::sync::Mutex;
157    use std::sync::atomic::{AtomicU32, Ordering};
158    use tower::ServiceExt;
159    use zeroize::Zeroizing;
160
161    fn make_exchange() -> Exchange {
162        Exchange::new(Message::new("test"))
163    }
164
165    fn ok_processor() -> BoxProcessor {
166        BoxProcessor::from_fn(|ex| Box::pin(async move { Ok(ex) }))
167    }
168
169    fn test_principal() -> Principal {
170        Principal {
171            subject: "user1".into(),
172            issuer: "test-issuer".into(),
173            audience: vec!["api".into()],
174            scopes: vec!["read".into()],
175            roles: vec!["admin".into()],
176            claims: serde_json::json!({"sub": "user1"}),
177        }
178    }
179
180    /// Build a `StaticTokenAuthenticator` that accepts `token` and returns
181    /// `test_principal()`.
182    fn static_authenticator(token: &str) -> Arc<dyn TokenAuthenticator> {
183        let store = NativeCredentialStore::try_new(vec![NativeCredential {
184            secret: NativeCredentialSecret::Plaintext {
185                value: Zeroizing::new(token.to_string()),
186            },
187            principal: test_principal(),
188        }])
189        .unwrap();
190        Arc::new(StaticTokenAuthenticator::new(store))
191    }
192
193    /// A registry holding a single provider `id` whose token is `token`.
194    fn provider_registry(id: &str, token: &str) -> ProviderRegistry {
195        let registry = ProviderRegistry::new();
196        registry.register(
197            id,
198            ProviderEntry {
199                authenticator: static_authenticator(token),
200                audience_binding: None,
201            },
202        );
203        registry
204    }
205
206    fn authenticated_plan(provider_ref: &str) -> RouteSecurityPlan {
207        RouteSecurityPlan {
208            access_mode: AccessMode::Authenticated,
209            provider_ref: Some(provider_ref.to_string()),
210            transport: TransportId::Http,
211            credential_sources: vec![CredentialSource::AuthorizationHeader],
212            audience_binding: None,
213        }
214    }
215
216    fn credentials(token: &str) -> ExtractedToken {
217        ExtractedToken {
218            token: token.to_string(),
219            source: CredentialSource::AuthorizationHeader,
220        }
221    }
222
223    /// Build a layer (policy-only route: no carrier minter in play).
224    fn policy_only_layer(policy: Arc<dyn SecurityPolicy>) -> SecurityPolicyLayer {
225        SecurityPolicyLayer::new(policy, TransportId::Http)
226    }
227
228    /// Mint a real carrier through the kernel (strict-mode input contract:
229    /// the layer only authorizes carrier-carrying Exchanges).
230    async fn minted_principal() -> camel_auth::AuthenticatedPrincipal {
231        let providers = provider_registry("idp-a", "t-a");
232        let plan = authenticated_plan("idp-a");
233        kernel_authenticate(&plan, &providers, &credentials("t-a"))
234            .await
235            .expect("kernel mints test carrier")
236    }
237
238    /// An Exchange carrying the kernel-minted carrier.
239    async fn carrier_exchange() -> Exchange {
240        let principal = minted_principal().await;
241        let mut exchange = make_exchange();
242        install_carrier(&mut exchange, &principal);
243        exchange
244    }
245
246    struct GrantPolicy;
247    #[async_trait]
248    impl SecurityPolicy for GrantPolicy {
249        async fn evaluate(
250            &self,
251            _exchange: &mut Exchange,
252            _auth: &AuthContext<'_>,
253        ) -> Result<AuthorizationDecision, CamelError> {
254            Ok(AuthorizationDecision::Granted {
255                principal: test_principal(),
256            })
257        }
258    }
259
260    struct DenyPolicy;
261    #[async_trait]
262    impl SecurityPolicy for DenyPolicy {
263        async fn evaluate(
264            &self,
265            _exchange: &mut Exchange,
266            _auth: &AuthContext<'_>,
267        ) -> Result<AuthorizationDecision, CamelError> {
268            Ok(AuthorizationDecision::Denied {
269                reason: "missing role".into(),
270                required: vec!["admin".into()],
271                actual: vec!["user".into()],
272            })
273        }
274    }
275
276    struct FailPolicy;
277    #[async_trait]
278    impl SecurityPolicy for FailPolicy {
279        async fn evaluate(
280            &self,
281            _exchange: &mut Exchange,
282            _auth: &AuthContext<'_>,
283        ) -> Result<AuthorizationDecision, CamelError> {
284            Err(CamelError::Unauthenticated("invalid token".into()))
285        }
286    }
287
288    /// Records the `AuthContext` principal each call observes, then grants.
289    struct RecordingGrantPolicy {
290        count: AtomicU32,
291        seen: Mutex<Vec<(String, String)>>, // (subject, provider_id)
292    }
293
294    #[async_trait]
295    impl SecurityPolicy for RecordingGrantPolicy {
296        async fn evaluate(
297            &self,
298            _exchange: &mut Exchange,
299            auth: &AuthContext<'_>,
300        ) -> Result<AuthorizationDecision, CamelError> {
301            self.count.fetch_add(1, Ordering::SeqCst);
302            self.seen.lock().unwrap().push((
303                auth.principal.principal().subject.clone(),
304                auth.principal.provider_id().to_string(),
305            ));
306            Ok(AuthorizationDecision::Granted {
307                principal: test_principal(),
308            })
309        }
310    }
311
312    #[tokio::test]
313    async fn test_granted_stores_properties() {
314        let layer = policy_only_layer(Arc::new(GrantPolicy));
315        let mut svc = layer.layer(ok_processor());
316        let result = svc
317            .ready()
318            .await
319            .unwrap()
320            .call(carrier_exchange().await)
321            .await;
322        assert!(result.is_ok());
323        let ex = result.unwrap();
324        assert_eq!(
325            ex.property(PRINCIPAL_SUBJECT_KEY),
326            Some(&serde_json::Value::String("user1".into()))
327        );
328        assert_eq!(
329            ex.property(PRINCIPAL_ISSUER_KEY),
330            Some(&serde_json::Value::String("test-issuer".into()))
331        );
332        assert!(ex.property(PRINCIPAL_KEY).is_some());
333    }
334
335    #[tokio::test]
336    async fn test_denied_returns_unauthorized_error() {
337        let layer = policy_only_layer(Arc::new(DenyPolicy));
338        let mut svc = layer.layer(ok_processor());
339        let result = svc
340            .ready()
341            .await
342            .unwrap()
343            .call(carrier_exchange().await)
344            .await;
345        assert!(result.is_err());
346        match result.unwrap_err() {
347            CamelError::Unauthorized(msg) => assert!(msg.contains("missing role")),
348            other => panic!("expected Unauthorized, got: {other:?}"),
349        }
350    }
351
352    #[tokio::test]
353    async fn test_denied_error_contains_required_actual() {
354        let layer = policy_only_layer(Arc::new(DenyPolicy));
355        let mut svc = layer.layer(ok_processor());
356        let result = svc
357            .ready()
358            .await
359            .unwrap()
360            .call(carrier_exchange().await)
361            .await;
362        let msg = match result.unwrap_err() {
363            CamelError::Unauthorized(msg) => msg,
364            other => panic!("expected Unauthorized, got: {other:?}"),
365        };
366        assert!(msg.contains("admin"));
367        assert!(msg.contains("user"));
368    }
369
370    #[tokio::test]
371    async fn test_evaluate_error_propagates() {
372        let layer = policy_only_layer(Arc::new(FailPolicy));
373        let mut svc = layer.layer(ok_processor());
374        let result = svc
375            .ready()
376            .await
377            .unwrap()
378            .call(carrier_exchange().await)
379            .await;
380        match result.unwrap_err() {
381            CamelError::Unauthenticated(msg) => assert!(msg.contains("invalid token")),
382            other => panic!("expected Unauthenticated, got: {other:?}"),
383        }
384    }
385
386    #[tokio::test]
387    async fn test_multiple_calls_share_policy() {
388        let count = Arc::new(AtomicU32::new(0));
389        struct CountingPolicy {
390            count: Arc<AtomicU32>,
391        }
392        #[async_trait]
393        impl SecurityPolicy for CountingPolicy {
394            async fn evaluate(
395                &self,
396                _exchange: &mut Exchange,
397                _auth: &AuthContext<'_>,
398            ) -> Result<AuthorizationDecision, CamelError> {
399                self.count.fetch_add(1, Ordering::SeqCst);
400                Ok(AuthorizationDecision::Granted {
401                    principal: Principal {
402                        subject: "user1".into(),
403                        issuer: "test".into(),
404                        audience: vec![],
405                        scopes: vec![],
406                        roles: vec![],
407                        claims: serde_json::Value::Null,
408                    },
409                })
410            }
411        }
412        let policy = Arc::new(CountingPolicy {
413            count: Arc::clone(&count),
414        });
415        let layer = policy_only_layer(Arc::clone(&policy) as Arc<dyn SecurityPolicy>);
416        let mut svc = layer.layer(ok_processor());
417        for _ in 0..3 {
418            let result = svc
419                .ready()
420                .await
421                .unwrap()
422                .call(carrier_exchange().await)
423                .await;
424            assert!(result.is_ok());
425        }
426        assert_eq!(count.load(Ordering::SeqCst), 3);
427    }
428
429    #[tokio::test]
430    async fn test_granted_all_property_json_formats() {
431        let layer = policy_only_layer(Arc::new(GrantPolicy));
432        let mut svc = layer.layer(ok_processor());
433        let result = svc
434            .ready()
435            .await
436            .unwrap()
437            .call(carrier_exchange().await)
438            .await;
439        let ex = result.unwrap();
440
441        let roles: Vec<String> =
442            serde_json::from_str(ex.property(PRINCIPAL_ROLES_KEY).unwrap().as_str().unwrap())
443                .unwrap();
444        assert_eq!(roles, vec!["admin"]);
445
446        let scopes: Vec<String> =
447            serde_json::from_str(ex.property(PRINCIPAL_SCOPES_KEY).unwrap().as_str().unwrap())
448                .unwrap();
449        assert_eq!(scopes, vec!["read"]);
450
451        let audience: Vec<String> = serde_json::from_str(
452            ex.property(PRINCIPAL_AUDIENCE_KEY)
453                .unwrap()
454                .as_str()
455                .unwrap(),
456        )
457        .unwrap();
458        assert_eq!(audience, vec!["api"]);
459
460        let claims: serde_json::Value =
461            serde_json::from_str(ex.property(PRINCIPAL_CLAIMS_KEY).unwrap().as_str().unwrap())
462                .unwrap();
463        assert_eq!(claims["sub"], "user1");
464    }
465
466    #[tokio::test]
467    async fn test_granted_empty_principal_fields() {
468        struct EmptyPrincipalPolicy;
469        #[async_trait]
470        impl SecurityPolicy for EmptyPrincipalPolicy {
471            async fn evaluate(
472                &self,
473                _exchange: &mut Exchange,
474                _auth: &AuthContext<'_>,
475            ) -> Result<AuthorizationDecision, CamelError> {
476                Ok(AuthorizationDecision::Granted {
477                    principal: Principal {
478                        subject: "minimal".into(),
479                        issuer: String::new(),
480                        audience: vec![],
481                        scopes: vec![],
482                        roles: vec![],
483                        claims: serde_json::Value::Null,
484                    },
485                })
486            }
487        }
488        let layer = policy_only_layer(Arc::new(EmptyPrincipalPolicy));
489        let mut svc = layer.layer(ok_processor());
490        let result = svc
491            .ready()
492            .await
493            .unwrap()
494            .call(carrier_exchange().await)
495            .await;
496        let ex = result.unwrap();
497
498        assert_eq!(
499            ex.property(PRINCIPAL_SUBJECT_KEY),
500            Some(&serde_json::Value::String("minimal".into()))
501        );
502        assert_eq!(
503            ex.property(PRINCIPAL_ISSUER_KEY),
504            Some(&serde_json::Value::String(String::new()))
505        );
506        let roles: Vec<String> =
507            serde_json::from_str(ex.property(PRINCIPAL_ROLES_KEY).unwrap().as_str().unwrap())
508                .unwrap();
509        assert!(roles.is_empty());
510    }
511
512    #[tokio::test]
513    async fn test_layer_clone_produces_working_service() {
514        let layer = policy_only_layer(Arc::new(GrantPolicy));
515        let mut svc1 = layer.layer(ok_processor());
516        let svc2 = svc1.clone();
517
518        let r1 = svc1
519            .ready()
520            .await
521            .unwrap()
522            .call(carrier_exchange().await)
523            .await;
524        let mut svc2 = svc2;
525        let r2 = svc2
526            .ready()
527            .await
528            .unwrap()
529            .call(carrier_exchange().await)
530            .await;
531        assert!(r1.is_ok());
532        assert!(r2.is_ok());
533    }
534
535    #[tokio::test]
536    async fn test_granted_preserves_original_exchange_properties() {
537        struct GrantPolicy;
538        #[async_trait]
539        impl SecurityPolicy for GrantPolicy {
540            async fn evaluate(
541                &self,
542                _exchange: &mut Exchange,
543                _auth: &AuthContext<'_>,
544            ) -> Result<AuthorizationDecision, CamelError> {
545                Ok(AuthorizationDecision::Granted {
546                    principal: Principal {
547                        subject: "u".into(),
548                        issuer: "i".into(),
549                        audience: vec![],
550                        scopes: vec![],
551                        roles: vec![],
552                        claims: serde_json::Value::Null,
553                    },
554                })
555            }
556        }
557        let layer = policy_only_layer(Arc::new(GrantPolicy));
558        let mut svc = layer.layer(ok_processor());
559        let mut ex = carrier_exchange().await;
560        ex.set_property("custom.key", "custom-value");
561        let result = svc.ready().await.unwrap().call(ex).await;
562        let ex = result.unwrap();
563        assert_eq!(
564            ex.property("custom.key"),
565            Some(&serde_json::Value::String("custom-value".into()))
566        );
567        assert!(ex.property(PRINCIPAL_SUBJECT_KEY).is_some());
568    }
569
570    // ── Task 1.7 dual-read tests (strict-mode contract since Task 2.9) ──
571
572    #[tokio::test]
573    async fn layer_denies_without_typed_principal_or_token() {
574        let policy = Arc::new(RecordingGrantPolicy {
575            count: AtomicU32::new(0),
576            seen: Mutex::new(Vec::new()),
577        });
578        let layer = SecurityPolicyLayer::new(
579            Arc::clone(&policy) as Arc<dyn SecurityPolicy>,
580            TransportId::Http,
581        );
582        let mut svc = layer.layer(ok_processor());
583        let result = svc.ready().await.unwrap().call(make_exchange()).await;
584        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
585        assert_eq!(policy.count.load(Ordering::SeqCst), 0);
586    }
587
588    #[tokio::test]
589    async fn layer_grants_with_typed_principal() {
590        let policy = Arc::new(RecordingGrantPolicy {
591            count: AtomicU32::new(0),
592            seen: Mutex::new(Vec::new()),
593        });
594        let layer = SecurityPolicyLayer::new(
595            Arc::clone(&policy) as Arc<dyn SecurityPolicy>,
596            TransportId::Http,
597        );
598
599        // Mint a principal through the REAL path.
600        let providers = provider_registry("idp-a", "t-a");
601        let plan = authenticated_plan("idp-a");
602        let principal = kernel_authenticate(&plan, &providers, &credentials("t-a"))
603            .await
604            .unwrap();
605        let mut exchange = make_exchange();
606        install_carrier(&mut exchange, &principal);
607
608        let mut svc = layer.layer(ok_processor());
609        let result = svc.ready().await.unwrap().call(exchange).await;
610        assert!(result.is_ok());
611        assert_eq!(policy.count.load(Ordering::SeqCst), 1);
612        let seen = policy.seen.lock().unwrap();
613        assert_eq!(seen.len(), 1);
614        assert_eq!(seen[0].0, "user1");
615        assert_eq!(seen[0].1, "idp-a");
616    }
617
618    #[tokio::test]
619    async fn layer_bearer_token_without_carrier_denies() {
620        // Strict mode (Task 2.9): a raw Bearer header is NOT authentication
621        // evidence at the layer. Only the kernel-minted typed carrier
622        // authorizes; the transport boundary mints it, never the layer.
623        let policy = Arc::new(RecordingGrantPolicy {
624            count: AtomicU32::new(0),
625            seen: Mutex::new(Vec::new()),
626        });
627        let layer = SecurityPolicyLayer::new(
628            Arc::clone(&policy) as Arc<dyn SecurityPolicy>,
629            TransportId::Http,
630        );
631        let mut exchange = make_exchange();
632        exchange.input.set_header("Authorization", "Bearer t-a");
633        let mut svc = layer.layer(ok_processor());
634        let result = svc.ready().await.unwrap().call(exchange).await;
635        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
636        assert_eq!(
637            policy.count.load(Ordering::SeqCst),
638            0,
639            "the policy must never evaluate a carrier-less Exchange"
640        );
641    }
642
643    #[tokio::test]
644    async fn policy_only_route_without_carrier_denies() {
645        // Strict mode (Task 2.9) removed the anonymous-principal evaluation:
646        // a policy-only route (no carrier minter in play) fails closed. The
647        // Phase-1 interim contract (anonymous evaluation) is gone.
648        let policy = Arc::new(RecordingGrantPolicy {
649            count: AtomicU32::new(0),
650            seen: Mutex::new(Vec::new()),
651        });
652        let layer = SecurityPolicyLayer::new(
653            Arc::clone(&policy) as Arc<dyn SecurityPolicy>,
654            TransportId::Http,
655        );
656        let mut svc = layer.layer(ok_processor());
657        let result = svc.ready().await.unwrap().call(make_exchange()).await;
658        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
659        assert_eq!(policy.count.load(Ordering::SeqCst), 0);
660    }
661
662    #[tokio::test]
663    async fn spoofed_extension_value_does_not_authorize() {
664        let layer = SecurityPolicyLayer::new(Arc::new(GrantPolicy), TransportId::Http);
665        let mut exchange = make_exchange();
666        // Wrong type under the carrier key: downcast fails → treated as
667        // absent → deny.
668        exchange.set_extension(KERNEL_PRINCIPAL_KEY, Arc::new("x".to_string()));
669        let mut svc = layer.layer(ok_processor());
670        let result = svc.ready().await.unwrap().call(exchange).await;
671        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
672    }
673
674    #[tokio::test]
675    async fn spoofed_legacy_property_without_token_denies() {
676        let layer = SecurityPolicyLayer::new(Arc::new(GrantPolicy), TransportId::Http);
677        let mut exchange = make_exchange();
678        // Raw `camel.auth.principal` property (valid-format principal data) is
679        // NOT carrier evidence — property evidence never authorizes.
680        store_principal_properties(&mut exchange, &test_principal());
681        let mut svc = layer.layer(ok_processor());
682        let result = svc.ready().await.unwrap().call(exchange).await;
683        assert!(matches!(result, Err(CamelError::Unauthenticated(_))));
684    }
685}