feat(core): FK for user_id columns; MCP search requires user
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 3m10s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 5s

- Add fk_entries_user_id, fk_entries_history_user_id, fk_audit_log_user_id (ON DELETE SET NULL)
- Add scripts/cleanup-orphan-user-ids.sql for pre-deploy orphan user_id cleanup
- Remove deprecated SERVER_MASTER_KEY / per-user key wrap helpers from secrets-core
- secrets-mcp: require authenticated user for secrets_search; improve body-read failure response
- Bump secrets-mcp to 0.2.1

Made-with: Cursor
This commit is contained in:
voson
2026-03-22 15:40:02 +08:00
parent e3ca43ca3f
commit 1e597559a2
7 changed files with 80 additions and 88 deletions

View File

@@ -5,11 +5,11 @@ use axum::{
body::{Body, Bytes, to_bytes},
extract::{ConnectInfo, Request},
http::{
HeaderMap, Method,
HeaderMap, Method, StatusCode,
header::{CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT},
},
middleware::Next,
response::Response,
response::{IntoResponse, Response},
};
/// Axum middleware that logs structured info for every HTTP request.
@@ -68,10 +68,23 @@ pub async fn request_logging_middleware(req: Request, next: Next) -> Response {
}
Err(e) => {
tracing::warn!(path, error = %e, "failed to buffer MCP request body for logging");
// Reconstruct with empty body; request was consumed — return 500.
// This branch is highly unlikely in practice.
let resp = next.run(Request::from_parts(parts, Body::empty())).await;
return resp;
let elapsed = start.elapsed().as_millis();
tracing::info!(
method = method.as_str(),
path,
status = StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
elapsed_ms = elapsed,
client_ip = ip.as_deref(),
ua = ua.as_deref(),
content_length = content_len,
mcp_session = mcp_session.as_deref(),
"mcp request",
);
return (
StatusCode::INTERNAL_SERVER_ERROR,
"failed to read request body",
)
.into_response();
}
}
}

View File

@@ -298,8 +298,8 @@ struct EnvMapInput {
#[tool_router]
impl SecretsService {
#[tool(
description = "Search entries in the secrets store. Returns entries with metadata and \
secret field names (not values). Use secrets_get to decrypt secret values.",
description = "Search entries in the secrets store. Requires Bearer API key. Returns \
entries with metadata and secret field names (not values). Use secrets_get to decrypt secret values.",
annotations(
title = "Search Secrets",
read_only_hint = true,
@@ -312,7 +312,7 @@ impl SecretsService {
ctx: RequestContext<RoleServer>,
) -> Result<CallToolResult, rmcp::ErrorData> {
let t = Instant::now();
let user_id = Self::user_id_from_ctx(&ctx)?;
let user_id = Self::require_user_id(&ctx)?;
tracing::info!(
tool = "secrets_search",
?user_id,
@@ -334,11 +334,11 @@ impl SecretsService {
sort: input.sort.as_deref().unwrap_or("name"),
limit: input.limit.unwrap_or(20),
offset: input.offset.unwrap_or(0),
user_id,
user_id: Some(user_id),
},
)
.await
.map_err(|e| mcp_err_internal_logged("secrets_search", user_id, e))?;
.map_err(|e| mcp_err_internal_logged("secrets_search", Some(user_id), e))?;
let summary = input.summary.unwrap_or(false);
let entries: Vec<serde_json::Value> = result
@@ -849,7 +849,7 @@ impl ServerHandler for SecretsService {
"Manage cross-device secrets and configuration securely. \
Data is encrypted with your passphrase-derived key. \
Include your 64-char hex key in the X-Encryption-Key header for all read/write operations. \
Use secrets_search to discover entries (no key needed), \
Use secrets_search to discover entries (Bearer token required; encryption key not needed), \
secrets_get to decrypt secret values, \
and secrets_add/secrets_update to write encrypted secrets."
.to_string(),