btlqql opened a new pull request, #4714:
URL: https://github.com/apache/rocketmq-dashboard/pull/4714

   <!-- Base branch: `rocketmq-studio`, the RocketMQ Studio trunk. -->
   
   ### Related issue
   
   No open issue covers this path; related prior work is #3045 (closed by 
#3046), which fixed two other
   fail-open paths of the same limiter and stated the invariant this change 
restores - "an active lock must
   remain in force until its lock duration expires".
   
   ### Brief Description
   
   `LoginRateLimiter` protects the login endpoint in two layers. The exact 
tracker holds at most
   `MAX_TRACKED_USERNAMES` usernames, and the overflow tracker 
(`OVERFLOW_BUCKET_COUNT` hash buckets)
   takes over for usernames that arrive while that tracker is full. Both of the 
limiter's expiry paths
   threw the *whole* overflow tracker away:
   
   - `activeExactState` (`LoginRateLimiter.java:159`) - `attempts.remove(key); 
clearOverflowAttempts();`
   - the lazy sweep in `reclaimExpiredExactAttemptsIfDue` 
(`LoginRateLimiter.java:178`) - `if (removed)
     clearOverflowAttempts();`
   
   Freeing one exact slot therefore also cancels every lock in the overflow 
tracker, and both of those
   paths run on the unauthenticated login endpoint: a caller who fills the 
exact tracker, waits for the
   first decoy's failure window to lapse, and then sends two new usernames has 
the tracker saturated
   again - with the overflow tracker empty. Any username that had earned a 
5-failure lock then answers
   `checkAllowed` with "allowed" for the rest of its `LOCK_DURATION`. That is 
attacker-triggered, cheap
   (one request per freed slot) and needs no credentials, and it contradicts 
the invariant the class
   documents for itself ("Collisions can share a lock only while the exact 
tracker is saturated; they
   cannot disable rate limiting.").
   
   The two paths now drop the overflow state of a finished episode only when no 
lock in it is still in
   force (`clearStaleOverflowAttempts`), so:
   
   - an active lock survives slot churn and stays enforced for its full 
`LOCK_DURATION`;
   - an episode's leftovers that carry no lock are still dropped, exactly as 
before, so stale overflow
     state cannot outlive the episode it belongs to;
   - the reset on a **successful** login (`recordSuccess`, which requires valid 
credentials) is unchanged.
   
   Nothing was weakened to make this pass: the change only ever *keeps* a 
rejection the base code already
   had, and the locked-state branch of `incrementFailure` is untouched, so a 
lock is still never extended
   by later failures. The one visible trade-off is stated for the record: a 
bucket lock is shared by
   hash collisions, so a colliding username stays rejected for the remainder of 
a lock whose duration
   overlaps - which is what #3045 asked for, since the alternative is a lock 
anybody can cancel.
   
   ### How Did You Test This Change?
   
   Three new tests in `LoginRateLimiterTest`, all driven by the existing 
`MutableClock` so the windows are
   exact. The two defect tests build the sequence above (saturate, lock 
`operator` in the overflow
   tracker while it still has ~4 minutes of `LOCK_DURATION` left, free a slot 
by expiry, take the freed
   slot again) and then assert the lock is still in force; the third is the 
control for the reset that
   must stay.
   
   Before the fix (red), on the unmodified tree:
   
   ```
   $ cd server && mvn -B -ntp test -Dtest=LoginRateLimiterTest
   [ERROR] Tests run: 15, Failures: 2, Errors: 0, Skipped: 0
   [ERROR] 
LoginRateLimiterTest.lookingUpAnExpiredUsernameMustNotReleaseAnotherUsersOverflowLockTest
 -- Time elapsed: 0.023 s <<< FAILURE!
   Expecting code to raise a throwable.
        at 
LoginRateLimiterTest.lookingUpAnExpiredUsernameMustNotReleaseAnotherUsersOverflowLockTest(LoginRateLimiterTest.java:277)
   [ERROR] 
LoginRateLimiterTest.refillingTheExactTrackerMustNotReleaseAnActiveOverflowLockTest
 -- Time elapsed: 0.007 s <<< FAILURE!
   Expecting code to raise a throwable.
        at 
LoginRateLimiterTest.refillingTheExactTrackerMustNotReleaseAnActiveOverflowLockTest(LoginRateLimiterTest.java:249)
   ```
   
   "Expecting code to raise a throwable" is the failure being reported: the 
assertion that
   `checkAllowed("operator")` still throws `429` did not throw, i.e. the base 
code allowed a locked
   username mid-lock. The control test 
(`freeingSlotStillDropsOverflowStateThatCarriesNoLockTest`) passed
   on the base tree, which is the point: it pins the behaviour that must not 
regress.
   
   After the fix (green) - the limiter plus the neighbouring login classes that 
construct it:
   
   ```
   $ cd server && mvn -B -ntp test 
-Dtest='LoginRateLimiterTest,AuthServiceDatabaseTest,AuthServiceTest,AuthControllerTest'
   [INFO] Tests run: 9, Failures: 0, Errors: 0, Skipped: 0 -- in 
org.apache.rocketmq.studio.auth.AuthControllerTest
   [INFO] Tests run: 27, Failures: 0, Errors: 0, Skipped: 0 -- in 
org.apache.rocketmq.studio.auth.AuthServiceDatabaseTest
   [INFO] Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 -- in 
org.apache.rocketmq.studio.auth.AuthServiceTest
   [INFO] Tests run: 15, Failures: 0, Errors: 0, Skipped: 0 -- in 
org.apache.rocketmq.studio.auth.LoginRateLimiterTest
   [INFO] Tests run: 67, Failures: 0, Errors: 0, Skipped: 0
   [INFO] BUILD SUCCESS
   [INFO] You have 0 Checkstyle violations.
   ```
   
   Note on the full suite: on a clean `rocketmq-studio` checkout `mvn -B -ntp 
test` already reports
   `Tests run: 3051, Failures: 6, Errors: 25, Skipped: 4`. The 11 red classes 
are the MySQL 8 backed
   Spring integration tests (`AuthServiceBootstrapIntegrationTest`,
   `AuthServiceConcurrencyIntegrationTest`, 
`AuthServiceSessionOverviewIntegrationTest`,
   `HealthProbeIntegrationTest`, `QueryHistoryServiceIntegrationTest`,
   `NativeAlertEvaluationTransactionTest`, 
`NotificationOutboxMapperIntegrationTest`,
   `RmqAlertStateMapperIntegrationTest`, `StudioApplicationTest`) plus the 
external-CLI ones
   (`CliAgentProviderTest`, `ClaudeCodeAgentProviderTest`). None of them are 
touched by this change; the
   tests here are plain unit tests with an injected clock and need no database.
   
   ### Checklist
   
   - [x] One coherent change; unrelated modifications are not bundled in
   - [x] Commit subject follows Conventional Commits (`fix:`)
   - [x] Tests added or updated for non-trivial changes, test methods named 
`...Test`
   - [x] New UI text has both Chinese and English entries under `web/src/i18n/` 
(no UI text in this change)
   - [x] Architecture constraints stay green (`mvn test` runs the ArchUnit 
checks)
   - [x] New source files carry the ASF license header (no new files)
   - [x] Documentation touched where behaviour changed (no user-visible 
contract change: a locked username keeps answering 429 for its full lock 
duration)
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to