Files
summercms/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-10-PLAN.md
2026-09-20 16:14:21 +02:00

122 lines
7.6 KiB
Markdown

---
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: <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"
---
<objective>
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.
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.
</objective>
<execution_context>
@/home/jin/.codex/get-shit-done/workflows/execute-plan.md
@/home/jin/.codex/get-shit-done/templates/summary.md
</execution_context>
<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
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Emit and assert exact no-newline InvScope denial bodies</name>
<files>fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope.go, fonoteka.go/plugins/golem15/fonoteka/middleware/token_scope_test.go</files>
<behavior>
- 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.
</behavior>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go &amp;&amp; go test ./plugins/golem15/fonoteka/middleware -run 'TestInvScope' -count=1 -race -short &amp;&amp; go vet ./plugins/golem15/fonoteka/middleware &amp;&amp; go test ./plugins/golem15/fonoteka/... -count=1 -short</automated>
</verify>
<acceptance_criteria>
- `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.
</acceptance_criteria>
<done>InvScope's 401 and 403 are byte-identical to PHP TokenScope, with exact tests that fail on any trailing newline.</done>
</task>
</tasks>
<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>
<verification>
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.
</verification>
<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>
<output>
Create `.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-10-SUMMARY.md` when done.
</output>