release(secrets-mcp): 0.5.6
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 5m8s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 1m36s

修复 OAuth 解绑时非法聚合 FOR UPDATE,Web OAuth 审计 IP 与 TRUST_PROXY 对齐并校验 IP,账号绑定写入 oauth_state 失败时回滚 bind 标记。回滚条目时恢复 folder/type,导入冲突检查在 DB 失败时传播错误,MCP delete/history 要求已登录用户,全局请求体 10MiB 限制。CI 部署支持 DEPLOY_KNOWN_HOSTS,默认 accept-new;文档与 deploy 示例补充连接池、限流、TRUST_PROXY。移除含明文凭据的 sync-test-to-prod 脚本。
This commit is contained in:
voson
2026-04-05 12:32:14 +08:00
parent 2b994141b8
commit 63cb3a8216
14 changed files with 217 additions and 194 deletions

View File

@@ -54,7 +54,13 @@ pub async fn run(
.bind(params.user_id)
.fetch_one(pool)
.await
.unwrap_or(false);
.map_err(|e| {
anyhow::anyhow!(
"Failed to check entry existence for '{}': {}",
entry.name,
e
)
})?;
if exists && !params.force {
return Err(anyhow::anyhow!(

View File

@@ -228,9 +228,11 @@ pub async fn run(
}
sqlx::query(
"UPDATE entries SET tags = $1, metadata = $2, version = version + 1, \
updated_at = NOW() WHERE id = $3",
"UPDATE entries SET folder = $1, type = $2, tags = $3, metadata = $4, version = version + 1, \
updated_at = NOW() WHERE id = $5",
)
.bind(&snap.folder)
.bind(&snap.entry_type)
.bind(&snap.tags)
.bind(&snap.metadata)
.bind(lr.id)

View File

@@ -200,10 +200,14 @@ pub async fn unbind_oauth_account(
);
}
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM oauth_accounts WHERE user_id = $1")
.bind(user_id)
.fetch_one(pool)
.await?;
let mut tx = pool.begin().await?;
let locked_accounts: Vec<(String,)> =
sqlx::query_as("SELECT provider FROM oauth_accounts WHERE user_id = $1 FOR UPDATE")
.bind(user_id)
.fetch_all(&mut *tx)
.await?;
let count = locked_accounts.len();
if count <= 1 {
anyhow::bail!("Cannot unbind the last OAuth account. Please link another account first.");
@@ -212,8 +216,87 @@ pub async fn unbind_oauth_account(
sqlx::query("DELETE FROM oauth_accounts WHERE user_id = $1 AND provider = $2")
.bind(user_id)
.bind(provider)
.execute(pool)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
async fn maybe_test_pool() -> Option<PgPool> {
let database_url = match std::env::var("SECRETS_DATABASE_URL") {
Ok(v) => v,
Err(_) => {
eprintln!("skip user service tests: SECRETS_DATABASE_URL not set");
return None;
}
};
let pool = match sqlx::PgPool::connect(&database_url).await {
Ok(pool) => pool,
Err(e) => {
eprintln!("skip user service tests: cannot connect to database: {e}");
return None;
}
};
if let Err(e) = crate::db::migrate(&pool).await {
eprintln!("skip user service tests: migrate failed: {e}");
return None;
}
Some(pool)
}
async fn cleanup_user_rows(pool: &PgPool, user_id: Uuid) -> Result<()> {
sqlx::query("DELETE FROM oauth_accounts WHERE user_id = $1")
.bind(user_id)
.execute(pool)
.await?;
sqlx::query("DELETE FROM users WHERE id = $1")
.bind(user_id)
.execute(pool)
.await?;
Ok(())
}
#[tokio::test]
async fn unbind_oauth_account_removes_only_requested_provider() -> Result<()> {
let Some(pool) = maybe_test_pool().await else {
return Ok(());
};
let user_id = Uuid::from_u128(rand::random());
cleanup_user_rows(&pool, user_id).await?;
sqlx::query("INSERT INTO users (id, name) VALUES ($1, '')")
.bind(user_id)
.execute(&pool)
.await?;
sqlx::query(
"INSERT INTO oauth_accounts (user_id, provider, provider_id, email, name, avatar_url) \
VALUES ($1, 'google', $2, NULL, NULL, NULL), \
($1, 'github', $3, NULL, NULL, NULL)",
)
.bind(user_id)
.bind(format!("google-{user_id}"))
.bind(format!("github-{user_id}"))
.execute(&pool)
.await?;
unbind_oauth_account(&pool, user_id, "github", Some("google")).await?;
let remaining: Vec<(String,)> = sqlx::query_as(
"SELECT provider FROM oauth_accounts WHERE user_id = $1 ORDER BY provider",
)
.bind(user_id)
.fetch_all(&pool)
.await?;
assert_eq!(remaining, vec![("google".to_string(),)]);
cleanup_user_rows(&pool, user_id).await?;
Ok(())
}
}