use tonic::service::Interceptor; use tonic::{Request, Status}; use uuid::Uuid; use admin::share::roles::{AdminActor, AdminRole}; pub const TRUSTED_KEY_HEADER: &str = "x-trusted-key"; pub const ADMIN_ID_HEADER: &str = "x-admin-id"; pub const ADMIN_USERNAME_HEADER: &str = "x-admin-username"; pub const ADMIN_DISPLAY_NAME_HEADER: &str = "x-admin-display-name"; pub const ADMIN_ROLE_HEADER: &str = "x-admin-role"; #[derive(Debug, Clone)] pub struct TrustedPrincipal { pub id: Uuid, pub username: String, pub display_name: String, pub role: AdminRole, } impl TrustedPrincipal { pub fn actor(&self) -> AdminActor { AdminActor { id: self.id, username: self.username.clone(), display_name: self.display_name.clone(), role: self.role, must_change_password: false, } } } #[derive(Clone)] pub struct TrustedBackend { keys: Vec, } impl TrustedBackend { pub fn new(keys: Vec) -> Self { Self { keys } } } impl Interceptor for TrustedBackend { fn call(&mut self, mut request: Request<()>) -> Result, Status> { if self.keys.is_empty() { return Err(Status::unauthenticated("no trusted keys are configured")); } let presented = header(request.metadata(), TRUSTED_KEY_HEADER).unwrap_or_default(); if !self .keys .iter() .any(|key| !key.is_empty() && key == &presented) { return Err(Status::unauthenticated("invalid trusted key")); } let username = header(request.metadata(), ADMIN_USERNAME_HEADER).unwrap_or("console".to_string()); let display_name = header(request.metadata(), ADMIN_DISPLAY_NAME_HEADER) .unwrap_or_else(|| username.clone()); let role = parse_role(header(request.metadata(), ADMIN_ROLE_HEADER).as_deref()); let id = header(request.metadata(), ADMIN_ID_HEADER) .and_then(|value| Uuid::parse_str(&value).ok()) .unwrap_or_else(Uuid::nil); request.extensions_mut().insert(TrustedPrincipal { id, username, display_name, role, }); Ok(request) } } pub fn principal(request: &Request) -> Result { request .extensions() .get::() .cloned() .ok_or_else(|| Status::unauthenticated("missing trusted principal")) } fn header(metadata: &tonic::metadata::MetadataMap, name: &str) -> Option { metadata .get(name) .and_then(|value| value.to_str().ok()) .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string) } fn parse_role(value: Option<&str>) -> AdminRole { match value.unwrap_or_default().to_ascii_lowercase().as_str() { "admin" | "super_admin" | "superadmin" => AdminRole::SuperAdmin, "operator" => AdminRole::Operator, _ => AdminRole::Viewer, } }