feat(secrets-broker): add 'secret rm' + a new-namespace heads-up on put
Both from jackdaw-dev feedback after a mis-namespaced item (missing host prefix) hid under a prefix nobody searches: - 'secret rm <name>' — delete an item by exact name (bw soft-delete to trash, recoverable); closes the 'no delete path, append-only in practice' gap. - 'secret put' now warns (stderr, non-blocking) when a name opens a brand-new top-level namespace, listing existing ones + suggesting the host prefix — catches a typo'd/missing prefix at store time. Installed copy at ~/.local/bin/secret synced.
This commit is contained in:
@@ -16,6 +16,7 @@ Commands:
|
||||
secret put <name> (--file P | --stdin) [--folder C] [--field k=v]...
|
||||
secret get <name> [--field F] [--file OUT]
|
||||
secret list [--prefix P]
|
||||
secret rm <name> # delete an item (soft-delete to trash)
|
||||
secret backfill [--dry-run] # scan THIS box's local secrets, upsert each
|
||||
"""
|
||||
import argparse
|
||||
@@ -186,6 +187,16 @@ def cmd_put(a):
|
||||
for kv in (a.field or []):
|
||||
k, v = kv.split("=", 1)
|
||||
extra.append({"name": k, "value": v, "type": 0})
|
||||
# namespace heads-up: warn (don't block) when this opens a brand-new top-level
|
||||
# namespace — catches a typo'd/missing host prefix at store time instead of
|
||||
# letting the item hide under a prefix nobody searches.
|
||||
if not find(env, s, a.name):
|
||||
top = a.name.split("/")[0]
|
||||
tops = {i["name"].split("/")[0] for i in json.loads(bw(["list", "items"], env, s).stdout.decode() or "[]")}
|
||||
if top not in tops:
|
||||
print(f"secret: note — '{a.name}' opens a NEW top-level namespace '{top}/' "
|
||||
f"(existing: {', '.join(sorted(tops)) or 'none'}). If you meant to host-prefix it, "
|
||||
f"'secret rm {a.name}' and re-put as {socket.gethostname()}/...", file=sys.stderr)
|
||||
_, sha, mode = store_secret(env, s, a.name, data, a.folder, extra)
|
||||
print(f"stored: {a.name} (sha256 {sha[:12]}, {len(data)} bytes, {mode})")
|
||||
|
||||
@@ -209,6 +220,16 @@ def cmd_get(a):
|
||||
sys.stdout.buffer.write(data if data.endswith(b"\n") else data + b"\n")
|
||||
|
||||
|
||||
def cmd_rm(a):
|
||||
env = load_env()
|
||||
s = session(env)
|
||||
it = find(env, s, a.name) or die(f"not found: {a.name}")
|
||||
full = json.loads(bw(["get", "item", it["id"]], env, s).stdout.decode())
|
||||
sha = next((f["value"][:12] for f in (full.get("fields") or []) if f["name"] == "sha256"), "?")
|
||||
bw(["delete", "item", it["id"]], env, s) # soft-delete to trash (recoverable in the vault)
|
||||
print(f"deleted: {a.name} (sha256 {sha}, id {it['id']}) -> trash")
|
||||
|
||||
|
||||
def cmd_list(a):
|
||||
env = load_env()
|
||||
s = session(env)
|
||||
@@ -294,6 +315,8 @@ def main():
|
||||
pg = sub.add_parser("get"); pg.add_argument("name")
|
||||
pg.add_argument("--field"); pg.add_argument("--file"); pg.set_defaults(fn=cmd_get)
|
||||
pl = sub.add_parser("list"); pl.add_argument("--prefix"); pl.set_defaults(fn=cmd_list)
|
||||
pr = sub.add_parser("rm", help="delete an item by exact name (soft-delete to trash)")
|
||||
pr.add_argument("name"); pr.set_defaults(fn=cmd_rm)
|
||||
pb = sub.add_parser("backfill", help="scan THIS box's local secret files and upsert each")
|
||||
pb.add_argument("--dry-run", action="store_true"); pb.set_defaults(fn=cmd_backfill)
|
||||
a = p.parse_args()
|
||||
|
||||
Reference in New Issue
Block a user