fix(06): revise gap plan security coverage and full gates

This commit is contained in:
Jakub Zych
2026-09-20 00:14:58 +02:00
parent a61c83e614
commit b20466d2e2

View File

@@ -7,6 +7,7 @@ depends_on: ["06-05"]
files_modified:
- fonoteka.go/plugins/golem15/fonoteka/routes.go
- fonoteka.go/plugins/golem15/fonoteka/routes_isolation_test.go
- .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md
autonomous: true
gap_closure: true
requirements: [HTTP-04]
@@ -16,6 +17,7 @@ must_haves:
- "An unauthenticated request to GET /api/v1/fonoteka/genres passes through inv_token, then throttle:fonoteka-api-token, then inv.scope:read; InvScope can no longer short-circuit before the limiter consumes the request"
- "The first 60 same-IP requests without credentials retain the PHP-compatible 401 Invalid token response, while request 61 returns HTTP 429 with exactly {\"message\":\"Too Many Attempts.\"}"
- "A valid personal token is still resolved before the limiter, so the fonoteka-api-token bucket can retain its tok:<id> key instead of collapsing valid credentials onto the IP fallback"
- "The phase security review covers Plan 06-06 and maps both newly identified rate-limit threats to the assembled-route regression and exact middleware-order invariant"
artifacts:
- path: "fonoteka.go/plugins/golem15/fonoteka/routes.go"
provides: "Correct live personal-token middleware declaration"
@@ -23,6 +25,9 @@ must_haves:
- path: "fonoteka.go/plugins/golem15/fonoteka/routes_isolation_test.go"
provides: "Assembled-route unauthenticated deny-path rate-limit regression"
contains: "TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited"
- path: ".planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md"
provides: "Final Phase 6 threat register and evidence map including Plan 06-06"
contains: "T-06-21"
key_links:
- from: "fonoteka.go/plugins/golem15/fonoteka/routes.go"
to: "summercms.go/surf/router.go"
@@ -32,13 +37,17 @@ must_haves:
to: "/api/v1/fonoteka/genres"
via: "surf.Assemble of the real golem15.user and golem15.fonoteka plugins followed by 61 same-IP requests"
pattern: "surf\.Assemble|/api/v1/fonoteka/genres"
- from: ".planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md"
to: "fonoteka.go/plugins/golem15/fonoteka/routes_isolation_test.go"
via: "T-06-21/T-06-22 proof entries naming the assembled-route regression and exact inv_token -> throttle -> inv.scope invariant"
pattern: "T-06-2[12].*TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited|inv_token.*throttle:fonoteka-api-token.*inv\.scope:read"
---
<objective>
Close the HTTP-04 verification gap by correcting the live personal-token genres middleware onion and proving the unauthenticated deny path consumes the 60/minute bucket.
Purpose: remove the rate-limit bypass that currently allows unlimited missing/invalid personal-token requests to reach InvScope's 401 path without incrementing the limiter.
Output: the exact `inv_token` -> `throttle:fonoteka-api-token` -> `inv.scope:read` declaration and an isolated assembled-router regression proving request 61 is denied with the PHP-compatible 429 body.
Output: the exact `inv_token` -> `throttle:fonoteka-api-token` -> `inv.scope:read` declaration, an isolated assembled-router regression proving request 61 is denied with the PHP-compatible 429 body, and a final security-review map for T-06-21/T-06-22.
</objective>
<execution_context>
@@ -55,6 +64,7 @@ Output: the exact `inv_token` -> `throttle:fonoteka-api-token` -> `inv.scope:rea
@.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-RESEARCH.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-SECURITY-REVIEW.md
@.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-02-SUMMARY.md
@.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-05-SUMMARY.md
@@ -100,7 +110,7 @@ From `fonoteka.go/plugins/golem15/fonoteka/plugin.go`:
Search production route declarations under `fonoteka.go/plugins/golem15/fonoteka` for any other route containing both `inv.scope:` and `throttle:`. Apply this same order only if another live production route exists in the current tree; do not change test-only boot probes, empty future groups, or unrelated route stacks speculatively.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && go test ./plugins/golem15/fonoteka -run 'TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited|TestFullRouteTableAuthGroupMutualExclusivity' -count=1 -short</automated>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go && go test ./plugins/golem15/fonoteka -run 'TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited|TestFullRouteTableAuthGroupMutualExclusivity' -count=1 -short && go vet ./... && go test ./... -short</automated>
</verify>
<acceptance_criteria>
- `routes.go` contains `surf.Use("inv_token", "throttle:fonoteka-api-token", "inv.scope:read")` on the live `/api/v1/fonoteka/genres` group and does not contain the old `inv_token`, `inv.scope:read`, `throttle:fonoteka-api-token` order.
@@ -108,10 +118,42 @@ From `fonoteka.go/plugins/golem15/fonoteka/plugin.go`:
- The test uses one fresh assembled handler and a stable explicit RemoteAddr for all 61 requests, with no `t.Parallel`, global handler, or shared limiter state.
- Requests 1-60 assert HTTP 401 plus trimmed body `{"error":"Invalid token"}`; request 61 asserts HTTP 429 plus raw body exactly `{"message":"Too Many Attempts."}`, `X-RateLimit-Limit: 60`, and `X-RateLimit-Remaining: 0`.
- The targeted command exits 0 under `-short`, proving the regression does not require Docker, Postgres, or shared test state.
- From the `fonoteka.go` repository root, `go vet ./...` and `go test ./... -short` both exit 0 before the plan can proceed to the security-review update or summary.
</acceptance_criteria>
<done>The live personal-token genres route consumes unauthenticated requests in the 60/minute bucket before InvScope short-circuits, while valid tokens remain resolvable to tok:<id>; an assembled-route regression fails if the middleware order is inverted again.</done>
</task>
<task type="auto">
<name>Task 2: Extend the Phase 6 security review through the gap closure</name>
<files>.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md</files>
<read_first>
.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md (existing verified scope, threat-register format, findings structure, accepted-risk count, and audit trail)
.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-06-PLAN.md (T-06-21/T-06-22 mitigations and Task 1 evidence contract)
.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VERIFICATION.md (failed HTTP-04 truth and required middleware order)
.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-REVIEW.md (CR-01 rate-limit bypass and valid-token keying constraint)
fonoteka.go/plugins/golem15/fonoteka/routes.go (executed exact middleware declaration)
fonoteka.go/plugins/golem15/fonoteka/routes_isolation_test.go (executed assembled-route 61-request regression)
</read_first>
<action>
After Task 1's code, regression, `go vet ./...`, and repository-wide `go test ./... -short` evidence are green, update `06-SECURITY-REVIEW.md` as the final documentation execution step. Extend the review header, introductory coverage statement, and Scope from Plans 06-01 through 06-05 to include gap-closure Plan 06-06. Preserve every existing threat, disposition, proof, accepted-risk rationale, and audit entry.
Add T-06-21 to the Threat Register as the unauthenticated personal-token rate-limit bypass mitigation. Its proof must name `TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited`, state that it drives 61 same-IP requests through the real handler returned by `surf.Assemble`, and record that requests 1-60 keep the 401 Invalid token response while request 61 receives the exact 429 response. Record the exact runtime-order invariant as source declaration `inv_token` -> `throttle:fonoteka-api-token` -> `inv.scope:read`; do not summarize it as merely "middleware reordered."
Add T-06-22 to the Threat Register as the valid-credential bucket-isolation mitigation. Its proof must state that `inv_token` remains before `throttle:fonoteka-api-token`, so `bouncer.Credential` is populated before the bucket key closure and valid credentials retain `tok:<id>` keying rather than collapsing onto the IP fallback. Add corresponding Findings by Threat prose for T-06-21/T-06-22 that cites the executed route and regression evidence. Update the closed-threat total and Security Audit Trail to reflect all 21 reviewed IDs (`T-06-01` through `T-06-18`, `T-06-21`, `T-06-22`, and `T-06-SC`) with zero open; do not create new accepted risks because both new threats are mitigated. Keep this documentation-only update in Task 2's separate atomic commit after Task 1's implementation-and-test commit.
</action>
<verify>
<automated>cd /media/nvme/dev/golem15/summercms.io/summercms/summercms.go && test "$(awk -F'|' '/^\| T-06-(21|22) / {c++} END {print c+0}' .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md)" -eq 2 && rg -n 'Plans 06-01 through 06-06|Plan 06-06' .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md && rg -n 'TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited|61 same-IP' .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md && rg -n 'inv_token.*throttle:fonoteka-api-token.*inv\.scope:read' .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md && rg -n 'T-06-22' .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md && rg -n 'tok:&lt;id&gt;|tok:<id>' .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md && rg -n '\| [0-9]{4}-[0-9]{2}-[0-9]{2} \| 21 \| 21 \| 0 \|' .planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-SECURITY-REVIEW.md</automated>
</verify>
<acceptance_criteria>
- The review Scope explicitly includes Plan 06-06, and its coverage statement includes T-06-21 and T-06-22 in addition to all previously reviewed threat IDs.
- T-06-21 is a mitigate row whose proof links the exact `inv_token` -> `throttle:fonoteka-api-token` -> `inv.scope:read` invariant to the real assembled-route 61-request regression and its 401-through-60/429-on-61 behavior.
- T-06-22 is a mitigate row whose proof states that `inv_token` remains before throttle so valid credentials preserve `tok:<id>` keying.
- Findings by Threat contains concrete evidence sections for both new threats; the review does not rely only on register-row prose.
- The status remains closed/verified with 21 total threats, 21 closed, zero open, and no new accepted risk.
</acceptance_criteria>
<done>The final Phase 6 security review covers Plan 06-06 and makes both the deny-path limiter regression and valid-token key-isolation invariant auditable under T-06-21/T-06-22.</done>
</task>
</tasks>
<threat_model>
@@ -134,23 +176,25 @@ From `fonoteka.go/plugins/golem15/fonoteka/plugin.go`:
<source_coverage_audit>
| Source | ID | Coverage | Status |
|--------|----|----------|--------|
| GOAL | Phase 6 success criterion 2 | Plans 06-01 through 06-05 provide the limiter/buckets; this plan repairs the failed live deny path | COVERED |
| REQ | HTTP-04 | Task 1 makes the live named bucket enforceable on unauthenticated traffic and adds regression proof | COVERED |
| GOAL | Phase 6 success criterion 2 | Plans 06-01 through 06-05 provide the limiter/buckets; Task 1 repairs the failed live deny path and Task 2 records its final security evidence | COVERED |
| REQ | HTTP-04 | Task 1 makes the live named bucket enforceable on unauthenticated traffic and adds regression proof; Task 2 maps the closed threats | COVERED |
| REQ | HTTP-03, HTTP-05, HTTP-06, HTTP-07, HTTP-08, HTTP-09 | Already satisfied by executed Plans 06-01 through 06-05; no verification gap requests further work | COVERED (existing) |
| RESEARCH | Common Pitfall 5 | Exact inv_token -> throttle -> InvScope order plus no-double-lookup assembled behavior | COVERED |
| CONTEXT | D-01, D-02, D-05, D-06, D-08 | Exact named bucket, 429 body, parameterized middleware, credential context, and InvScope ownership preserved in Task 1 | COVERED |
| RESEARCH | Common Pitfall 5 | Exact inv_token -> throttle -> InvScope order plus no-double-lookup assembled behavior in Task 1, with T-06-21/T-06-22 evidence recorded by Task 2 | COVERED |
| CONTEXT | D-01, D-02, D-05, D-06, D-08 | Exact named bucket, 429 body, parameterized middleware, credential context, and InvScope ownership preserved in Task 1 and audited in Task 2 | COVERED |
| CONTEXT | D-03, D-04, D-07, D-09 through D-18 | Already implemented by executed Plans 06-01 through 06-05 and not reopened by the authoritative gap | COVERED (existing) |
| CONTEXT | Deferred Ideas | Explicitly excluded from this gap plan | EXCLUDED |
</source_coverage_audit>
<verification>
From `fonoteka.go`, run `go test ./plugins/golem15/fonoteka -run 'TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited|TestFullRouteTableAuthGroupMutualExclusivity' -count=1 -short`, then `go test ./plugins/golem15/fonoteka/... -count=1 -short`. Confirm a source grep shows only the target live order for the personal-token genres route.
From `/media/nvme/dev/golem15/summercms.io/summercms/fonoteka.go`, run `go test ./plugins/golem15/fonoteka -run 'TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited|TestFullRouteTableAuthGroupMutualExclusivity' -count=1 -short`, then the commit-level gates `go vet ./...` and `go test ./... -short`. Confirm a source grep shows only the target live order for the personal-token genres route. Before commit and summary, verify `06-SECURITY-REVIEW.md` includes Plan 06-06, T-06-21/T-06-22, the named assembled-route test, the exact middleware invariant, valid-token `tok:<id>` preservation, and an audit-trail total of 21 closed / 0 open.
</verification>
<success_criteria>
- The live personal-token genres group declares `inv_token`, `throttle:fonoteka-api-token`, `inv.scope:read` in that exact order.
- A fresh assembled-router test proves requests 1-60 without credentials return the existing 401 body and request 61 from the same IP returns the exact PHP-compatible 429 body and exhausted-limit headers.
- The regression runs under `-short` with no database or external service and cannot inherit limiter counters from another test.
- `go vet ./...` and repository-wide `go test ./... -short` pass from the `fonoteka.go` repository root before commit/summary.
- `06-SECURITY-REVIEW.md` covers Plan 06-06 and closes T-06-21/T-06-22 with concrete links to the assembled-route regression, exact middleware order, and preserved `tok:<id>` keying.
- HTTP-04's verification blocker and REVIEW CR-01 are directly closed without changing unrelated warnings or deferred work.
</success_criteria>