Wire per-user identity into rate limit middleware#4718
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4718 +/- ##
==========================================
+ Coverage 68.82% 68.88% +0.05%
==========================================
Files 516 516
Lines 54153 54156 +3
==========================================
+ Hits 37272 37304 +32
+ Misses 14018 13989 -29
Partials 2863 2863 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
The core production change is correct and well-tested, but the hand-written mock added in this PR explicitly violates the project rule in .claude/rules/testing.md — worth a human look before merging.
Extended reasoning...
Overview\nThis PR fixes per-user rate limiting by wiring identity.Subject into rateLimitHandler (7 lines in middleware.go). The bulk of the diff is test infrastructure: hoisting the parameterized OIDC server into testutil, adding unit tests for identity passthrough, and a new E2E acceptance test for AC11/AC12.\n\n### Security risks\nRate limiting is security-adjacent but not auth itself. The change only reads an already-authenticated identity from context — it does not bypass or weaken any auth check. The fail-open behavior for missing identity (empty userID, skips per-user bucket) matches the documented design intent. No new security risks introduced.\n\n### Level of scrutiny\nThe production change is minimal and mechanical. However, the gomock convention violation (recordingLimiter hand-written mock in middleware_test.go) is an explicit project rule breach added in this PR — not grandfathered legacy. This warrants a human decision on whether to accept the deviation or require a generated mock before merging.\n\n### Other factors\nThe dead SendAuthenticatedToolCall export is a minor cleanliness issue in test helpers with no functional impact. Codecov reports all modified lines covered. No prior human or bot reviews on this PR.
jhrozek
left a comment
There was a problem hiding this comment.
Looks good -- the 4-line middleware change is clean and correct, identity extraction is idiomatic, unit tests cover both branches, and the E2E test infrastructure is solid. A few nits inline.
|
Thanks @jhrozek! All addressed in 8cf6cd8:
Re: the gomock convention — the |
|
looks like you need a rebase. |
Move DeployParameterizedOIDCServer and its Python script from the virtualmcp helpers into the shared testutil package so acceptance tests can reuse it for per-user rate limiting E2E tests. The virtualmcp package retains a thin wrapper that delegates to testutil for backwards compatibility. Part of #4550 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract identity.Subject from the auth context and pass it as the userID argument to limiter.Allow(). When no identity is present (unauthenticated requests), userID remains empty and per-user buckets are silently skipped. This connects PR 1's per-user limiter logic to the middleware so that per-user rate limits are enforced at runtime. Part of #4550 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deploy MCPServer with perUser rate limit (maxTokens=2) and inline OIDC pointing to the parameterized mock OIDC server. Verify: - user-a is rejected after exceeding per-user limit (HTTP 429) - JSON-RPC error -32029 with retryAfterSeconds in response body - user-b succeeds independently (per-user bucket isolation) Add SendAuthenticatedToolCall and GetOIDCToken helpers for authenticated E2E request patterns. Closes #4550 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Return Retry-After header from SendAuthenticatedToolCall and assert it is non-empty on 429 responses (AC12 was not actually verified) - Deploy Redis independently in per-user test block to avoid ordering dependency on the shared rate limit test's AfterAll cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The per-user and shared rate limit test blocks run in parallel in Ginkgo, so both calling DeployRedis causes an "already exists" conflict. Add EnsureRedis that checks for an existing deployment before creating, and use it in the per-user block. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both Describe blocks run their BeforeAll concurrently in Ginkgo, so both must use the idempotent EnsureRedis instead of DeployRedis to avoid "already exists" conflicts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ginkgo runs top-level Ordered Describe blocks with concurrent BeforeAll, so both rate limit test blocks race to create Redis. Replace the check-then-create pattern (TOCTOU) with create that tolerates AlreadyExists on both Deployment and Service. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
With OIDC auth enabled, the proxy enforces MCP session lifecycle — tools/call requires a prior initialize handshake. Add SendInitialize helper that returns the Mcp-Session-Id, and SendAuthenticatedToolCallWithSession that includes both Bearer token and session ID headers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restructure into a single Ordered Describe with nested Context blocks for shared and per-user tests. Redis is deployed once in the outer BeforeAll and cleaned up once in the outer AfterAll. This eliminates the concurrent BeforeAll race and ensures Redis is available before either MCPServer is created. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove dead SendAuthenticatedToolCall helper — superseded by SendAuthenticatedToolCallWithSession which handles the no-session case when sessionID is empty - Accept context.Context in all HTTP test helpers (SendToolCall, SendInitialize, SendAuthenticatedToolCallWithSession, GetOIDCToken) so the framework can cancel in-flight requests on suite timeout - Add comment documenting the empty-userID design assumption: when no identity is present, only shared rate limits apply and CEL validation is the primary gate ensuring perUser requires auth Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8cf6cd8 to
026252d
Compare
Summary
The rate limit middleware was passing an empty string for userID, meaning per-user buckets (added in #4704) were never checked. This wires
identity.Subjectfrom the auth context into the limiter so per-user rate limits are enforced at runtime.auth.IdentityFromContext()inrateLimitHandlerand passidentity.Subjectas the userID tolimiter.Allow()virtualmcp/helpers.gointo the sharedtestutilpackage for reuse across test suitesperUserrate limit + inline OIDC auth, verify user-a is rejected after limit, user-b succeeds independentlyCloses #4550
Type of change
Test plan
task test)task lint-fix)task test-e2e) — per-user rate limit with mock OIDC authChanges
pkg/ratelimit/middleware.gopkg/ratelimit/middleware_test.gotest/e2e/thv-operator/testutil/oidc.gotest/e2e/thv-operator/virtualmcp/helpers.gotest/e2e/thv-operator/acceptance_tests/helpers.gotest/e2e/thv-operator/acceptance_tests/ratelimit_test.goDoes this introduce a user-facing change?
Per-user rate limits configured via
rateLimiting.perUserare now enforced at runtime. Previously the CRD field was accepted but the middleware did not use the authenticated user's identity for rate limiting.Special notes for reviewers
middleware.go— the identity extraction and passthrough. Everything else is tests and test infrastructure.userIDremains empty and per-user buckets are silently skipped. This is the correct behavior: per-user limits require auth, and the CEL rule + reconciler condition from PR 1 enforce that.virtualmcpnow delegates totestutil.DeployParameterizedOIDCServer.Generated with Claude Code