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}