Skip to main content

camel_api/
security_policy.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5
6use crate::{CamelError, Exchange};
7
8/// Represents an authenticated principal extracted from token claims.
9///
10/// Provider-neutral: the `ClaimsMapper` trait in `camel-auth` is responsible
11/// for mapping provider-specific claim shapes into this structure.
12#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
13pub struct Principal {
14    pub subject: String,
15    #[serde(default)]
16    pub issuer: String,
17    #[serde(default)]
18    pub audience: Vec<String>,
19    pub scopes: Vec<String>,
20    pub roles: Vec<String>,
21    pub claims: serde_json::Value,
22}
23
24impl Principal {
25    /// Check if the principal has a specific role.
26    pub fn has_role(&self, role: &str) -> bool {
27        self.roles.iter().any(|r| r == role)
28    }
29
30    /// Check if the principal has a specific scope.
31    pub fn has_scope(&self, scope: &str) -> bool {
32        self.scopes.iter().any(|s| s == scope)
33    }
34}
35
36#[derive(Debug, Clone, PartialEq)]
37pub enum AuthorizationDecision {
38    Granted {
39        principal: Principal,
40    },
41    Denied {
42        reason: String,
43        required: Vec<String>,
44        actual: Vec<String>,
45    },
46}
47
48impl std::fmt::Display for AuthorizationDecision {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        match self {
51            Self::Granted { principal } => {
52                write!(f, "Access granted for {}", principal.subject)
53            }
54            Self::Denied { reason, .. } => write!(f, "Access denied: {reason}"),
55        }
56    }
57}
58
59#[async_trait]
60pub trait SecurityPolicy: Send + Sync {
61    async fn evaluate(&self, exchange: &mut Exchange) -> Result<AuthorizationDecision, CamelError>;
62}
63
64pub struct SecurityPolicyConfig {
65    pub policy: Arc<dyn SecurityPolicy>,
66}
67
68impl SecurityPolicyConfig {
69    pub fn new(policy: impl SecurityPolicy + 'static) -> Self {
70        Self {
71            policy: Arc::new(policy),
72        }
73    }
74
75    pub fn from_arc(policy: Arc<dyn SecurityPolicy>) -> Self {
76        Self { policy }
77    }
78}
79
80impl Clone for SecurityPolicyConfig {
81    fn clone(&self) -> Self {
82        Self {
83            policy: Arc::clone(&self.policy),
84        }
85    }
86}
87
88impl std::fmt::Debug for SecurityPolicyConfig {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.debug_struct("SecurityPolicyConfig")
91            .field("policy", &"<SecurityPolicy>")
92            .finish()
93    }
94}
95
96// --- Principal property storage helpers ---
97
98/// Exchange property key for the principal's subject.
99pub const PRINCIPAL_SUBJECT_KEY: &str = "camel.auth.subject";
100/// Exchange property key for the principal's roles (JSON array).
101pub const PRINCIPAL_ROLES_KEY: &str = "camel.auth.roles";
102/// Exchange property key for the principal's scopes (JSON array).
103pub const PRINCIPAL_SCOPES_KEY: &str = "camel.auth.scopes";
104/// Exchange property key for the principal's issuer.
105pub const PRINCIPAL_ISSUER_KEY: &str = "camel.auth.issuer";
106/// Exchange property key for the principal's raw claims (JSON object).
107pub const PRINCIPAL_CLAIMS_KEY: &str = "camel.auth.claims";
108/// Exchange property key for the principal's audience (JSON array).
109pub const PRINCIPAL_AUDIENCE_KEY: &str = "camel.auth.audience";
110/// Exchange property key for the full serialized principal.
111pub const PRINCIPAL_KEY: &str = "camel.auth.principal";
112
113/// Store all principal properties as exchange properties under well-known keys.
114pub fn store_principal_properties(exchange: &mut Exchange, principal: &Principal) {
115    exchange.set_property(PRINCIPAL_SUBJECT_KEY, principal.subject.clone());
116    exchange.set_property(
117        PRINCIPAL_ROLES_KEY,
118        serde_json::to_string(&principal.roles).unwrap_or_default(),
119    );
120    exchange.set_property(
121        PRINCIPAL_SCOPES_KEY,
122        serde_json::to_string(&principal.scopes).unwrap_or_default(),
123    );
124    exchange.set_property(PRINCIPAL_ISSUER_KEY, principal.issuer.clone());
125    exchange.set_property(
126        PRINCIPAL_CLAIMS_KEY,
127        serde_json::to_string(&principal.claims).unwrap_or_default(),
128    );
129    exchange.set_property(
130        PRINCIPAL_AUDIENCE_KEY,
131        serde_json::to_string(&principal.audience).unwrap_or_default(),
132    );
133    exchange.set_property(
134        PRINCIPAL_KEY,
135        serde_json::to_string(principal).unwrap_or_default(),
136    );
137}
138
139pub fn principal_from_exchange(exchange: &Exchange) -> Option<Principal> {
140    exchange
141        .property(PRINCIPAL_KEY)
142        .and_then(|v| v.as_str())
143        .and_then(|s| serde_json::from_str(s).ok())
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use crate::Body;
150
151    fn test_principal(roles: Vec<&str>, scopes: Vec<&str>) -> Principal {
152        Principal {
153            subject: "user1".into(),
154            issuer: "test".into(),
155            audience: vec![],
156            scopes: scopes.into_iter().map(String::from).collect(),
157            roles: roles.into_iter().map(String::from).collect(),
158            claims: serde_json::Value::Null,
159        }
160    }
161
162    #[test]
163    fn principal_has_role_is_case_sensitive() {
164        let p = test_principal(vec!["Admin", "User"], vec![]);
165        assert!(!p.has_role("admin"));
166        assert!(!p.has_role("ADMIN"));
167        assert!(p.has_role("User"));
168        assert!(!p.has_role("guest"));
169    }
170
171    #[test]
172    fn principal_has_scope() {
173        let p = test_principal(vec![], vec!["read", "write"]);
174        assert!(p.has_scope("read"));
175        assert!(!p.has_scope("delete"));
176    }
177
178    #[test]
179    fn authorization_decision_granted_display() {
180        let p = test_principal(vec![], vec![]);
181        let d = AuthorizationDecision::Granted { principal: p };
182        assert!(format!("{d}").contains("user1"));
183    }
184
185    #[test]
186    fn authorization_decision_denied_display() {
187        let d = AuthorizationDecision::Denied {
188            reason: "missing role".into(),
189            required: vec!["admin".into()],
190            actual: vec![],
191        };
192        assert!(format!("{d}").contains("missing role"));
193    }
194
195    #[test]
196    fn security_policy_config_debug_redacts_policy() {
197        struct DummyPolicy;
198
199        #[async_trait]
200        impl SecurityPolicy for DummyPolicy {
201            async fn evaluate(
202                &self,
203                _exchange: &mut Exchange,
204            ) -> Result<AuthorizationDecision, CamelError> {
205                Ok(AuthorizationDecision::Granted {
206                    principal: test_principal(vec![], vec![]),
207                })
208            }
209        }
210
211        let config = SecurityPolicyConfig::new(DummyPolicy);
212        let debug = format!("{config:?}");
213        assert!(debug.contains("SecurityPolicyConfig"));
214        assert!(debug.contains("<SecurityPolicy>"));
215    }
216
217    #[test]
218    fn store_principal_properties_populates_all_keys() {
219        let principal = Principal {
220            subject: "alice".into(),
221            issuer: "keycloak".into(),
222            audience: vec!["api".into()],
223            scopes: vec!["read".into(), "write".into()],
224            roles: vec!["admin".into()],
225            claims: serde_json::json!({"sub": "alice", "custom": true}),
226        };
227        let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
228        store_principal_properties(&mut exchange, &principal);
229
230        assert_eq!(
231            exchange.property(PRINCIPAL_SUBJECT_KEY).unwrap(),
232            &serde_json::Value::String("alice".into())
233        );
234        assert_eq!(
235            exchange.property(PRINCIPAL_ISSUER_KEY).unwrap(),
236            &serde_json::Value::String("keycloak".into())
237        );
238        let roles: Vec<String> = serde_json::from_str(
239            exchange
240                .property(PRINCIPAL_ROLES_KEY)
241                .unwrap()
242                .as_str()
243                .unwrap(),
244        )
245        .unwrap();
246        assert_eq!(roles, vec!["admin"]);
247        let scopes: Vec<String> = serde_json::from_str(
248            exchange
249                .property(PRINCIPAL_SCOPES_KEY)
250                .unwrap()
251                .as_str()
252                .unwrap(),
253        )
254        .unwrap();
255        assert_eq!(scopes, vec!["read", "write"]);
256        let audience: Vec<String> = serde_json::from_str(
257            exchange
258                .property(PRINCIPAL_AUDIENCE_KEY)
259                .unwrap()
260                .as_str()
261                .unwrap(),
262        )
263        .unwrap();
264        assert_eq!(audience, vec!["api"]);
265        let claims: serde_json::Value = serde_json::from_str(
266            exchange
267                .property(PRINCIPAL_CLAIMS_KEY)
268                .unwrap()
269                .as_str()
270                .unwrap(),
271        )
272        .unwrap();
273        assert!(claims.as_object().unwrap().contains_key("custom"));
274        let full: serde_json::Value =
275            serde_json::from_str(exchange.property(PRINCIPAL_KEY).unwrap().as_str().unwrap())
276                .unwrap();
277        assert_eq!(full["subject"], "alice");
278    }
279
280    #[test]
281    fn security_policy_config_clone() {
282        struct DummyPolicy;
283
284        #[async_trait]
285        impl SecurityPolicy for DummyPolicy {
286            async fn evaluate(
287                &self,
288                _exchange: &mut Exchange,
289            ) -> Result<AuthorizationDecision, CamelError> {
290                Ok(AuthorizationDecision::Granted {
291                    principal: test_principal(vec![], vec![]),
292                })
293            }
294        }
295
296        let config = SecurityPolicyConfig::new(DummyPolicy);
297        let cloned = config.clone();
298        // Both point to same Arc
299        assert!(Arc::ptr_eq(&config.policy, &cloned.policy));
300    }
301
302    #[test]
303    fn test_principal_from_exchange_round_trip() {
304        let principal = Principal {
305            subject: "bob".into(),
306            issuer: "keycloak".into(),
307            audience: vec!["api".into()],
308            scopes: vec!["read".into()],
309            roles: vec!["user".into()],
310            claims: serde_json::json!({"sub": "bob"}),
311        };
312        let mut exchange = Exchange::new(crate::Message::new(Body::Empty));
313        store_principal_properties(&mut exchange, &principal);
314
315        let recovered = principal_from_exchange(&exchange).expect("principal should be recovered");
316        assert_eq!(recovered.subject, "bob");
317        assert_eq!(recovered.issuer, "keycloak");
318        assert_eq!(recovered.audience, vec!["api"]);
319        assert_eq!(recovered.scopes, vec!["read"]);
320        assert_eq!(recovered.roles, vec!["user"]);
321    }
322}