v0.2.0.0 feat: sync gradle.properties keys via .secrets-files (EGB-531)
Add a committed .secrets-files manifest that lets secrets track designated keys from files outside the project root (motivating case: ~/.gradle/gradle.properties for Android Clerk publishable keys, which Android Studio GUI builds read but terminal env vars can't reach). - push extracts only the named keys, encrypts under <project>/external/ - pull MERGES them into the target, preserving unrelated keys/comments/order - pure-bash merge (no sed/regex): exact-string key match, opaque values - path validator: basename gradle.properties, within $HOME, no symlink/.. - external/ subdir keeps blobs out of the dotenv *.age globs; rekey + list recurse explicitly - which reads back the manifest; list shows [external]; pre-commit blocks plaintext gradle.properties Also fixes two latent bugs in 'secrets rekey' (never completed before, no prior test): age-keygen refusing to overwrite key.txt, and an EXIT trap referencing an out-of-scope local under set -u. Tests: 80 -> 104. Reviewed via /autoplan (CEO/Eng/DX). EGB-531.
This commit is contained in:
parent
ac2195d830
commit
110ac514cc
7 changed files with 816 additions and 13 deletions
|
|
@ -1045,3 +1045,307 @@ PKG
|
|||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"git+ssh://user@host:2222/path/to-repo_v2.git"* ]]
|
||||
}
|
||||
|
||||
# ─── EGB-531: gradle.properties external file support ──────────────────
|
||||
|
||||
# Helper: write a fake global gradle.properties under the sandboxed HOME.
|
||||
gradle_src() {
|
||||
mkdir -p "$HOME/.gradle"
|
||||
printf '%s' "$1" > "$HOME/.gradle/gradle.properties"
|
||||
}
|
||||
|
||||
# Helper: bind a project dir to a gradle entry via .secrets-files, cd into it.
|
||||
gradle_project() {
|
||||
local name="${1:-gproj}"
|
||||
local keys="${2:-beaconClerkPkTest beaconClerkPkLive}"
|
||||
local dir="$WORK_DIR/$name"
|
||||
mkdir -p "$dir"
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties %s\n' "$keys" > "$dir/.secrets-files"
|
||||
cd "$dir"
|
||||
}
|
||||
|
||||
@test "EGB-531: which shows parsed .secrets-files entries" {
|
||||
init_with_remote
|
||||
gradle_project gproj
|
||||
run "$SECRETS_BIN" which
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"gradle-properties"* ]]
|
||||
[[ "$output" == *"~/.gradle/gradle.properties"* ]]
|
||||
[[ "$output" == *"beaconClerkPkTest"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: push extracts managed keys into external/ blob (no .env needed)" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\nbeaconClerkPkLive=pk_live_xyz\nunrelated=keep\n'
|
||||
gradle_project gproj
|
||||
run "$SECRETS_BIN" push gproj
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Extracted 2 key"* ]]
|
||||
run bash -c "ls $SECRETS_DIR/gproj/external/*.gradle-properties.age"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "EGB-531: push dies if all managed keys missing from source" {
|
||||
init_with_remote
|
||||
gradle_src $'somethingelse=1\n'
|
||||
gradle_project gproj
|
||||
run "$SECRETS_BIN" push gproj
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"not found"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: pull merges managed keys, preserves unrelated entries" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\nbeaconClerkPkLive=pk_live_xyz\n'
|
||||
gradle_project gproj
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
# Simulate a second machine: target holds only unrelated keys
|
||||
gradle_src $'unrelated.key=keepme\norg.gradle.jvmargs=-Xmx2g\n'
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"Merged 2 key"* ]]
|
||||
grep -q 'beaconClerkPkTest=pk_test_abc' "$HOME/.gradle/gradle.properties"
|
||||
grep -q 'beaconClerkPkLive=pk_live_xyz' "$HOME/.gradle/gradle.properties"
|
||||
grep -q 'unrelated.key=keepme' "$HOME/.gradle/gradle.properties"
|
||||
grep -q 'org.gradle.jvmargs=-Xmx2g' "$HOME/.gradle/gradle.properties"
|
||||
}
|
||||
|
||||
@test "EGB-531: merge does NOT touch a substring key" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=secretval\n'
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
# Target has the shorter key AND a stale managed key
|
||||
gradle_src $'beaconClerkPk=DONOTCHANGE\nbeaconClerkPkTest=old\n'
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q '^beaconClerkPk=DONOTCHANGE$' "$HOME/.gradle/gradle.properties"
|
||||
grep -q '^beaconClerkPkTest=secretval$' "$HOME/.gradle/gradle.properties"
|
||||
}
|
||||
|
||||
@test "EGB-531: value with sed/regex metacharacters round-trips byte-exact" {
|
||||
init_with_remote
|
||||
local val='a/b&c\d.e|f$g'
|
||||
gradle_src "$(printf 'beaconClerkPkTest=%s\n' "$val")"
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
gradle_src $'other=1\n'
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
run grep -F "beaconClerkPkTest=$val" "$HOME/.gradle/gradle.properties"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "EGB-531: colon and space separators are parsed" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest : pk_colon\nbeaconClerkPkLive pk_space\n'
|
||||
gradle_project gproj
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
gradle_src $'x=1\n'
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q '^beaconClerkPkTest=pk_colon$' "$HOME/.gradle/gradle.properties"
|
||||
grep -q '^beaconClerkPkLive=pk_space$' "$HOME/.gradle/gradle.properties"
|
||||
}
|
||||
|
||||
@test "EGB-531: pull is idempotent (second pull leaves file byte-identical)" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\nbeaconClerkPkLive=pk_live_xyz\n'
|
||||
gradle_project gproj
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
gradle_src $'unrelated=x\n# a comment\n'
|
||||
"$SECRETS_BIN" pull gproj >/dev/null 2>&1
|
||||
cp "$HOME/.gradle/gradle.properties" "$TEST_TMPDIR/snap1"
|
||||
"$SECRETS_BIN" pull gproj >/dev/null 2>&1
|
||||
run diff "$TEST_TMPDIR/snap1" "$HOME/.gradle/gradle.properties"
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "EGB-531: merge preserves comments and blank lines" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
gradle_src $'# header comment\n\nunrelated=x\n'
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q '^# header comment$' "$HOME/.gradle/gradle.properties"
|
||||
grep -q '^unrelated=x$' "$HOME/.gradle/gradle.properties"
|
||||
grep -q '^beaconClerkPkTest=pk_test_abc$' "$HOME/.gradle/gradle.properties"
|
||||
}
|
||||
|
||||
@test "EGB-531: duplicate managed key in target collapses to one canonical line" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=newval\n'
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
gradle_src $'beaconClerkPkTest=old1\nx=1\nbeaconClerkPkTest=old2\n'
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
run grep -c '^beaconClerkPkTest=' "$HOME/.gradle/gradle.properties"
|
||||
[ "$output" -eq 1 ]
|
||||
grep -q '^beaconClerkPkTest=newval$' "$HOME/.gradle/gradle.properties"
|
||||
grep -q '^x=1$' "$HOME/.gradle/gradle.properties"
|
||||
}
|
||||
|
||||
@test "EGB-531: continuation-line-adjacent managed key is not clobbered" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=realval\n'
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
# 'beaconClerkPkTest=...' here is a CONTINUATION of unrelated's value, not a definition
|
||||
printf 'unrelated=foo\\\nbeaconClerkPkTest=continuation\n' > "$HOME/.gradle/gradle.properties"
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
# continuation line preserved verbatim
|
||||
grep -q '^beaconClerkPkTest=continuation$' "$HOME/.gradle/gradle.properties"
|
||||
# and the real managed key appended
|
||||
grep -q '^beaconClerkPkTest=realval$' "$HOME/.gradle/gradle.properties"
|
||||
}
|
||||
|
||||
@test "EGB-531: first-create target gets mode 600" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
rm -f "$HOME/.gradle/gradle.properties"
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
[ -f "$HOME/.gradle/gradle.properties" ]
|
||||
local mode
|
||||
mode=$(stat -f '%Lp' "$HOME/.gradle/gradle.properties" 2>/dev/null || stat -c '%a' "$HOME/.gradle/gradle.properties")
|
||||
[ "$mode" = "600" ]
|
||||
}
|
||||
|
||||
@test "EGB-531: target with wrong basename is refused" {
|
||||
init_with_remote
|
||||
mkdir -p "$HOME/.gradle"
|
||||
printf 'beaconClerkPkTest=x\n' > "$HOME/.gradle/custom.properties"
|
||||
mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj"
|
||||
printf 'gradle-properties ~/.gradle/custom.properties beaconClerkPkTest\n' > .secrets-files
|
||||
run "$SECRETS_BIN" push gproj
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"gradle.properties"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: target outside HOME is refused" {
|
||||
init_with_remote
|
||||
local outside
|
||||
outside=$(mktemp -d)
|
||||
mkdir -p "$outside/.gradle"
|
||||
printf 'beaconClerkPkTest=x\n' > "$outside/.gradle/gradle.properties"
|
||||
mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj"
|
||||
printf 'gradle-properties %s/.gradle/gradle.properties beaconClerkPkTest\n' "$outside" > .secrets-files
|
||||
run "$SECRETS_BIN" push gproj
|
||||
rm -rf "$outside"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"HOME"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: symlinked target is refused" {
|
||||
init_with_remote
|
||||
mkdir -p "$HOME/.gradle"
|
||||
printf 'beaconClerkPkTest=x\n' > "$HOME/realgradle"
|
||||
ln -s "$HOME/realgradle" "$HOME/.gradle/gradle.properties"
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
run "$SECRETS_BIN" push gproj
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"symlink"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: unknown type in manifest warns and skips" {
|
||||
init_with_remote
|
||||
mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj"
|
||||
printf 'gradle-props ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
|
||||
echo "X=1" > .env
|
||||
run "$SECRETS_BIN" push gproj
|
||||
[[ "$output" == *"unknown type"* ]]
|
||||
[ ! -d "$SECRETS_DIR/gproj/external" ]
|
||||
}
|
||||
|
||||
@test "EGB-531: malformed manifest line (no path/keys) is skipped with warning" {
|
||||
init_with_remote
|
||||
mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj"
|
||||
printf 'gradle-properties\n' > .secrets-files
|
||||
run "$SECRETS_BIN" which
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"WARNING"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: manifest path with command-substitution chars is rejected" {
|
||||
init_with_remote
|
||||
mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj"
|
||||
local pwn="$TEST_TMPDIR/pwn-$$"
|
||||
rm -f "$pwn"
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties$(touch %s) beaconClerkPkTest\n' "$pwn" > .secrets-files
|
||||
run "$SECRETS_BIN" which
|
||||
[ ! -f "$pwn" ]
|
||||
[[ "$output" == *"WARNING"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: symlinked .secrets-files is ignored" {
|
||||
init_with_remote
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > "$HOME/realmanifest"
|
||||
mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj"
|
||||
ln -s "$HOME/realmanifest" .secrets-files
|
||||
run "$SECRETS_BIN" which
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" != *"beaconClerkPkTest"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: rekey re-encrypts the external blob (still decryptable after)" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
"$SECRETS_BIN" rekey >/dev/null 2>&1
|
||||
gradle_src $'other=1\n'
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q '^beaconClerkPkTest=pk_test_abc$' "$HOME/.gradle/gradle.properties"
|
||||
}
|
||||
|
||||
@test "EGB-531: gradle blob is NOT decrypted into cwd by dotenv pull" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
mkdir -p "$WORK_DIR/gproj"; cd "$WORK_DIR/gproj"
|
||||
printf 'gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest\n' > .secrets-files
|
||||
echo "DOTENV=1" > .env
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
local pulldir="$WORK_DIR/pull-gproj"
|
||||
mkdir -p "$pulldir"
|
||||
cp "$WORK_DIR/gproj/.secrets-files" "$pulldir/.secrets-files"
|
||||
cd "$pulldir"
|
||||
run "$SECRETS_BIN" pull gproj
|
||||
[ "$status" -eq 0 ]
|
||||
[ -f "$pulldir/.env" ]
|
||||
[ ! -f "$pulldir/gradle.properties" ]
|
||||
}
|
||||
|
||||
@test "EGB-531: list shows external entry" {
|
||||
init_with_remote
|
||||
gradle_src $'beaconClerkPkTest=pk_test_abc\n'
|
||||
gradle_project gproj beaconClerkPkTest
|
||||
"$SECRETS_BIN" push gproj >/dev/null 2>&1
|
||||
run "$SECRETS_BIN" list
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"external"* ]]
|
||||
}
|
||||
|
||||
@test "EGB-531: no .secrets-files behaves exactly as before (backward compat)" {
|
||||
init_with_remote
|
||||
create_project_dir plainproj
|
||||
run "$SECRETS_BIN" push plainproj
|
||||
[ "$status" -eq 0 ]
|
||||
[ ! -d "$SECRETS_DIR/plainproj/external" ]
|
||||
}
|
||||
|
||||
@test "EGB-531: pre-commit blocks plaintext gradle.properties in store" {
|
||||
init_with_remote
|
||||
cd "$SECRETS_DIR"
|
||||
echo "beaconClerkPkTest=leak" > gradle.properties
|
||||
git add -f gradle.properties
|
||||
run git commit -m "should fail"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"Plaintext"* ]]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue