use std::net::SocketAddr; use axum::Json; use axum::body::Body; use axum::extract::{ConnectInfo, State}; use axum::http::{HeaderMap, HeaderValue, Request, StatusCode, header}; use axum::middleware::Next; use axum::response::{IntoResponse, Response}; use base64::Engine as _; use models::users::access_scope::AccessKeyScope; use rmcp::ErrorData; use rmcp::RoleServer; use rmcp::service::RequestContext; use serde_json::json; use uuid::Uuid; use crate::{McpRuntime, net}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PrincipalSource { UserKey, InternalKey, SpaceToken, } #[derive(Clone)] pub struct TemporaryKey { pub token: String, pub token_id: Uuid, pub issued_by: Option, pub expires_at: time::OffsetDateTime, } impl std::fmt::Debug for TemporaryKey { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TemporaryKey") .field("token", &"") .field("token_id", &self.token_id) .field("issued_by", &self.issued_by) .field("expires_at", &self.expires_at) .finish() } } #[derive(Debug, Clone)] pub struct Principal { pub uid: i64, pub scope: AccessKeyScope, pub full_access: bool, pub source: PrincipalSource, /// `Some((space_id, lower_name))` for temporary space top-level tokens: /// every tool call must stay inside that single space. pub bound_space: Option<(Uuid, String)>, /// `Some` only for temporary space top-level tokens; carries the presented /// token and its allocation details so `me_get` can hand it back. pub temporary_key: Option, } impl Principal { pub fn can_write(&self) -> bool { self.full_access || self.scope.allows(true) } pub(crate) fn from_ctx(ctx: &RequestContext) -> Result { let parts = ctx .extensions .get::() .ok_or_else(|| ErrorData::internal_error("missing http request parts", None))?; parts .extensions .get::() .cloned() .ok_or_else(|| ErrorData::invalid_request("unauthenticated", None)) } } pub(crate) async fn auth_middleware( State(runtime): State, ConnectInfo(peer): ConnectInfo, mut request: Request, next: Next, ) -> Response { let Some(token) = extract_token(request.headers()) else { return unauthorized("缺少访问密钥"); }; let peer = Some(peer); let client_ip = net::resolve_client_ip(peer, request.headers(), runtime.trusted_proxies()); let principal = if let Some(name) = runtime.match_internal_key(&token) { if !runtime.is_internal_ip(client_ip) { tracing::warn!(key = %name, "internal mcp key rejected from a non-internal source"); return forbidden("内部访问密钥仅允许内网来源使用"); } let Some(uid) = runtime.system_uid() else { return unavailable("内部系统账号尚未就绪"); }; Principal { uid, scope: AccessKeyScope::Write, full_access: true, source: PrincipalSource::InternalKey, bound_space: None, temporary_key: None, } } else if export::space_token::looks_like(&token) { match export::space_token::verify(&runtime.services().state.redis, &token).await { Ok(Some(record)) => { // The bot passes every owner-level authorization check; the // space binding below is what keeps it scoped to one space. export::authz::register_space_bot(record.bot_uid); let expires_at = time::OffsetDateTime::from_unix_timestamp(record.expires_at_unix) .unwrap_or_else(|_| time::OffsetDateTime::now_utc()); Principal { uid: record.bot_uid, scope: AccessKeyScope::Write, full_access: true, source: PrincipalSource::SpaceToken, bound_space: Some((record.space_id, record.space_name)), temporary_key: Some(TemporaryKey { token: token.clone(), token_id: record.token_id, issued_by: record.issued_by, expires_at, }), } } Ok(None) => return unauthorized("访问密钥无效或已过期"), Err(error) => { tracing::warn!(%error, "mcp space token verification failed"); return unavailable("鉴权服务暂不可用"); } } } else { match runtime.services().auth.verify_access_token(&token).await { Ok(Some(verified)) => Principal { uid: verified.user_id, scope: verified.scope, full_access: false, source: PrincipalSource::UserKey, bound_space: None, temporary_key: None, }, Ok(None) => return unauthorized("访问密钥无效或已过期"), Err(error) => { tracing::warn!(%error, "mcp access key verification failed"); return unavailable("鉴权服务暂不可用"); } } }; request.extensions_mut().insert(principal); next.run(request).await } fn extract_token(headers: &HeaderMap) -> Option { let value = headers.get(header::AUTHORIZATION)?.to_str().ok()?; if let Some(rest) = value .strip_prefix("Bearer ") .or_else(|| value.strip_prefix("bearer ")) { let token = rest.trim(); return (!token.is_empty()).then(|| token.to_string()); } if let Some(rest) = value.strip_prefix("Basic ") { let decoded = base64::engine::general_purpose::STANDARD .decode(rest.trim()) .ok()?; let credentials = String::from_utf8(decoded).ok()?; return credentials .split_once(':') .map(|(_, password)| password.to_string()); } None } fn error_response(status: StatusCode, message: &str) -> Response { let body = Json(json!({ "error": status.canonical_reason().unwrap_or("error").to_lowercase(), "message": message, })); (status, body).into_response() } fn unauthorized(message: &str) -> Response { let mut response = error_response(StatusCode::UNAUTHORIZED, message); response .headers_mut() .insert(header::WWW_AUTHENTICATE, HeaderValue::from_static("Bearer")); response } fn forbidden(message: &str) -> Response { error_response(StatusCode::FORBIDDEN, message) } fn unavailable(message: &str) -> Response { error_response(StatusCode::SERVICE_UNAVAILABLE, message) }