122 lines
7.6 KiB
Markdown
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 && 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</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>
|