fix(firewall): account for authentication failures exactly once

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-30 23:01:38 -04:00
parent 7aa8a27b1b
commit a5c10e9b9b
9 changed files with 112 additions and 11 deletions
+24 -1
View File
@@ -339,6 +339,7 @@ class FirewallServiceTest extends TestCase
#[TestDox('Typed tenant firewall settings drive brute-force thresholds')]
public function testFirewallConfiguration(): void
{
$this->store->method('createLogOnce')->willReturn(true);
$this->currentConfiguration = (new TenantConfiguration())->jsonDeserialize([
'firewall' => [
'enabled' => true,
@@ -364,6 +365,7 @@ class FirewallServiceTest extends TestCase
#[TestDox('Unsafe numeric firewall settings fall back to safe defaults')]
public function testConfigurationBounds(): void
{
$this->store->method('createLogOnce')->willReturn(true);
$this->currentConfiguration = (new TenantConfiguration())->jsonDeserialize([
'firewall' => [
'maxAuthFailures' => 0,
@@ -384,11 +386,12 @@ class FirewallServiceTest extends TestCase
#[TestDox('Automatic blocks retain the tenant carried by the authentication event')]
public function testAutomaticBlockTenant(): void
{
$this->store->method('createLogOnce')->willReturn(true);
$this->currentTenant = 'tenant-context';
$this->store->expects($this->once())
->method('countRecentFailures')
->with('tenant-event', '203.0.113.10', 300)
->willReturn(4);
->willReturn(5);
$this->store->expects($this->once())
->method('findExactIpRule')
->with(
@@ -431,6 +434,7 @@ class FirewallServiceTest extends TestCase
#[TestDox('Authentication events without a tenant use the current tenant')]
public function testAutomaticBlockTenantFallback(): void
{
$this->store->method('createLogOnce')->willReturn(true);
$this->store->expects($this->once())
->method('countRecentFailures')
->with('tenant-a', '203.0.113.10', 300)
@@ -453,6 +457,25 @@ class FirewallServiceTest extends TestCase
);
}
#[TestDox('Repeated delivery of one authentication event is counted once')]
public function testAuthenticationFailureIdempotency(): void
{
$this->store->expects($this->exactly(2))
->method('createLogOnce')
->willReturnOnConsecutiveCalls(true, false);
$this->store->expects($this->once())
->method('countRecentFailures')
->with('tenant-a', '203.0.113.10', 300)
->willReturn(1);
$event = \KTXF\Event\SecurityEvent::authFailure('203.0.113.10');
$eventId = $event->getEventId();
$this->service->handleAuthFailure($event);
$this->service->handleAuthFailure($event);
self::assertSame($eventId, $event->getEventId());
}
private function rule(
string $id,
string $scope,