diff --git a/.agents/docs/architecture/testing-strategy.md b/.agents/docs/architecture/testing-strategy.md index 338fbe34c..7a0b9f50e 100644 --- a/.agents/docs/architecture/testing-strategy.md +++ b/.agents/docs/architecture/testing-strategy.md @@ -23,6 +23,12 @@ of operations. | End-to-end | Playwright | `testing/src/specs/` | `pnpm test:e2e` | | Type-check | `tsc` | — | `pnpm lint` | +**Image-only changes are the one exception to "unit test and e2e test".** Nothing in either tier +runs a production image: the e2e suite starts each app's `dev:test` directly. A change confined to +a `Dockerfile`, a container entrypoint such as `apps/gateway/scripts/run.sh`, or +`docker-compose.yaml` therefore cannot get a test in this repo. Verify it against the built image +instead, and list the commands and their results in the PR. + There is no integration tier. `apps/api` unit tests mock the Prisma layer entirely; the only code path exercised against a real database is Playwright. diff --git a/apps/gateway/AGENTS.md b/apps/gateway/AGENTS.md index c2a8ecc89..8b49c34f2 100644 --- a/apps/gateway/AGENTS.md +++ b/apps/gateway/AGENTS.md @@ -82,6 +82,18 @@ state `apps/api` owns — so persisting either would only add a second copy that `db:push` before `dev`, `lint`, `test:e2e` and — via the `@opendatacapture/gateway#build` key in `turbo.json` — before `build`, so a gateway build needs `GATEWAY_DATABASE_URL` set. +**The production image starts as root on purpose and drops to `node` in `scripts/run.sh`.** The +gateway is the one service published straight to the internet, so its server must not run as root, +but compose bind-mounts `./sqlite` at `/app/sqlite`, and on deployments made before this change that +directory and `gateway.db` are owned by root. `run.sh`, while still root, creates the database +directory, `chown`s anything in it not owned by `node`, then re-executes itself through +`setpriv --reuid=node --regid=node --init-groups --no-new-privs`. That directory is the only path the +server writes; `/app/dist` stays root-owned. Do not add `--reset-env`: it would drop +`GATEWAY_API_KEY` and `GATEWAY_DATABASE_URL`. Started as any other user (`docker run --user`, a +Kubernetes `runAsUser`), `run.sh` exits with a message if the directory or the database file is not +writable, instead of failing on the first write. So `docker compose exec gateway id` still prints +root; check the server's uid with `docker compose exec gateway grep ^Uid /proc/1/status`. + ## Build and dev `pnpm build` is three passes into `dist/`: Vite client build (`dist/client`), Vite SSR build diff --git a/apps/gateway/scripts/run.sh b/apps/gateway/scripts/run.sh index a025e5706..a7d7d75b3 100755 --- a/apps/gateway/scripts/run.sh +++ b/apps/gateway/scripts/run.sh @@ -3,8 +3,26 @@ set -eu DATABASE_FILE=${GATEWAY_DATABASE_URL#file:} +DATABASE_DIR=$(dirname "$DATABASE_FILE") EMPTY_DATABASE_FILE="/app/gateway.tmpl.db" +# The gateway faces the public internet, so its server must not run as root. The image still starts +# as root: on existing deployments the bind-mounted database directory (and the database file the +# old root entrypoint copied into it) is owned by root, and only root can hand it to `node`. Fix the +# ownership of that directory, the only path the server writes, then re-run this script as `node`. +if [ "$(id -u)" = '0' ]; then + mkdir -p "$DATABASE_DIR" + find "$DATABASE_DIR" \! -user node -exec chown node:node '{}' + + exec setpriv --reuid=node --regid=node --init-groups --no-new-privs "$0" "$@" +fi + +# Started as another user (`docker run --user`, a Kubernetes `runAsUser`): nothing can fix the +# ownership here, so fail now rather than on the first write to the database. +if [ ! -w "$DATABASE_DIR" ] || { [ -e "$DATABASE_FILE" ] && [ ! -w "$DATABASE_FILE" ]; }; then + echo "error: the gateway database at '$DATABASE_FILE' is not writable by uid $(id -u). Make '$DATABASE_DIR' and its contents writable by uid $(id -u), or start the container as root so it can fix the ownership itself." >&2 + exit 1 +fi + if [ ! -s "$DATABASE_FILE" ]; then cp "$EMPTY_DATABASE_FILE" "$DATABASE_FILE" fi