feat: secrets recipients rm with lockout guards (EGB-283)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4ba0234bd7
commit
cad5b66f77
2 changed files with 99 additions and 1 deletions
52
secrets
52
secrets
|
|
@ -2077,7 +2077,8 @@ cmd_recipients() {
|
|||
case "$sub" in
|
||||
list) _recipients_list ;;
|
||||
add) _recipients_add "$@" ;;
|
||||
*) die "Unknown recipients subcommand: '$sub'. Usage: secrets recipients [list|add <age1...> [--name N]]" ;;
|
||||
rm|remove) _recipients_rm "$@" ;;
|
||||
*) die "Unknown recipients subcommand: '$sub'. Usage: secrets recipients [list|add <age1...> [--name N]|rm <age1...|name> [--yes]]" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -2137,6 +2138,55 @@ _recipients_add() {
|
|||
_reencrypt_all "recipients: add ${name:-$key}; re-encrypt all"
|
||||
}
|
||||
|
||||
# Rewrite recipients.txt canonically (one "# name"? + key per entry), dropping
|
||||
# the entry whose key == <drop>. Atomic-ish via temp file in the same dir.
|
||||
_recipients_write_without() {
|
||||
local drop="$1" tmp k n
|
||||
tmp=$(mktemp "$SECRETS_DIR/.recipients.XXXXXX")
|
||||
while IFS=$'\t' read -r k n; do
|
||||
[ "$k" = "$drop" ] && continue
|
||||
[ -n "$n" ] && printf '# %s\n' "$n" >> "$tmp"
|
||||
printf '%s\n' "$k" >> "$tmp"
|
||||
done < <(_recipients_dump)
|
||||
mv "$tmp" "$RECIPIENTS_FILE"
|
||||
}
|
||||
|
||||
_recipients_rm() {
|
||||
check_cmd age
|
||||
check_cmd git
|
||||
check_initialized
|
||||
check_key
|
||||
local target="" assume_yes=false
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--yes|-y) assume_yes=true; shift ;;
|
||||
-*) die "Unknown flag: $1. Usage: secrets recipients rm <age1...|name> [--yes]" ;;
|
||||
*) if [ -z "$target" ]; then target="$1"; else die "Unexpected argument: $1"; fi; shift ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$target" ] || die "Usage: secrets recipients rm <age1...|name> [--yes]"
|
||||
[ -e "$RECIPIENTS_FILE" ] || die "No $RECIPIENTS_FILE_NAME — store is single-key; nothing to remove."
|
||||
[ -L "$RECIPIENTS_FILE" ] && die "Refusing to write symlinked $RECIPIENTS_FILE_NAME."
|
||||
local k n match="" count=0 total=0
|
||||
while IFS=$'\t' read -r k n; do
|
||||
total=$((total + 1))
|
||||
if [ "$k" = "$target" ] || { [ -n "$n" ] && [ "$n" = "$target" ]; }; then
|
||||
match="$k"; count=$((count + 1))
|
||||
fi
|
||||
done < <(_recipients_dump)
|
||||
[ "$count" -eq 0 ] && die "No recipient matches '$target'."
|
||||
[ "$count" -gt 1 ] && die "'$target' matches $count recipients by name — remove by key (age1...) instead."
|
||||
[ "$total" -le 1 ] && die "Refusing to remove the last recipient — a store must have at least one."
|
||||
local self; self="$(get_pubkey)"
|
||||
if [ "$match" = "$self" ] && [ "$assume_yes" != true ]; then
|
||||
die "Refusing to remove your own key (you would lose access to future pushes). Re-run with --yes to confirm."
|
||||
fi
|
||||
_recipients_write_without "$match"
|
||||
info "Removed recipient: $match"
|
||||
_load_recipients
|
||||
_reencrypt_all "recipients: remove $match; re-encrypt all"
|
||||
}
|
||||
|
||||
cmd_which() {
|
||||
resolve_store
|
||||
echo "store: $SECRETS_DIR"
|
||||
|
|
|
|||
|
|
@ -181,3 +181,51 @@ make_second_identity() {
|
|||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"--name requires a value"* ]] || false
|
||||
}
|
||||
|
||||
@test "recipients rm removes a recipient and re-encrypts to the rest" {
|
||||
init_with_remote
|
||||
create_project_dir myproj
|
||||
run "$SECRETS_BIN" push
|
||||
make_second_identity
|
||||
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob
|
||||
run "$SECRETS_BIN" recipients rm bob
|
||||
[ "$status" -eq 0 ]
|
||||
run "$SECRETS_BIN" recipients list
|
||||
[[ "$output" == *"recipients: 1"* ]] || false
|
||||
# Store key still reads its own blobs.
|
||||
run age -d -i "$SECRETS_DIR/key.txt" "$SECRETS_DIR/myproj/.env.age"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "recipients rm refuses to remove the last recipient" {
|
||||
init_with_remote
|
||||
make_second_identity
|
||||
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob # store = self + bob
|
||||
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
|
||||
run "$SECRETS_BIN" recipients rm bob # back to self only
|
||||
[ "$status" -eq 0 ]
|
||||
run "$SECRETS_BIN" recipients rm "$STORE_PUB" # would be the last
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"last recipient"* ]] || false
|
||||
}
|
||||
|
||||
@test "recipients rm of your own key requires --yes" {
|
||||
init_with_remote
|
||||
make_second_identity
|
||||
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob
|
||||
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
|
||||
run "$SECRETS_BIN" recipients rm "$STORE_PUB"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"your own key"* ]] || false
|
||||
run "$SECRETS_BIN" recipients rm "$STORE_PUB" --yes
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "recipients rm of a non-existent target errors" {
|
||||
init_with_remote
|
||||
make_second_identity
|
||||
run "$SECRETS_BIN" recipients add "$BOB_PUB" --name bob
|
||||
run "$SECRETS_BIN" recipients rm carol
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"No recipient matches"* ]] || false
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue