|
|
@@ -78,16 +78,50 @@ final class RateLimitTest extends AppTestCase
|
|
|
self::assertSame('1', $resp->getHeaderLine('Retry-After'));
|
|
|
}
|
|
|
|
|
|
- public function testAdminRoutesNotRateLimited(): void
|
|
|
+ /**
|
|
|
+ * SEC_REVIEW F29. The /api/v1/admin/* group previously attached only
|
|
|
+ * tokenAuth → impersonation → auditContext, no rate limit. Combined
|
|
|
+ * with F30 (IP-search full-table scan), F31 (deep-offset audit), and
|
|
|
+ * F32 (N+1 enrichment), an authenticated Viewer (the OIDC default
|
|
|
+ * role) could drive arbitrarily expensive queries at unbounded rate.
|
|
|
+ * RateLimitMiddleware is now attached, so a burst against any admin
|
|
|
+ * endpoint must produce 429s once the per-token bucket drains.
|
|
|
+ */
|
|
|
+ public function testAdminRoutesAreRateLimited(): void
|
|
|
{
|
|
|
$admin = $this->createToken(TokenKind::Admin, role: Role::Admin);
|
|
|
- // Admin routes should never 429 even when smashed.
|
|
|
- for ($i = 0; $i < 50; $i++) {
|
|
|
- $resp = $this->request('GET', '/api/v1/admin/me', [
|
|
|
- 'Authorization' => 'Bearer ' . $admin,
|
|
|
- ]);
|
|
|
- self::assertNotSame(429, $resp->getStatusCode());
|
|
|
+ $headers = ['Authorization' => 'Bearer ' . $admin];
|
|
|
+
|
|
|
+ $statuses = [];
|
|
|
+ for ($i = 0; $i < 20; $i++) {
|
|
|
+ $statuses[] = $this->request('GET', '/api/v1/admin/me', $headers)->getStatusCode();
|
|
|
+ }
|
|
|
+
|
|
|
+ $accepted = count(array_filter($statuses, static fn (int $s): bool => $s === 200));
|
|
|
+ $limited = count(array_filter($statuses, static fn (int $s): bool => $s === 429));
|
|
|
+
|
|
|
+ // Capacity is 4 (refill=2, capacity=4 from setUp); the rest must 429.
|
|
|
+ self::assertSame(4, $accepted, 'capacity-bounded successes');
|
|
|
+ self::assertSame(16, $limited, 'remainder rate-limited');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * SEC_REVIEW F29 — heavier admin endpoints (the very ones called out
|
|
|
+ * by F30/F31/F32) are gated by the same per-token bucket so a Viewer
|
|
|
+ * cannot pound them at unbounded rate.
|
|
|
+ */
|
|
|
+ public function testAdminAuditLogIsRateLimitedPerToken(): void
|
|
|
+ {
|
|
|
+ $viewer = $this->createToken(TokenKind::Admin, role: Role::Viewer);
|
|
|
+ $headers = ['Authorization' => 'Bearer ' . $viewer];
|
|
|
+
|
|
|
+ $statuses = [];
|
|
|
+ for ($i = 0; $i < 20; $i++) {
|
|
|
+ $statuses[] = $this->request('GET', '/api/v1/admin/audit-log', $headers)->getStatusCode();
|
|
|
}
|
|
|
+
|
|
|
+ $limited = count(array_filter($statuses, static fn (int $s): bool => $s === 429));
|
|
|
+ self::assertGreaterThan(0, $limited, 'admin/audit-log must rate-limit');
|
|
|
}
|
|
|
|
|
|
/**
|