Add a test suite, a linter and a CI gate in front of the build (0.45.0)
The repo had no tests, no lint config, and a CI that went straight from push
to docker push. That is the reason F1 could ship: authorization lives in the
routers, each of 171 routes independently picks require_admin or
get_current_user, and nothing checked the choice was right.
670 tests, no Docker daemon needed. The app is driven through TestClient
without entering it as a context manager, which skips the lifespan — no
background loops, no socket — and conftest points DATA_DIR/STACKS_DIR at a
temp directory before anything is imported.
test_route_authorization.py is the load-bearing one. Rather than 171 implied
decisions it states the policy once — every route requires admin unless it is
listed in USER_READABLE or PUBLIC — and fails on any route that disagrees. A
new route defaults to admin, which is the safe direction; what it catches is a
route written with get_current_user that nobody weighed against "can this
return a credential". Writing the allowlist meant auditing all 53 user-readable
routes, which turned up one more leak: GET /api/templates/{id} returns a
template's env, and "save stack as template" snapshots the stack's real .env
into it. Now admin-only; the listing stays open.
test_agent_authorization.py pins the same invariant on the agent, where the
whole access model is one shared token declared per route and a single
forgotten Depends(verify_token) would hand over the host.
Both were checked by reintroducing the bug: re-opening /api/files/read fails
three tests with actionable messages, dropping a token guard fails two.
test_bundled_templates.py covers the 83 templates — parse, image per service,
.env.example in sync with what compose reads, every bind-mounted file actually
shipped, and no working default password. It found one on its first run:
authentik shipped PG_PASS=change-me and AUTHENTIK_SECRET_KEY=change-me against
a compose that marks both required, so the stack would have come up with a
known password instead of refusing to start. Fixed.
The rest ports the ad-hoc harnesses from 0.44.0 into permanent tests (crypto
round-trip incl. plaintext passthrough and key-loss handling, the browse
sandbox) and covers compose_service's slug/status/file handling and
secret_service's name validation.
ruff is configured as a floor, not a style bar: F, E9 and B only. Import
sorting is deliberately out — it is style, and enabling it would rewrite the
imports of nine files that have nothing else wrong. The 12 findings it did have
are fixed here (unused imports, an unused local, four raise-without-from that
were swallowing exception context).
CI now runs check (ruff, pytest, tsc) and only builds if it passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dk43rmEeRfYi5wsLDbmfyG
This commit is contained in:
@@ -15,14 +15,16 @@ as intuitive as Dockge, as capable as Portainer for Compose workflows.
|
||||
> (Auto-update) + Phase 23 (Secrets & configs) + Phase 24 (Design System v2)
|
||||
> complete.
|
||||
|
||||
## Upgrading to 0.44.0 — two defaults changed
|
||||
## Upgrading to 0.44.0 / 0.45.0 — two defaults changed
|
||||
|
||||
0.44.0 closes a privilege-escalation hole and tightens two defaults. Both
|
||||
changes can affect an existing install:
|
||||
|
||||
1. **The `user` role loses read access to secrets.** The file browser (page and
|
||||
`/api/files/*`), the host-path picker, the audit log, `GET /api/stacks/{id}/export`
|
||||
and a stack's `.env` are now admin-only. Previously any logged-in account
|
||||
`/api/files/*`), the host-path picker, the audit log, `GET /api/stacks/{id}/export`,
|
||||
a stack's `.env` and the template *detail* route (0.45.0 — "save stack as
|
||||
template" snapshots the stack's real `.env`) are now admin-only. The template
|
||||
*listing* stays open. Previously any logged-in account
|
||||
could download `stackpilot.db`, every `.env` and every `.secrets/*` file —
|
||||
and none of it was audit-logged. If you gave someone a `user` account so they
|
||||
could look at stacks, they still can; they just no longer get the
|
||||
@@ -457,11 +459,17 @@ most important ones:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|-----------------|--------------------|-------------------------------------------|
|
||||
| `SECRET_KEY` | _(auto, dev only)_ | JWT signing key — **set this in prod** |
|
||||
| `SECRET_KEY` | _(auto, persisted)_| JWT + at-rest encryption key (see below) |
|
||||
| `STACKS_DIR` | `/opt/stacks` | Where stack folders live (in-container) |
|
||||
| `DATA_DIR` | `/data` | SQLite DB + app data |
|
||||
| `CORS_ORIGINS` | localhost | Allowed API origins (comma separated) |
|
||||
|
||||
`SECRET_KEY` signs JWTs **and** derives the key that encrypts backup-destination
|
||||
credentials in the database. Leave it unset and one is generated and written to
|
||||
`${DATA_DIR}/secret_key` (mode 0600) on first start, so sessions and stored
|
||||
credentials survive restarts — that file is then part of your backup. Setting it
|
||||
explicitly always wins and nothing is written.
|
||||
|
||||
The host path for stacks is set via `STACKS_HOST_DIR` in `.env`, and it should
|
||||
be **the same path as `STACKS_DIR`** (`/opt/stacks` by default). Compose runs
|
||||
inside the backend container, so a stack's relative bind mounts (`./config`) are
|
||||
@@ -491,6 +499,37 @@ npm install
|
||||
npm run dev # http://localhost:5173
|
||||
```
|
||||
|
||||
### Tests & linting
|
||||
|
||||
Same three commands the CI runs — `build-and-push` only starts once they pass.
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
pip install -r requirements-dev.txt
|
||||
pytest # 670 tests, no Docker daemon needed
|
||||
ruff check .
|
||||
cd ../frontend && npx tsc --noEmit -p tsconfig.json
|
||||
```
|
||||
|
||||
The suite drives the app through `TestClient` **without** the lifespan, so it
|
||||
never opens a Docker socket and never starts the background loops; `conftest.py`
|
||||
points `DATA_DIR`/`STACKS_DIR` at a temp directory before anything is imported.
|
||||
|
||||
The load-bearing one is `tests/test_route_authorization.py`. Authorization lives
|
||||
in the routers — each of 171 routes independently picks `require_admin` or
|
||||
`get_current_user`, and nothing checked that the choice was right, which is how
|
||||
0.43.0 shipped a read-only role that could download the auth database. That file
|
||||
states the policy once — *every route requires admin unless it is listed* — and
|
||||
fails on any route that disagrees. Adding a route the `user` role may reach means
|
||||
adding it to `USER_READABLE` with a note on why it cannot return a credential.
|
||||
`tests/test_agent_authorization.py` does the same for the agent, where a single
|
||||
forgotten `Depends(verify_token)` would expose a whole host.
|
||||
|
||||
`tests/test_bundled_templates.py` covers the 83 shipped templates: each must
|
||||
parse, name an image per service, keep `.env.example` in sync with the variables
|
||||
compose actually reads, ship every file it bind-mounts, and never come with a
|
||||
working default password.
|
||||
|
||||
## API surface (Phase 1)
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user