feat(secrets-mcp): 审计页、audit_log user_id、OAuth 登录与仪表盘 footer
All checks were successful
Secrets MCP — Build & Release / 版本 & Release (push) Successful in 3s
Secrets MCP — Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 7m20s
Secrets MCP — Build & Release / Build Linux (musl) (push) Successful in 8m23s
Secrets MCP — Build & Release / 发布草稿 Release (push) Successful in 1s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 6s
All checks were successful
Secrets MCP — Build & Release / 版本 & Release (push) Successful in 3s
Secrets MCP — Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 7m20s
Secrets MCP — Build & Release / Build Linux (musl) (push) Successful in 8m23s
Secrets MCP — Build & Release / 发布草稿 Release (push) Successful in 1s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 6s
- audit_log 增加 user_id;业务写审计透传 user_id - Web /audit 与侧边栏;Dashboard 版本 footer 贴底(margin-top: auto) - 停止 API Key 鉴权成功写入登录审计 - 文档、CI、release-check 配套更新 Made-with: Cursor
This commit is contained in:
@@ -9,7 +9,6 @@ use axum::{
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use secrets_core::audit::log_login;
|
||||
use secrets_core::service::api_key::validate_api_key;
|
||||
|
||||
/// Injected into request extensions after Bearer token validation.
|
||||
@@ -35,15 +34,6 @@ fn log_client_ip(req: &Request) -> Option<String> {
|
||||
.map(|c| c.ip().to_string())
|
||||
}
|
||||
|
||||
fn log_user_agent(req: &Request) -> Option<String> {
|
||||
req.headers()
|
||||
.get(axum::http::header::USER_AGENT)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
/// Axum middleware that validates Bearer API keys for the /mcp route.
|
||||
/// Passes all non-MCP paths through without authentication.
|
||||
pub async fn bearer_auth_middleware(
|
||||
@@ -54,7 +44,6 @@ pub async fn bearer_auth_middleware(
|
||||
let path = req.uri().path();
|
||||
let method = req.method().as_str();
|
||||
let client_ip = log_client_ip(&req);
|
||||
let user_agent = log_user_agent(&req);
|
||||
|
||||
// Only authenticate /mcp paths
|
||||
if !path.starts_with("/mcp") {
|
||||
@@ -95,15 +84,6 @@ pub async fn bearer_auth_middleware(
|
||||
|
||||
match validate_api_key(&pool, raw_key).await {
|
||||
Ok(Some(user_id)) => {
|
||||
log_login(
|
||||
&pool,
|
||||
"api_key",
|
||||
"bearer",
|
||||
user_id,
|
||||
client_ip.as_deref(),
|
||||
user_agent.as_deref(),
|
||||
)
|
||||
.await;
|
||||
tracing::debug!(?user_id, "api key authenticated");
|
||||
let mut req = req;
|
||||
req.extensions_mut().insert(AuthUser { user_id });
|
||||
|
||||
@@ -17,6 +17,7 @@ use secrets_core::audit::log_login;
|
||||
use secrets_core::crypto::hex;
|
||||
use secrets_core::service::{
|
||||
api_key::{ensure_api_key, regenerate_api_key},
|
||||
audit_log::list_for_user,
|
||||
user::{
|
||||
OAuthProfile, bind_oauth_account, find_or_create_user, get_user_by_id,
|
||||
unbind_oauth_account, update_user_key_setup,
|
||||
@@ -50,6 +51,22 @@ struct DashboardTemplate {
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "audit.html")]
|
||||
struct AuditPageTemplate {
|
||||
user_name: String,
|
||||
user_email: String,
|
||||
entries: Vec<AuditEntryView>,
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
struct AuditEntryView {
|
||||
created_at: String,
|
||||
action: String,
|
||||
target: String,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
// ── App state helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
fn google_cfg(state: &AppState) -> Option<&OAuthConfig> {
|
||||
@@ -103,6 +120,7 @@ pub fn web_router() -> Router<AppState> {
|
||||
.route("/auth/google/callback", get(auth_google_callback))
|
||||
.route("/auth/logout", post(auth_logout))
|
||||
.route("/dashboard", get(dashboard))
|
||||
.route("/audit", get(audit_page))
|
||||
.route("/account/bind/google", get(account_bind_google))
|
||||
.route(
|
||||
"/account/bind/google/callback",
|
||||
@@ -364,6 +382,49 @@ async fn dashboard(
|
||||
render_template(tmpl)
|
||||
}
|
||||
|
||||
async fn audit_page(
|
||||
State(state): State<AppState>,
|
||||
session: Session,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let Some(user_id) = current_user_id(&session).await else {
|
||||
return Ok(Redirect::to("/").into_response());
|
||||
};
|
||||
|
||||
let user = match get_user_by_id(&state.pool, user_id)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
{
|
||||
Some(u) => u,
|
||||
None => return Ok(Redirect::to("/").into_response()),
|
||||
};
|
||||
|
||||
let rows = list_for_user(&state.pool, user_id, 100)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(error = %e, "failed to load audit log for user");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
||||
let entries = rows
|
||||
.into_iter()
|
||||
.map(|row| AuditEntryView {
|
||||
created_at: row.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string(),
|
||||
action: row.action,
|
||||
target: format_audit_target(&row.namespace, &row.kind, &row.name),
|
||||
detail: serde_json::to_string_pretty(&row.detail).unwrap_or_else(|_| "{}".to_string()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tmpl = AuditPageTemplate {
|
||||
user_name: user.name.clone(),
|
||||
user_email: user.email.clone().unwrap_or_default(),
|
||||
entries,
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
};
|
||||
|
||||
render_template(tmpl)
|
||||
}
|
||||
|
||||
// ── Account bind/unbind ───────────────────────────────────────────────────────
|
||||
|
||||
async fn account_bind_google(
|
||||
@@ -577,3 +638,11 @@ fn render_template<T: Template>(tmpl: T) -> Result<Response, StatusCode> {
|
||||
})?;
|
||||
Ok(Html(html).into_response())
|
||||
}
|
||||
|
||||
fn format_audit_target(namespace: &str, kind: &str, name: &str) -> String {
|
||||
if namespace == "auth" {
|
||||
format!("{}/{}", kind, name)
|
||||
} else {
|
||||
format!("[{}/{}] {}", namespace, kind, name)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user