feat(mcp): persist login audit for OAuth and API key
All checks were successful
Secrets MCP — Build & Release / 版本 & Release (push) Successful in 3s
Secrets MCP — Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 3m16s
Secrets MCP — Build & Release / Build Linux (secrets-mcp, musl) (push) Successful in 4m32s
Secrets MCP — Build & Release / 发布草稿 Release (push) Successful in 3s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 4m33s
All checks were successful
Secrets MCP — Build & Release / 版本 & Release (push) Successful in 3s
Secrets MCP — Build & Release / 质量检查 (fmt / clippy / test) (push) Successful in 3m16s
Secrets MCP — Build & Release / Build Linux (secrets-mcp, musl) (push) Successful in 4m32s
Secrets MCP — Build & Release / 发布草稿 Release (push) Successful in 3s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 4m33s
- Add audit::log_login in secrets-core (audit_log detail: user_id, provider, client_ip, user_agent) - Log web Google OAuth success after session established - Log MCP Bearer API key auth success in middleware - Bump secrets-mcp to 0.1.6 (tag 0.1.5 existed) Made-with: Cursor
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
use askama::Template;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
body::Body,
|
||||
extract::{Path, Query, State},
|
||||
http::{StatusCode, header},
|
||||
extract::{ConnectInfo, Path, Query, State},
|
||||
http::{HeaderMap, StatusCode, header},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
routing::{get, post},
|
||||
};
|
||||
@@ -11,6 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||
use tower_sessions::Session;
|
||||
use uuid::Uuid;
|
||||
|
||||
use secrets_core::audit::log_login;
|
||||
use secrets_core::crypto::hex;
|
||||
use secrets_core::service::{
|
||||
api_key::{ensure_api_key, regenerate_api_key},
|
||||
@@ -62,6 +65,30 @@ async fn current_user_id(session: &Session) -> Option<Uuid> {
|
||||
.and_then(|s| Uuid::parse_str(&s).ok())
|
||||
}
|
||||
|
||||
fn request_client_ip(headers: &HeaderMap, connect_info: ConnectInfo<SocketAddr>) -> Option<String> {
|
||||
if let Some(first) = headers
|
||||
.get("x-forwarded-for")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.split(',').next())
|
||||
{
|
||||
let value = first.trim();
|
||||
if !value.is_empty() {
|
||||
return Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Some(connect_info.ip().to_string())
|
||||
}
|
||||
|
||||
fn request_user_agent(headers: &HeaderMap) -> Option<String> {
|
||||
headers
|
||||
.get(header::USER_AGENT)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
// ── Routes ────────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn web_router() -> Router<AppState> {
|
||||
@@ -141,16 +168,28 @@ struct OAuthCallbackQuery {
|
||||
|
||||
async fn auth_google_callback(
|
||||
State(state): State<AppState>,
|
||||
connect_info: ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
session: Session,
|
||||
Query(params): Query<OAuthCallbackQuery>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
handle_oauth_callback(&state, &session, params, "google", |s, cfg, code| {
|
||||
Box::pin(crate::oauth::google::exchange_code(
|
||||
&s.http_client,
|
||||
cfg,
|
||||
code,
|
||||
))
|
||||
})
|
||||
let client_ip = request_client_ip(&headers, connect_info);
|
||||
let user_agent = request_user_agent(&headers);
|
||||
handle_oauth_callback(
|
||||
&state,
|
||||
&session,
|
||||
params,
|
||||
"google",
|
||||
client_ip.as_deref(),
|
||||
user_agent.as_deref(),
|
||||
|s, cfg, code| {
|
||||
Box::pin(crate::oauth::google::exchange_code(
|
||||
&s.http_client,
|
||||
cfg,
|
||||
code,
|
||||
))
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -161,6 +200,8 @@ async fn handle_oauth_callback<F>(
|
||||
session: &Session,
|
||||
params: OAuthCallbackQuery,
|
||||
provider: &str,
|
||||
client_ip: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
exchange_fn: F,
|
||||
) -> Result<Response, StatusCode>
|
||||
where
|
||||
@@ -274,6 +315,16 @@ where
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
log_login(
|
||||
&state.pool,
|
||||
"oauth",
|
||||
provider,
|
||||
user.id,
|
||||
client_ip,
|
||||
user_agent,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Redirect::to("/dashboard").into_response())
|
||||
}
|
||||
|
||||
@@ -342,16 +393,28 @@ async fn account_bind_google(
|
||||
|
||||
async fn account_bind_google_callback(
|
||||
State(state): State<AppState>,
|
||||
connect_info: ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
session: Session,
|
||||
Query(params): Query<OAuthCallbackQuery>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
handle_oauth_callback(&state, &session, params, "google", |s, cfg, code| {
|
||||
Box::pin(crate::oauth::google::exchange_code(
|
||||
&s.http_client,
|
||||
cfg,
|
||||
code,
|
||||
))
|
||||
})
|
||||
let client_ip = request_client_ip(&headers, connect_info);
|
||||
let user_agent = request_user_agent(&headers);
|
||||
handle_oauth_callback(
|
||||
&state,
|
||||
&session,
|
||||
params,
|
||||
"google",
|
||||
client_ip.as_deref(),
|
||||
user_agent.as_deref(),
|
||||
|s, cfg, code| {
|
||||
Box::pin(crate::oauth::google::exchange_code(
|
||||
&s.http_client,
|
||||
cfg,
|
||||
code,
|
||||
))
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user