diff --git a/services/secrets-broker/secret b/services/secrets-broker/secret index b0316af..17dfdfa 100755 --- a/services/secrets-broker/secret +++ b/services/secrets-broker/secret @@ -16,6 +16,7 @@ Commands: secret put (--file P | --stdin) [--folder C] [--field k=v]... secret get [--field F] [--file OUT] secret list [--prefix P] + secret rm # 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()