feat(firewall): audit rule lifecycle changes
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -13,6 +13,7 @@ class FirewallLogObject implements \JsonSerializable, JsonDeserializable
|
||||
{
|
||||
public const RESULT_ALLOWED = 'allowed';
|
||||
public const RESULT_BLOCKED = 'blocked';
|
||||
public const RESULT_RECORDED = 'recorded';
|
||||
|
||||
public const EVENT_AUTH_FAILURE = 'auth_failure';
|
||||
public const EVENT_RATE_LIMIT = 'rate_limit';
|
||||
@@ -20,6 +21,9 @@ class FirewallLogObject implements \JsonSerializable, JsonDeserializable
|
||||
public const EVENT_SUSPICIOUS = 'suspicious';
|
||||
public const EVENT_RULE_MATCH = 'rule_match';
|
||||
public const EVENT_ACCESS_CHECK = 'access_check';
|
||||
public const EVENT_RULE_CREATED = 'rule_created';
|
||||
public const EVENT_RULE_DISABLED = 'rule_disabled';
|
||||
public const EVENT_RULE_REMOVED = 'rule_removed';
|
||||
|
||||
private ?string $id = null;
|
||||
private ?string $tenantId = null;
|
||||
|
||||
@@ -41,6 +41,9 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
||||
SecurityEvent::BRUTE_FORCE_DETECTED,
|
||||
SecurityEvent::RATE_LIMIT_EXCEEDED,
|
||||
SecurityEvent::SUSPICIOUS_ACTIVITY,
|
||||
SecurityEvent::FIREWALL_RULE_CREATED,
|
||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
||||
] as $event) {
|
||||
$this->events->listen(
|
||||
'core',
|
||||
|
||||
@@ -11,6 +11,9 @@ use KTXF\Event\SecurityEvent;
|
||||
|
||||
final class FirewallRuleManager
|
||||
{
|
||||
public const ORIGIN_MANUAL = 'manual';
|
||||
public const ORIGIN_AUTOMATIC = 'automatic';
|
||||
|
||||
public function __construct(
|
||||
private readonly FirewallStore $store,
|
||||
private readonly FirewallRuleCache $cache,
|
||||
@@ -30,7 +33,8 @@ final class FirewallRuleManager
|
||||
string $ipAddress,
|
||||
?string $reason,
|
||||
?string $createdBy,
|
||||
?int $durationSeconds = null
|
||||
?int $durationSeconds = null,
|
||||
string $origin = self::ORIGIN_MANUAL
|
||||
): FirewallRuleObject {
|
||||
$ipAddress = FirewallRuleValidator::ipAddress($ipAddress);
|
||||
FirewallRuleValidator::duration($durationSeconds);
|
||||
@@ -52,7 +56,8 @@ final class FirewallRuleManager
|
||||
$ipAddress,
|
||||
$reason ?? 'Blocked by administrator',
|
||||
$createdBy,
|
||||
$durationSeconds
|
||||
$durationSeconds,
|
||||
$origin
|
||||
);
|
||||
$this->publishIpEvent(SecurityEvent::IP_BLOCKED, $scope, $ipAddress, $reason);
|
||||
|
||||
@@ -63,7 +68,8 @@ final class FirewallRuleManager
|
||||
FirewallRuleScope $scope,
|
||||
string $ipAddress,
|
||||
?string $reason,
|
||||
?string $createdBy
|
||||
?string $createdBy,
|
||||
string $origin = self::ORIGIN_MANUAL
|
||||
): FirewallRuleObject {
|
||||
$ipAddress = FirewallRuleValidator::ipAddress($ipAddress);
|
||||
$rule = $this->create(
|
||||
@@ -72,7 +78,9 @@ final class FirewallRuleManager
|
||||
FirewallRuleObject::ACTION_ALLOW,
|
||||
$ipAddress,
|
||||
$reason ?? 'Allowed by administrator',
|
||||
$createdBy
|
||||
$createdBy,
|
||||
null,
|
||||
$origin
|
||||
);
|
||||
$this->publishIpEvent(SecurityEvent::IP_ALLOWED, $scope, $ipAddress, $reason);
|
||||
|
||||
@@ -83,7 +91,8 @@ final class FirewallRuleManager
|
||||
FirewallRuleScope $scope,
|
||||
string $cidr,
|
||||
?string $reason,
|
||||
?string $createdBy
|
||||
?string $createdBy,
|
||||
string $origin = self::ORIGIN_MANUAL
|
||||
): FirewallRuleObject {
|
||||
return $this->create(
|
||||
$scope,
|
||||
@@ -91,7 +100,9 @@ final class FirewallRuleManager
|
||||
FirewallRuleObject::ACTION_BLOCK,
|
||||
FirewallRuleValidator::cidr($cidr),
|
||||
$reason ?? 'Range blocked by administrator',
|
||||
$createdBy
|
||||
$createdBy,
|
||||
null,
|
||||
$origin
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,7 +111,8 @@ final class FirewallRuleManager
|
||||
string $fingerprint,
|
||||
?string $reason,
|
||||
?string $createdBy,
|
||||
?int $durationSeconds = null
|
||||
?int $durationSeconds = null,
|
||||
string $origin = self::ORIGIN_MANUAL
|
||||
): FirewallRuleObject {
|
||||
FirewallRuleValidator::duration($durationSeconds);
|
||||
$fingerprint = FirewallRuleValidator::deviceFingerprint($fingerprint);
|
||||
@@ -111,7 +123,8 @@ final class FirewallRuleManager
|
||||
$fingerprint,
|
||||
$reason ?? 'Device blocked by administrator',
|
||||
$createdBy,
|
||||
$durationSeconds
|
||||
$durationSeconds,
|
||||
$origin
|
||||
);
|
||||
|
||||
$event = new SecurityEvent(SecurityEvent::DEVICE_BLOCKED, ['device' => $fingerprint, 'reason' => $reason]);
|
||||
@@ -121,7 +134,7 @@ final class FirewallRuleManager
|
||||
return $rule;
|
||||
}
|
||||
|
||||
public function disable(FirewallRuleScope $scope, string $ruleId): bool
|
||||
public function disable(FirewallRuleScope $scope, string $ruleId, ?string $actorId = null): bool
|
||||
{
|
||||
$rule = $this->ownedRule($scope, $ruleId);
|
||||
if (!$rule) {
|
||||
@@ -131,11 +144,12 @@ final class FirewallRuleManager
|
||||
$rule->setEnabled(false);
|
||||
$this->store->depositRule($rule);
|
||||
$this->cache->invalidate();
|
||||
$this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_DISABLED, $rule, $actorId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function remove(FirewallRuleScope $scope, string $ruleId): bool
|
||||
public function remove(FirewallRuleScope $scope, string $ruleId, ?string $actorId = null): bool
|
||||
{
|
||||
$rule = $this->ownedRule($scope, $ruleId);
|
||||
if (!$rule) {
|
||||
@@ -144,6 +158,7 @@ final class FirewallRuleManager
|
||||
|
||||
$this->store->destroyRule($rule);
|
||||
$this->cache->invalidate();
|
||||
$this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_REMOVED, $rule, $actorId);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -155,8 +170,13 @@ final class FirewallRuleManager
|
||||
string $value,
|
||||
string $reason,
|
||||
?string $createdBy,
|
||||
?int $durationSeconds = null
|
||||
?int $durationSeconds = null,
|
||||
string $origin = self::ORIGIN_MANUAL
|
||||
): FirewallRuleObject {
|
||||
if (!in_array($origin, [self::ORIGIN_MANUAL, self::ORIGIN_AUTOMATIC], true)) {
|
||||
throw new \InvalidArgumentException("Invalid firewall rule origin: {$origin}");
|
||||
}
|
||||
|
||||
$rule = (new FirewallRuleObject())
|
||||
->setScope($scope->scope)
|
||||
->setTenantId($scope->tenantId)
|
||||
@@ -166,6 +186,7 @@ final class FirewallRuleManager
|
||||
->setReason($reason)
|
||||
->setCreatedBy($createdBy)
|
||||
->setCreatedAt(new \DateTimeImmutable())
|
||||
->setMetadata(['origin' => $origin])
|
||||
->setEnabled(true);
|
||||
|
||||
if ($durationSeconds !== null) {
|
||||
@@ -174,6 +195,7 @@ final class FirewallRuleManager
|
||||
|
||||
$this->store->depositRule($rule);
|
||||
$this->cache->invalidate();
|
||||
$this->publishLifecycleEvent(SecurityEvent::FIREWALL_RULE_CREATED, $rule);
|
||||
|
||||
return $rule;
|
||||
}
|
||||
@@ -195,4 +217,25 @@ final class FirewallRuleManager
|
||||
$event->setIpAddress($ipAddress)->setReason($reason)->setTenantId($scope->tenantId);
|
||||
$this->events->dispatch($event);
|
||||
}
|
||||
|
||||
private function publishLifecycleEvent(
|
||||
string $name,
|
||||
FirewallRuleObject $rule,
|
||||
?string $actorId = null
|
||||
): void
|
||||
{
|
||||
$event = new SecurityEvent($name, [
|
||||
'ruleId' => $rule->getId(),
|
||||
'ruleScope' => $rule->getScope(),
|
||||
'ruleType' => $rule->getType(),
|
||||
'ruleAction' => $rule->getAction(),
|
||||
'ruleValue' => $rule->getValue(),
|
||||
'reason' => $rule->getReason(),
|
||||
'origin' => $rule->getMetadata()['origin'] ?? self::ORIGIN_MANUAL,
|
||||
'expiresAt' => $rule->getExpiresAt()?->format(\DateTimeInterface::ATOM),
|
||||
]);
|
||||
$event->setTenantId($rule->getTenantId())
|
||||
->setIdentityId($actorId ?? $rule->getCreatedBy());
|
||||
$this->events->dispatch($event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +191,8 @@ class FirewallService
|
||||
$ipAddress,
|
||||
sprintf('Auto-blocked: %d failed auth attempts in %d seconds', $failureCount, $windowSeconds),
|
||||
null, // System-created
|
||||
$blockDuration
|
||||
$blockDuration,
|
||||
FirewallRuleManager::ORIGIN_AUTOMATIC
|
||||
);
|
||||
}
|
||||
|
||||
@@ -214,10 +215,10 @@ class FirewallService
|
||||
->setRequestPath($event->getRequestPath())
|
||||
->setRequestMethod($event->getRequestMethod())
|
||||
->setEventType($this->mapEventToLogType($event->getName()))
|
||||
->setResult($this->mapEventToResult($event->getName()))
|
||||
->setResult($this->mapEventToResult($event))
|
||||
->setRuleId($event->get('ruleId'))
|
||||
->setRuleScope($ruleScope)
|
||||
->setIdentityId($event->getUserId())
|
||||
->setIdentityId($event->getUserId() ?? $event->getIdentityId())
|
||||
->setTimestamp(new \DateTimeImmutable())
|
||||
->setMetadata($event->getData());
|
||||
|
||||
@@ -236,6 +237,9 @@ class FirewallService
|
||||
SecurityEvent::RATE_LIMIT_EXCEEDED => FirewallLogObject::EVENT_RATE_LIMIT,
|
||||
SecurityEvent::ACCESS_DENIED => FirewallLogObject::EVENT_RULE_MATCH,
|
||||
SecurityEvent::SUSPICIOUS_ACTIVITY => FirewallLogObject::EVENT_SUSPICIOUS,
|
||||
SecurityEvent::FIREWALL_RULE_CREATED => FirewallLogObject::EVENT_RULE_CREATED,
|
||||
SecurityEvent::FIREWALL_RULE_DISABLED => FirewallLogObject::EVENT_RULE_DISABLED,
|
||||
SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::EVENT_RULE_REMOVED,
|
||||
default => FirewallLogObject::EVENT_ACCESS_CHECK,
|
||||
};
|
||||
}
|
||||
@@ -243,11 +247,14 @@ class FirewallService
|
||||
/**
|
||||
* Map security event to result
|
||||
*/
|
||||
private function mapEventToResult(string $eventName): string
|
||||
private function mapEventToResult(SecurityEvent $event): string
|
||||
{
|
||||
return match ($eventName) {
|
||||
return match ($event->getName()) {
|
||||
SecurityEvent::AUTH_SUCCESS,
|
||||
SecurityEvent::ACCESS_GRANTED => FirewallLogObject::RESULT_ALLOWED,
|
||||
SecurityEvent::FIREWALL_RULE_CREATED,
|
||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
||||
SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::RESULT_RECORDED,
|
||||
default => FirewallLogObject::RESULT_BLOCKED,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,13 +66,21 @@ final class SystemFirewallRuleService
|
||||
public function disableRule(string $ruleId): bool
|
||||
{
|
||||
$this->requirePermission(self::PERMISSION_MANAGE);
|
||||
return $this->rules->disable(FirewallRuleScope::system(), $ruleId);
|
||||
return $this->rules->disable(
|
||||
FirewallRuleScope::system(),
|
||||
$ruleId,
|
||||
$this->identity->identifier()
|
||||
);
|
||||
}
|
||||
|
||||
public function removeRule(string $ruleId): bool
|
||||
{
|
||||
$this->requirePermission(self::PERMISSION_MANAGE);
|
||||
return $this->rules->remove(FirewallRuleScope::system(), $ruleId);
|
||||
return $this->rules->remove(
|
||||
FirewallRuleScope::system(),
|
||||
$ruleId,
|
||||
$this->identity->identifier()
|
||||
);
|
||||
}
|
||||
|
||||
private function requirePermission(string $permission): void
|
||||
|
||||
@@ -60,13 +60,13 @@ final class TenantFirewallRuleService
|
||||
public function disableRule(string $ruleId): bool
|
||||
{
|
||||
$this->requirePermission(self::PERMISSION_MANAGE);
|
||||
return $this->rules->disable($this->scope(), $ruleId);
|
||||
return $this->rules->disable($this->scope(), $ruleId, $this->identity->identifier());
|
||||
}
|
||||
|
||||
public function removeRule(string $ruleId): bool
|
||||
{
|
||||
$this->requirePermission(self::PERMISSION_MANAGE);
|
||||
return $this->rules->remove($this->scope(), $ruleId);
|
||||
return $this->rules->remove($this->scope(), $ruleId, $this->identity->identifier());
|
||||
}
|
||||
|
||||
private function scope(): FirewallRuleScope
|
||||
|
||||
@@ -26,6 +26,9 @@ class SecurityEvent extends Event
|
||||
public const IP_BLOCKED = 'security.ip.blocked';
|
||||
public const IP_ALLOWED = 'security.ip.allowed';
|
||||
public const DEVICE_BLOCKED = 'security.device.blocked';
|
||||
public const FIREWALL_RULE_CREATED = 'security.firewall.rule.created';
|
||||
public const FIREWALL_RULE_DISABLED = 'security.firewall.rule.disabled';
|
||||
public const FIREWALL_RULE_REMOVED = 'security.firewall.rule.removed';
|
||||
|
||||
private ?string $ipAddress = null;
|
||||
private ?string $deviceFingerprint = null;
|
||||
|
||||
@@ -196,6 +196,29 @@ class FirewallStoreTest extends TestCase
|
||||
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $logs[0]->getRuleScope());
|
||||
}
|
||||
|
||||
#[TestDox('Rule lifecycle audit persistence retains actor and origin')]
|
||||
public function testLifecycleAuditPersistence(): void
|
||||
{
|
||||
$log = (new FirewallLogObject())
|
||||
->setTenantId('tenant-a')
|
||||
->setEventType(FirewallLogObject::EVENT_RULE_CREATED)
|
||||
->setResult(FirewallLogObject::RESULT_RECORDED)
|
||||
->setRuleId('rule-456')
|
||||
->setRuleScope(FirewallRuleObject::SCOPE_TENANT)
|
||||
->setIdentityId('admin-a')
|
||||
->setTimestamp(new \DateTimeImmutable())
|
||||
->setMetadata(['origin' => FirewallRuleManager::ORIGIN_MANUAL]);
|
||||
$this->store->createLog($log);
|
||||
|
||||
$logs = $this->store->listLogs('tenant-a');
|
||||
|
||||
self::assertCount(1, $logs);
|
||||
self::assertSame(FirewallLogObject::EVENT_RULE_CREATED, $logs[0]->getEventType());
|
||||
self::assertSame(FirewallLogObject::RESULT_RECORDED, $logs[0]->getResult());
|
||||
self::assertSame('admin-a', $logs[0]->getIdentityId());
|
||||
self::assertSame(FirewallRuleManager::ORIGIN_MANUAL, $logs[0]->getMetadata()['origin']);
|
||||
}
|
||||
|
||||
private function rule(
|
||||
string $reason,
|
||||
string $scope,
|
||||
|
||||
@@ -28,13 +28,19 @@ final class CoreModuleTest extends TestCase
|
||||
$module->boot();
|
||||
$definitions = $registry->definitions();
|
||||
|
||||
self::assertCount(7, $definitions);
|
||||
self::assertCount(10, $definitions);
|
||||
self::assertSame(['core'], array_values(array_unique(array_column($definitions, 'module'))));
|
||||
self::assertSame(
|
||||
FirewallService::class,
|
||||
$registry->listeners(SecurityEvent::AUTH_FAILURE, DeliveryMode::Immediate)[0]->service,
|
||||
);
|
||||
foreach ([SecurityEvent::RATE_LIMIT_EXCEEDED, SecurityEvent::SUSPICIOUS_ACTIVITY] as $event) {
|
||||
foreach ([
|
||||
SecurityEvent::RATE_LIMIT_EXCEEDED,
|
||||
SecurityEvent::SUSPICIOUS_ACTIVITY,
|
||||
SecurityEvent::FIREWALL_RULE_CREATED,
|
||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
||||
] as $event) {
|
||||
$listeners = $registry->listeners($event, DeliveryMode::Deferred);
|
||||
self::assertCount(1, $listeners);
|
||||
self::assertSame(FirewallService::class, $listeners[0]->service);
|
||||
|
||||
@@ -10,6 +10,7 @@ use KTXC\Service\FirewallRuleManager;
|
||||
use KTXC\Service\FirewallRuleScope;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Event\SecurityEvent;
|
||||
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
@@ -106,4 +107,64 @@ class FirewallRuleManagerTest extends TestCase
|
||||
$manager->blockIp(FirewallRuleScope::tenant('tenant-a'), '203.0.113.10', null, 'admin');
|
||||
self::assertSame([], $cache->tenant('tenant-a'));
|
||||
}
|
||||
|
||||
#[TestDox('Rule creation emits complete lifecycle audit context')]
|
||||
public function testCreationAudit(): void
|
||||
{
|
||||
$this->store->method('findExactIpRule')->willReturn(null);
|
||||
$this->store->method('depositRule')->willReturnCallback(
|
||||
static function (FirewallRuleObject $rule): FirewallRuleObject {
|
||||
return $rule->setId('rule-123');
|
||||
}
|
||||
);
|
||||
$events = [];
|
||||
$this->events->expects($this->exactly(2))
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function (\KTXF\Event\Event $event) use (&$events): void {
|
||||
$events[$event->getName()] = $event;
|
||||
});
|
||||
|
||||
$this->manager->blockIp(
|
||||
FirewallRuleScope::tenant('tenant-a'),
|
||||
'203.0.113.10',
|
||||
'Repeated abuse',
|
||||
'admin-a',
|
||||
300
|
||||
);
|
||||
|
||||
$audit = $events[SecurityEvent::FIREWALL_RULE_CREATED];
|
||||
self::assertSame('rule-123', $audit->get('ruleId'));
|
||||
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $audit->get('ruleScope'));
|
||||
self::assertSame(FirewallRuleObject::TYPE_IP, $audit->get('ruleType'));
|
||||
self::assertSame(FirewallRuleObject::ACTION_BLOCK, $audit->get('ruleAction'));
|
||||
self::assertSame(FirewallRuleManager::ORIGIN_MANUAL, $audit->get('origin'));
|
||||
self::assertSame('admin-a', $audit->getIdentityId());
|
||||
self::assertNotNull($audit->get('expiresAt'));
|
||||
}
|
||||
|
||||
#[TestDox('Disable and remove audits identify the acting administrator')]
|
||||
public function testLifecycleActors(): void
|
||||
{
|
||||
$rule = (new FirewallRuleObject())
|
||||
->setId('rule-123')
|
||||
->setScope(FirewallRuleObject::SCOPE_SYSTEM)
|
||||
->setTenantId(null)
|
||||
->setType(FirewallRuleObject::TYPE_IP)
|
||||
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
||||
->setValue('203.0.113.10')
|
||||
->setCreatedBy('creator');
|
||||
$this->store->method('fetchRule')->willReturn($rule);
|
||||
$events = [];
|
||||
$this->events->expects($this->exactly(2))
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function (\KTXF\Event\Event $event) use (&$events): void {
|
||||
$events[$event->getName()] = $event;
|
||||
});
|
||||
|
||||
self::assertTrue($this->manager->disable(FirewallRuleScope::system(), 'rule-123', 'operator'));
|
||||
self::assertTrue($this->manager->remove(FirewallRuleScope::system(), 'rule-123', 'operator'));
|
||||
|
||||
self::assertSame('operator', $events[SecurityEvent::FIREWALL_RULE_DISABLED]->getIdentityId());
|
||||
self::assertSame('operator', $events[SecurityEvent::FIREWALL_RULE_REMOVED]->getIdentityId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,33 @@ class FirewallServiceTest extends TestCase
|
||||
$this->service->logSecurityEvent($event);
|
||||
}
|
||||
|
||||
#[TestDox('Rule lifecycle events map to recorded audit entries')]
|
||||
public function testRuleLifecycleAudit(): void
|
||||
{
|
||||
$this->currentTenant = null;
|
||||
$this->store->expects($this->once())
|
||||
->method('createLog')
|
||||
->with(self::callback(static function (FirewallLogObject $log): bool {
|
||||
return $log->getEventType() === FirewallLogObject::EVENT_RULE_DISABLED
|
||||
&& $log->getResult() === FirewallLogObject::RESULT_RECORDED
|
||||
&& $log->getRuleId() === 'rule-123'
|
||||
&& $log->getRuleScope() === FirewallRuleObject::SCOPE_SYSTEM
|
||||
&& $log->getIdentityId() === 'operator';
|
||||
}))
|
||||
->willReturnArgument(0);
|
||||
$event = new \KTXF\Event\SecurityEvent(
|
||||
\KTXF\Event\SecurityEvent::FIREWALL_RULE_DISABLED,
|
||||
[
|
||||
'ruleId' => 'rule-123',
|
||||
'ruleScope' => FirewallRuleObject::SCOPE_SYSTEM,
|
||||
'origin' => FirewallRuleManager::ORIGIN_MANUAL,
|
||||
]
|
||||
);
|
||||
$event->setIdentityId('operator');
|
||||
|
||||
$this->service->logSecurityEvent($event);
|
||||
}
|
||||
|
||||
#[TestDox('Typed tenant firewall settings drive brute-force thresholds')]
|
||||
public function testFirewallConfiguration(): void
|
||||
{
|
||||
@@ -380,17 +407,25 @@ class FirewallServiceTest extends TestCase
|
||||
->willReturnArgument(0);
|
||||
|
||||
$publishedTenants = [];
|
||||
$this->events->expects($this->exactly(2))
|
||||
$lifecycleOrigin = null;
|
||||
$this->events->expects($this->exactly(3))
|
||||
->method('dispatch')
|
||||
->willReturnCallback(static function (\KTXF\Event\Event $event) use (&$publishedTenants): void {
|
||||
->willReturnCallback(static function (\KTXF\Event\Event $event) use (
|
||||
&$publishedTenants,
|
||||
&$lifecycleOrigin
|
||||
): void {
|
||||
$publishedTenants[] = $event->getTenantId();
|
||||
if ($event->getName() === \KTXF\Event\SecurityEvent::FIREWALL_RULE_CREATED) {
|
||||
$lifecycleOrigin = $event->get('origin');
|
||||
}
|
||||
});
|
||||
|
||||
$event = \KTXF\Event\SecurityEvent::authFailure('203.0.113.10');
|
||||
$event->setTenantId('tenant-event');
|
||||
$this->service->handleAuthFailure($event);
|
||||
|
||||
self::assertSame(['tenant-event', 'tenant-event'], $publishedTenants);
|
||||
self::assertSame(['tenant-event', 'tenant-event', 'tenant-event'], $publishedTenants);
|
||||
self::assertSame(FirewallRuleManager::ORIGIN_AUTOMATIC, $lifecycleOrigin);
|
||||
}
|
||||
|
||||
#[TestDox('Authentication events without a tenant use the current tenant')]
|
||||
|
||||
Reference in New Issue
Block a user