Skip to main content

camel_auth/
claims.rs

1use camel_api::security_policy::Principal;
2use serde::{Deserialize, Serialize};
3
4use crate::types::AuthError;
5
6pub trait ClaimsMapper: Send + Sync {
7    fn to_principal(&self, claims: &serde_json::Value) -> Result<Principal, AuthError>;
8}
9
10#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
11pub struct ClaimPaths {
12    pub subject: String,
13    pub roles: Vec<String>,
14    pub scopes: Option<String>,
15}
16
17pub struct JsonPointerClaimsMapper {
18    subject_path: String,
19    role_paths: Vec<String>,
20    scope_path: Option<String>,
21}
22
23impl JsonPointerClaimsMapper {
24    pub fn new(paths: ClaimPaths) -> Self {
25        Self {
26            subject_path: paths.subject,
27            role_paths: paths.roles,
28            scope_path: paths.scopes,
29        }
30    }
31}
32
33impl ClaimsMapper for JsonPointerClaimsMapper {
34    fn to_principal(&self, claims: &serde_json::Value) -> Result<Principal, AuthError> {
35        let subject = claims
36            .pointer(&self.subject_path)
37            .and_then(|v| v.as_str())
38            .filter(|s| !s.is_empty())
39            .ok_or_else(|| {
40                AuthError::TokenInvalid(format!(
41                    "missing or empty subject at JSON pointer {}",
42                    self.subject_path
43                ))
44            })?
45            .to_string();
46
47        let mut roles: Vec<String> = Vec::new();
48        for path in &self.role_paths {
49            if let Some(arr) = claims.pointer(path).and_then(|v| v.as_array()) {
50                roles.extend(arr.iter().filter_map(|v| v.as_str()).map(String::from));
51            }
52        }
53        roles.sort();
54        roles.dedup();
55
56        let scopes = self
57            .scope_path
58            .as_ref()
59            .and_then(|p| claims.pointer(p))
60            .map(|v| match v {
61                serde_json::Value::String(s) => s.split_whitespace().map(String::from).collect(),
62                serde_json::Value::Array(arr) => arr
63                    .iter()
64                    .filter_map(|v| v.as_str().map(String::from))
65                    .collect(),
66                _ => Vec::new(),
67            })
68            .unwrap_or_default();
69
70        Ok(Principal {
71            subject,
72            issuer: claims
73                .pointer("/iss")
74                .and_then(|v| v.as_str())
75                .unwrap_or("")
76                .to_string(),
77            audience: claims
78                .pointer("/aud")
79                .and_then(|v| match v {
80                    serde_json::Value::String(s) => Some(vec![s.clone()]),
81                    serde_json::Value::Array(arr) => Some(
82                        arr.iter()
83                            .filter_map(|v| v.as_str())
84                            .map(String::from)
85                            .collect(),
86                    ),
87                    _ => None,
88                })
89                .unwrap_or_default(),
90            roles,
91            scopes,
92            claims: claims.clone(),
93        })
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use serde_json::json;
101
102    fn mapper(paths: ClaimPaths) -> JsonPointerClaimsMapper {
103        JsonPointerClaimsMapper::new(paths)
104    }
105
106    fn default_paths() -> ClaimPaths {
107        ClaimPaths {
108            subject: "/sub".into(),
109            roles: vec!["/groups".into()],
110            scopes: None,
111        }
112    }
113
114    #[test]
115    fn extracts_subject() {
116        let claims = json!({"sub": "user-1"});
117        let principal = mapper(default_paths()).to_principal(&claims).unwrap();
118        assert_eq!(principal.subject, "user-1");
119    }
120
121    #[test]
122    fn missing_subject_returns_error() {
123        let claims = json!({"no_sub": "x"});
124        let result = mapper(default_paths()).to_principal(&claims);
125        assert!(result.is_err());
126    }
127
128    #[test]
129    fn empty_subject_returns_error() {
130        let claims = json!({"sub": ""});
131        let result = mapper(default_paths()).to_principal(&claims);
132        assert!(result.is_err());
133    }
134
135    #[test]
136    fn extracts_roles_from_single_path() {
137        let claims = json!({
138            "sub": "u",
139            "groups": ["admin", "user"]
140        });
141        let principal = mapper(default_paths()).to_principal(&claims).unwrap();
142        assert!(principal.has_role("admin"));
143        assert!(principal.has_role("user"));
144    }
145
146    #[test]
147    fn extracts_roles_from_multiple_paths_and_deduplicates() {
148        let paths = ClaimPaths {
149            subject: "/sub".into(),
150            roles: vec!["/groups".into(), "/app_roles".into()],
151            scopes: None,
152        };
153        let claims = json!({
154            "sub": "u",
155            "groups": ["admin"],
156            "app_roles": ["admin", "editor"]
157        });
158        let principal = mapper(paths).to_principal(&claims).unwrap();
159        assert_eq!(principal.roles, vec!["admin", "editor"]);
160    }
161
162    #[test]
163    fn no_role_paths_produces_empty_roles() {
164        let paths = ClaimPaths {
165            subject: "/sub".into(),
166            roles: vec![],
167            scopes: None,
168        };
169        let claims = json!({"sub": "u"});
170        let principal = mapper(paths).to_principal(&claims).unwrap();
171        assert!(principal.roles.is_empty());
172    }
173
174    #[test]
175    fn extracts_scopes_from_space_separated_string() {
176        let paths = ClaimPaths {
177            subject: "/sub".into(),
178            roles: vec![],
179            scopes: Some("/scope".into()),
180        };
181        let claims = json!({"sub": "u", "scope": "read write"});
182        let principal = mapper(paths).to_principal(&claims).unwrap();
183        assert_eq!(principal.scopes, vec!["read", "write"]);
184    }
185
186    #[test]
187    fn extracts_scopes_from_array() {
188        let paths = ClaimPaths {
189            subject: "/sub".into(),
190            roles: vec![],
191            scopes: Some("/scope".into()),
192        };
193        let claims = json!({"sub": "u", "scope": ["read", "write", "admin"]});
194        let principal = mapper(paths).to_principal(&claims).unwrap();
195        assert_eq!(principal.scopes, vec!["read", "write", "admin"]);
196    }
197
198    #[test]
199    fn claims_stored_in_principal() {
200        let claims = json!({"sub": "u", "custom": "value"});
201        let principal = mapper(default_paths()).to_principal(&claims).unwrap();
202        assert_eq!(principal.claims["custom"], "value");
203    }
204
205    #[test]
206    fn custom_subject_path() {
207        let paths = ClaimPaths {
208            subject: "/preferred_username".into(),
209            roles: vec![],
210            scopes: None,
211        };
212        let claims = json!({"preferred_username": "alice"});
213        let principal = mapper(paths).to_principal(&claims).unwrap();
214        assert_eq!(principal.subject, "alice");
215    }
216}