c1cc781279
- argon2id (t=3, m=64MiB, p=2) PHC hashing honoring embedded params - token-bucket rate limiting (10/15min per IP+email) on login/register - opaque 32B session tokens in Mongo, 30-day sliding expiry, logout-all - CSRF double-submit cookie/header on authenticated mutations - bootstrap admin from env with forced first-login password change - requireAuth/requireRole middleware with disabled-account enforcement - OIDC code flow with PKCE: lazy discovery, account linking only on verified email, auto-created developer accounts - unit tests (RBAC matrix, CSRF, password, rate limiter) + integration suite covering the full auth matrix incl. an in-test fake IdP Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85 lines
4.9 KiB
Markdown
85 lines
4.9 KiB
Markdown
# Decisions
|
|
|
|
Spec-silent choices, recorded as required by the build instructions.
|
|
|
|
## Phase 1
|
|
|
|
- **Go toolchain 1.26.4** (latest stable; spec requires ≥ 1.22). Installed at
|
|
`/usr/local/go`.
|
|
- **Module name `bountyboard`** — no remote repository exists; a single-word
|
|
module keeps import paths short (`bountyboard/internal/...`).
|
|
- **Package in `internal/http` is named `httpx`** to avoid clashing with
|
|
stdlib `net/http` inside its own files. The directory stays `internal/http`
|
|
per spec §14; importers alias it (`httpx "bountyboard/internal/http"`).
|
|
- **`/readyz` uses a pluggable checker registry** (`AddReadinessCheck`):
|
|
required checks (Mongo, Phase 2) gate readiness with 503; optional checks
|
|
(atomizer / work performer, Phase 11) are reported as `degraded` but stay
|
|
200, matching §6's "non-fatal, reported".
|
|
- **Runtime image is `alpine:3.21`, not distroless** — busybox `wget` enables
|
|
the compose healthcheck without adding tooling, and `ca-certificates` is
|
|
needed for outbound HTTPS to Jira/ADO/YouTrack. Non-root user.
|
|
- **Config validation is strict and fail-fast**: `CREDENTIALS_ENC_KEY` is
|
|
mandatory at startup (base64, exactly 32 bytes) and `SESSION_SECRET` must be
|
|
≥ 16 chars, even before the features using them land — a misconfigured
|
|
deployment should die at boot, not at first use. All validation errors are
|
|
reported together via `errors.Join`.
|
|
- **Trusted-proxy resolution walks X-Forwarded-For right-to-left**, skipping
|
|
addresses inside `TRUSTED_PROXY_CIDRS`; the first untrusted address is the
|
|
client. A malformed entry stops the walk (everything left of it is
|
|
attacker-controllable). If the whole chain is trusted, the leftmost entry is
|
|
used. Headers are ignored entirely when the direct peer is untrusted.
|
|
- **ULID**: canonical 48-bit ms timestamp + 80-bit crypto randomness, no
|
|
intra-millisecond monotonicity (spec only needs sortability at ms
|
|
resolution; randomness makes collisions negligible).
|
|
- **`.env` loader semantics**: real environment variables win over `.env`
|
|
values; unquoted values support trailing ` # comment`; missing `.env` file
|
|
is not an error (containers receive env via compose `env_file`).
|
|
- **Makefile `seed`/`backup`/`restore` are failing stubs until Phase 12** so
|
|
the targets exist but cannot be mistaken for working.
|
|
|
|
## Phase 2
|
|
|
|
- **Integration tests reach Mongo via `docker-compose.test.yml`**, an explicit
|
|
test-only overlay publishing Mongo on `127.0.0.1:27017`. The normal compose
|
|
file still never publishes Mongo (§12); the spec's integration-test
|
|
requirement (§2.1) needs host access, and a loopback-only opt-in overlay is
|
|
the smallest hole. Each run uses a `bountyboard_test_<ulid>` database and
|
|
drops it on cleanup.
|
|
- **`UpdateVersioned` rejects updates that touch `version`** and merges
|
|
`updatedAt`/`$inc version` into the caller's operators; it distinguishes
|
|
`ErrNotFound` from `ErrVersionConflict` with a follow-up existence check.
|
|
- **File MIME type = sniffed (`http.DetectContentType`) unless inconclusive**
|
|
(`application/octet-stream`), in which case the client-declared type is
|
|
kept. A confident sniff overrides a lying declaration.
|
|
- **sha256 of uploads is recorded post-upload** via an update on `fs.files`
|
|
metadata (GridFS metadata must be supplied before streaming; the hash is
|
|
only known after).
|
|
- **Signed file tokens** are `base64url(exp || hmac-sha256(fileID|exp))` with
|
|
a key derived as `sha256("bountyboard/file-url/v1" + SESSION_SECRET)` so
|
|
file tokens can never collide with other uses of the session secret.
|
|
|
|
## Phase 3
|
|
|
|
- **OIDC linking requires `email_verified=true`.** An unverified IdP email
|
|
matching an existing local account is rejected (`oidc_email_unverified`)
|
|
instead of linking or creating a duplicate — linking on unverified email
|
|
would allow account takeover via a rogue IdP account.
|
|
- **OIDC provider discovery is lazy** (first login attempt, retried on
|
|
failure) so an unreachable IdP cannot block app startup.
|
|
- **CSRF protection applies to authenticated mutations only**; login and
|
|
register are pre-session and protected by rate limiting + SameSite=Lax.
|
|
The CSRF cookie is intentionally JS-readable (double-submit pattern).
|
|
- **Password change revokes all other sessions** of the user (recovery
|
|
semantics) but keeps the current one. `SetPassword` is deliberately not
|
|
version-checked: a password change must never lose an optimistic-locking
|
|
race against a profile edit.
|
|
- **Forced password change (`mustChange`)** is enforced in `requireAuth` via
|
|
a path allowlist (`me`, `logout`, `logout-all`, `change-password`).
|
|
- **Sessions slide at most once per hour** (refreshedAt watermark) instead of
|
|
updating expiry on every request.
|
|
- **Login rate-limit key is `clientIP|email`** (and `register|clientIP` for
|
|
registration), using the trusted-proxy-resolved IP.
|
|
- **Mongo runs with `--wiredTigerCacheSizeGB 0.5`** in compose — the target
|
|
host has 4 GB RAM; default cache (≈50% of RAM) caused a restart under the
|
|
integration-test load.
|