Repository navigation
fix: address #1103-#1109 and #1115 - #1121
Merged
Merged
Conversation
RateLimitMiddleware read the Gin key user_id, which no REST route sets, so every REST request skipped the per-user limiter. Key on userInternalUUID (the quota store key), falling back to userID. Fixes #1103 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Fixes #1103 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
The limited response reports 0 remaining, like the last allowed request. Refs #1103 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
… 400 POST /me/client_credentials documents no 409, but a store constraint or duplicate error returned one. Return 400 invalid_input instead, via a testable helper. Fixes #1105 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Refs #1105 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
The documented is_confidential query parameter was ignored. It now intersects with the reviewer-group scope from #1100: it can narrow the result but never widen it, and a value the scope excludes returns an empty page with total 0. Fixes #1106 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
WebSocket access checks passed nil groups, so a user whose reader or writer role came only from a group grant was denied (connect-time, mutation, heartbeat re-check, presenter reassignment). Ticket-authenticated upgrades also never set userGroups. Record the minting token's groups in the WebSocket ticket, set userGroups in the ticket validator, capture them on UserInfo and WebSocketClient, and pass them at all four checks. Groups are frozen for the socket's lifetime, like JWT claims on REST. Old tickets without groups decode to none (fail closed). Fixes #1107 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Session participant listings resolved other participants' permissions with the requester's request-context groups (or email-only when no context). Resolve each connected client with its own groups, and the requester with the request's groups. Also order the UserInfo literal as declared. Fixes #1107 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
… settings dbtool wrote system_settings rows directly, bypassing the Timmy validators added in #1102. Export api.ValidateSettingSemantics (the single key->validator map) and call it for --import-config and data-seed setting entries, validating every value before any write so nothing partial is stored. Fixes #1108 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Add the missing timmy.llm_max_tokens validator to the shared settings map so Set and dbtool reject what the startup Timmy check refuses, and validate before the --import-config dry-run exit so dry-run reports the same failures. Fixes #1108 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Fixes #1108 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
…ontain Refs #1108 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
…m fixture
CATS walks paths in lexical order, so DELETE on
/threat_models/{threat_model_id}/diagrams/{diagram_id} ran before every nested
diagram path and destroyed the only seeded diagram (run 20261018T072650Z,
test 202); every later test hit a deleted diagram. The reported 10 s PUT hang
did not reproduce (0 transport errors, max 1,007 ms), so it was most likely a
transient node/network stall.
Seed a throwaway diagram and point the exact diagram anchor at it. Same
failure class for threats, documents, assets, notes and repositories (each has
a DELETE plus nested metadata/restore/audit_trail paths), so they get decoys
too. Real fixtures are now resolved by name, not kind, and the fixture
manifest marks all six anchors decoyed.
Fixes #1115
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
DELETE /admin/surveys/{survey_id} sorts before its nested paths, the same
failure class as #1115. Seed a separate "CATS Throwaway Survey" with no
responses (the existing decoy survey carries the survey_response decoy, and
/admin/surveys sorts before /intake/survey_responses, so deleting it would
cascade that decoy away), resolve the global survey_id by name, and mark the
anchor decoyed in the fixture manifest. Also give the real seeded repository a
name so its ref is no longer empty-named.
Fixes #1115
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Renaming the seeded repository left its metadata target pointing at the old empty-named ref, so the live seed failed with an unresolved target_ref. Point the target at the renamed ref and add an offline test asserting every target_ref and *_ref in the seed file resolves to an earlier entry. Fixes #1115 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
cats-check-fixtures.py probed a hard-coded table of six anchors, so the decoyed diagram, threat, document, asset, note, repository and survey fixtures were never reported. Read the fixture manifest (--fixtures, default cats-fixtures.json) and fall back to the table only when it is missing; a malformed manifest is reported clearly rather than silently ignored. Fixes #1115 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Add enum [active, inactive, archived] to SurveyBase.status in the spec (x-go-type string keeps the Go field a *string) and validate status in POST and PUT /admin/surveys through the same validateSurveyStatus used by PATCH, so an unknown or oversize status returns 400 invalid_input naming the field instead of failing at the size:20 column. Fixes #1104 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Add enum [active, inactive, archived] to SurveyStatusQueryParam (x-go-type string keeps the generated param a string) so GET /admin/surveys?status= rejects unknown values with 400, and reword the is_confidential filter description. Reset the SEM marker on validatePatchedSurvey. Fixes #1104 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Add ValidatePatchOperationPaths, called from ParsePatchRequest (all 14 PATCH handlers), the diagram PATCH handler and bulk threat PATCH. Root or non-pointer paths and move/copy "from" values now return 400 invalid_input instead of bypassing exact-path prohibited-field checks. Fixes #1109 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Survey response and diagram PATCH handler tests, and assert the store is not invoked in TestPatchThreat/InvalidPatchOperations. Fixes #1109 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
JsonPatchDocument path pattern now requires a leading "/" (rejects the root ""), and move/copy "from" is declared (it was rejected by additionalProperties: false). Adds a request-validator test. Fixes #1109 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Add FindProhibitedPatchPath, which checks op.From for move/copy as well as the target and matches sub-paths. Use it in ValidatePatchProhibitedPaths and the project, team and survey handlers, and apply the read-only check in BulkPatchThreats. Fixes #1109 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
BulkPatchThreats validated inside the apply loop, so an invalid later entry returned 400 after earlier entries were committed. Validate every entry first, then apply. Also cover survey no-write and project/team prohibited from/sub-path cases. Fixes #1109 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
CheckOwnershipChanges now counts move/copy targets and a move source, the team and project note sharable guards use FindProhibitedPatchPath, and the survey status-change detection counts a move from /status. Fixes #1109 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Removing /status (remove, or move from it) silently dropped the change and returned 200; now 400. CheckOwnershipChanges also treats /owner/* sub-paths as owner changes. Fixes #1109 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
…a false positive
The seeded document is accessible, not pending_access, so every fuzzed
POST .../documents/{document_id}/request_access gets the documented 409
conflict. The rule matches only that operation, status and server
message (14 results in run 20261018T170611Z; no 5xx matched).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Eight bug fixes, combined into one PR. Each issue's commits are kept in order; the squash title decides the version bump (
fix:, so a PATCH bump; the spec changes in #1104 and #1109 also bump the schema version).Verification (combined branch)
make verify:verify: OK.make test-integration(PostgreSQL): 97 passed, 0 failed, 9 skipped; 8 packages passed.20261018T170611Z): valid. 110,703 requests; 0.07% connection errors; 0.13% unauthenticated; no 5xx other than the expected 501s from DummyTransferEncoding; no undocumented status codes; all 19 fixtures survived. The only true positives were the 14 request_access 409s that the new rule 104 classifies; after reclassification there are 0.#1103: Count authenticated REST requests in the per-user rate limiter
Summary
The per-user REST rate limiter read the Gin context key
user_id, which only the WebSocket handler sets, so it never counted a REST request. It now keys onuserInternalUUID(the key the quota store and seeded quotas use), falling back touserID.Client-visible change
Per-user limiting is enforced for the first time: authenticated responses carry
X-RateLimit-*headers, and a user over the limit gets 429 withRetry-After(documented on every operation).api/user_api_quota_store.go). Admins can raise a user's quota with/admin/quotas/users/{user_id}.Tests
TestRateLimitMiddleware_JWTContextKeysuses the real JWT context keys. All 3 subtests failed before the fix and pass after it.TestUserAPIRateLimiting_Integration(opt-in,TMI_TEST_ENABLE_RATE_LIMITING=true make test-integration). It logs in a real user through OAuth and sets that user's quota to 5/minute, then callsGET /threat_models.X-RateLimit-Limitheader.X-RateLimit-Remainingdecreasing, and request 6 gets 429 withRetry-Afterandrate_limit_exceeded. Suite: 100 passed, 0 failed.server.disable_rate_limiting=true, so CATS on k3s does not exercise this limiter.#1104: Restrict survey template status to active, inactive or archived
Summary
SurveyBase.statusallowed any string up to 128 characters, but the column issize:20and onlyactive,inactiveandarchivedare valid. POST and PUT/admin/surveyscopiedstatusthrough unvalidated, so a long value failed in the database.enum: [active, inactive, archived]on the field (still nullable) and on theGET /admin/surveys?status=filter.validateSurveyStatusis used by POST, PUT and PATCH. It returns 400invalid_inputnamingstatus.IsConfidentialQueryParam's stale "secret_project" description is corrected.x-go-type: stringkeeps the generated fields*string.Client-visible change
POST, PUT and PATCH with any other status, and
GET /admin/surveys?status=<other>, now return 400 (the GET previously returned 200 with an empty list).Tests
#1105: Return documented 400 for client credential create constraint errors
Summary
POST /me/client_credentialsmapped a DB constraint or duplicate error to 409, which the operation does not document. It now returns 400invalid_input. The only unique column,client_id, is server-generated, so no real conflict case exists.The sweep checked all 17 other
StatusConflicthandler sites, and every one belongs to an operation that documents 409. The shared store-error mapper's duplicate branch is tracked separately in #1118.Tests
TestRespondClientCredentialCreateError: the constraint and duplicate cases got 409 before the fix and get 400 after it.#1106: Apply the is_confidential filter to the triage survey response list
Summary
GET /triage/survey_responsesdocumentedis_confidentialbut ignored it. The parameter is now intersected with the caller's reviewer scope from #1100 and can never widen it:is_confidential=truegets 200 with an empty list.Handler-only change; not DB-touching.
Tests
#1107: Honor group-based access in collaboration sessions
Summary
Collaboration authorization passed
nilgroups at four sites:checkMutationPermissioncheckReadPermissionA user whose writer or reader role came only from a group grant was therefore refused in a collaboration session, although REST allowed them. The WebSocket ticket path also never carried groups.
groupsclaim. The ticket validator setsuserGroups, andWebSocketClient.Groupsholds them for the socket's lifetime, the same freeze semantics as JWT claims on REST.GetActiveSessionsForUserandGetActiveSessions) now resolves each participant's permissions with that participant's own groups, not the requester's.Tests
TestWebSocketGroupBasedAccess: a group-only writer can mutate, a group-only reader can read but not mutate, and no-group users are denied.TestBuildSessionParticipants_UsesParticipantGroups.#1108: Validate system settings in dbtool before writing them
Summary
tmi-dbtoolwrotesystem_settingsdirectly, bypassing the semantic validatorsSettingsService.Setuses (#1102).api.ValidateSettingSemanticsis now exported, so the rules stay in one table. dbtool validates every setting before its first write in--import-config(including--dry-run), in the data seed pre-pass, and inseedSetting.timmy.llm_max_tokensgains a validator matching the startup check, so all three keys the startup check validates are covered at runtime.Oracle review (
oracle-db-admin): APPROVED WITH NOTES. No SQL, model or write-order change. The non-transactional write loop and non-string seed values are tracked in #1117.Tests
TestRunConfigSeed_RejectsInvalidTimmySetting,TestRunConfigSeed_RejectsNegativeMaxTokensandTestRunConfigSeed_DryRunRejectsInvalideach failed before their fix.#1109: Reject root-path and prohibited-field JSON Patch operations
Summary
JSON Patch path and prohibited-field checks now live in shared helpers that every JSON Patch handler uses.
ValidatePatchOperationPathsrejects a root path (""), anypathnot starting with/, andmoveorcopywith such afrom. All 20 JSON Patch operations go through it, 15 viaParsePatchRequestand two directly (diagram PATCH and bulk threat PATCH).FindProhibitedPatchPathmatches by segment, checkspathand thefromof move and copy, and covers sub-paths. It replaces the per-handler exact-match loops for project, team, notes and surveys.CheckOwnershipChangesnow counts move and copy targets and sources, and/owner/...sub-paths./status, or moving it away, with 400 instead of returning 200 and dropping the change.JsonPatchDocumentpath pattern is now^(/[^/]*)+$. Afromproperty is declared (additionalProperties: falsepreviously rejected every move and copy).Client-visible change
Tests
#1115: Seed CATS decoys so DELETE does not consume nested-path fixtures
Summary
The hang: it didn't reproduce in isolation. A rerun of only
/threat_models/{threat_model_id}/diagrams/{diagram_id}on the k3s NodePort, with server logs captured (run20261018T072650Z), made 16,854 requests with 0 transport errors, a maximum response time of 1,007 ms, and no 5xx. The original timed-out request was one that request validation rejects in 152 ms or less.The real defect: the rerun exposed it. CATS walks paths in lexical order, so
DELETEon the diagram path ran at test 202 and consumed the only seeded diagram before every nested diagram path ran.TestCatsSeedRefsAllResolvenow checks every seed ref offline.scripts/cats-check-fixtures.pyreads the fixture manifest instead of a hard-coded list of 6 anchors (pytest: 10 passed).Verification
20261018T075612Z) passed the run-validity gates: 0 transport errors, 0 true positives.DOCUMENT_REQUEST_ACCESS_NOT_PENDING_409(rule 104): with the seeded document now reachable, every fuzzedPOST …/documents/{document_id}/request_accessgets the documented 409 because that document is not inpending_access. The rule matches only that operation, status and server message.Fixes #1103
Fixes #1104
Fixes #1105
Fixes #1106
Fixes #1107
Fixes #1108
Fixes #1109
Fixes #1115
🤖 Generated with Claude Code
https://claude.ai/code/session_015szS68rpMLaq712AFsbMfr