261 lines
9.4 KiB
Shell
261 lines
9.4 KiB
Shell
# Cases for ../host-modules/lib/attic-init.nix. Each case runs the key script or
|
|
# the cache script against stubs that record every call, over a store kept in
|
|
# files, and compares the record and what the script left behind.
|
|
set -uo pipefail
|
|
|
|
failures=0
|
|
count=0
|
|
case="setup"
|
|
|
|
fail() {
|
|
echo "FAILED: $case: $*" >&2
|
|
failures=$((failures + 1))
|
|
}
|
|
|
|
dir=$(mktemp -d)
|
|
mkdir -p "$dir/bin" "$dir/store" "$dir/secrets"
|
|
|
|
# The scripts name their files under @DIR@, which is this run's directory.
|
|
for s in ENSURE_JWT PROVISION; do
|
|
sed "s|@DIR@|$dir|g" "${!s}" >"$dir/$s.sh"
|
|
export "$s=$dir/$s.sh"
|
|
done
|
|
calls=$dir/calls
|
|
export DIR=$dir
|
|
|
|
# The store is a directory with one file per path. `get` exits 2 on a miss, as
|
|
# `bao kv get` does. `put -cas=0` refuses an existing path; without it a put
|
|
# overwrites. $RACE_PATH makes another run create that path just before our put
|
|
# lands.
|
|
cat >"$dir/bin/bao" <<STUB
|
|
#!$BASH
|
|
file() { printf '%s/store/%s' "\$DIR" "\$(printf '%s' "\$1" | tr / _)"; }
|
|
case "\$1 \$2" in
|
|
"login -method=cert")
|
|
echo "bao login" >>"$calls"
|
|
if [ "\${LOGIN_FAIL:-0}" = 1 ]; then echo denied >&2; exit 2; fi
|
|
echo store-token
|
|
;;
|
|
"kv get")
|
|
path=\${4}
|
|
f=\$(file "\$path")
|
|
echo "bao get \$path" >>"$calls"
|
|
[ -f "\$f" ] || exit 2
|
|
cat "\$f"
|
|
;;
|
|
"kv put")
|
|
if [ "\$3" = "-cas=0" ]; then cas=1; path=\$4; else cas=0; path=\$3; fi
|
|
f=\$(file "\$path")
|
|
echo "bao put cas=\$cas \$path" >>"$calls"
|
|
value=\$(cat)
|
|
if [ "\${RACE_PATH:-}" = "\$path" ] && [ ! -f "\$f" ]; then
|
|
printf '%s' raced >"\$f"
|
|
fi
|
|
if [ "\$cas" = 1 ] && [ -f "\$f" ]; then echo "check-and-set parameter did not match" >&2; exit 1; fi
|
|
printf '%s' "\$value" >"\$f"
|
|
;;
|
|
esac
|
|
STUB
|
|
|
|
# The key is a fixed word, so the file the script writes is predictable.
|
|
cat >"$dir/bin/openssl" <<STUB
|
|
#!$BASH
|
|
echo openssl >>"$calls"
|
|
echo FAKEPEM
|
|
STUB
|
|
|
|
# A token carries the subject and a serial, so a case can tell which one a call
|
|
# used. The key comes in the environment, as the real tool takes it.
|
|
cat >"$dir/bin/atticadm" <<STUB
|
|
#!$BASH
|
|
[ -n "\${ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64:-}" ] || { echo "no key in the environment" >&2; exit 1; }
|
|
echo "atticadm \$*" >>"$calls"
|
|
n=\$(grep -c '^atticadm' "$calls")
|
|
sub=\$2
|
|
[ "\$1" = "-f" ] && sub=\$5
|
|
echo "tok-\$sub-\$n"
|
|
STUB
|
|
|
|
# The cache exists once \`cache create\` ran. Each call records the token the
|
|
# client config names.
|
|
cat >"$dir/bin/attic" <<STUB
|
|
#!$BASH
|
|
tokenfile=\$(sed -n 's/^token-file = "\(.*\)"$/\1/p' "\$XDG_CONFIG_HOME/attic/config.toml")
|
|
token=\$(cat "\$tokenfile")
|
|
echo "attic \$* | token=\$token" >>"$calls"
|
|
case "\$1 \$2" in
|
|
"cache info")
|
|
[ -f "\$DIR/cache-exists" ] && echo " Public Key: grid:FAKEKEY" >&2
|
|
;;
|
|
"cache create") touch "\$DIR/cache-exists" ;;
|
|
*) exit 1 ;;
|
|
esac
|
|
STUB
|
|
chmod +x "$dir/bin/"*
|
|
|
|
cat >"$dir/bin/curl" <<STUB
|
|
#!$BASH
|
|
echo curl >>"$calls"
|
|
[ "\${CURL_FAIL:-0}" != 1 ]
|
|
STUB
|
|
printf '#!%s\nexit 0\n' "$BASH" >"$dir/bin/sleep"
|
|
# A container that is not running has nothing to refresh.
|
|
printf '#!%s\nexit 1\n' "$BASH" >"$dir/bin/systemctl"
|
|
chmod +x "$dir/bin/"*
|
|
|
|
export PATH="$dir/bin:$PATH"
|
|
|
|
# n <pattern>: how many recorded calls match.
|
|
n() { grep -c -- "$1" "$calls" || true; }
|
|
|
|
# run <script> [VAR=value…]: the script's exit status.
|
|
run() {
|
|
local script=$1
|
|
shift
|
|
env "$@" bash "$script" >"$dir/out" 2>"$dir/err"
|
|
}
|
|
|
|
reset() {
|
|
rm -rf "$dir/store" "$dir/secrets" "$dir/cache-exists" "$calls"
|
|
mkdir -p "$dir/store" "$dir/secrets"
|
|
: >"$calls"
|
|
}
|
|
|
|
stored() { cat "$dir/store/$1" 2>/dev/null || true; }
|
|
JWT=secret_swarm_services_attic_jwt-secret
|
|
PULL=secret_swarm_services_attic_pull-token
|
|
PUSH=secret_swarm_services_attic_push-token
|
|
URL=secret_swarm_services_attic_url
|
|
CACHE=secret_swarm_services_attic_cache
|
|
KEY=secret_swarm_services_attic_public-key
|
|
ENVFILE=$dir/secrets/server.env
|
|
|
|
expect() { # <description> <got> <want>
|
|
count=$((count + 1))
|
|
if [ "$2" != "$3" ]; then fail "$1: got '$2', wanted '$3'"; fi
|
|
}
|
|
|
|
# ── the key ──────────────────────────────────────────────────────────────
|
|
case="first start creates the key and hands it over"
|
|
reset
|
|
run "$ENSURE_JWT"
|
|
expect "exit status" $? 0
|
|
expect "key minted once" "$(n '^openssl')" 1
|
|
expect "stored key" "$(stored $JWT)" "$(printf 'FAKEPEM\n' | base64 -w0)"
|
|
expect "env file" "$(cat "$ENVFILE")" "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=$(stored $JWT)"
|
|
expect "env file mode" "$(stat -c %a "$ENVFILE")" 400
|
|
|
|
case="a second start mints nothing and keeps the key"
|
|
first=$(stored $JWT)
|
|
run "$ENSURE_JWT"
|
|
expect "exit status" $? 0
|
|
expect "key minted still once" "$(n '^openssl')" 1
|
|
expect "no second create" "$(n '^bao put')" 1
|
|
expect "stored key unchanged" "$(stored $JWT)" "$first"
|
|
|
|
case="a stored key is used, not replaced, when the file is gone"
|
|
rm -f "$ENVFILE"
|
|
run "$ENSURE_JWT"
|
|
expect "exit status" $? 0
|
|
expect "key minted still once" "$(n '^openssl')" 1
|
|
expect "env file rewritten from the store" "$(cat "$ENVFILE")" "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=$first"
|
|
|
|
case="a create lost to another run uses the winner's key"
|
|
reset
|
|
run "$ENSURE_JWT" RACE_PATH=secret/swarm/services/attic/jwt-secret
|
|
expect "exit status" $? 0
|
|
expect "stored key is the winner's" "$(stored $JWT)" raced
|
|
expect "env file has the winner's" "$(cat "$ENVFILE")" "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=raced"
|
|
|
|
case="a refused login stops before anything is minted"
|
|
reset
|
|
run "$ENSURE_JWT" LOGIN_FAIL=1
|
|
expect "exit status" $? 1
|
|
expect "nothing minted" "$(n '^openssl')" 0
|
|
expect "nothing written" "$(ls "$dir/secrets" | wc -l)" 0
|
|
|
|
# ── the cache and the tokens ─────────────────────────────────────────────
|
|
case="first start creates the cache and both tokens"
|
|
reset
|
|
run "$ENSURE_JWT"
|
|
: >"$calls"
|
|
run "$PROVISION"
|
|
expect "exit status" $? 0
|
|
expect "pull token stored" "$(stored $PULL)" "tok-swarm-hives-pull-1"
|
|
expect "push token stored" "$(stored $PUSH)" "tok-swarm-builder-push-2"
|
|
expect "cache created once" "$(n '^attic cache create')" 1
|
|
expect "the address is stored" "$(stored $URL)" "http://cache.example"
|
|
expect "the cache name is stored" "$(stored $CACHE)" "grid"
|
|
expect "the cache's public key is stored" "$(stored $KEY)" "grid:FAKEKEY"
|
|
expect "cache created by the short-lived token" "$(n '^attic cache create local:grid | token=tok-swarm-attic-init-3')" 1
|
|
expect "tokens minted: pull, push, the creator" "$(n '^atticadm')" 3
|
|
expect "pull token reads only the cache" "$(n 'make-token --sub swarm-hives-pull --validity 10y --pull grid$')" 1
|
|
expect "push token pulls and pushes" "$(n 'make-token --sub swarm-builder-push --validity 10y --pull grid --push grid$')" 1
|
|
expect "the creator may create and pull, for minutes" "$(n 'make-token --sub swarm-attic-init --validity 5m --create-cache grid --pull grid$')" 1
|
|
|
|
case="a second start mints nothing and creates nothing"
|
|
pull=$(stored $PULL)
|
|
push=$(stored $PUSH)
|
|
: >"$calls"
|
|
run "$PROVISION"
|
|
expect "exit status" $? 0
|
|
expect "no token minted" "$(n '^atticadm')" 0
|
|
expect "no create call" "$(n '^bao put')" 0
|
|
expect "no cache create" "$(n '^attic cache create')" 0
|
|
expect "the pull token looked the cache up" "$(n "^attic cache info local:grid | token=$pull")" 1
|
|
expect "pull token unchanged" "$(stored $PULL)" "$pull"
|
|
expect "push token unchanged" "$(stored $PUSH)" "$push"
|
|
expect "address, name and key unchanged" "$(stored $URL)$(stored $CACHE)$(stored $KEY)" "http://cache.examplegridgrid:FAKEKEY"
|
|
|
|
case="values the operator stored over the minted ones are kept"
|
|
for leaf in $PULL $PUSH $URL $CACHE $KEY; do
|
|
printf '%s' "operator-$leaf" >"$dir/store/$leaf"
|
|
done
|
|
: >"$calls"
|
|
run "$PROVISION"
|
|
expect "exit status" $? 0
|
|
expect "no token minted" "$(n '^atticadm')" 0
|
|
expect "nothing written" "$(n '^bao put')" 0
|
|
expect "the operator's push token is still stored" "$(stored $PUSH)" "operator-$PUSH"
|
|
expect "the operator's pull token is still stored" "$(stored $PULL)" "operator-$PULL"
|
|
expect "the operator's address, name and key are still stored" \
|
|
"$(stored $URL)$(stored $CACHE)$(stored $KEY)" "operator-${URL}operator-${CACHE}operator-${KEY}"
|
|
expect "the cache lookup used the operator's pull token" "$(n "^attic cache info local:grid | token=operator-$PULL")" 1
|
|
printf '%s' "$pull" >"$dir/store/$PULL"
|
|
printf '%s' "$push" >"$dir/store/$PUSH"
|
|
printf '%s' http://cache.example >"$dir/store/$URL"
|
|
printf '%s' grid >"$dir/store/$CACHE"
|
|
printf '%s' grid:FAKEKEY >"$dir/store/$KEY"
|
|
|
|
case="an empty server gets its cache back with the stored tokens"
|
|
rm -f "$dir/cache-exists"
|
|
: >"$calls"
|
|
run "$PROVISION"
|
|
expect "exit status" $? 0
|
|
expect "only the creator is minted" "$(n '^atticadm')" 1
|
|
expect "no secret rewritten" "$(n '^bao put')" 0
|
|
expect "cache created" "$(n '^attic cache create')" 1
|
|
expect "pull token unchanged" "$(stored $PULL)" "$pull"
|
|
|
|
case="no tokens are minted while the server is down"
|
|
reset
|
|
run "$ENSURE_JWT"
|
|
: >"$calls"
|
|
run "$PROVISION" CURL_FAIL=1
|
|
expect "exit status" $? 1
|
|
expect "nothing minted" "$(n '^atticadm')" 0
|
|
expect "nothing stored" "$(n '^bao put')" 0
|
|
|
|
case="the client reads its token from a file, not from an argument"
|
|
reset
|
|
run "$ENSURE_JWT"
|
|
run "$PROVISION"
|
|
expect "no token among the client's arguments" "$(grep -c -- '^attic [^|]*tok-' "$calls" || true)" 0
|
|
# The control: the client did see tokens, through its config file.
|
|
expect "tokens seen through the config file" "$(grep -c -- '| token=tok-' "$calls" || true)" 4
|
|
|
|
if [ "$failures" -ne 0 ]; then
|
|
echo "$failures of $count checks failed" >&2
|
|
exit 1
|
|
fi
|
|
echo "$count checks passed"
|