//! RPC 入口门禁。 //! //! 这些 RPC 只在受信网络里被其它服务调用,不会暴露到公网,所以入口只核对一个 //! 静态共享密钥:调用方把 `auth.rpc_key` 的值放进 `x-rpc-key` 头即可。 //! 不再有调用方服务身份、用户主体(principal)或 scope 判定,具体操作谁由请求 //! 体里的 `user_id` 决定。 use std::sync::Arc; use tonic::metadata::MetadataMap; use tonic::service::Interceptor as TonicInterceptor; use tonic::{Request, Status}; use uuid::Uuid; use crate::api::error; pub const RPC_KEY_METADATA: &str = "x-rpc-key"; pub const REQUEST_ID_METADATA: &str = "gitdata-request-id"; pub const REQUEST_ID_HEADER: &str = "x-request-id"; pub const CALLER_SERVICE_METADATA: &str = "gitdata-service-id"; pub const MAX_REQUEST_ID_LEN: usize = 64; /// 每个请求携带的元信息:只用于日志与事件关联。 #[derive(Debug, Clone, Default)] pub struct RequestMeta { pub request_id: String, /// 调用方自报的服务名,仅用于日志,不参与鉴权。 pub caller_service: String, } impl RequestMeta { pub fn internal() -> Self { Self { request_id: Uuid::now_v7().to_string(), caller_service: "kenapi".to_owned(), } } pub fn request_id(&self) -> Option<&str> { if self.request_id.is_empty() { None } else { Some(self.request_id.as_str()) } } } pub fn validate_request_id(value: &str) -> Option { let trimmed = value.trim(); if trimmed.is_empty() || trimmed.len() > MAX_REQUEST_ID_LEN { return None; } let valid_chars = trimmed .bytes() .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':')); if !valid_chars { return None; } Some(trimmed.to_owned()) } pub fn request_id_from(metadata: &MetadataMap) -> Option { metadata .get(REQUEST_ID_HEADER) .or_else(|| metadata.get(REQUEST_ID_METADATA)) .and_then(|value| value.to_str().ok()) .and_then(validate_request_id) } pub fn caller_service_from(metadata: &MetadataMap) -> Option { metadata .get(CALLER_SERVICE_METADATA) .and_then(|value| value.to_str().ok()) .map(|value| value.trim().to_owned()) .filter(|value| !value.is_empty()) } pub fn new_request_id() -> String { Uuid::now_v7().to_string() } /// 共享密钥门禁:校验 `x-rpc-key`,并把 `RequestMeta` 挂到请求扩展上。 #[derive(Clone)] pub struct Interceptor { key: Arc, } impl Interceptor { pub fn new(key: impl Into>) -> Self { Self { key: key.into() } } /// 空密钥会让所有「缺失/非法」的请求一起通过,所以构造后必须先确认门禁真的能拒绝。 pub fn is_armed(&self) -> bool { !self.key.trim().is_empty() } } impl TonicInterceptor for Interceptor { fn call(&mut self, mut request: Request<()>) -> Result, Status> { let metadata = request.metadata().clone(); let request_id = request_id_from(&metadata); let presented = metadata .get(RPC_KEY_METADATA) .and_then(|value| value.to_str().ok()) .map(str::trim) .unwrap_or_default(); if !self.is_armed() { return Err(error::unauthenticated( "rpc gate is not configured", request_id.as_deref(), )); } if presented.is_empty() || !crate::share::crypto::constant_time_eq(presented.as_bytes(), self.key.as_bytes()) { return Err(error::unauthenticated( "rpc key is missing or invalid", request_id.as_deref(), )); } let meta = RequestMeta { request_id: request_id.unwrap_or_else(new_request_id), caller_service: caller_service_from(&metadata).unwrap_or_default(), }; request.extensions_mut().insert(meta); Ok(request) } } /// 读取门禁挂上的元信息。拦截器已经校验过密钥,这里只做扩展读取。 pub async fn meta(request: &Request) -> Result { request .extensions() .get::() .cloned() .ok_or_else(|| { error::unauthenticated("rpc gate did not attach request metadata", None::<&str>) }) }