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 const READ_ONLY_ROLE: &str = "manta-read-only";
135
136pub fn has_role(token: &str, role: &str) -> bool {
144 get_roles(token).is_ok_and(|roles| roles.iter().any(|r| r == role))
145}
146
147pub fn is_user_admin(token: &str) -> bool {
155 has_role(token, PA_ADMIN)
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 fn make_jwt(payload: &serde_json::Value) -> String {
164 let header = BASE64_URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
165 let body = BASE64_URL_SAFE_NO_PAD.encode(payload.to_string());
166 format!("{header}.{body}.sig")
167 }
168
169 #[test]
172 fn get_name_present() {
173 let token = make_jwt(&serde_json::json!({
174 "name": "Alice Smith",
175 "preferred_username": "alice"
176 }));
177 assert_eq!(get_name(&token).unwrap(), "Alice Smith");
178 }
179
180 #[test]
181 fn get_name_missing_returns_missing() {
182 let token = make_jwt(&serde_json::json!({
183 "preferred_username": "alice"
184 }));
185 assert_eq!(get_name(&token).unwrap(), "MISSING");
186 }
187
188 #[test]
189 fn get_name_with_bearer_prefix() {
190 let token = make_jwt(&serde_json::json!({
191 "name": "Bob Jones"
192 }));
193 let bearer_token = format!("Bearer {token}");
194 assert_eq!(get_name(&bearer_token).unwrap(), "Bob Jones");
195 }
196
197 #[test]
200 fn get_preferred_username_present() {
201 let token = make_jwt(&serde_json::json!({
202 "name": "Alice",
203 "preferred_username": "alice123"
204 }));
205 assert_eq!(get_preferred_username(&token).unwrap(), "alice123");
206 }
207
208 #[test]
209 fn get_preferred_username_missing_returns_missing() {
210 let token = make_jwt(&serde_json::json!({"name": "Alice"}));
211 assert_eq!(get_preferred_username(&token).unwrap(), "MISSING");
212 }
213
214 #[test]
217 fn malformed_jwt_no_dots() {
218 assert!(get_claims_from_jwt_token("nodots").is_err());
219 }
220
221 #[test]
222 fn malformed_jwt_invalid_base64() {
223 assert!(get_claims_from_jwt_token("header.!!!invalid.sig").is_err());
224 }
225
226 #[test]
227 fn jwt_with_standard_base64_padding() {
228 let payload = serde_json::json!({"name": "Test"});
230 let header = BASE64_STANDARD.encode(r#"{"alg":"none"}"#);
231 let body = BASE64_STANDARD.encode(payload.to_string());
232 let token = format!("{header}.{body}.sig");
233 assert_eq!(get_name(&token).unwrap(), "Test");
234 }
235
236 #[test]
237 fn empty_token_string_is_err() {
238 assert!(get_claims_from_jwt_token("").is_err());
239 }
240
241 #[test]
242 fn jwt_with_valid_base64_but_invalid_json() {
243 let body = BASE64_URL_SAFE_NO_PAD.encode("not json at all");
245 let token = format!("header.{body}.sig");
246 assert!(get_claims_from_jwt_token(&token).is_err());
247 }
248
249 #[test]
250 fn jwt_with_valid_base64_but_invalid_utf8() {
251 let body = BASE64_URL_SAFE_NO_PAD.encode([0xFF, 0xFE, 0xFD]);
253 let token = format!("header.{body}.sig");
254 assert!(get_claims_from_jwt_token(&token).is_err());
255 }
256
257 #[test]
258 fn get_name_with_empty_string_name() {
259 let token = make_jwt(&serde_json::json!({"name": ""}));
260 assert_eq!(get_name(&token).unwrap(), "");
261 }
262
263 #[test]
264 fn bearer_prefix_with_extra_spaces() {
265 let token = make_jwt(&serde_json::json!({"name": "Test"}));
267 let bad_bearer = format!("Bearer {token}");
268 assert!(get_name(&bad_bearer).is_err());
270 }
271
272 #[test]
275 fn has_role_finds_role_when_present() {
276 let token = make_jwt(&serde_json::json!({
277 "realm_access": { "roles": ["other-role", "manta-read-only"] }
278 }));
279 assert!(has_role(&token, READ_ONLY_ROLE));
280 }
281
282 #[test]
283 fn has_role_returns_false_when_role_absent() {
284 let token = make_jwt(&serde_json::json!({
285 "realm_access": { "roles": ["other-role"] }
286 }));
287 assert!(!has_role(&token, READ_ONLY_ROLE));
288 }
289
290 #[test]
291 fn has_role_returns_false_on_missing_realm_access() {
292 let token = make_jwt(&serde_json::json!({ "name": "Alice" }));
293 assert!(!has_role(&token, READ_ONLY_ROLE));
294 }
295
296 #[test]
297 fn has_role_returns_false_on_empty_roles_array() {
298 let token = make_jwt(&serde_json::json!({
299 "realm_access": { "roles": [] }
300 }));
301 assert!(!has_role(&token, READ_ONLY_ROLE));
302 }
303
304 #[test]
305 fn has_role_returns_false_on_malformed_jwt() {
306 assert!(!has_role("not.a.jwt", READ_ONLY_ROLE));
307 assert!(!has_role("only-two.dots", READ_ONLY_ROLE));
308 assert!(!has_role("", READ_ONLY_ROLE));
309 }
310
311 #[test]
312 fn has_role_with_bearer_prefix() {
313 let token = make_jwt(&serde_json::json!({
314 "realm_access": { "roles": ["manta-read-only"] }
315 }));
316 let bearer_token = format!("Bearer {token}");
317 assert!(has_role(&bearer_token, READ_ONLY_ROLE));
318 }
319
320 #[test]
321 fn has_role_can_check_any_role_string() {
322 let token = make_jwt(&serde_json::json!({
325 "realm_access": { "roles": ["pa_admin"] }
326 }));
327 assert!(has_role(&token, "pa_admin"));
328 assert!(!has_role(&token, "manta-read-only"));
329 }
330
331 #[test]
332 fn read_only_role_constant_is_expected_string() {
333 assert_eq!(READ_ONLY_ROLE, "manta-read-only");
335 }
336}