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