fix(secrets-mcp 0.5.20): code review plan — export secret types, env map, rollback, API key, MCP tools, web session & validation
- Export/import: optional secret_types map; AddResult includes entry_id - env_map: dot→__ segment encoding; collision errors - rollback: FOR UPDATE + txn-consistent snapshot; restore name from history - regenerate_api_key: rows_affected guard - MCP: find count propagates errors; add uses entry_id for relations; rollback no encryption key - Web: load_session_user_strict + JSON handlers key_version; PATCH length limits - Tests: ExportEntry serde, env segment
This commit is contained in:
@@ -30,58 +30,61 @@ pub async fn run(
|
||||
folder: String,
|
||||
#[sqlx(rename = "type")]
|
||||
entry_type: String,
|
||||
name: String,
|
||||
version: i64,
|
||||
action: String,
|
||||
tags: Vec<String>,
|
||||
metadata: Value,
|
||||
}
|
||||
|
||||
let live_entry: Option<EntryWriteRow> = if let Some(uid) = user_id {
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
let live: Option<EntryWriteRow> = if let Some(uid) = user_id {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, folder, type, name, tags, metadata, notes, deleted_at FROM entries \
|
||||
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL",
|
||||
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL FOR UPDATE",
|
||||
)
|
||||
.bind(entry_id)
|
||||
.bind(uid)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"SELECT id, version, folder, type, name, tags, metadata, notes, deleted_at FROM entries \
|
||||
WHERE id = $1 AND user_id IS NULL AND deleted_at IS NULL",
|
||||
WHERE id = $1 AND user_id IS NULL AND deleted_at IS NULL FOR UPDATE",
|
||||
)
|
||||
.bind(entry_id)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
};
|
||||
|
||||
let live_entry = live_entry.ok_or(AppError::NotFoundEntry)?;
|
||||
let lr = live.ok_or(AppError::NotFoundEntry)?;
|
||||
|
||||
let snap: Option<EntryHistoryRow> = if let Some(ver) = to_version {
|
||||
sqlx::query_as(
|
||||
"SELECT folder, type, version, action, tags, metadata \
|
||||
"SELECT folder, type, name, version, action, tags, metadata \
|
||||
FROM entries_history \
|
||||
WHERE entry_id = $1 AND version = $2 ORDER BY id ASC LIMIT 1",
|
||||
)
|
||||
.bind(entry_id)
|
||||
.bind(ver)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
"SELECT folder, type, version, action, tags, metadata \
|
||||
"SELECT folder, type, name, version, action, tags, metadata \
|
||||
FROM entries_history \
|
||||
WHERE entry_id = $1 ORDER BY id DESC LIMIT 1",
|
||||
)
|
||||
.bind(entry_id)
|
||||
.fetch_optional(pool)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?
|
||||
};
|
||||
|
||||
let snap = snap.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"No history found for entry '{}'{}.",
|
||||
live_entry.name,
|
||||
lr.name,
|
||||
to_version
|
||||
.map(|v| format!(" at version {}", v))
|
||||
.unwrap_or_default()
|
||||
@@ -91,17 +94,7 @@ pub async fn run(
|
||||
let snap_secret_snapshot = db::entry_secret_snapshot_from_metadata(&snap.metadata);
|
||||
let snap_metadata = db::strip_secret_snapshot_from_metadata(&snap.metadata);
|
||||
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
let live: Option<EntryWriteRow> = sqlx::query_as(
|
||||
"SELECT id, version, folder, type, name, tags, metadata, notes, deleted_at FROM entries \
|
||||
WHERE id = $1 AND deleted_at IS NULL FOR UPDATE",
|
||||
)
|
||||
.bind(entry_id)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let live_entry_id = if let Some(ref lr) = live {
|
||||
let live_entry_id = {
|
||||
let history_metadata =
|
||||
match db::metadata_with_secret_snapshot(&mut tx, lr.id, &lr.metadata).await {
|
||||
Ok(v) => v,
|
||||
@@ -168,8 +161,8 @@ pub async fn run(
|
||||
)
|
||||
.bind(&snap.folder)
|
||||
.bind(&snap.entry_type)
|
||||
.bind(&live_entry.name)
|
||||
.bind(&live_entry.notes)
|
||||
.bind(&snap.name)
|
||||
.bind(&lr.notes)
|
||||
.bind(&snap.tags)
|
||||
.bind(&snap_metadata)
|
||||
.bind(lr.id)
|
||||
@@ -177,8 +170,6 @@ pub async fn run(
|
||||
.await?;
|
||||
|
||||
lr.id
|
||||
} else {
|
||||
return Err(AppError::NotFoundEntry.into());
|
||||
};
|
||||
|
||||
if let Some(secret_snapshot) = snap_secret_snapshot {
|
||||
@@ -191,7 +182,7 @@ pub async fn run(
|
||||
"rollback",
|
||||
&snap.folder,
|
||||
&snap.entry_type,
|
||||
&live_entry.name,
|
||||
&snap.name,
|
||||
serde_json::json!({
|
||||
"entry_id": entry_id,
|
||||
"restored_version": snap.version,
|
||||
@@ -203,7 +194,7 @@ pub async fn run(
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(RollbackResult {
|
||||
name: live_entry.name,
|
||||
name: snap.name,
|
||||
folder: snap.folder,
|
||||
entry_type: snap.entry_type,
|
||||
restored_version: snap.version,
|
||||
|
||||
Reference in New Issue
Block a user