use base64::prelude::*;
use serde_json::Value;
use crate::common::error::MantaError;
pub static PA_ADMIN: &str = "pa_admin";
fn get_claims_from_jwt_token(token: &str) -> Result<Value, MantaError> {
let jwt_body = token.split(' ').nth(1).unwrap_or(token);
let base64_claims = jwt_body.split('.').nth(1).ok_or_else(|| {
MantaError::JwtMalformed(
"expected header.payload.signature format".to_string(),
)
})?;
let claims_u8 = BASE64_URL_SAFE_NO_PAD
.decode(base64_claims)
.or_else(|_| BASE64_STANDARD.decode(base64_claims))
.map_err(|e| {
MantaError::JwtMalformed(format!("could not decode claims: {e}"))
})?;
let claims_str = std::str::from_utf8(&claims_u8).map_err(|e| {
MantaError::JwtMalformed(format!("claims are not valid UTF-8: {e}"))
})?;
Ok(serde_json::from_str::<Value>(claims_str)?)
}
pub fn get_name(token: &str) -> Result<String, MantaError> {
let jwt_claims = get_claims_from_jwt_token(token)?;
let jwt_name = jwt_claims.get("name").and_then(Value::as_str);
match jwt_name {
Some(name) => Ok(name.to_string()),
None => Ok("MISSING".to_string()),
}
}
pub fn get_preferred_username(token: &str) -> Result<String, MantaError> {
let jwt_claims = get_claims_from_jwt_token(token)?;
let jwt_preferred_username =
jwt_claims.get("preferred_username").and_then(Value::as_str);
match jwt_preferred_username {
Some(name) => Ok(name.to_string()),
None => Ok("MISSING".to_string()),
}
}
pub fn get_roles(token: &str) -> Result<Vec<String>, MantaError> {
Ok(
get_claims_from_jwt_token(token)?
.pointer("/realm_access/roles")
.unwrap_or(&serde_json::json!([]))
.as_array()
.cloned()
.unwrap_or_default()
.iter()
.filter_map(|role_value| role_value.as_str().map(str::to_string))
.collect(),
)
}
pub const READ_ONLY_ROLE: &str = "manta-read-only";
pub fn has_role(token: &str, role: &str) -> bool {
get_roles(token).is_ok_and(|roles| roles.iter().any(|r| r == role))
}
pub fn is_user_admin(token: &str) -> bool {
has_role(token, PA_ADMIN)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_jwt(payload: &serde_json::Value) -> String {
let header = BASE64_URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
let body = BASE64_URL_SAFE_NO_PAD.encode(payload.to_string());
format!("{header}.{body}.sig")
}
#[test]
fn get_name_present() {
let token = make_jwt(&serde_json::json!({
"name": "Alice Smith",
"preferred_username": "alice"
}));
assert_eq!(get_name(&token).unwrap(), "Alice Smith");
}
#[test]
fn get_name_missing_returns_missing() {
let token = make_jwt(&serde_json::json!({
"preferred_username": "alice"
}));
assert_eq!(get_name(&token).unwrap(), "MISSING");
}
#[test]
fn get_name_with_bearer_prefix() {
let token = make_jwt(&serde_json::json!({
"name": "Bob Jones"
}));
let bearer_token = format!("Bearer {token}");
assert_eq!(get_name(&bearer_token).unwrap(), "Bob Jones");
}
#[test]
fn get_preferred_username_present() {
let token = make_jwt(&serde_json::json!({
"name": "Alice",
"preferred_username": "alice123"
}));
assert_eq!(get_preferred_username(&token).unwrap(), "alice123");
}
#[test]
fn get_preferred_username_missing_returns_missing() {
let token = make_jwt(&serde_json::json!({"name": "Alice"}));
assert_eq!(get_preferred_username(&token).unwrap(), "MISSING");
}
#[test]
fn malformed_jwt_no_dots() {
assert!(get_claims_from_jwt_token("nodots").is_err());
}
#[test]
fn malformed_jwt_invalid_base64() {
assert!(get_claims_from_jwt_token("header.!!!invalid.sig").is_err());
}
#[test]
fn jwt_with_standard_base64_padding() {
let payload = serde_json::json!({"name": "Test"});
let header = BASE64_STANDARD.encode(r#"{"alg":"none"}"#);
let body = BASE64_STANDARD.encode(payload.to_string());
let token = format!("{header}.{body}.sig");
assert_eq!(get_name(&token).unwrap(), "Test");
}
#[test]
fn empty_token_string_is_err() {
assert!(get_claims_from_jwt_token("").is_err());
}
#[test]
fn jwt_with_valid_base64_but_invalid_json() {
let body = BASE64_URL_SAFE_NO_PAD.encode("not json at all");
let token = format!("header.{body}.sig");
assert!(get_claims_from_jwt_token(&token).is_err());
}
#[test]
fn jwt_with_valid_base64_but_invalid_utf8() {
let body = BASE64_URL_SAFE_NO_PAD.encode([0xFF, 0xFE, 0xFD]);
let token = format!("header.{body}.sig");
assert!(get_claims_from_jwt_token(&token).is_err());
}
#[test]
fn get_name_with_empty_string_name() {
let token = make_jwt(&serde_json::json!({"name": ""}));
assert_eq!(get_name(&token).unwrap(), "");
}
#[test]
fn bearer_prefix_with_extra_spaces() {
let token = make_jwt(&serde_json::json!({"name": "Test"}));
let bad_bearer = format!("Bearer {token}");
assert!(get_name(&bad_bearer).is_err());
}
#[test]
fn has_role_finds_role_when_present() {
let token = make_jwt(&serde_json::json!({
"realm_access": { "roles": ["other-role", "manta-read-only"] }
}));
assert!(has_role(&token, READ_ONLY_ROLE));
}
#[test]
fn has_role_returns_false_when_role_absent() {
let token = make_jwt(&serde_json::json!({
"realm_access": { "roles": ["other-role"] }
}));
assert!(!has_role(&token, READ_ONLY_ROLE));
}
#[test]
fn has_role_returns_false_on_missing_realm_access() {
let token = make_jwt(&serde_json::json!({ "name": "Alice" }));
assert!(!has_role(&token, READ_ONLY_ROLE));
}
#[test]
fn has_role_returns_false_on_empty_roles_array() {
let token = make_jwt(&serde_json::json!({
"realm_access": { "roles": [] }
}));
assert!(!has_role(&token, READ_ONLY_ROLE));
}
#[test]
fn has_role_returns_false_on_malformed_jwt() {
assert!(!has_role("not.a.jwt", READ_ONLY_ROLE));
assert!(!has_role("only-two.dots", READ_ONLY_ROLE));
assert!(!has_role("", READ_ONLY_ROLE));
}
#[test]
fn has_role_with_bearer_prefix() {
let token = make_jwt(&serde_json::json!({
"realm_access": { "roles": ["manta-read-only"] }
}));
let bearer_token = format!("Bearer {token}");
assert!(has_role(&bearer_token, READ_ONLY_ROLE));
}
#[test]
fn has_role_can_check_any_role_string() {
let token = make_jwt(&serde_json::json!({
"realm_access": { "roles": ["pa_admin"] }
}));
assert!(has_role(&token, "pa_admin"));
assert!(!has_role(&token, "manta-read-only"));
}
#[test]
fn read_only_role_constant_is_expected_string() {
assert_eq!(READ_ONLY_ROLE, "manta-read-only");
}
}