7.6 KiB
7.6 KiB
phase: 06-http-routing-auth-groups-and-rate-limiting
plan: 10
type: execute
wave: 6
depends_on: ["06-06"]
files_modified:
- fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go
- fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go
autonomous: true
gap_closure: true
requirements: [HTTP-05, HTTP-06]
must_haves:
truths:
- "InvScope without a resolved user returns byte-exact 401 body {"error":"Invalid token"} with no trailing newline"
- "InvScope with a token missing the requested scope returns byte-exact 403 body {"error":"Missing required scope: "} with no trailing newline"
- "Tests compare raw recorder bytes directly and cannot hide whitespace with TrimSpace"
- "A correctly scoped token still reaches the next handler unchanged"
artifacts:
- path: fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go
provides: "InvScope 401/403 responses delegated to framework wire.WriteJSON"
- path: fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go
provides: "exact raw-byte assertions for both TokenScope denial branches"
key_links:
- from: fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go
to: summercms.go/wire/response.go
via: "both denial branches call wire.WriteJSON, the shared PHP-compatible no-newline writer"
pattern: "wire.WriteJSON"
Restore PHP byte parity for personal-token scope denial by using the shared no-newline JSON writer and making the 401/403 tests compare exact raw bytes.
Run exact InvScope tests under `-race`, then the full fonoteka plugin suite. Grep the production and test files to prove `json.NewEncoder` and TrimSpace are absent from the TokenScope path.
Purpose: the guard registry and response-convention contracts are only satisfied when TokenScope responses are byte-identical; json.Encoder.Encode adds a byte PHP does not send.
Output: InvScope delegated to wire.WriteJSON and exact-byte denial tests that cannot mask the regression.
<execution_context> @/home/jin/.codex/get-shit-done/workflows/execute-plan.md @/home/jin/.codex/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VERIFICATION.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-REVIEW.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-01-SUMMARY.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-03-SUMMARY.md Task 1: Emit and assert exact no-newline InvScope denial bodies fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go, fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go - No bouncer.User context -> 401, Content-Type application/json, raw body bytes exactly `{"error":"Invalid token"}`. - Resolved user with an ApiToken lacking `write` -> 403, Content-Type application/json, raw body bytes exactly `{"error":"Missing required scope: write"}`. - Both bodies have final byte `}` and contain no `\n` or `\r`; tests perform no trimming or whitespace normalization. - Resolved user plus a token containing the requested scope -> next handler runs and returns its original 204 response. fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go (current local json.Encoder writer) fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go (assertErrorBody currently hides the newline with strings.TrimSpace) fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_coverage_test.go (wrong-credential fail-closed path that reuses the assertion helper) summercms.go/wire/response.go (`wire.WriteJSON` exact behavior: SetEscapeHTML(false), trailing newline removed) /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/middleware/TokenScope.php (source-of-truth 401/403 payloads) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md (D-08 exact TokenScope bodies) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VERIFICATION.md (fourth authoritative gap) .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-REVIEW.md (WR-11) In `token_scope.go`, remove the local `writeJSON` helper and its `encoding/json` import. Import `git.golem15.com/golem15/summercms/wire` and call `wire.WriteJSON` in both denial branches with the existing statuses and exact `map[string]string` payloads. Do not change user/credential lookup, fail-closed wrong-credential behavior, scope text, status codes, or middleware ordering (D-08).In `token_scope_test.go`, remove `strings.TrimSpace` and compare `rec.Body.Bytes()` or `rec.Body.String()` directly to the expected literal. Keep JSON decoding only as a secondary shape/type assertion after the exact-byte comparison; it must not replace or normalize the wire assertion. Add explicit checks that neither denial body ends in newline/carriage-return. Ensure the named no-user 401 and missing-scope 403 subtests each make their own exact expected-body assertion. Keep the read-scope success case and the wrong-credential 403 coverage green.
cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && go test ./plugins/golem15/fonoteka/middleware -run 'TestInvScope' -count=1 -race -short && go vet ./plugins/golem15/fonoteka/middleware && go test ./plugins/golem15/fonoteka/... -count=1 -short
- `token_scope.go` imports and calls `wire.WriteJSON`; it contains no `json.NewEncoder` or local response writer.
- The 401 raw body equals exactly `{"error":"Invalid token"}` and the 403 raw body equals exactly `{"error":"Missing required scope: write"}`.
- `token_scope_test.go` contains no `TrimSpace`, `Trim`, or normalized-body comparison on either denial path.
- Both denial tests retain exact status and Content-Type assertions, while the valid-scope next-handler and wrong-credential fail-closed tests pass.
InvScope's 401 and 403 are byte-identical to PHP TokenScope, with exact tests that fail on any trailing newline.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| personal token context -> HTTP denial | Authentication/scope state crosses into a public wire response whose status and bytes are part of the PHP compatibility contract |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-06-27 | Tampering | InvScope 401/403 serialization |
mitigate | Use the established wire.WriteJSON implementation and raw-byte assertions for both branches; forbid trimming in the regression tests |
| T-06-SC | Tampering | package supply chain | accept | No install or manifest change; wire is an existing framework package already used by the phase |
| </threat_model> |
<success_criteria>
- Personal-token 401 and 403 bodies contain no trailing newline and match PHP bytes exactly.
- Tests compare raw bytes before optional JSON shape checks.
- Scope authorization and fail-closed credential behavior are unchanged.
- The fonoteka middleware and plugin suites pass. </success_criteria>