use micromegas_tracing::prelude::*;
use percent_encoding::percent_decode_str;
use tonic::{Status, metadata::MetadataMap};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserAttribution {
pub user_id: String,
pub user_email: String,
pub user_name: Option<String>,
pub service_account: Option<String>,
}
fn get_header_string_lossy(metadata: &MetadataMap, key: &str) -> Option<String> {
let value = metadata.get(key)?;
match value.to_str() {
Ok(s) => {
match percent_decode_str(s).decode_utf8() {
Ok(decoded) => Some(decoded.into_owned()),
Err(e) => {
warn!("Header '{key}' has invalid percent-encoded UTF-8: {e}");
Some(s.to_string()) }
}
}
Err(_) => {
let bytes = value.as_bytes();
let printable: String = bytes
.iter()
.filter(|&&b| (0x20..=0x7E).contains(&b))
.map(|&b| b as char)
.collect();
warn!(
"Header '{key}' contains non-ASCII bytes, extracted printable portion: '{printable}'"
);
if !printable.is_empty() {
Some(printable)
} else {
None
}
}
}
}
pub fn validate_and_resolve_user_attribution_grpc(
metadata: &MetadataMap,
) -> Result<UserAttribution, Box<Status>> {
let auth_subject = metadata.get("x-auth-subject").and_then(|v| v.to_str().ok());
let auth_email = metadata.get("x-auth-email").and_then(|v| v.to_str().ok());
let allow_delegation = metadata
.get("x-allow-delegation")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<bool>().ok())
.unwrap_or(false);
let claimed_user_id = get_header_string_lossy(metadata, "x-user-id");
let claimed_user_email = get_header_string_lossy(metadata, "x-user-email");
let claimed_user_name = get_header_string_lossy(metadata, "x-user-name");
let Some(authenticated_subject) = auth_subject else {
return Ok(UserAttribution {
user_id: claimed_user_id.unwrap_or_else(|| "unknown".to_string()),
user_email: claimed_user_email.unwrap_or_else(|| "unknown".to_string()),
user_name: claimed_user_name,
service_account: None,
});
};
if allow_delegation {
let has_delegation = claimed_user_id.is_some() || claimed_user_email.is_some();
let user_id = claimed_user_id.unwrap_or_else(|| authenticated_subject.to_string());
let user_email = claimed_user_email
.or_else(|| auth_email.map(|s| s.to_string()))
.unwrap_or_else(|| "service-account".to_string());
let service_account = if has_delegation {
Some(authenticated_subject.to_string())
} else {
None
};
Ok(UserAttribution {
user_id,
user_email,
user_name: claimed_user_name,
service_account,
})
} else {
if let Some(ref claimed_id) = claimed_user_id
&& claimed_id != authenticated_subject
{
return Err(Box::new(Status::permission_denied(format!(
"User impersonation not allowed: x-user-id '{}' does not match authenticated subject '{}'",
claimed_id, authenticated_subject
))));
}
if let (Some(claimed_email), Some(authenticated_email)) = (&claimed_user_email, auth_email)
&& claimed_email != authenticated_email
{
return Err(Box::new(Status::permission_denied(format!(
"User impersonation not allowed: x-user-email '{}' does not match authenticated email '{}'",
claimed_email, authenticated_email
))));
}
Ok(UserAttribution {
user_id: authenticated_subject.to_string(),
user_email: auth_email.unwrap_or("unknown").to_string(),
user_name: claimed_user_name,
service_account: None,
})
}
}