02d8172
//! 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<String> {
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<String> {
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<String> {
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<str>,
}
impl Interceptor {
pub fn new(key: impl Into<Arc<str>>) -> 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<Request<()>, 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<T>(request: &Request<T>) -> Result<RequestMeta, Status> {
request
.extensions()
.get::<RequestMeta>()
.cloned()
.ok_or_else(|| {
error::unauthenticated("rpc gate did not attach request metadata", None::<&str>)
})
}