Fix plugin updates.

This commit is contained in:
Greyson Parrelli
2026-08-06 22:52:15 -04:00
parent 0f43f6c1f2
commit 3406c622c5
6 changed files with 224 additions and 35 deletions
+77 -17
View File
@@ -187,6 +187,17 @@ grep -qx "GOOGLE_AUTH_ALLOWED_EMAILS=guest@partner.com" "$ENV_FILE" || fail "env
grep -qx "GOOGLE_AUTH_DENIED_EMAILS=former@signal.org" "$ENV_FILE" || fail "env file missing global denied emails"
grep -qx "GOOGLE_AUTH_APP_CONFIG_DIR=/data/apps" "$ENV_FILE" || fail "env file must point the service at the mount"
grep -q "ceo@signal.org" "$ENV_FILE" && fail "per-app entries must not be baked into the env file"
[[ "$(stat -c '%a' "$ENV_FILE")" == "600" ]] || fail "the env file holds the client secret and must stay 0600"
# Writing the env file tightens the umask to protect that secret. It must not
# tighten anything else: google-auth:enable writes the env file (starting the
# service) and then creates the app's config directory, which the unprivileged
# service has to be able to traverse.
fn-ga-app-set-enabled umask-app true
[[ "$(stat -c '%a' "$DATA/apps/umask-app")" == "711" ]] ||
fail "a directory created after the env file must still be traversable by the container uid"
fn-ga-exclude-add umask-app /api/hook
[[ "$(stat -c '%a' "$DATA/apps/umask-app")" == "711" ]] ||
fail "adding an exclusion must leave the app directory traversable"
echo "ok: service env file"
# --- report shows both scopes, enabled or not ---
@@ -216,40 +227,89 @@ grep -q 'proxy_set_header X-Google-Auth-App "my-app";' "$APP_CONF" ||
echo "ok: per-app list change refreshes the nginx config"
# --- a running service only counts if it can really read per-app lists ---
# It takes both the bind mount and the env var naming it. A container recreated
# from a service.env written before per-app lists existed has the mount but not
# the variable, ignores every per-app list, and still looks healthy — so the
# check must not be satisfied by the mount alone.
# It takes the bind mount, the env var naming it, and a binary that reads both.
# Every combination that is missing one still looks healthy from outside while
# quietly falling back to the global lists, so none of them may satisfy the
# check.
mkdir -p "$WORK/bin-docker"
cat >"$WORK/bin-docker/docker" <<'EOF'
#!/bin/sh
# Stands in for `docker container inspect`, replaying a canned inspection.
if [ "$1" = "container" ] && [ "$2" = "inspect" ]; then
cat "$DOCKER_INSPECT_FIXTURE"
exit 0
fi
# Stands in for the docker CLI, replaying canned inspections.
case "$1 $2" in
"container inspect") cat "$DOCKER_INSPECT_FIXTURE" ;;
"image inspect") printf '%s\n' "$DOCKER_IMAGE_FIXTURE" ;;
esac
exit 0
EOF
chmod +x "$WORK/bin-docker/docker"
cat >"$WORK/bin-docker/curl" <<'EOF'
#!/bin/sh
# Stands in for the healthz probe against the running service. An empty
# fixture means the service did not answer.
[ -s "$CURL_BODY_FIXTURE" ] || exit 7
cat "$CURL_BODY_FIXTURE"
EOF
chmod +x "$WORK/bin-docker/docker" "$WORK/bin-docker/curl"
# Runs the check against one canned inspection, in a subshell so the stub and
# its fixture do not leak into the rest of the file.
# Runs the check against one canned healthz body and one canned container
# inspection, in a subshell so the stubs and fixtures do not leak into the rest
# of the file.
# shellcheck disable=SC2030,SC2031 # the subshell is what keeps the stubs local
reads_app_lists() (
local healthz="$1"
shift
export DOCKER_INSPECT_FIXTURE="$WORK/inspect-fixture"
export CURL_BODY_FIXTURE="$WORK/healthz-fixture"
export DOCKER_IMAGE_FIXTURE=""
printf '%s\n' "$@" >"$DOCKER_INSPECT_FIXTURE"
printf '%s' "$healthz" >"$CURL_BODY_FIXTURE"
PATH="$WORK/bin-docker:$PATH"
fn-ga-service-reads-app-lists
)
reads_app_lists "mount=/data/apps" "env=GOOGLE_AUTH_APP_CONFIG_DIR=/data/apps" "env=GOOGLE_AUTH_CLIENT_ID=x" ||
fail "a container with both the mount and the env var should read per-app lists"
reads_app_lists "mount=/data/apps" "env=GOOGLE_AUTH_CLIENT_ID=x" &&
CURRENT_HEALTHZ='{"ok":true,"app_config_dir":"/data/apps"}'
# The reply from a build made before per-app lists existed.
LEGACY_HEALTHZ='ok'
reads_app_lists "$CURRENT_HEALTHZ" "mount=/data/apps" "env=GOOGLE_AUTH_APP_CONFIG_DIR=/data/apps" "env=GOOGLE_AUTH_CLIENT_ID=x" ||
fail "a container with the mount, the env var, and a current binary should read per-app lists"
# The case the plugin used to miss entirely: docker metadata is perfect because
# the current shell created the container, but the image it started is old, so
# the binary inside ignores the mount and every per-app list with it.
reads_app_lists "$LEGACY_HEALTHZ" "mount=/data/apps" "env=GOOGLE_AUTH_APP_CONFIG_DIR=/data/apps" "env=GOOGLE_AUTH_CLIENT_ID=x" &&
fail "a container running a pre-per-app-lists binary must not count, however well configured"
reads_app_lists '{"ok":true,"app_config_dir":"/somewhere/else"}' "mount=/data/apps" "env=GOOGLE_AUTH_APP_CONFIG_DIR=/data/apps" &&
fail "a service reading some other directory is not reading the mounted one"
reads_app_lists "$CURRENT_HEALTHZ" "mount=/data/apps" "env=GOOGLE_AUTH_CLIENT_ID=x" &&
fail "a container with the mount but no GOOGLE_AUTH_APP_CONFIG_DIR ignores per-app lists"
reads_app_lists "env=GOOGLE_AUTH_APP_CONFIG_DIR=/data/apps" &&
reads_app_lists "$CURRENT_HEALTHZ" "env=GOOGLE_AUTH_APP_CONFIG_DIR=/data/apps" &&
fail "a container with the env var but no mount has nothing to read"
reads_app_lists "" && fail "a container with neither should not count"
reads_app_lists "$CURRENT_HEALTHZ" "" && fail "a container with neither should not count"
# Unreachable service: with no answer to go on, an image that cannot be shown
# to match the plugin's source is assumed stale rather than assumed good.
reads_app_lists "" "mount=/data/apps" "env=GOOGLE_AUTH_APP_CONFIG_DIR=/data/apps" &&
fail "an unanswering service with an unverifiable image should not count"
echo "ok: per-app list readiness check"
# --- the built image has to match the source the plugin is running from ---
# The shell half of the plugin upgrades as soon as the files change; the binary
# only upgrades when the image is rebuilt. Reusing an image just because one
# exists is what lets a per-app list be configured, reported, and ignored.
# shellcheck disable=SC2030,SC2031 # the subshell is what keeps the stubs local
image_current() (
export DOCKER_IMAGE_FIXTURE="$1"
PATH="$WORK/bin-docker:$PATH"
fn-ga-image-current
)
SOURCE_HASH="$(fn-ga-source-hash)"
[[ -n "$SOURCE_HASH" ]] || fail "source hash should not be empty"
[[ "$SOURCE_HASH" == "$(fn-ga-source-hash)" ]] || fail "source hash should be stable across calls"
image_current "$SOURCE_HASH" || fail "an image labelled with the current source hash is current"
image_current "0000000000000000" && fail "an image built from other source is not current"
image_current "<no value>" && fail "an unlabelled image (built before this check) is not current"
image_current "" && fail "an image with no label value is not current"
echo "ok: service image tracks the plugin source"
# --- lifecycle triggers carry per-app lists ---
"$ROOT/post-app-rename" my-app renamed-app
[[ ! -d "$DATA/apps/my-app" ]] || fail "rename should move the app's directory"