feat: make event state constructor-only and immutable

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-08-05 22:46:33 -04:00
parent ba4deccea9
commit 52afd35d6f
8 changed files with 266 additions and 193 deletions
+104 -111
View File
@@ -9,7 +9,7 @@ use KTXF\Event\Event;
/**
* Security-specific event for authentication and access control events
*/
class SecurityEvent extends Event
final class SecurityEvent extends Event
{
// Event names
public const AUTH_SUCCESS = 'security.auth.success';
@@ -35,15 +35,6 @@ class SecurityEvent extends Event
public const FIREWALL_RULE_REMOVED = 'security.firewall.rule.removed';
public const FIREWALL_SETTINGS_UPDATED = 'security.firewall.settings.updated';
private ?string $ipAddress = null;
private ?string $deviceFingerprint = null;
private ?string $userAgent = null;
private ?string $requestPath = null;
private ?string $requestMethod = null;
private ?string $userId = null;
private ?string $reason = null;
private int $severity = self::SEVERITY_INFO;
// Severity levels
public const SEVERITY_DEBUG = 0;
public const SEVERITY_INFO = 1;
@@ -51,11 +42,25 @@ class SecurityEvent extends Event
public const SEVERITY_ERROR = 3;
public const SEVERITY_CRITICAL = 4;
public function __construct(string $name, array $data = [])
{
parent::__construct($name, $data);
private readonly int $severity;
$this->severity = self::getSeverityForEvent($name);
public function __construct(
string $name,
array $data = [],
?string $tenantId = null,
?string $identityId = null,
private readonly ?string $ipAddress = null,
private readonly ?string $deviceFingerprint = null,
private readonly ?string $userAgent = null,
private readonly ?string $requestPath = null,
private readonly ?string $requestMethod = null,
private readonly ?string $userId = null,
private readonly ?string $reason = null,
?int $severity = null,
) {
parent::__construct($name, $data, $tenantId, $identityId);
$this->severity = $severity ?? self::getSeverityForEvent($name);
}
/**
@@ -65,13 +70,30 @@ class SecurityEvent extends Event
string $name,
?string $ipAddress = null,
?string $deviceFingerprint = null,
array $data = []
array $data = [],
?string $tenantId = null,
?string $identityId = null,
?string $userAgent = null,
?string $requestPath = null,
?string $requestMethod = null,
?string $userId = null,
?string $reason = null,
?int $severity = null,
): self {
$event = new self($name, $data);
$event->ipAddress = $ipAddress;
$event->deviceFingerprint = $deviceFingerprint;
return $event;
return new self(
$name,
$data,
$tenantId,
$identityId,
$ipAddress,
$deviceFingerprint,
$userAgent,
$requestPath,
$requestMethod,
$userId,
$reason,
$severity,
);
}
/**
@@ -81,15 +103,26 @@ class SecurityEvent extends Event
string $ipAddress,
?string $deviceFingerprint = null,
?string $userId = null,
?string $reason = null
?string $reason = null,
?string $tenantId = null,
?string $identityId = null,
?string $userAgent = null,
?string $requestPath = null,
?string $requestMethod = null,
): self {
$event = self::create(self::AUTH_FAILURE, $ipAddress, $deviceFingerprint, [
'userId' => $userId,
'reason' => $reason,
]);
$event->userId = $userId;
$event->reason = $reason;
return $event;
return self::create(
self::AUTH_FAILURE,
$ipAddress,
$deviceFingerprint,
['userId' => $userId, 'reason' => $reason],
$tenantId,
$identityId,
$userAgent,
$requestPath,
$requestMethod,
$userId,
$reason,
);
}
/**
@@ -98,13 +131,17 @@ class SecurityEvent extends Event
public static function authSuccess(
string $ipAddress,
?string $deviceFingerprint = null,
string $userId = null
?string $userId = null,
?string $tenantId = null,
): self {
$event = self::create(self::AUTH_SUCCESS, $ipAddress, $deviceFingerprint, [
'userId' => $userId,
]);
$event->userId = $userId;
return $event;
return self::create(
self::AUTH_SUCCESS,
$ipAddress,
$deviceFingerprint,
['userId' => $userId],
tenantId: $tenantId,
userId: $userId,
);
}
/**
@@ -113,18 +150,16 @@ class SecurityEvent extends Event
public static function bruteForceDetected(
string $ipAddress,
int $failureCount,
int $windowSeconds
int $windowSeconds,
?string $tenantId = null,
): self {
$event = self::create(self::BRUTE_FORCE_DETECTED, $ipAddress, null, [
'failureCount' => $failureCount,
'windowSeconds' => $windowSeconds,
]);
$event->reason = sprintf(
'%d failed attempts in %d seconds',
$failureCount,
$windowSeconds
return self::create(
self::BRUTE_FORCE_DETECTED,
$ipAddress,
data: ['failureCount' => $failureCount, 'windowSeconds' => $windowSeconds],
tenantId: $tenantId,
reason: sprintf('%d failed attempts in %d seconds', $failureCount, $windowSeconds),
);
return $event;
}
/**
@@ -134,20 +169,21 @@ class SecurityEvent extends Event
string $ipAddress,
int $requestCount,
int $windowSeconds,
?string $endpoint = null
?string $endpoint = null,
?string $tenantId = null,
): self {
$event = self::create(self::RATE_LIMIT_EXCEEDED, $ipAddress, null, [
'requestCount' => $requestCount,
'windowSeconds' => $windowSeconds,
'endpoint' => $endpoint,
]);
$event->requestPath = $endpoint;
$event->reason = sprintf(
'%d requests in %d seconds',
$requestCount,
$windowSeconds
return self::create(
self::RATE_LIMIT_EXCEEDED,
$ipAddress,
data: [
'requestCount' => $requestCount,
'windowSeconds' => $windowSeconds,
'endpoint' => $endpoint,
],
tenantId: $tenantId,
requestPath: $endpoint,
reason: sprintf('%d requests in %d seconds', $requestCount, $windowSeconds),
);
return $event;
}
/**
@@ -158,15 +194,19 @@ class SecurityEvent extends Event
?string $deviceFingerprint = null,
?string $ruleId = null,
?string $ruleScope = null,
?string $reason = null
?string $reason = null,
?string $tenantId = null,
?string $identityId = null,
): self {
$event = self::create(self::ACCESS_DENIED, $ipAddress, $deviceFingerprint, [
'ruleId' => $ruleId,
'ruleScope' => $ruleScope,
'reason' => $reason,
]);
$event->reason = $reason;
return $event;
return self::create(
self::ACCESS_DENIED,
$ipAddress,
$deviceFingerprint,
['ruleId' => $ruleId, 'ruleScope' => $ruleScope, 'reason' => $reason],
$tenantId,
$identityId,
reason: $reason,
);
}
/**
@@ -202,86 +242,39 @@ class SecurityEvent extends Event
return $this->ipAddress;
}
public function setIpAddress(?string $ipAddress): self
{
$this->ipAddress = $ipAddress;
return $this;
}
public function getDeviceFingerprint(): ?string
{
return $this->deviceFingerprint;
}
public function setDeviceFingerprint(?string $deviceFingerprint): self
{
$this->deviceFingerprint = $deviceFingerprint;
return $this;
}
public function getUserAgent(): ?string
{
return $this->userAgent;
}
public function setUserAgent(?string $userAgent): self
{
$this->userAgent = $userAgent;
return $this;
}
public function getRequestPath(): ?string
{
return $this->requestPath;
}
public function setRequestPath(?string $requestPath): self
{
$this->requestPath = $requestPath;
return $this;
}
public function getRequestMethod(): ?string
{
return $this->requestMethod;
}
public function setRequestMethod(?string $requestMethod): self
{
$this->requestMethod = $requestMethod;
return $this;
}
public function getUserId(): ?string
{
return $this->userId;
}
public function setUserId(?string $userId): self
{
$this->userId = $userId;
return $this;
}
public function getReason(): ?string
{
return $this->reason;
}
public function setReason(?string $reason): self
{
$this->reason = $reason;
return $this;
}
public function getSeverity(): int
{
return $this->severity;
}
public function setSeverity(int $severity): self
{
$this->severity = $severity;
return $this;
}
}
+31 -18
View File
@@ -254,8 +254,13 @@ final class FirewallRuleManager
$origin
);
$event = new SecurityEvent(SecurityEvent::DEVICE_BLOCKED, ['device' => $fingerprint, 'reason' => $reason]);
$event->setDeviceFingerprint($fingerprint)->setReason($reason)->setTenantId($scope->tenantId);
$event = new SecurityEvent(
SecurityEvent::DEVICE_BLOCKED,
['device' => $fingerprint, 'reason' => $reason],
tenantId: $scope->tenantId,
deviceFingerprint: $fingerprint,
reason: $reason,
);
$this->events->dispatch($event);
return $rule;
@@ -499,8 +504,13 @@ final class FirewallRuleManager
string $ipAddress,
?string $reason
): void {
$event = new SecurityEvent($name, ['ip' => $ipAddress, 'reason' => $reason]);
$event->setIpAddress($ipAddress)->setReason($reason)->setTenantId($scope->tenantId);
$event = new SecurityEvent(
$name,
['ip' => $ipAddress, 'reason' => $reason],
tenantId: $scope->tenantId,
ipAddress: $ipAddress,
reason: $reason,
);
$this->events->dispatch($event);
}
@@ -511,20 +521,23 @@ final class FirewallRuleManager
array $change = []
): 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),
...($rule->getMetadata() ?? []),
...$change,
]);
$event->setTenantId($rule->getTenantId())
->setIdentityId($actorId ?? $rule->getCreatedBy());
$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),
...($rule->getMetadata() ?? []),
...$change,
],
tenantId: $rule->getTenantId(),
identityId: $actorId ?? $rule->getCreatedBy(),
);
$this->events->dispatch($event);
}
}
+8 -5
View File
@@ -139,7 +139,6 @@ class FirewallService
return;
}
$event->setTenantId($tenantId);
$log = $this->securityLog($event);
if ($log === null || !$this->store->createLogOnce($log)) {
return;
@@ -195,8 +194,12 @@ class FirewallService
int $blockDuration
): void {
// Publish brute force event
$event = SecurityEvent::bruteForceDetected($ipAddress, $failureCount, $windowSeconds);
$event->setTenantId($tenantId);
$event = SecurityEvent::bruteForceDetected(
$ipAddress,
$failureCount,
$windowSeconds,
$tenantId,
);
$this->events->dispatch($event);
$this->rules->blockIp(
@@ -308,9 +311,9 @@ class FirewallService
$deviceFingerprint,
$rule->getId(),
$rule->getScope(),
$rule->getReason()
$rule->getReason(),
$this->tenantContext->identifier(),
);
$event->setTenantId($this->tenantContext->identifier());
$this->events->dispatch($event);
}
+11 -7
View File
@@ -52,13 +52,17 @@ final class FirewallSettingsService
$tenant->setConfiguration($configuration);
$this->tenants->deposit($tenant);
$event = new SecurityEvent(SecurityEvent::FIREWALL_SETTINGS_UPDATED, [
'changeReason' => $reason,
'changeOrigin' => FirewallRuleManager::ORIGIN_MANUAL,
'previous' => $previous,
'current' => $current,
]);
$event->setTenantId($tenantId)->setIdentityId($actorId);
$event = new SecurityEvent(
SecurityEvent::FIREWALL_SETTINGS_UPDATED,
[
'changeReason' => $reason,
'changeOrigin' => FirewallRuleManager::ORIGIN_MANUAL,
'previous' => $previous,
'current' => $current,
],
tenantId: $tenantId,
identityId: $actorId,
);
$this->events->dispatch($event);
return $current;