feat: secrets recipients list (EGB-283)

This commit is contained in:
Brian Majewski 2026-06-24 12:39:52 -07:00
parent 14cf86fba9
commit 649aa78063
2 changed files with 72 additions and 0 deletions

51
secrets
View file

@ -150,6 +150,29 @@ _load_recipients() {
fi fi
} }
# Emit "<key>\t<name>" for each recipient in recipients.txt. <name> is the most
# recent preceding "# <name>" comment, or empty. Read-only; no validation
# (callers that need rails call _load_recipients separately).
_recipients_dump() {
[ -e "$RECIPIENTS_FILE" ] || return 0
local line trimmed name=""
while IFS= read -r line || [ -n "$line" ]; do
trimmed="${line#"${line%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
[ -z "$trimmed" ] && continue
case "$trimmed" in
'#'*)
name="${trimmed#\#}"
name="${name#"${name%%[![:space:]]*}"}"
;;
*)
printf '%s\t%s\n' "$trimmed" "$name"
name=""
;;
esac
done < "$RECIPIENTS_FILE"
}
derive_project_name() { derive_project_name() {
local explicit="${1:-}" local explicit="${1:-}"
if [ -n "$explicit" ]; then if [ -n "$explicit" ]; then
@ -1954,6 +1977,33 @@ cmd_run() {
exit "$rc" exit "$rc"
} }
cmd_recipients() {
resolve_store
local sub="${1:-list}"
[ $# -gt 0 ] && shift
case "$sub" in
list) _recipients_list ;;
*) die "Unknown recipients subcommand: '$sub'. Usage: secrets recipients [list]" ;;
esac
}
_recipients_list() {
check_initialized
if [ ! -e "$RECIPIENTS_FILE" ]; then
check_key
echo "recipients: single-key (no $RECIPIENTS_FILE_NAME)"
echo " $(get_pubkey)"
return 0
fi
_load_recipients # validates the file (dies on bad key / symlink)
local count=0 k n
while IFS=$'\t' read -r k n; do count=$((count + 1)); done < <(_recipients_dump)
echo "recipients: $count (from $RECIPIENTS_FILE_NAME)"
while IFS=$'\t' read -r k n; do
if [ -n "$n" ]; then echo " $k ($n)"; else echo " $k"; fi
done < <(_recipients_dump)
}
cmd_which() { cmd_which() {
resolve_store resolve_store
echo "store: $SECRETS_DIR" echo "store: $SECRETS_DIR"
@ -2564,6 +2614,7 @@ case "${1:-help}" in
rekey) cmd_rekey ;; rekey) cmd_rekey ;;
verify) shift; cmd_verify "$@" ;; verify) shift; cmd_verify "$@" ;;
migrate) shift; cmd_migrate "$@" ;; migrate) shift; cmd_migrate "$@" ;;
recipients) shift; cmd_recipients "$@" ;;
which|where|status) cmd_which ;; which|where|status) cmd_which ;;
help|--help|-h) cmd_help ;; help|--help|-h) cmd_help ;;
*) die "Unknown command: $1. Run 'secrets help' for usage." ;; *) die "Unknown command: $1. Run 'secrets help' for usage." ;;

View file

@ -55,3 +55,24 @@ make_second_identity() {
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
[[ "$output" == *"symlink"* ]] || false [[ "$output" == *"symlink"* ]] || false
} }
@test "recipients list on a legacy store shows the single derived key" {
init_with_remote
run "$SECRETS_BIN" recipients list
[ "$status" -eq 0 ]
[[ "$output" == *"single-key"* ]] || false
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
[[ "$output" == *"$STORE_PUB"* ]] || false
}
@test "recipients list shows names and keys from recipients.txt" {
init_with_remote
make_second_identity
STORE_PUB=$(age-keygen -y "$SECRETS_DIR/key.txt")
printf '# alice\n%s\n# bob\n%s\n' "$STORE_PUB" "$BOB_PUB" > "$SECRETS_DIR/recipients.txt"
run "$SECRETS_BIN" recipients list
[ "$status" -eq 0 ]
[[ "$output" == *"recipients: 2"* ]] || false
[[ "$output" == *"alice"* ]] || false
[[ "$output" == *"bob"* ]] || false
}