manta_shared/common/
jwt_ops.rs1use base64::prelude::*;
29use serde_json::Value;
30
31use crate::common::error::MantaError;
32
33pub static PA_ADMIN: &str = "pa_admin";
37
38fn get_claims_from_jwt_token(token: &str) -> Result<Value, MantaError> {
39 let jwt_body = token.split(' ').nth(1).unwrap_or(token);
41
42 let base64_claims = jwt_body.split('.').nth(1).ok_or_else(|| {
43 MantaError::JwtMalformed(
44 "expected header.payload.signature format".to_string(),
45 )
46 })?;
47
48 let claims_u8 = BASE64_URL_SAFE_NO_PAD
49 .decode(base64_claims)
50 .or_else(|_| BASE64_STANDARD.decode(base64_claims))
51 .map_err(|e| {
52 MantaError::JwtMalformed(format!("could not decode claims: {e}"))
53 })?;
54
55 let claims_str = std::str::from_utf8(&claims_u8).map_err(|e| {
56 MantaError::JwtMalformed(format!("claims are not valid UTF-8: {e}"))
57 })?;
58
59 Ok(serde_json::from_str::<Value>(claims_str)?)
60}
61
62pub fn get_name(token: &str) -> Result<String, MantaError> {
72 let jwt_claims = get_claims_from_jwt_token(token)?;
73
74 let jwt_name = jwt_claims.get("name").and_then(Value::as_str);
75
76 match jwt_name {
77 Some(name) => Ok(name.to_string()),
78 None => Ok("MISSING".to_string()),
79 }
80}
81
82pub fn get_preferred_username(token: &str) -> Result<String, MantaError> {
92 let jwt_claims = get_claims_from_jwt_token(token)?;
93
94 let jwt_preferred_username =
95 jwt_claims.get("preferred_username").and_then(Value::as_str);
96
97 match jwt_preferred_username {
98 Some(name) => Ok(name.to_string()),
99 None => Ok("MISSING".to_string()),
100 }
101}
102
103pub fn get_roles(token: &str) -> Result<Vec<String>, MantaError> {
116 Ok(
118 get_claims_from_jwt_token(token)?
119 .pointer("/realm_access/roles")
120 .unwrap_or(&serde_json::json!([]))
121 .as_array()
122 .cloned()
123 .unwrap_or_default()
124 .iter()
125 .filter_map(|role_value| role_value.as_str().map(str::to_string))
126 .collect(),
127 )
128}
129
130pub fn has_role(token: &str, role: &str) -> bool {
137 get_roles(token).is_ok_and(|roles| roles.iter().any(|r| r == role))
138}
139
140pub fn is_user_admin(token: &str) -> bool {
148 has_role(token, PA_ADMIN)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 fn make_jwt(payload: &serde_json::Value) -> String {
157 let header = BASE64_URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
158 let body = BASE64_URL_SAFE_NO_PAD.encode(payload.to_string());
159 format!("{header}.{body}.sig")
160 }
161
162 #[test]
165 fn get_name_present() {
166 let token = make_jwt(&serde_json::json!({
167 "name": "Alice Smith",
168 "preferred_username": "alice"
169 }));
170 assert_eq!(get_name(&token).unwrap(), "Alice Smith");
171 }
172
173 #[test]
174 fn get_name_missing_returns_missing() {
175 let token = make_jwt(&serde_json::json!({
176 "preferred_username": "alice"
177 }));
178 assert_eq!(get_name(&token).unwrap(), "MISSING");
179 }
180
181 #[test]
182 fn get_name_with_bearer_prefix() {
183 let token = make_jwt(&serde_json::json!({
184 "name": "Bob Jones"
185 }));
186 let bearer_token = format!("Bearer {token}");
187 assert_eq!(get_name(&bearer_token).unwrap(), "Bob Jones");
188 }
189
190 #[test]
193 fn get_preferred_username_present() {
194 let token = make_jwt(&serde_json::json!({
195 "name": "Alice",
196 "preferred_username": "alice123"
197 }));
198 assert_eq!(get_preferred_username(&token).unwrap(), "alice123");
199 }
200
201 #[test]
202 fn get_preferred_username_missing_returns_missing() {
203 let token = make_jwt(&serde_json::json!({"name": "Alice"}));
204 assert_eq!(get_preferred_username(&token).unwrap(), "MISSING");
205 }
206
207 #[test]
210 fn malformed_jwt_no_dots() {
211 assert!(get_claims_from_jwt_token("nodots").is_err());
212 }
213
214 #[test]
215 fn malformed_jwt_invalid_base64() {
216 assert!(get_claims_from_jwt_token("header.!!!invalid.sig").is_err());
217 }
218
219 #[test]
220 fn jwt_with_standard_base64_padding() {
221 let payload = serde_json::json!({"name": "Test"});
223 let header = BASE64_STANDARD.encode(r#"{"alg":"none"}"#);
224 let body = BASE64_STANDARD.encode(payload.to_string());
225 let token = format!("{header}.{body}.sig");
226 assert_eq!(get_name(&token).unwrap(), "Test");
227 }
228
229 #[test]
230 fn empty_token_string_is_err() {
231 assert!(get_claims_from_jwt_token("").is_err());
232 }
233
234 #[test]
235 fn jwt_with_valid_base64_but_invalid_json() {
236 let body = BASE64_URL_SAFE_NO_PAD.encode("not json at all");
238 let token = format!("header.{body}.sig");
239 assert!(get_claims_from_jwt_token(&token).is_err());
240 }
241
242 #[test]
243 fn jwt_with_valid_base64_but_invalid_utf8() {
244 let body = BASE64_URL_SAFE_NO_PAD.encode([0xFF, 0xFE, 0xFD]);
246 let token = format!("header.{body}.sig");
247 assert!(get_claims_from_jwt_token(&token).is_err());
248 }
249
250 #[test]
251 fn get_name_with_empty_string_name() {
252 let token = make_jwt(&serde_json::json!({"name": ""}));
253 assert_eq!(get_name(&token).unwrap(), "");
254 }
255
256 #[test]
257 fn bearer_prefix_with_extra_spaces() {
258 let token = make_jwt(&serde_json::json!({"name": "Test"}));
260 let bad_bearer = format!("Bearer {token}");
261 assert!(get_name(&bad_bearer).is_err());
263 }
264
265 #[test]
268 fn has_role_finds_role_when_present() {
269 let token = make_jwt(&serde_json::json!({
270 "realm_access": { "roles": ["other-role", "sample-role"] }
271 }));
272 assert!(has_role(&token, "sample-role"));
273 }
274
275 #[test]
276 fn has_role_returns_false_when_role_absent() {
277 let token = make_jwt(&serde_json::json!({
278 "realm_access": { "roles": ["other-role"] }
279 }));
280 assert!(!has_role(&token, "sample-role"));
281 }
282
283 #[test]
284 fn has_role_returns_false_on_missing_realm_access() {
285 let token = make_jwt(&serde_json::json!({ "name": "Alice" }));
286 assert!(!has_role(&token, "sample-role"));
287 }
288
289 #[test]
290 fn has_role_returns_false_on_empty_roles_array() {
291 let token = make_jwt(&serde_json::json!({
292 "realm_access": { "roles": [] }
293 }));
294 assert!(!has_role(&token, "sample-role"));
295 }
296
297 #[test]
298 fn has_role_returns_false_on_malformed_jwt() {
299 assert!(!has_role("not.a.jwt", "sample-role"));
300 assert!(!has_role("only-two.dots", "sample-role"));
301 assert!(!has_role("", "sample-role"));
302 }
303
304 #[test]
305 fn has_role_with_bearer_prefix() {
306 let token = make_jwt(&serde_json::json!({
307 "realm_access": { "roles": ["sample-role"] }
308 }));
309 let bearer_token = format!("Bearer {token}");
310 assert!(has_role(&bearer_token, "sample-role"));
311 }
312
313 #[test]
314 fn has_role_can_check_any_role_string() {
315 let token = make_jwt(&serde_json::json!({
318 "realm_access": { "roles": ["pa_admin"] }
319 }));
320 assert!(has_role(&token, "pa_admin"));
321 assert!(!has_role(&token, "sample-role"));
322 }
323}