Files
secrets/crates/secrets-core/src/audit.rs
voson f2344b7543
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
feat(secrets-mcp): 审计页、audit_log user_id、OAuth 登录与仪表盘 footer
- audit_log 增加 user_id;业务写审计透传 user_id
- Web /audit 与侧边栏;Dashboard 版本 footer 贴底(margin-top: auto)
- 停止 API Key 鉴权成功写入登录审计
- 文档、CI、release-check 配套更新

Made-with: Cursor
2026-03-21 11:12:11 +08:00

106 lines
2.8 KiB
Rust

use serde_json::{Value, json};
use sqlx::{PgPool, Postgres, Transaction};
use uuid::Uuid;
pub const ACTION_LOGIN: &str = "login";
pub const NAMESPACE_AUTH: &str = "auth";
/// Return the current OS user as the audit actor (falls back to empty string).
pub fn current_actor() -> String {
std::env::var("USER").unwrap_or_default()
}
fn login_detail(
user_id: Uuid,
provider: &str,
client_ip: Option<&str>,
user_agent: Option<&str>,
) -> Value {
json!({
"user_id": user_id,
"provider": provider,
"client_ip": client_ip,
"user_agent": user_agent,
})
}
/// Write a login audit entry without requiring an explicit transaction.
pub async fn log_login(
pool: &PgPool,
kind: &str,
provider: &str,
user_id: Uuid,
client_ip: Option<&str>,
user_agent: Option<&str>,
) {
let actor = current_actor();
let detail = login_detail(user_id, provider, client_ip, user_agent);
let result: Result<_, sqlx::Error> = sqlx::query(
"INSERT INTO audit_log (user_id, action, namespace, kind, name, detail, actor) \
VALUES ($1, $2, $3, $4, $5, $6, $7)",
)
.bind(user_id)
.bind(ACTION_LOGIN)
.bind(NAMESPACE_AUTH)
.bind(kind)
.bind(provider)
.bind(&detail)
.bind(&actor)
.execute(pool)
.await;
if let Err(e) = result {
tracing::warn!(error = %e, kind, provider, "failed to write login audit log");
} else {
tracing::debug!(kind, provider, ?user_id, actor, "login audit logged");
}
}
/// Write an audit entry within an existing transaction.
pub async fn log_tx(
tx: &mut Transaction<'_, Postgres>,
user_id: Option<Uuid>,
action: &str,
namespace: &str,
kind: &str,
name: &str,
detail: Value,
) {
let actor = current_actor();
let result: Result<_, sqlx::Error> = sqlx::query(
"INSERT INTO audit_log (user_id, action, namespace, kind, name, detail, actor) \
VALUES ($1, $2, $3, $4, $5, $6, $7)",
)
.bind(user_id)
.bind(action)
.bind(namespace)
.bind(kind)
.bind(name)
.bind(&detail)
.bind(&actor)
.execute(&mut **tx)
.await;
if let Err(e) = result {
tracing::warn!(error = %e, "failed to write audit log");
} else {
tracing::debug!(action, namespace, kind, name, actor, "audit logged");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn login_detail_includes_expected_fields() {
let user_id = Uuid::nil();
let detail = login_detail(user_id, "google", Some("127.0.0.1"), Some("Mozilla/5.0"));
assert_eq!(detail["user_id"], json!(user_id));
assert_eq!(detail["provider"], "google");
assert_eq!(detail["client_ip"], "127.0.0.1");
assert_eq!(detail["user_agent"], "Mozilla/5.0");
}
}