1use axum::body::Body;
16use axum::http::{HeaderMap, Request, StatusCode};
17use axum::middleware::Next;
18use axum::response::Response;
19
20use crate::api::rate_limit::nz;
21use crate::db::pool::get_conn;
22use crate::models::mcp_token::TokenScope;
23use crate::models::user::UserRole;
24
25pub const TOKEN_PREFIX: &str = "ygg_";
27
28#[derive(Clone, Debug)]
30pub struct McpPrincipal {
31 pub user_id: i32,
32 pub scope: TokenScope,
33 pub token_id: String,
35}
36
37pub fn extract_bearer(headers: &HeaderMap) -> Option<String> {
40 let raw = headers
41 .get(axum::http::header::AUTHORIZATION)?
42 .to_str()
43 .ok()?;
44 let scheme = raw.strip_prefix("Bearer ")?.trim();
45 if scheme.starts_with(TOKEN_PREFIX) {
46 Some(scheme.to_string())
47 } else {
48 None
49 }
50}
51
52pub fn hash_token(token: &str) -> String {
54 crate::utils::server::sha256_hex(token)
55}
56
57static MCP_LIMITER: std::sync::LazyLock<governor::DefaultKeyedRateLimiter<String>> =
64 std::sync::LazyLock::new(|| {
65 let c = crate::config::rate_limit();
66 governor::RateLimiter::keyed(
67 governor::Quota::per_second(nz(c.mcp_per_sec)).allow_burst(nz(c.mcp_burst)),
68 )
69 });
70
71static MCP_UPLOAD_LIMITER: std::sync::LazyLock<governor::DefaultKeyedRateLimiter<String>> =
76 std::sync::LazyLock::new(|| {
77 let c = crate::config::rate_limit();
78 governor::RateLimiter::keyed(
79 governor::Quota::per_second(nz(c.mcp_upload_per_sec))
80 .allow_burst(nz(c.mcp_upload_burst)),
81 )
82 });
83
84pub(crate) fn check_mcp_upload_limit(token_id: &str) -> Result<(), &'static str> {
86 let key = token_id.to_string();
88 if MCP_UPLOAD_LIMITER.check_key(&key).is_err() {
89 return Err("上传过于频繁,请稍后再试");
90 }
91 Ok(())
92}
93
94const LAST_USED_REFRESH_SECS: i64 = 60;
97
98pub async fn mcp_auth_middleware(
104 mut req: Request<Body>,
105 next: Next,
106) -> Result<Response, StatusCode> {
107 let token = match extract_bearer(req.headers()) {
108 Some(t) => t,
109 None => return Err(StatusCode::UNAUTHORIZED),
110 };
111 let peer = req
114 .extensions()
115 .get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
116 .map(|axum::extract::ConnectInfo(addr)| *addr);
117 let ip = crate::api::rate_limit::get_client_ip_with_peer(req.headers(), peer).await;
118 if crate::api::rate_limit::check_mcp_preauth_limit(&ip).is_err() {
119 return Err(StatusCode::TOO_MANY_REQUESTS);
120 }
121 let principal = resolve_principal(&token)
122 .await
123 .ok_or(StatusCode::UNAUTHORIZED)?;
124
125 if MCP_LIMITER.check_key(&principal.token_id).is_err() {
127 return Err(StatusCode::TOO_MANY_REQUESTS);
128 }
129
130 tracing::info!(
133 user_id = principal.user_id,
134 scope = principal.scope.as_str(),
135 token_id = %principal.token_id,
136 "mcp request authenticated"
137 );
138
139 req.extensions_mut().insert(principal);
140 Ok(next.run(req).await)
141}
142
143pub(crate) fn role_can_hold_scope(role: &UserRole, scope: TokenScope) -> bool {
149 match (role, scope) {
150 (UserRole::Admin, TokenScope::Read | TokenScope::Write | TokenScope::Admin) => true,
151 (UserRole::Blocked, _) => false,
152 }
153}
154
155pub(crate) async fn resolve_principal(token: &str) -> Option<McpPrincipal> {
157 let hash = hash_token(token);
158 let client = get_conn().await.ok()?;
159 let row = client
161 .query_opt(
162 "SELECT t.id, t.user_id, t.scope, t.last_used_at, u.role
163 FROM mcp_tokens t
164 JOIN users u ON u.id = t.user_id
165 WHERE t.token_hash = $1
166 AND t.revoked_at IS NULL
167 AND (t.expires_at IS NULL OR t.expires_at > NOW())",
168 &[&hash],
169 )
170 .await
171 .ok()??;
172
173 let token_id: uuid::Uuid = row.get(0);
174 let user_id: i32 = row.get(1);
175 let scope_str: &str = row.get(2);
176 let scope = TokenScope::from_db(scope_str)?;
177 let last_used: Option<chrono::DateTime<chrono::Utc>> = row.get(3);
178 let role = UserRole::from_str(row.get(4))?;
179 if !role_can_hold_scope(&role, scope) {
180 return None;
181 }
182
183 let needs_refresh = last_used
185 .map(|t| (chrono::Utc::now() - t).num_seconds() >= LAST_USED_REFRESH_SECS)
186 .unwrap_or(true);
187 if needs_refresh {
188 let _ = client
189 .execute(
190 "UPDATE mcp_tokens SET last_used_at = NOW() WHERE id = $1",
191 &[&token_id],
192 )
193 .await;
194 }
195
196 Some(McpPrincipal {
197 user_id,
198 scope,
199 token_id: token_id.to_string(),
200 })
201}
202pub(crate) async fn resolve_bearer_principal(
208 headers: &HeaderMap,
209) -> Result<McpPrincipal, StatusCode> {
210 let token = extract_bearer(headers).ok_or(StatusCode::UNAUTHORIZED)?;
211 resolve_principal(&token)
212 .await
213 .ok_or(StatusCode::UNAUTHORIZED)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn admin_role_keeps_every_token_scope() {
222 for scope in [TokenScope::Read, TokenScope::Write, TokenScope::Admin] {
223 assert!(role_can_hold_scope(&UserRole::Admin, scope), "{scope:?}");
224 }
225 }
226
227 #[test]
228 fn blocked_role_loses_every_token_scope() {
229 for scope in [TokenScope::Read, TokenScope::Write, TokenScope::Admin] {
230 assert!(!role_can_hold_scope(&UserRole::Blocked, scope), "{scope:?}");
231 }
232 }
233}