refactor(firewall): separate enforcement from rule management

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-30 22:35:24 -04:00
parent 266b364c93
commit da81f1ddf1
14 changed files with 814 additions and 436 deletions
+23 -1
View File
@@ -3,6 +3,8 @@
namespace KTXC\Module;
use KTXC\Service\FirewallService;
use KTXC\Service\SystemFirewallRuleService;
use KTXC\Service\TenantFirewallRuleService;
use KTXF\Event\DeliveryMode;
use KTXF\Event\EventListenerRegistry;
use KTXF\Event\SecurityEvent;
@@ -115,7 +117,27 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
'group' => 'Module Management'
],
// System Administration
// Firewall Management
TenantFirewallRuleService::PERMISSION_READ => [
'label' => 'View Tenant Firewall Rules',
'description' => 'View firewall rules owned by the current tenant',
'group' => 'Firewall Management'
],
TenantFirewallRuleService::PERMISSION_MANAGE => [
'label' => 'Manage Tenant Firewall Rules',
'description' => 'Create, disable, and remove firewall rules owned by the current tenant',
'group' => 'Firewall Management'
],
SystemFirewallRuleService::PERMISSION_READ => [
'label' => 'View System Firewall Rules',
'description' => 'View firewall rules that apply to every tenant',
'group' => 'System Administration'
],
SystemFirewallRuleService::PERMISSION_MANAGE => [
'label' => 'Manage System Firewall Rules',
'description' => 'Create, disable, and remove firewall rules that apply to every tenant',
'group' => 'System Administration'
],
'system.admin' => [
'label' => 'System Administrator',
'description' => 'Full system access (superuser)',
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace KTXC\Service;
use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Stores\FirewallStore;
final class FirewallRuleCache
{
/** @var array<string, FirewallRuleObject[]> */
private array $rules = [];
public function __construct(private readonly FirewallStore $store)
{
}
/** @return FirewallRuleObject[] */
public function applicable(string $tenantId): array
{
return $this->rules[$tenantId] ??= $this->store->listApplicableRules($tenantId);
}
public function invalidate(): void
{
$this->rules = [];
}
}
+198
View File
@@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
namespace KTXC\Service;
use KTXC\Models\Firewall\FirewallRuleObject;
use KTXC\Stores\FirewallStore;
use KTXF\Event\EventDispatcherInterface;
use KTXF\Event\SecurityEvent;
final class FirewallRuleManager
{
public function __construct(
private readonly FirewallStore $store,
private readonly FirewallRuleCache $cache,
private readonly EventDispatcherInterface $events,
) {
}
public function list(FirewallRuleScope $scope, bool $activeOnly = true): array
{
return $scope->scope === FirewallRuleObject::SCOPE_SYSTEM
? $this->store->listSystemRules($activeOnly)
: $this->store->listRules($scope->tenantId, $activeOnly);
}
public function blockIp(
FirewallRuleScope $scope,
string $ipAddress,
?string $reason,
?string $createdBy,
?int $durationSeconds = null
): FirewallRuleObject {
$ipAddress = FirewallRuleValidator::ipAddress($ipAddress);
FirewallRuleValidator::duration($durationSeconds);
$existing = $this->store->findExactIpRule(
$scope->tenantId,
$ipAddress,
FirewallRuleObject::ACTION_BLOCK,
$scope->scope
);
if ($existing) {
return $existing;
}
$rule = $this->create(
$scope,
FirewallRuleObject::TYPE_IP,
FirewallRuleObject::ACTION_BLOCK,
$ipAddress,
$reason ?? 'Blocked by administrator',
$createdBy,
$durationSeconds
);
$this->publishIpEvent(SecurityEvent::IP_BLOCKED, $scope, $ipAddress, $reason);
return $rule;
}
public function allowIp(
FirewallRuleScope $scope,
string $ipAddress,
?string $reason,
?string $createdBy
): FirewallRuleObject {
$ipAddress = FirewallRuleValidator::ipAddress($ipAddress);
$rule = $this->create(
$scope,
FirewallRuleObject::TYPE_IP,
FirewallRuleObject::ACTION_ALLOW,
$ipAddress,
$reason ?? 'Allowed by administrator',
$createdBy
);
$this->publishIpEvent(SecurityEvent::IP_ALLOWED, $scope, $ipAddress, $reason);
return $rule;
}
public function blockIpRange(
FirewallRuleScope $scope,
string $cidr,
?string $reason,
?string $createdBy
): FirewallRuleObject {
return $this->create(
$scope,
FirewallRuleObject::TYPE_IP_RANGE,
FirewallRuleObject::ACTION_BLOCK,
FirewallRuleValidator::cidr($cidr),
$reason ?? 'Range blocked by administrator',
$createdBy
);
}
public function blockDevice(
FirewallRuleScope $scope,
string $fingerprint,
?string $reason,
?string $createdBy,
?int $durationSeconds = null
): FirewallRuleObject {
FirewallRuleValidator::duration($durationSeconds);
$fingerprint = FirewallRuleValidator::deviceFingerprint($fingerprint);
$rule = $this->create(
$scope,
FirewallRuleObject::TYPE_DEVICE,
FirewallRuleObject::ACTION_BLOCK,
$fingerprint,
$reason ?? 'Device blocked by administrator',
$createdBy,
$durationSeconds
);
$event = new SecurityEvent(SecurityEvent::DEVICE_BLOCKED, ['device' => $fingerprint, 'reason' => $reason]);
$event->setDeviceFingerprint($fingerprint)->setReason($reason)->setTenantId($scope->tenantId);
$this->events->dispatch($event);
return $rule;
}
public function disable(FirewallRuleScope $scope, string $ruleId): bool
{
$rule = $this->ownedRule($scope, $ruleId);
if (!$rule) {
return false;
}
$rule->setEnabled(false);
$this->store->depositRule($rule);
$this->cache->invalidate();
return true;
}
public function remove(FirewallRuleScope $scope, string $ruleId): bool
{
$rule = $this->ownedRule($scope, $ruleId);
if (!$rule) {
return false;
}
$this->store->destroyRule($rule);
$this->cache->invalidate();
return true;
}
private function create(
FirewallRuleScope $scope,
string $type,
string $action,
string $value,
string $reason,
?string $createdBy,
?int $durationSeconds = null
): FirewallRuleObject {
$rule = (new FirewallRuleObject())
->setScope($scope->scope)
->setTenantId($scope->tenantId)
->setType($type)
->setAction($action)
->setValue($value)
->setReason($reason)
->setCreatedBy($createdBy)
->setCreatedAt(new \DateTimeImmutable())
->setEnabled(true);
if ($durationSeconds !== null) {
$rule->setExpiresAt((new \DateTimeImmutable())->modify("+{$durationSeconds} seconds"));
}
$this->store->depositRule($rule);
$this->cache->invalidate();
return $rule;
}
private function ownedRule(FirewallRuleScope $scope, string $ruleId): ?FirewallRuleObject
{
$rule = $this->store->fetchRule($ruleId);
return $rule && $scope->owns($rule) ? $rule : null;
}
private function publishIpEvent(
string $name,
FirewallRuleScope $scope,
string $ipAddress,
?string $reason
): void {
$event = new SecurityEvent($name, ['ip' => $ipAddress, 'reason' => $reason]);
$event->setIpAddress($ipAddress)->setReason($reason)->setTenantId($scope->tenantId);
$this->events->dispatch($event);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace KTXC\Service;
use KTXC\Models\Firewall\FirewallRuleObject;
final class FirewallRuleScope
{
private function __construct(
public readonly string $scope,
public readonly ?string $tenantId,
) {
}
public static function tenant(string $tenantId): self
{
if ($tenantId === '') {
throw new \InvalidArgumentException('Tenant firewall rule scope requires a tenant ID.');
}
return new self(FirewallRuleObject::SCOPE_TENANT, $tenantId);
}
public static function system(): self
{
return new self(FirewallRuleObject::SCOPE_SYSTEM, null);
}
public function owns(FirewallRuleObject $rule): bool
{
return $rule->getScope() === $this->scope && $rule->getTenantId() === $this->tenantId;
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace KTXC\Service;
final class FirewallRuleValidator
{
public const MAX_DEVICE_FINGERPRINT_LENGTH = 512;
private function __construct()
{
}
public static function ipAddress(string $ipAddress): string
{
$ipAddress = trim($ipAddress);
if (filter_var($ipAddress, \FILTER_VALIDATE_IP) === false) {
throw new \InvalidArgumentException("Invalid IP address: {$ipAddress}");
}
return $ipAddress;
}
public static function cidr(string $cidr): string
{
$cidr = trim($cidr);
if (substr_count($cidr, '/') !== 1) {
throw new \InvalidArgumentException("Invalid CIDR range: {$cidr}");
}
[$address, $prefix] = explode('/', $cidr, 2);
if (filter_var($address, \FILTER_VALIDATE_IP) === false || !ctype_digit($prefix)) {
throw new \InvalidArgumentException("Invalid CIDR range: {$cidr}");
}
$maximumPrefix = str_contains($address, ':') ? 128 : 32;
if ((int)$prefix > $maximumPrefix) {
throw new \InvalidArgumentException("Invalid CIDR range: {$cidr}");
}
return $cidr;
}
public static function deviceFingerprint(string $fingerprint): string
{
$fingerprint = trim($fingerprint);
if ($fingerprint === '' || strlen($fingerprint) > self::MAX_DEVICE_FINGERPRINT_LENGTH) {
throw new \InvalidArgumentException(
sprintf('Device fingerprint must contain between 1 and %d bytes.', self::MAX_DEVICE_FINGERPRINT_LENGTH)
);
}
return $fingerprint;
}
public static function duration(?int $durationSeconds): void
{
if ($durationSeconds !== null && $durationSeconds < 1) {
throw new \InvalidArgumentException('Firewall rule duration must be greater than zero.');
}
}
}
+5 -347
View File
@@ -32,7 +32,6 @@ class FirewallService
private const MAX_AUTH_FAILURES = 1000;
private const MAX_AUTH_FAILURE_WINDOW = 86400; // 1 day
private const MAX_AUTO_BLOCK_DURATION = 31536000; // 1 year
private const MAX_DEVICE_FINGERPRINT_LENGTH = 512;
// Configuration keys
private const CONFIG_MAX_FAILURES = 'firewall.maxAuthFailures';
@@ -40,13 +39,12 @@ class FirewallService
private const CONFIG_AUTO_BLOCK_DURATION = 'firewall.autoBlockDuration';
private const CONFIG_ENABLED = 'firewall.enabled';
/** @var array<string, FirewallRuleObject[]> */
private array $rulesCache = [];
public function __construct(
private readonly FirewallStore $store,
private readonly TenantContextInterface $tenantContext,
private readonly EventDispatcherInterface $events,
private readonly FirewallRuleManager $rules,
private readonly FirewallRuleCache $ruleCache,
) {
}
@@ -192,8 +190,8 @@ class FirewallService
self::MAX_AUTO_BLOCK_DURATION
);
$this->blockIpForTenant(
$tenantId,
$this->rules->blockIp(
FirewallRuleScope::tenant($tenantId),
$ipAddress,
sprintf('Auto-blocked: %d failed auth attempts in %d seconds', $failureCount, $windowSeconds),
null, // System-created
@@ -273,297 +271,6 @@ class FirewallService
$this->events->dispatch($event);
}
// ========================================
// Rule Management
// ========================================
/**
* Block an IP address
*/
public function blockIp(
string $ipAddress,
?string $reason = null,
?string $createdBy = null,
?int $durationSeconds = null
): FirewallRuleObject {
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
throw new \RuntimeException('Cannot create firewall rule: no tenant configured');
}
return $this->blockIpForTenant(
$tenantId,
$ipAddress,
$reason,
$createdBy,
$durationSeconds
);
}
private function blockIpForTenant(
string $tenantId,
string $ipAddress,
?string $reason,
?string $createdBy,
?int $durationSeconds
): FirewallRuleObject {
$ipAddress = $this->validateIpAddress($ipAddress);
$this->validateDuration($durationSeconds);
// Check if already blocked
$existing = $this->store->findExactIpRule(
$tenantId,
$ipAddress,
FirewallRuleObject::ACTION_BLOCK
);
if ($existing) {
return $existing;
}
$rule = new FirewallRuleObject();
$rule->setScope(FirewallRuleObject::SCOPE_TENANT)
->setTenantId($tenantId)
->setType(FirewallRuleObject::TYPE_IP)
->setAction(FirewallRuleObject::ACTION_BLOCK)
->setValue($ipAddress)
->setReason($reason ?? 'Blocked by administrator')
->setCreatedBy($createdBy)
->setCreatedAt(new \DateTimeImmutable())
->setEnabled(true);
if ($durationSeconds !== null) {
$rule->setExpiresAt(
(new \DateTimeImmutable())->modify("+{$durationSeconds} seconds")
);
}
$this->store->depositRule($rule);
$this->clearRulesCache();
// Publish event
$event = new SecurityEvent(SecurityEvent::IP_BLOCKED, ['ip' => $ipAddress, 'reason' => $reason]);
$event->setIpAddress($ipAddress)
->setReason($reason)
->setTenantId($tenantId);
$this->events->dispatch($event);
return $rule;
}
/**
* Allow an IP address (whitelist)
*/
public function allowIp(
string $ipAddress,
?string $reason = null,
?string $createdBy = null
): FirewallRuleObject {
$ipAddress = $this->validateIpAddress($ipAddress);
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
throw new \RuntimeException('Cannot create firewall rule: no tenant configured');
}
$rule = new FirewallRuleObject();
$rule->setScope(FirewallRuleObject::SCOPE_TENANT)
->setTenantId($tenantId)
->setType(FirewallRuleObject::TYPE_IP)
->setAction(FirewallRuleObject::ACTION_ALLOW)
->setValue($ipAddress)
->setReason($reason ?? 'Allowed by administrator')
->setCreatedBy($createdBy)
->setCreatedAt(new \DateTimeImmutable())
->setEnabled(true);
$this->store->depositRule($rule);
$this->clearRulesCache();
// Publish event
$event = new SecurityEvent(SecurityEvent::IP_ALLOWED, ['ip' => $ipAddress, 'reason' => $reason]);
$event->setIpAddress($ipAddress)
->setReason($reason)
->setTenantId($tenantId);
$this->events->dispatch($event);
return $rule;
}
/**
* Block an IP range (CIDR notation)
*/
public function blockIpRange(
string $cidr,
?string $reason = null,
?string $createdBy = null
): FirewallRuleObject {
$cidr = $this->validateCidr($cidr);
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
throw new \RuntimeException('Cannot create firewall rule: no tenant configured');
}
$rule = new FirewallRuleObject();
$rule->setScope(FirewallRuleObject::SCOPE_TENANT)
->setTenantId($tenantId)
->setType(FirewallRuleObject::TYPE_IP_RANGE)
->setAction(FirewallRuleObject::ACTION_BLOCK)
->setValue($cidr)
->setReason($reason ?? 'Range blocked by administrator')
->setCreatedBy($createdBy)
->setCreatedAt(new \DateTimeImmutable())
->setEnabled(true);
$this->store->depositRule($rule);
$this->clearRulesCache();
return $rule;
}
/**
* Block a device fingerprint
*/
public function blockDevice(
string $fingerprint,
?string $reason = null,
?string $createdBy = null,
?int $durationSeconds = null
): FirewallRuleObject {
$fingerprint = trim($fingerprint);
if ($fingerprint === '' || strlen($fingerprint) > self::MAX_DEVICE_FINGERPRINT_LENGTH) {
throw new \InvalidArgumentException(
sprintf('Device fingerprint must contain between 1 and %d bytes.', self::MAX_DEVICE_FINGERPRINT_LENGTH)
);
}
$this->validateDuration($durationSeconds);
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
throw new \RuntimeException('Cannot create firewall rule: no tenant configured');
}
$rule = new FirewallRuleObject();
$rule->setScope(FirewallRuleObject::SCOPE_TENANT)
->setTenantId($tenantId)
->setType(FirewallRuleObject::TYPE_DEVICE)
->setAction(FirewallRuleObject::ACTION_BLOCK)
->setValue($fingerprint)
->setReason($reason ?? 'Device blocked by administrator')
->setCreatedBy($createdBy)
->setCreatedAt(new \DateTimeImmutable())
->setEnabled(true);
if ($durationSeconds !== null) {
$rule->setExpiresAt(
(new \DateTimeImmutable())->modify("+{$durationSeconds} seconds")
);
}
$this->store->depositRule($rule);
$this->clearRulesCache();
// Publish event
$event = new SecurityEvent(SecurityEvent::DEVICE_BLOCKED, ['device' => $fingerprint, 'reason' => $reason]);
$event->setDeviceFingerprint($fingerprint)
->setReason($reason)
->setTenantId($tenantId);
$this->events->dispatch($event);
return $rule;
}
/**
* Remove a rule by ID
*/
public function removeRule(string $ruleId): bool
{
$rule = $this->store->fetchRule($ruleId);
if (!$rule) {
return false;
}
// Verify tenant ownership
if ($rule->getTenantId() !== $this->tenantContext->identifier()) {
return false;
}
$this->store->destroyRule($rule);
$this->clearRulesCache();
return true;
}
/**
* Disable a rule (soft delete)
*/
public function disableRule(string $ruleId): bool
{
$rule = $this->store->fetchRule($ruleId);
if (!$rule) {
return false;
}
// Verify tenant ownership
if ($rule->getTenantId() !== $this->tenantContext->identifier()) {
return false;
}
$rule->setEnabled(false);
$this->store->depositRule($rule);
$this->clearRulesCache();
return true;
}
/**
* Get all rules for current tenant
*/
public function listRules(bool $activeOnly = true): array
{
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
return [];
}
return $this->store->listRules($tenantId, $activeOnly);
}
/**
* Get firewall logs for current tenant
*/
public function getLogs(
?string $ipAddress = null,
?string $eventType = null,
?string $result = null,
int $limit = 100
): array {
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
return [];
}
return $this->store->listLogs($tenantId, $ipAddress, $eventType, $result, $limit);
}
/**
* Get blocked requests count
*/
public function getBlockedCount(?\DateTimeImmutable $since = null): int
{
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
return 0;
}
return $this->store->countBlockedRequests($tenantId, $since);
}
// ========================================
// Helpers
// ========================================
/**
* Check if firewall is enabled for current tenant
*/
@@ -603,43 +310,6 @@ class FirewallService
return $value;
}
private function validateIpAddress(string $ipAddress): string
{
$ipAddress = trim($ipAddress);
if (filter_var($ipAddress, \FILTER_VALIDATE_IP) === false) {
throw new \InvalidArgumentException("Invalid IP address: {$ipAddress}");
}
return $ipAddress;
}
private function validateCidr(string $cidr): string
{
$cidr = trim($cidr);
if (substr_count($cidr, '/') !== 1) {
throw new \InvalidArgumentException("Invalid CIDR range: {$cidr}");
}
[$address, $prefix] = explode('/', $cidr, 2);
if (filter_var($address, \FILTER_VALIDATE_IP) === false || !ctype_digit($prefix)) {
throw new \InvalidArgumentException("Invalid CIDR range: {$cidr}");
}
$maximumPrefix = str_contains($address, ':') ? 128 : 32;
if ((int)$prefix > $maximumPrefix) {
throw new \InvalidArgumentException("Invalid CIDR range: {$cidr}");
}
return $cidr;
}
private function validateDuration(?int $durationSeconds): void
{
if ($durationSeconds !== null && $durationSeconds < 1) {
throw new \InvalidArgumentException('Firewall rule duration must be greater than zero.');
}
}
/**
* Get active rules (cached)
* @return FirewallRuleObject[]
@@ -651,19 +321,7 @@ class FirewallService
return [];
}
if (!array_key_exists($tenantId, $this->rulesCache)) {
$this->rulesCache[$tenantId] = $this->store->listApplicableRules($tenantId);
}
return $this->rulesCache[$tenantId];
}
/**
* Clear rules cache
*/
private function clearRulesCache(): void
{
$this->rulesCache = [];
return $this->ruleCache->applicable($tenantId);
}
/**
@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace KTXC\Service;
use KTXC\Context\IdentityContextInterface;
use KTXC\Models\Firewall\FirewallRuleObject;
final class SystemFirewallRuleService
{
public const PERMISSION_READ = 'firewall.system.rules.read';
public const PERMISSION_MANAGE = 'firewall.system.rules.manage';
public function __construct(
private readonly FirewallRuleManager $rules,
private readonly IdentityContextInterface $identity,
) {
}
public function listRules(bool $activeOnly = true): array
{
$this->requirePermission(self::PERMISSION_READ);
return $this->rules->list(FirewallRuleScope::system(), $activeOnly);
}
public function blockIp(string $ip, ?string $reason = null, ?int $durationSeconds = null): FirewallRuleObject
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->blockIp(
FirewallRuleScope::system(), $ip, $reason, $this->identity->identifier(), $durationSeconds
);
}
public function allowIp(string $ip, ?string $reason = null): FirewallRuleObject
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->allowIp(
FirewallRuleScope::system(), $ip, $reason, $this->identity->identifier()
);
}
public function blockIpRange(string $cidr, ?string $reason = null): FirewallRuleObject
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->blockIpRange(
FirewallRuleScope::system(), $cidr, $reason, $this->identity->identifier()
);
}
public function blockDevice(
string $fingerprint,
?string $reason = null,
?int $durationSeconds = null
): FirewallRuleObject {
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->blockDevice(
FirewallRuleScope::system(),
$fingerprint,
$reason,
$this->identity->identifier(),
$durationSeconds
);
}
public function disableRule(string $ruleId): bool
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->disable(FirewallRuleScope::system(), $ruleId);
}
public function removeRule(string $ruleId): bool
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->remove(FirewallRuleScope::system(), $ruleId);
}
private function requirePermission(string $permission): void
{
if (!$this->identity->hasPermission($permission)) {
throw new \RuntimeException("Missing required permission: {$permission}");
}
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace KTXC\Service;
use KTXC\Context\IdentityContextInterface;
use KTXC\Context\TenantContextInterface;
use KTXC\Models\Firewall\FirewallRuleObject;
final class TenantFirewallRuleService
{
public const PERMISSION_READ = 'firewall.tenant.rules.read';
public const PERMISSION_MANAGE = 'firewall.tenant.rules.manage';
public function __construct(
private readonly FirewallRuleManager $rules,
private readonly TenantContextInterface $tenant,
private readonly IdentityContextInterface $identity,
) {
}
public function listRules(bool $activeOnly = true): array
{
$this->requirePermission(self::PERMISSION_READ);
return $this->rules->list($this->scope(), $activeOnly);
}
public function blockIp(string $ip, ?string $reason = null, ?int $durationSeconds = null): FirewallRuleObject
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->blockIp(
$this->scope(), $ip, $reason, $this->identity->identifier(), $durationSeconds
);
}
public function allowIp(string $ip, ?string $reason = null): FirewallRuleObject
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->allowIp($this->scope(), $ip, $reason, $this->identity->identifier());
}
public function blockIpRange(string $cidr, ?string $reason = null): FirewallRuleObject
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->blockIpRange($this->scope(), $cidr, $reason, $this->identity->identifier());
}
public function blockDevice(
string $fingerprint,
?string $reason = null,
?int $durationSeconds = null
): FirewallRuleObject {
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->blockDevice(
$this->scope(), $fingerprint, $reason, $this->identity->identifier(), $durationSeconds
);
}
public function disableRule(string $ruleId): bool
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->disable($this->scope(), $ruleId);
}
public function removeRule(string $ruleId): bool
{
$this->requirePermission(self::PERMISSION_MANAGE);
return $this->rules->remove($this->scope(), $ruleId);
}
private function scope(): FirewallRuleScope
{
return FirewallRuleScope::tenant($this->tenant->requireIdentifier());
}
private function requirePermission(string $permission): void
{
if (!$this->identity->hasPermission($permission)) {
throw new \RuntimeException("Missing required permission: {$permission}");
}
}
}
+20
View File
@@ -92,6 +92,26 @@ class FirewallStore
return $list;
}
public function listSystemRules(bool $activeOnly = true): array
{
$filter = ['scope' => FirewallRuleObject::SCOPE_SYSTEM, 'tenantId' => null];
if ($activeOnly) {
$filter['enabled'] = true;
$filter['$or'] = [
['expiresAt' => null],
['expiresAt' => ['$gt' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM)]],
];
}
$cursor = $this->dataStore->selectCollection(self::RULES_COLLECTION)->find($filter);
$list = [];
foreach ($cursor as $entry) {
$list[] = (new FirewallRuleObject())->jsonDeserialize((array)$entry);
}
return $list;
}
/**
* Find rules by IP address
*/