|
|
@@ -106,6 +106,80 @@ final class AuditLogControllerTest extends AppTestCase
|
|
|
self::assertSame(401, $resp->getStatusCode());
|
|
|
}
|
|
|
|
|
|
+ public function testOversizedFilterRejected(): void
|
|
|
+ {
|
|
|
+ // SEC_REVIEW F31: free-form filter strings are bounded at 128 chars.
|
|
|
+ $token = $this->createToken(TokenKind::Admin, Role::Viewer);
|
|
|
+ $long = str_repeat('a', 200);
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?action=' . $long,
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(400, $resp->getStatusCode());
|
|
|
+ self::assertArrayHasKey('action', $this->decode($resp)['details']);
|
|
|
+
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?entity_type=' . $long,
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(400, $resp->getStatusCode());
|
|
|
+ self::assertArrayHasKey('entity_type', $this->decode($resp)['details']);
|
|
|
+
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?entity_id=' . $long,
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(400, $resp->getStatusCode());
|
|
|
+ self::assertArrayHasKey('entity_id', $this->decode($resp)['details']);
|
|
|
+
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?subject_kind=' . $long . '&subject_id=5',
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(400, $resp->getStatusCode());
|
|
|
+ self::assertArrayHasKey('subject', $this->decode($resp)['details']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testDeepOffsetRejected(): void
|
|
|
+ {
|
|
|
+ // SEC_REVIEW F31: pagination is capped at 10 000 offset rows so a
|
|
|
+ // Viewer can't force `LIMIT 200 OFFSET huge` deep scans.
|
|
|
+ $token = $this->createToken(TokenKind::Admin, Role::Viewer);
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?page=999999&page_size=200',
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(400, $resp->getStatusCode());
|
|
|
+ self::assertArrayHasKey('page', $this->decode($resp)['details']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testDeepOffsetAtBoundaryAccepted(): void
|
|
|
+ {
|
|
|
+ // Right at offset = MAX_OFFSET (10 000) is fine; one more page over
|
|
|
+ // is rejected.
|
|
|
+ $token = $this->createToken(TokenKind::Admin, Role::Viewer);
|
|
|
+ // page=51, page_size=200 → offset=10000 (boundary, allowed)
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?page=51&page_size=200',
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(200, $resp->getStatusCode());
|
|
|
+
|
|
|
+ // page=52, page_size=200 → offset=10200 (over boundary, rejected)
|
|
|
+ $resp = $this->request(
|
|
|
+ 'GET',
|
|
|
+ '/api/v1/admin/audit-log?page=52&page_size=200',
|
|
|
+ ['Authorization' => 'Bearer ' . $token],
|
|
|
+ );
|
|
|
+ self::assertSame(400, $resp->getStatusCode());
|
|
|
+ }
|
|
|
+
|
|
|
private function seedAudit(string $kind, ?string $actorId, string $action, string $type, string $id, string $details, string $when): void
|
|
|
{
|
|
|
$this->db->insert('audit_log', [
|