Files
meshcore-analyzer/docker/test-select-service-user.sh
T
a164a44e7f fix(docker): run the Go services as an unprivileged user (#2125)
Runs `corescope-server` and `corescope-ingestor` unprivileged, opt-in
with `RUN_AS_UID`. A code-execution bug in either one then no longer
lands as root next to the mounted data and `config.json`. `supervisord`
stays root, for mosquitto and caddy.

| `RUN_AS_UID` | Go services run as |
|---|---|
| unset (default) | root, as before. Upgrades change nothing |
| set | `RUN_AS_UID` / `RUN_AS_GID` |

With `RUN_AS_UID` set:
- Each start hands the data directory to that uid (`chown -h`), except
`.env` and anything mounted into it on its own. Failures are logged.
- `.env` is read only if it is root-owned and only root can write it.
`RUN_AS_UID` and `RUN_AS_GID` are never taken from it.
- `/app` stays root's. **Depends on #2137.**
- A uid with no account in the image gets one at start; `caddy` and
`mosquitto` are reused. A uid that can't be provided fails startup
instead of falling back to root.

The logic is in `docker/select-service-user.sh`, tested by
`docker/test-select-service-user.sh`, which needs no Docker or root and
runs in CI. See also "Running the Go services unprivileged" in
`docs/deployment.md`.

Verified in the built image (master + this + #2137) across 14 cases.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
Co-authored-by: nullrouten <201262769+nullrouten0@users.noreply.github.com>
2026-10-15 09:49:29 +02:00

480 lines
19 KiB
Bash
Executable File

#!/usr/bin/env bash
# test-select-service-user.sh — unit tests for docker/select-service-user.sh,
# the uid selection the container entrypoint performs at start.
#
# Every branch is exercised without docker and without root: getent, adduser,
# addgroup, deluser, delgroup, id, chown, find and stat are replaced by stubs
# on PATH, and the mount table is a fixture file. The account stubs read and
# write a passwd/group pair of files seeded with the accounts the real image has — verified against alpine:3.20 with
# mosquitto, supervisor and caddy installed, plus the Dockerfile's corescope —
# so an account created mid-run is visible to the lookups that follow it. chown
# and find record their arguments rather than touching the filesystem, which is
# what lets the ownership hand-over be asserted exactly.
#
# The driver calls prepare_service_user, the one function docker/entrypoint-go.sh
# calls: resolve the account, read .env, hand over the data directory. A static
# check at the end pins that the entrypoint calls it and nothing else from the
# script, so what gets tested is the sequence the container runs: a non-zero
# return must never be read as root, and .env cannot choose the account.
#
# Run: bash docker/test-select-service-user.sh
# Exits non-zero if any case fails.
set -u
SCRIPT="$(cd "$(dirname "$0")" && pwd)/select-service-user.sh"
[ -f "$SCRIPT" ] || { echo "FAIL: $SCRIPT missing" >&2; exit 1; }
# Accounts the image actually has. uid:gid differ for caddy and mosquitto,
# which is what makes the "uid already belongs to an account" branch testable.
IMAGE_PASSWD='root:0:0
caddy:100:101
mosquitto:101:102
corescope:1000:1000'
IMAGE_GROUP='root:0
caddy:101
mosquitto:102
corescope:1000'
PASS=0
FAIL=0
CASE=""
fail() {
FAIL=$((FAIL + 1))
echo "FAIL: $CASE — $1" >&2
}
assert_eq() {
if [ "$2" = "$3" ]; then PASS=$((PASS + 1)); else
fail "$1 — expected '$2', got '$3'"
fi
}
assert_has() {
case "$2" in
*"$3"*) PASS=$((PASS + 1)) ;;
*) fail "$1 — expected to find '$3' in: $2" ;;
esac
}
assert_lacks() {
case "$2" in
*"$3"*) fail "$1 — did not expect '$3' in: $2" ;;
*) PASS=$((PASS + 1)) ;;
esac
}
# ----- stub environment ------------------------------------------------------
make_fixture() {
FIX=$(mktemp -d)
printf '%s\n' "$IMAGE_PASSWD" > "$FIX/passwd"
printf '%s\n' "$IMAGE_GROUP" > "$FIX/group"
: > "$FIX/calls"
: > "$FIX/log"
printf '%s' "${FIX_MOUNTINFO-}" > "$FIX/mountinfo"
if [ -n "${FIX_ENV-}" ]; then
printf '%s\n' "$FIX_ENV" > "$FIX/env.real"
if [ "${FIX_ENV_SYMLINK:-0}" = 1 ]; then
ln -s "$FIX/env.real" "$FIX/env"
[ -L "$FIX/env" ] || { echo "FAIL: fixture could not create the .env symlink" >&2; exit 1; }
else
mv "$FIX/env.real" "$FIX/env"
fi
fi
mkdir -p "$FIX/bin"
cat > "$FIX/bin/getent" <<'STUB'
#!/bin/sh
case "$1" in
passwd)
while IFS=: read -r name uid gid; do
[ -n "$name" ] || continue
if [ "$2" = "$name" ] || [ "$2" = "$uid" ]; then
echo "$name:x:$uid:$gid::/:/sbin/nologin"; exit 0
fi
done < "$FIX/passwd" ;;
group)
while IFS=: read -r name gid; do
[ -n "$name" ] || continue
if [ "$2" = "$name" ] || [ "$2" = "$gid" ]; then
echo "$name:x:$gid:"; exit 0
fi
done < "$FIX/group" ;;
esac
exit 2
STUB
cat > "$FIX/bin/adduser" <<'STUB'
#!/bin/sh
[ "${STUB_ADDUSER_FAIL:-0}" = 1 ] && exit 1
uid=""; grp=""; name=""; flags=""
while [ $# -gt 0 ]; do
case "$1" in
-u) uid="$2"; shift 2 ;;
-G) grp="$2"; shift 2 ;;
-h|-s) shift 2 ;;
-S|-D|-H) flags="$flags $1"; shift ;;
*) name="$1"; shift ;;
esac
done
[ -n "$uid" ] && [ -n "$name" ] && [ -n "$grp" ] || exit 1
# busybox refuses a uid that is already taken
awk -F: -v u="$uid" '$2==u{found=1} END{exit !found}' "$FIX/passwd" && exit 1
gid=$(awk -F: -v g="$grp" '$1==g{print $2}' "$FIX/group")
[ -n "$gid" ] || exit 1
echo "$name:$uid:$gid" >> "$FIX/passwd"
echo "adduser $name uid=$uid gid=$gid flags=$flags" >> "$FIX/calls"
STUB
cat > "$FIX/bin/addgroup" <<'STUB'
#!/bin/sh
[ "${STUB_ADDGROUP_FAIL:-0}" = 1 ] && exit 1
gid=""; name=""
while [ $# -gt 0 ]; do
case "$1" in
-g) gid="$2"; shift 2 ;;
-S|-D) shift ;;
*) name="$1"; shift ;;
esac
done
[ -n "$gid" ] && [ -n "$name" ] || exit 1
awk -F: -v g="$gid" '$2==g{found=1} END{exit !found}' "$FIX/group" && exit 1
echo "$name:$gid" >> "$FIX/group"
echo "addgroup $name gid=$gid" >> "$FIX/calls"
STUB
cat > "$FIX/bin/deluser" <<'STUB'
#!/bin/sh
grep -v "^$1:" "$FIX/passwd" > "$FIX/passwd.new" || true
mv "$FIX/passwd.new" "$FIX/passwd"
echo "deluser $1" >> "$FIX/calls"
STUB
cat > "$FIX/bin/delgroup" <<'STUB'
#!/bin/sh
grep -v "^$1:" "$FIX/group" > "$FIX/group.new" || true
mv "$FIX/group.new" "$FIX/group"
echo "delgroup $1" >> "$FIX/calls"
STUB
cat > "$FIX/bin/id" <<'STUB'
#!/bin/sh
while IFS=: read -r name uid gid; do
if [ "$2" = "$name" ]; then
case "$1" in
-u) echo "$uid"; exit 0 ;;
-g) echo "$gid"; exit 0 ;;
esac
exit 2
fi
done < "$FIX/passwd"
exit 1
STUB
cat > "$FIX/bin/chown" <<'STUB'
#!/bin/sh
echo "chown $*" >> "$FIX/calls"
STUB
# The hand-over walk is recorded; the verification walk (-print) answers
# with STUB_LEFTOVER, the paths a failed chown would have left behind.
cat > "$FIX/bin/find" <<'STUB'
#!/bin/sh
echo "find $*" >> "$FIX/calls"
case " $* " in
*" -print "*) [ -n "${STUB_LEFTOVER-}" ] && printf '%s\n' "$STUB_LEFTOVER" ;;
esac
exit 0
STUB
# Answers STUB_STAT ("uid mode"), by default a uid 1000 file: the uid
# must never be inferred from that, and the .env check must refuse it.
# Any call is recorded so the test can prove when none happens.
cat > "$FIX/bin/stat" <<'STUB'
#!/bin/sh
echo "stat $*" >> "$FIX/calls"
echo "${STUB_STAT:-1000 -rw-r--r--}"
STUB
chmod +x "$FIX"/bin/*
}
# drive [VAR=VAL ...] — run prepare_service_user as docker/entrypoint-go.sh
# does and exit non-zero if it fails. env -i keeps the host environment (a
# RUN_AS_UID of its own, a real PATH) out of the case. FIX_ENV (the .env
# contents), FIX_ENV_SYMLINK and FIX_MOUNTINFO shape the fixture; a .env that
# sets CS_ENV_READ shows up as "env-read" in CALLS. OUT is the account the
# services will run as (CORESCOPE_SERVICE_USER); FINAL_ENV is RUN_AS_UID/GID as
# the services would see them.
drive() {
make_fixture
OUT=$(env -i PATH="$FIX/bin:/usr/bin:/bin" FIX="$FIX" SCRIPT="$SCRIPT" "$@" sh -c '
. "$SCRIPT"
SERVICE_ENV_FILE="$FIX/env"
SERVICE_MOUNTINFO="$FIX/mountinfo"
prepare_service_user || exit 1
[ -z "${CS_ENV_READ-}" ] || echo "env-read" >> "$FIX/calls"
echo "RUN_AS_UID=${RUN_AS_UID-<unset>} RUN_AS_GID=${RUN_AS_GID-<unset>}" > "$FIX/final-env"
echo "$CORESCOPE_SERVICE_USER"
' 2>"$FIX/log")
RC=$?
LOG=$(cat "$FIX/log")
CALLS=$(cat "$FIX/calls")
FINAL_ENV=$(cat "$FIX/final-env" 2>/dev/null)
}
# ----- 1. RUN_AS_UID unset: root, exactly as earlier releases ----------------
CASE="RUN_AS_UID unset"
drive
assert_eq "account" "root" "$OUT"
assert_eq "exit status" 0 "$RC"
assert_eq "nothing was chowned" "" "$CALLS"
assert_has "log explains the default" "$LOG" "RUN_AS_UID is unset"
# ----- 2. the dropped inferred branch must not come back ---------------------
# The data directory is reported as owned by uid 1000 (`mkdir data` + bind
# mount, the ordinary setup). Earlier revisions of this change ran the services
# as that uid, which broke every upgrade whose data directory held root-owned
# database files. Staying root here is the point of the whole arrangement.
CASE="non-root data dir, RUN_AS_UID unset"
drive
assert_eq "account" "root" "$OUT"
assert_lacks "data dir owner never consulted" "$CALLS" "stat"
assert_eq "nothing was chowned" "" "$CALLS"
# ----- 3. RUN_AS_UID=1000 reuses the account baked into the image ------------
CASE="RUN_AS_UID=1000"
drive RUN_AS_UID=1000 RUN_AS_GID=1000
assert_eq "account" "corescope" "$OUT"
assert_eq "exit status" 0 "$RC"
assert_lacks "/app stays root's" "$CALLS" "chown 1000:1000 /app"
assert_has "data dir handed over, .env left out, symlinks not followed" "$CALLS" \
"find /app/data ( -path $FIX/env ) -prune -o ! -user 1000 -exec chown -h 1000:1000 {} +"
assert_has "hand-over checked afterwards" "$CALLS" \
"find /app/data ( -path $FIX/env ) -prune -o ! -user 1000 -print"
assert_lacks "nothing reported left over" "$LOG" "WARNING"
assert_lacks "no account created" "$CALLS" "adduser"
assert_lacks "baked-in account kept" "$CALLS" "deluser"
# ----- 4. RUN_AS_GID defaults to RUN_AS_UID ---------------------------------
CASE="RUN_AS_UID=1000, RUN_AS_GID unset"
drive RUN_AS_UID=1000
assert_eq "account" "corescope" "$OUT"
assert_has "gid defaulted to the uid" "$CALLS" "chown -h 1000:1000"
assert_lacks "no group mismatch reported" "$LOG" "note:"
# ----- 5. a uid nothing holds: the account is recreated at it ----------------
CASE="RUN_AS_UID=5000"
drive RUN_AS_UID=5000
assert_eq "account" "corescope" "$OUT"
assert_eq "exit status" 0 "$RC"
assert_has "baked-in account freed" "$CALLS" "deluser corescope"
assert_has "baked-in group freed" "$CALLS" "delgroup corescope"
assert_has "group created at the gid" "$CALLS" "addgroup corescope gid=5000"
assert_has "account created at the uid" "$CALLS" "adduser corescope uid=5000 gid=5000"
assert_has "home directory not created or re-owned" "$CALLS" "flags= -S -H"
assert_has "data dir handed over" "$CALLS" "chown -h 5000:5000"
# ----- 6. a separate RUN_AS_GID ---------------------------------------------
CASE="RUN_AS_UID=5000, RUN_AS_GID=6000"
drive RUN_AS_UID=5000 RUN_AS_GID=6000
assert_eq "account" "corescope" "$OUT"
assert_has "group created at the gid" "$CALLS" "addgroup corescope gid=6000"
assert_has "account created with that group" "$CALLS" "adduser corescope uid=5000 gid=6000"
assert_has "data dir handed over" "$CALLS" "chown -h 5000:6000"
# ----- 7. a uid an image account already holds ------------------------------
# caddy is uid 100 but gid 101, so the requested gid cannot be honoured; the
# account is used as it is and the mismatch is reported rather than hidden.
CASE="RUN_AS_UID=100 (caddy)"
drive RUN_AS_UID=100
assert_eq "account" "caddy" "$OUT"
assert_eq "exit status" 0 "$RC"
assert_lacks "existing account untouched" "$CALLS" "adduser"
assert_has "mismatch reported" "$LOG" "primary group is 101 and not 100"
assert_has "account's own gid used" "$CALLS" "chown -h 100:101"
# ----- 8. RUN_AS_UID=0 is a request for the old behaviour -------------------
CASE="RUN_AS_UID=0"
drive RUN_AS_UID=0
assert_eq "account" "root" "$OUT"
assert_eq "exit status" 0 "$RC"
assert_eq "nothing was chowned" "" "$CALLS"
# ----- 9. non-numeric input is rejected -------------------------------------
CASE="RUN_AS_UID=nobody"
drive RUN_AS_UID=nobody
assert_eq "exit status" 1 "$RC"
assert_eq "no account on stdout" "" "$OUT"
assert_has "reason given" "$LOG" "RUN_AS_UID must be a number"
CASE="RUN_AS_GID=wheel"
drive RUN_AS_UID=5000 RUN_AS_GID=wheel
assert_eq "exit status" 1 "$RC"
assert_eq "no account on stdout" "" "$OUT"
assert_has "reason given" "$LOG" "RUN_AS_GID must be a number"
# ----- 10. an explicit request that cannot be met must not become root ------
CASE="RUN_AS_UID=5000, adduser fails"
drive RUN_AS_UID=5000 STUB_ADDUSER_FAIL=1
assert_eq "exit status" 1 "$RC"
assert_eq "no account on stdout" "" "$OUT"
assert_has "refusal is explicit" "$LOG" "refusing to fall back to root"
assert_lacks "nothing handed over" "$CALLS" "chown"
CASE="RUN_AS_UID=5000, addgroup fails"
drive RUN_AS_UID=5000 STUB_ADDGROUP_FAIL=1
assert_eq "exit status" 1 "$RC"
assert_eq "no account on stdout" "" "$OUT"
assert_has "refusal is explicit" "$LOG" "refusing to fall back to root"
# ----- 11. RUN_AS_GID is honoured for the baked-in account -----------------
# uid 1000 is the image's own corescope account, gid 1000. Asked for gid 1001,
# it is rebuilt in that group rather than used with the wrong one.
CASE="RUN_AS_UID=1000, RUN_AS_GID=1001"
drive RUN_AS_UID=1000 RUN_AS_GID=1001
assert_eq "account" "corescope" "$OUT"
assert_has "baked-in account rebuilt" "$CALLS" "deluser corescope"
assert_has "group created at the gid" "$CALLS" "addgroup corescope gid=1001"
assert_has "account recreated in it" "$CALLS" "adduser corescope uid=1000 gid=1001"
assert_has "data dir handed to that gid" "$CALLS" "chown -h 1000:1001"
assert_lacks "no group mismatch left to report" "$LOG" "note:"
# ----- 12. .env as root: read as before, whoever owns it -------------------
CASE=".env, RUN_AS_UID unset"
FIX_ENV='CS_ENV_READ=1' drive
assert_eq "account" "root" "$OUT"
assert_has ".env read" "$CALLS" "env-read"
assert_lacks "owner not checked as root" "$CALLS" "stat"
# ----- 13. .env, unprivileged: only a root-owned, root-only-writable file ----
CASE=".env root-owned 0644, RUN_AS_UID=1000"
FIX_ENV='CS_ENV_READ=1' drive RUN_AS_UID=1000 STUB_STAT='0 -rw-r--r--'
assert_eq "account" "corescope" "$OUT"
assert_has ".env read" "$CALLS" "env-read"
CASE=".env root-owned 0600, RUN_AS_UID=1000"
FIX_ENV='CS_ENV_READ=1' drive RUN_AS_UID=1000 STUB_STAT='0 -rw-------'
assert_has ".env read" "$CALLS" "env-read"
CASE=".env owned by the service uid, RUN_AS_UID=1000"
FIX_ENV='CS_ENV_READ=1' drive RUN_AS_UID=1000 STUB_STAT='1000 -rw-r--r--'
assert_eq "account" "corescope" "$OUT"
assert_lacks ".env not read" "$CALLS" "env-read"
assert_has "refusal explained" "$LOG" "not reading $FIX/env"
CASE=".env root-owned but group-writable, RUN_AS_UID=1000"
FIX_ENV='CS_ENV_READ=1' drive RUN_AS_UID=1000 STUB_STAT='0 -rw-rw-r--'
assert_lacks ".env not read" "$CALLS" "env-read"
CASE=".env root-owned but world-writable, RUN_AS_UID=1000"
FIX_ENV='CS_ENV_READ=1' drive RUN_AS_UID=1000 STUB_STAT='0 -rw-r--rw-'
assert_lacks ".env not read" "$CALLS" "env-read"
CASE=".env a symlink, RUN_AS_UID=1000"
FIX_ENV='CS_ENV_READ=1' FIX_ENV_SYMLINK=1 drive RUN_AS_UID=1000 STUB_STAT='0 -rw-r--r--'
assert_lacks ".env not read" "$CALLS" "env-read"
assert_lacks "refused before its owner is asked" "$CALLS" "stat"
# ----- 14. .env cannot choose the account -----------------------------------
# A service that can delete .env could otherwise drop its own RUN_AS_UID, and
# one that can write it could set RUN_AS_UID= and be root after a restart.
CASE="RUN_AS_UID in .env, unset on the container"
FIX_ENV='RUN_AS_UID=1000' drive
assert_eq "account" "root" "$OUT"
assert_eq "nothing was chowned" "" "$CALLS"
assert_has "ignored, and said so" "$LOG" "ignoring RUN_AS_UID/RUN_AS_GID"
assert_eq "and not left exported to the services" "RUN_AS_UID=<unset> RUN_AS_GID=<unset>" "$FINAL_ENV"
CASE="RUN_AS_UID= in a trusted .env, 1000 on the container"
FIX_ENV='RUN_AS_UID=' drive RUN_AS_UID=1000 STUB_STAT='0 -rw-r--r--'
assert_eq "account" "corescope" "$OUT"
assert_has "ignored, and said so" "$LOG" "ignoring RUN_AS_UID/RUN_AS_GID"
assert_eq "the container's value is put back" "RUN_AS_UID=1000 RUN_AS_GID=<unset>" "$FINAL_ENV"
CASE="RUN_AS_UID=0 and RUN_AS_GID in a trusted .env, 1000 on the container"
FIX_ENV='RUN_AS_UID=0
RUN_AS_GID=0' drive RUN_AS_UID=1000 RUN_AS_GID=1000 STUB_STAT='0 -rw-r--r--'
assert_eq "account" "corescope" "$OUT"
assert_eq "both put back" "RUN_AS_UID=1000 RUN_AS_GID=1000" "$FINAL_ENV"
# The account is resolved before .env is read. If it were read first, a
# RUN_AS_UID there would make the resolver create an account at that uid.
CASE="order: .env read after the account is resolved"
FIX_ENV='RUN_AS_UID=5000' drive RUN_AS_UID=1000 STUB_STAT='0 -rw-r--r--'
assert_eq "account" "corescope" "$OUT"
assert_lacks "no account was created at the uid from .env" "$CALLS" "adduser"
assert_lacks "none was freed for it either" "$CALLS" "deluser"
assert_has "the data dir went to the container's uid" "$CALLS" "chown -h 1000:1000"
# A trusted .env that assigns the names the entrypoint and these functions use
# for the account must not change who the services run as, or who is handed
# the data directory.
CASE=".env assigns the account's variable names"
FIX_ENV='account=root
service_account=root
_account=root
CORESCOPE_SERVICE_USER=root' drive RUN_AS_UID=1000 STUB_STAT='0 -rw-r--r--'
assert_eq "account" "corescope" "$OUT"
assert_has "the data dir was still handed over to it" "$CALLS" "chown -h 1000:1000"
# ----- 15. a file mounted into the data directory keeps its host owner -----
# The documented single-file config mount, plus one with a space in its path,
# as the kernel escapes it. The data directory's own mount is not excluded.
CASE="single-file mounts in the data directory"
FIX_MOUNTINFO='600 500 0:50 /srv/data /app/data rw,relatime - ext4 /dev/sda1 rw
601 600 0:50 /srv/config.json /app/data/config.json ro,relatime - ext4 /dev/sda1 rw
602 600 0:50 /srv/my\040keys /app/data/my\040keys rw,relatime - ext4 /dev/sda1 rw
603 500 0:51 / /data/caddy rw,relatime - ext4 /dev/sda1 rw
' drive RUN_AS_UID=1000
assert_has "mounts pruned along with .env" "$CALLS" \
"find /app/data ( -path $FIX/env -o -path /app/data/config.json -o -path /app/data/my keys ) -prune -o ! -user 1000 -exec chown -h 1000:1000 {} +"
assert_has "config.json reported as left alone" "$LOG" "left alone: /app/data/config.json"
assert_lacks "data dir itself still handed over" "$CALLS" "-path /app/data )"
assert_lacks "other mounts ignored" "$CALLS" "/data/caddy"
# Names the kernel escapes (tab) or that find would read as patterns ([ * ?)
# must exclude exactly that path.
CASE="mount points with a tab and glob characters"
FIX_MOUNTINFO='601 600 0:50 /srv/a /app/data/tab\011name rw,relatime - ext4 /dev/sda1 rw
602 600 0:50 /srv/b /app/data/b[1]*? rw,relatime - ext4 /dev/sda1 rw
' drive RUN_AS_UID=1000
TAB=$(printf '\t')
assert_has "tab decoded" "$CALLS" "-path /app/data/tab${TAB}name"
assert_has "glob characters escaped" "$CALLS" '-path /app/data/b\[1\]\*\?'
# A newline in a mount point cannot be listed one per line. Failing closed
# means nothing is handed over, not that the file is handed over by mistake.
CASE="mount point with a newline"
FIX_MOUNTINFO='601 600 0:50 /srv/c /app/data/x\012y rw,relatime - ext4 /dev/sda1 rw
' drive RUN_AS_UID=1000
assert_eq "account" "corescope" "$OUT"
assert_eq "startup continues" 0 "$RC"
assert_lacks "nothing handed over" "$CALLS" "chown"
assert_has "and said why" "$LOG" "has a newline in its name"
# ----- 16. a hand-over that could not complete is logged -------------------
CASE="chown refused (root_squash NFS)"
drive RUN_AS_UID=1000 STUB_LEFTOVER='/app/data
/app/data/meshcore.db'
assert_eq "account" "corescope" "$OUT"
assert_eq "startup continues" 0 "$RC"
assert_has "warning names the count" "$LOG" "WARNING: 2 path(s) in /app/data could not be given to uid 1000"
assert_has "and the paths" "$LOG" " /app/data/meshcore.db"
# ----- 17. the entrypoint runs the sequence through prepare_service_user ----
# The driver above tests the function; this pins that the entrypoint uses it
# and does not rebuild the sequence from the steps, in an order of its own.
CASE="entrypoint-go.sh"
ENTRYPOINT="$(dirname "$SCRIPT")/entrypoint-go.sh"
EP=$(cat "$ENTRYPOINT")
assert_has "calls prepare_service_user" "$EP" "prepare_service_user || exit 1"
assert_lacks "does not resolve the account itself" "$EP" "resolve_service_account"
assert_lacks "does not read .env itself" "$EP" "load_env_file"
assert_lacks "does not hand over paths itself" "$EP" "claim_service_paths"
# ----- report ---------------------------------------------------------------
echo "select-service-user: $PASS assertions passed, $FAIL failed"
[ "$FAIL" -eq 0 ] || exit 1