Skip to main content

camel_auth/
built_in.rs

1use async_trait::async_trait;
2
3use camel_api::security_policy::{AuthContext, AuthorizationDecision, SecurityPolicy};
4use camel_api::{CamelError, Exchange};
5
6/// Property key used to store the authenticated principal in the exchange.
7///
8/// Re-exported from the camel-api contract so `camel_auth::built_in::PRINCIPAL_KEY`
9/// and `camel_api::security_policy::PRINCIPAL_KEY` are the same constant.
10pub use camel_api::security_policy::PRINCIPAL_KEY;
11
12/// Role-based access control policy.
13///
14/// Reads the authenticated principal from the [`AuthContext`] (never from raw
15/// Exchange properties) and evaluates whether it holds the required roles.
16/// When `all_required` is true, every listed role must be present.
17/// When `all_required` is false, at least one listed role must be present.
18pub struct RolePolicy {
19    required_roles: Vec<String>,
20    all_required: bool,
21}
22
23impl RolePolicy {
24    pub fn new(required_roles: Vec<String>, all_required: bool) -> Self {
25        Self {
26            required_roles,
27            all_required,
28        }
29    }
30}
31
32#[async_trait]
33impl SecurityPolicy for RolePolicy {
34    async fn evaluate(
35        &self,
36        _exchange: &mut Exchange,
37        auth: &AuthContext<'_>,
38    ) -> Result<AuthorizationDecision, CamelError> {
39        let principal = auth.principal.principal();
40
41        let missing: Vec<String> = self
42            .required_roles
43            .iter()
44            .filter(|r| !principal.has_role(r))
45            .cloned()
46            .collect();
47
48        let granted = if self.all_required {
49            missing.is_empty()
50        } else {
51            self.required_roles.is_empty() || missing.len() < self.required_roles.len()
52        };
53
54        if granted {
55            Ok(AuthorizationDecision::Granted {
56                principal: principal.clone(),
57            })
58        } else {
59            let actual = principal.roles.clone();
60            Ok(AuthorizationDecision::Denied {
61                reason: format!("missing required role(s): {}", missing.join(", ")), // allow-secret
62                required: self.required_roles.clone(),
63                actual,
64            })
65        }
66    }
67}
68
69/// Scope-based access control policy.
70///
71/// Reads the authenticated principal from the [`AuthContext`] (never from raw
72/// Exchange properties) and evaluates whether it holds the required scopes.
73/// When `all_required` is true, every listed scope must be present.
74/// When `all_required` is false, at least one listed scope must be present.
75pub struct ScopePolicy {
76    required_scopes: Vec<String>,
77    all_required: bool,
78}
79
80impl ScopePolicy {
81    pub fn new(required_scopes: Vec<String>, all_required: bool) -> Self {
82        Self {
83            required_scopes,
84            all_required,
85        }
86    }
87}
88
89#[async_trait]
90impl SecurityPolicy for ScopePolicy {
91    async fn evaluate(
92        &self,
93        _exchange: &mut Exchange,
94        auth: &AuthContext<'_>,
95    ) -> Result<AuthorizationDecision, CamelError> {
96        let principal = auth.principal.principal();
97
98        let missing: Vec<String> = self
99            .required_scopes
100            .iter()
101            .filter(|s| !principal.has_scope(s))
102            .cloned()
103            .collect();
104
105        let granted = if self.all_required {
106            missing.is_empty()
107        } else {
108            self.required_scopes.is_empty() || missing.len() < self.required_scopes.len()
109        };
110
111        if granted {
112            Ok(AuthorizationDecision::Granted {
113                principal: principal.clone(),
114            })
115        } else {
116            let actual = principal.scopes.clone();
117            Ok(AuthorizationDecision::Denied {
118                reason: format!("missing required scope(s): {}", missing.join(", ")),
119                required: self.required_scopes.clone(),
120                actual,
121            })
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use camel_api::Message;
130    use camel_api::security_policy::{
131        AuthPrincipal, Principal, TransportId, store_principal_properties,
132    };
133
134    fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
135        Principal {
136            subject: "test-user".into(),
137            issuer: "test".into(),
138            audience: vec![],
139            roles: roles.iter().map(|s| s.to_string()).collect(),
140            scopes: scopes.iter().map(|s| s.to_string()).collect(),
141            claims: serde_json::Value::Null,
142        }
143    }
144
145    /// Throwaway `AuthPrincipal` for tests. The trait is open, so implementing
146    /// it for a test stub is legal and grants NO minting power — only the
147    /// concrete `AuthenticatedPrincipal` (camel-auth kernel) is unforgeable.
148    struct TestPrincipal(Principal);
149
150    impl AuthPrincipal for TestPrincipal {
151        fn principal(&self) -> &Principal {
152            &self.0
153        }
154        fn provider_id(&self) -> &str {
155            "test"
156        }
157    }
158
159    fn auth_ctx<'a>(principal: &'a TestPrincipal) -> AuthContext<'a> {
160        AuthContext {
161            principal,
162            transport: TransportId::Http,
163        }
164    }
165
166    fn empty_exchange() -> Exchange {
167        Exchange::new(Message::default())
168    }
169
170    #[tokio::test]
171    async fn role_policy_reads_typed_principal_roles() {
172        let principal = TestPrincipal(test_principal(vec!["admin"], vec![]));
173        let policy = RolePolicy::new(vec!["admin".into()], true);
174        let mut ex = empty_exchange();
175        let auth = auth_ctx(&principal);
176        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
177        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
178    }
179
180    #[tokio::test]
181    async fn property_only_evidence_denies() {
182        // Exchange carries a spoofed `camel.auth.principal` property with
183        // valid-format principal data, but the typed `AuthContext` principal
184        // lacks the required role — RolePolicy must deny (property evidence
185        // never authorizes).
186        let spoofed = test_principal(vec!["admin"], vec![]);
187        let mut ex = empty_exchange();
188        store_principal_properties(&mut ex, &spoofed);
189
190        let typed = TestPrincipal(test_principal(vec!["user"], vec![]));
191        let policy = RolePolicy::new(vec!["admin".into()], true);
192        let auth = auth_ctx(&typed);
193        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
194        assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
195    }
196
197    #[tokio::test]
198    async fn role_policy_grants_when_role_present() {
199        let principal = TestPrincipal(test_principal(vec!["admin"], vec![]));
200        let policy = RolePolicy::new(vec!["admin".into()], true);
201        let mut ex = empty_exchange();
202        let auth = auth_ctx(&principal);
203        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
204        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
205    }
206
207    #[tokio::test]
208    async fn role_policy_denies_when_role_missing() {
209        let principal = TestPrincipal(test_principal(vec!["user"], vec![]));
210        let policy = RolePolicy::new(vec!["admin".into()], true);
211        let mut ex = empty_exchange();
212        let auth = auth_ctx(&principal);
213        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
214        assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
215    }
216
217    #[tokio::test]
218    async fn role_policy_any_required() {
219        let principal = TestPrincipal(test_principal(vec!["user"], vec![]));
220        let policy = RolePolicy::new(vec!["admin".into(), "user".into()], false);
221        let mut ex = empty_exchange();
222        let auth = auth_ctx(&principal);
223        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
224        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
225    }
226
227    #[tokio::test]
228    async fn scope_policy_grants() {
229        let principal = TestPrincipal(test_principal(vec![], vec!["read"]));
230        let policy = ScopePolicy::new(vec!["read".into()], true);
231        let mut ex = empty_exchange();
232        let auth = auth_ctx(&principal);
233        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
234        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
235    }
236
237    #[tokio::test]
238    async fn scope_policy_denies_when_scope_missing() {
239        let principal = TestPrincipal(test_principal(vec![], vec!["read"]));
240        let policy = ScopePolicy::new(vec!["write".into()], true);
241        let mut ex = empty_exchange();
242        let auth = auth_ctx(&principal);
243        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
244        assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
245    }
246
247    #[tokio::test]
248    async fn scope_policy_any_required() {
249        let principal = TestPrincipal(test_principal(vec![], vec!["read"]));
250        let policy = ScopePolicy::new(vec!["write".into(), "read".into()], false);
251        let mut ex = empty_exchange();
252        let auth = auth_ctx(&principal);
253        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
254        assert!(matches!(decision, AuthorizationDecision::Granted { .. }));
255    }
256}