feat: entry update links existing secrets (link_secret_names)
- secrets-core: update flow validates and applies secret links - secrets-mcp: MCP tool params and UI for managing links on edit - Align errors and templates; minor crypto/.gitignore tweaks Made-with: Cursor
This commit is contained in:
@@ -25,6 +25,8 @@ pub struct UpdateResult {
|
||||
pub remove_meta: Vec<String>,
|
||||
pub secret_keys: Vec<String>,
|
||||
pub remove_secrets: Vec<String>,
|
||||
pub linked_secrets: Vec<String>,
|
||||
pub unlinked_secrets: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct UpdateParams<'a> {
|
||||
@@ -39,6 +41,8 @@ pub struct UpdateParams<'a> {
|
||||
pub secret_entries: &'a [String],
|
||||
pub secret_types: &'a std::collections::HashMap<String, String>,
|
||||
pub remove_secrets: &'a [String],
|
||||
pub link_secret_names: &'a [String],
|
||||
pub unlink_secret_names: &'a [String],
|
||||
pub user_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
@@ -295,6 +299,101 @@ pub async fn run(
|
||||
}
|
||||
}
|
||||
|
||||
// Link existing secrets by name
|
||||
let mut linked_secrets = Vec::new();
|
||||
for link_name in params.link_secret_names {
|
||||
let link_name = link_name.trim();
|
||||
if link_name.is_empty() {
|
||||
anyhow::bail!("link_secret_names contains an empty name");
|
||||
}
|
||||
let secret_ids: Vec<Uuid> = if let Some(uid) = params.user_id {
|
||||
sqlx::query_scalar("SELECT id FROM secrets WHERE user_id = $1 AND name = $2")
|
||||
.bind(uid)
|
||||
.bind(link_name)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_scalar("SELECT id FROM secrets WHERE user_id IS NULL AND name = $1")
|
||||
.bind(link_name)
|
||||
.fetch_all(&mut *tx)
|
||||
.await?
|
||||
};
|
||||
|
||||
match secret_ids.len() {
|
||||
0 => anyhow::bail!("Not found: secret named '{}'", link_name),
|
||||
1 => {
|
||||
sqlx::query(
|
||||
"INSERT INTO entry_secrets (entry_id, secret_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||
)
|
||||
.bind(row.id)
|
||||
.bind(secret_ids[0])
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
linked_secrets.push(link_name.to_string());
|
||||
}
|
||||
n => anyhow::bail!(
|
||||
"Ambiguous: {} secrets named '{}' found. Please deduplicate names first.",
|
||||
n,
|
||||
link_name
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Unlink secrets by name
|
||||
let mut unlinked_secrets = Vec::new();
|
||||
for unlink_name in params.unlink_secret_names {
|
||||
let unlink_name = unlink_name.trim();
|
||||
if unlink_name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct SecretToUnlink {
|
||||
id: Uuid,
|
||||
encrypted: Vec<u8>,
|
||||
}
|
||||
let secret: Option<SecretToUnlink> = sqlx::query_as(
|
||||
"SELECT s.id, s.encrypted \
|
||||
FROM entry_secrets es \
|
||||
JOIN secrets s ON s.id = es.secret_id \
|
||||
WHERE es.entry_id = $1 AND s.name = $2",
|
||||
)
|
||||
.bind(row.id)
|
||||
.bind(unlink_name)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
if let Some(s) = secret {
|
||||
if let Err(e) = db::snapshot_secret_history(
|
||||
&mut tx,
|
||||
db::SecretSnapshotParams {
|
||||
secret_id: s.id,
|
||||
name: unlink_name,
|
||||
encrypted: &s.encrypted,
|
||||
action: "delete",
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = %e, "failed to snapshot secret field history before unlink");
|
||||
}
|
||||
sqlx::query("DELETE FROM entry_secrets WHERE entry_id = $1 AND secret_id = $2")
|
||||
.bind(row.id)
|
||||
.bind(s.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query(
|
||||
"DELETE FROM secrets s \
|
||||
WHERE s.id = $1 \
|
||||
AND NOT EXISTS (SELECT 1 FROM entry_secrets es WHERE es.secret_id = s.id)",
|
||||
)
|
||||
.bind(s.id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
unlinked_secrets.push(unlink_name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let meta_keys = collect_key_paths(params.meta_entries)?;
|
||||
let remove_meta_keys = collect_field_paths(params.remove_meta)?;
|
||||
let secret_keys = collect_key_paths(params.secret_entries)?;
|
||||
@@ -314,6 +413,8 @@ pub async fn run(
|
||||
"remove_meta": remove_meta_keys,
|
||||
"secret_keys": secret_keys,
|
||||
"remove_secrets": remove_secret_keys,
|
||||
"linked_secrets": linked_secrets,
|
||||
"unlinked_secrets": unlinked_secrets,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -330,6 +431,8 @@ pub async fn run(
|
||||
remove_meta: remove_meta_keys,
|
||||
secret_keys,
|
||||
remove_secrets: remove_secret_keys,
|
||||
linked_secrets,
|
||||
unlinked_secrets,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user