Initial Version
This commit is contained in:
146
shared/lib/Event/Event.php
Normal file
146
shared/lib/Event/Event.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Event;
|
||||
|
||||
/**
|
||||
* Base event class for the event bus system
|
||||
*/
|
||||
class Event
|
||||
{
|
||||
private bool $propagationStopped = false;
|
||||
private array $data = [];
|
||||
private float $timestamp;
|
||||
private ?string $tenantId = null;
|
||||
private ?string $identityId = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
array $data = []
|
||||
) {
|
||||
$this->data = $data;
|
||||
$this->timestamp = microtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the event name
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a data value by key
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->data[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a data value
|
||||
*/
|
||||
public function set(string $key, mixed $value): self
|
||||
{
|
||||
$this->data[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a data key exists
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return array_key_exists($key, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all data
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for getData() for backward compatibility
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the event timestamp
|
||||
*/
|
||||
public function getTimestamp(): float
|
||||
{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop event propagation to subsequent listeners
|
||||
*/
|
||||
public function stopPropagation(): void
|
||||
{
|
||||
$this->propagationStopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if propagation is stopped
|
||||
*/
|
||||
public function isPropagationStopped(): bool
|
||||
{
|
||||
return $this->propagationStopped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tenant ID for multi-tenant context
|
||||
*/
|
||||
public function getTenantId(): ?string
|
||||
{
|
||||
return $this->tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set tenant ID for multi-tenant context
|
||||
*/
|
||||
public function setTenantId(?string $tenantId): self
|
||||
{
|
||||
$this->tenantId = $tenantId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get identity ID (user who triggered the event)
|
||||
*/
|
||||
public function getIdentityId(): ?string
|
||||
{
|
||||
return $this->identityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set identity ID
|
||||
*/
|
||||
public function setIdentityId(?string $identityId): self
|
||||
{
|
||||
$this->identityId = $identityId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert event to array for serialization/logging
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'data' => $this->data,
|
||||
'timestamp' => $this->timestamp,
|
||||
'tenantId' => $this->tenantId,
|
||||
'identityId' => $this->identityId,
|
||||
];
|
||||
}
|
||||
}
|
||||
186
shared/lib/Event/EventBus.php
Normal file
186
shared/lib/Event/EventBus.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Event;
|
||||
|
||||
/**
|
||||
* Simple event bus for decoupled pub/sub communication between services
|
||||
*
|
||||
* Features:
|
||||
* - Priority-based listener ordering
|
||||
* - Synchronous and asynchronous (deferred) event handling
|
||||
* - Event propagation control
|
||||
*/
|
||||
class EventBus
|
||||
{
|
||||
/** @var array<string, array<array{callback: callable, priority: int}>> */
|
||||
private array $listeners = [];
|
||||
|
||||
/** @var array<string, array<callable>> */
|
||||
private array $asyncListeners = [];
|
||||
|
||||
/** @var Event[] */
|
||||
private array $deferredEvents = [];
|
||||
|
||||
/**
|
||||
* Subscribe to an event with optional priority
|
||||
* Higher priority listeners are called first
|
||||
*/
|
||||
public function subscribe(string $eventName, callable $listener, int $priority = 0): self
|
||||
{
|
||||
$this->listeners[$eventName][] = [
|
||||
'callback' => $listener,
|
||||
'priority' => $priority,
|
||||
];
|
||||
|
||||
// Sort by priority (higher first)
|
||||
usort(
|
||||
$this->listeners[$eventName],
|
||||
fn($a, $b) => $b['priority'] <=> $a['priority']
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event for async/deferred processing
|
||||
* These handlers run at the end of the request cycle
|
||||
*/
|
||||
public function subscribeAsync(string $eventName, callable $listener): self
|
||||
{
|
||||
$this->asyncListeners[$eventName][] = $listener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe a listener from an event
|
||||
*/
|
||||
public function unsubscribe(string $eventName, callable $listener): self
|
||||
{
|
||||
if (isset($this->listeners[$eventName])) {
|
||||
$this->listeners[$eventName] = array_filter(
|
||||
$this->listeners[$eventName],
|
||||
fn($item) => $item['callback'] !== $listener
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($this->asyncListeners[$eventName])) {
|
||||
$this->asyncListeners[$eventName] = array_filter(
|
||||
$this->asyncListeners[$eventName],
|
||||
fn($item) => $item !== $listener
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish an event to all subscribers
|
||||
*/
|
||||
public function publish(Event $event): self
|
||||
{
|
||||
$eventName = $event->getName();
|
||||
|
||||
// Execute synchronous listeners
|
||||
if (isset($this->listeners[$eventName])) {
|
||||
foreach ($this->listeners[$eventName] as $listenerData) {
|
||||
if ($event->isPropagationStopped()) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
call_user_func($listenerData['callback'], $event);
|
||||
} catch (\Throwable $e) {
|
||||
// Log error but don't break the chain
|
||||
error_log(sprintf(
|
||||
'Event listener error for %s: %s',
|
||||
$eventName,
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Queue for async processing if there are async listeners
|
||||
if (isset($this->asyncListeners[$eventName]) && !empty($this->asyncListeners[$eventName])) {
|
||||
$this->deferredEvents[] = $event;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process deferred/async events
|
||||
* Call this at the end of the request cycle
|
||||
*/
|
||||
public function processDeferred(): int
|
||||
{
|
||||
$processed = 0;
|
||||
|
||||
foreach ($this->deferredEvents as $event) {
|
||||
$eventName = $event->getName();
|
||||
|
||||
if (!isset($this->asyncListeners[$eventName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($this->asyncListeners[$eventName] as $listener) {
|
||||
try {
|
||||
call_user_func($listener, $event);
|
||||
$processed++;
|
||||
} catch (\Throwable $e) {
|
||||
// Log but don't fail - these are non-critical
|
||||
error_log(sprintf(
|
||||
'Async event handler error for %s: %s',
|
||||
$eventName,
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->deferredEvents = [];
|
||||
|
||||
return $processed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event has any listeners
|
||||
*/
|
||||
public function hasListeners(string $eventName): bool
|
||||
{
|
||||
return !empty($this->listeners[$eventName]) || !empty($this->asyncListeners[$eventName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of listeners for an event
|
||||
*/
|
||||
public function getListenerCount(string $eventName): int
|
||||
{
|
||||
$sync = isset($this->listeners[$eventName]) ? count($this->listeners[$eventName]) : 0;
|
||||
$async = isset($this->asyncListeners[$eventName]) ? count($this->asyncListeners[$eventName]) : 0;
|
||||
|
||||
return $sync + $async;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of pending deferred events
|
||||
*/
|
||||
public function getDeferredCount(): int
|
||||
{
|
||||
return count($this->deferredEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all listeners (useful for testing)
|
||||
*/
|
||||
public function clear(): self
|
||||
{
|
||||
$this->listeners = [];
|
||||
$this->asyncListeners = [];
|
||||
$this->deferredEvents = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
303
shared/lib/Event/SecurityEvent.php
Normal file
303
shared/lib/Event/SecurityEvent.php
Normal file
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXF\Event;
|
||||
|
||||
/**
|
||||
* Security-specific event for authentication and access control events
|
||||
*/
|
||||
class SecurityEvent extends Event
|
||||
{
|
||||
// Event names
|
||||
public const AUTH_SUCCESS = 'security.auth.success';
|
||||
public const AUTH_FAILURE = 'security.auth.failure';
|
||||
public const AUTH_LOGOUT = 'security.auth.logout';
|
||||
public const TOKEN_REFRESH = 'security.token.refresh';
|
||||
public const TOKEN_REVOKED = 'security.token.revoked';
|
||||
|
||||
public const ACCESS_DENIED = 'security.access.denied';
|
||||
public const ACCESS_GRANTED = 'security.access.granted';
|
||||
|
||||
public const BRUTE_FORCE_DETECTED = 'security.brute_force.detected';
|
||||
public const RATE_LIMIT_EXCEEDED = 'security.rate_limit.exceeded';
|
||||
public const SUSPICIOUS_ACTIVITY = 'security.suspicious.activity';
|
||||
|
||||
public const IP_BLOCKED = 'security.ip.blocked';
|
||||
public const IP_ALLOWED = 'security.ip.allowed';
|
||||
public const DEVICE_BLOCKED = 'security.device.blocked';
|
||||
|
||||
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;
|
||||
public const SEVERITY_WARNING = 2;
|
||||
public const SEVERITY_ERROR = 3;
|
||||
public const SEVERITY_CRITICAL = 4;
|
||||
|
||||
/**
|
||||
* Create a security event with common parameters
|
||||
*/
|
||||
public static function create(
|
||||
string $name,
|
||||
?string $ipAddress = null,
|
||||
?string $deviceFingerprint = null,
|
||||
array $data = []
|
||||
): self {
|
||||
$event = new self($name, $data);
|
||||
$event->ipAddress = $ipAddress;
|
||||
$event->deviceFingerprint = $deviceFingerprint;
|
||||
|
||||
// Set default severity based on event type
|
||||
$event->severity = self::getSeverityForEvent($name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an authentication failure event
|
||||
*/
|
||||
public static function authFailure(
|
||||
string $ipAddress,
|
||||
?string $deviceFingerprint = null,
|
||||
?string $userId = null,
|
||||
?string $reason = null
|
||||
): self {
|
||||
$event = self::create(self::AUTH_FAILURE, $ipAddress, $deviceFingerprint, [
|
||||
'userId' => $userId,
|
||||
'reason' => $reason,
|
||||
]);
|
||||
$event->userId = $userId;
|
||||
$event->reason = $reason;
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an authentication success event
|
||||
*/
|
||||
public static function authSuccess(
|
||||
string $ipAddress,
|
||||
?string $deviceFingerprint = null,
|
||||
string $userId = null
|
||||
): self {
|
||||
$event = self::create(self::AUTH_SUCCESS, $ipAddress, $deviceFingerprint, [
|
||||
'userId' => $userId,
|
||||
]);
|
||||
$event->userId = $userId;
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a brute force detection event
|
||||
*/
|
||||
public static function bruteForceDetected(
|
||||
string $ipAddress,
|
||||
int $failureCount,
|
||||
int $windowSeconds
|
||||
): 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 $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a rate limit exceeded event
|
||||
*/
|
||||
public static function rateLimitExceeded(
|
||||
string $ipAddress,
|
||||
int $requestCount,
|
||||
int $windowSeconds,
|
||||
?string $endpoint = 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 $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an access denied event
|
||||
*/
|
||||
public static function accessDenied(
|
||||
string $ipAddress,
|
||||
?string $deviceFingerprint = null,
|
||||
?string $ruleId = null,
|
||||
?string $reason = null
|
||||
): self {
|
||||
$event = self::create(self::ACCESS_DENIED, $ipAddress, $deviceFingerprint, [
|
||||
'ruleId' => $ruleId,
|
||||
'reason' => $reason,
|
||||
]);
|
||||
$event->reason = $reason;
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default severity for event types
|
||||
*/
|
||||
private static function getSeverityForEvent(string $eventName): int
|
||||
{
|
||||
return match ($eventName) {
|
||||
self::AUTH_SUCCESS,
|
||||
self::ACCESS_GRANTED,
|
||||
self::TOKEN_REFRESH => self::SEVERITY_INFO,
|
||||
|
||||
self::AUTH_FAILURE,
|
||||
self::ACCESS_DENIED,
|
||||
self::AUTH_LOGOUT,
|
||||
self::TOKEN_REVOKED => self::SEVERITY_WARNING,
|
||||
|
||||
self::RATE_LIMIT_EXCEEDED,
|
||||
self::SUSPICIOUS_ACTIVITY => self::SEVERITY_ERROR,
|
||||
|
||||
self::BRUTE_FORCE_DETECTED,
|
||||
self::IP_BLOCKED,
|
||||
self::DEVICE_BLOCKED => self::SEVERITY_CRITICAL,
|
||||
|
||||
default => self::SEVERITY_INFO,
|
||||
};
|
||||
}
|
||||
|
||||
// Getters and setters
|
||||
|
||||
public function getIpAddress(): ?string
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
public function getSeverityLabel(): string
|
||||
{
|
||||
return match ($this->severity) {
|
||||
self::SEVERITY_DEBUG => 'DEBUG',
|
||||
self::SEVERITY_INFO => 'INFO',
|
||||
self::SEVERITY_WARNING => 'WARNING',
|
||||
self::SEVERITY_ERROR => 'ERROR',
|
||||
self::SEVERITY_CRITICAL => 'CRITICAL',
|
||||
default => 'UNKNOWN',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Override toArray to include security-specific fields
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return array_merge(parent::toArray(), [
|
||||
'ipAddress' => $this->ipAddress,
|
||||
'deviceFingerprint' => $this->deviceFingerprint,
|
||||
'userAgent' => $this->userAgent,
|
||||
'requestPath' => $this->requestPath,
|
||||
'requestMethod' => $this->requestMethod,
|
||||
'userId' => $this->userId,
|
||||
'reason' => $this->reason,
|
||||
'severity' => $this->severity,
|
||||
'severityLabel' => $this->getSeverityLabel(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user