feat(firewall): add audited configuration management
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -314,6 +314,43 @@ final class FirewallController extends ControllerAbstract
|
||||
return new JsonResponse($this->tenantStatus->configuration());
|
||||
}
|
||||
|
||||
#[AuthenticatedRoute(
|
||||
'/firewall/configuration',
|
||||
name: 'firewall.tenant.configuration.update',
|
||||
methods: ['PUT'],
|
||||
permissions: [TenantFirewallStatusService::PERMISSION_SETTINGS_MANAGE],
|
||||
)]
|
||||
public function updateTenantConfiguration(
|
||||
bool $enabled,
|
||||
int $maxAuthFailures,
|
||||
int $authFailureWindow,
|
||||
int $autoBlockDuration,
|
||||
string $reason
|
||||
): JsonResponse {
|
||||
return $this->settingsResponse(fn() => $this->tenantStatus->updateConfiguration(
|
||||
$enabled, $maxAuthFailures, $authFailureWindow, $autoBlockDuration, $reason
|
||||
));
|
||||
}
|
||||
|
||||
#[AuthenticatedRoute(
|
||||
'/firewall/system/tenants/{tenantId}/configuration',
|
||||
name: 'firewall.system.tenant.configuration.update',
|
||||
methods: ['PUT'],
|
||||
permissions: [SystemFirewallStatusService::PERMISSION_SETTINGS_MANAGE],
|
||||
)]
|
||||
public function updateSystemTenantConfiguration(
|
||||
string $tenantId,
|
||||
bool $enabled,
|
||||
int $maxAuthFailures,
|
||||
int $authFailureWindow,
|
||||
int $autoBlockDuration,
|
||||
string $reason
|
||||
): JsonResponse {
|
||||
return $this->settingsResponse(fn() => $this->systemStatus->updateTenantConfiguration(
|
||||
$tenantId, $enabled, $maxAuthFailures, $authFailureWindow, $autoBlockDuration, $reason
|
||||
));
|
||||
}
|
||||
|
||||
#[AuthenticatedRoute(
|
||||
'/firewall/system/metrics',
|
||||
name: 'firewall.system.metrics.read',
|
||||
@@ -405,4 +442,24 @@ final class FirewallController extends ControllerAbstract
|
||||
]], JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private function settingsResponse(callable $mutation): JsonResponse
|
||||
{
|
||||
try {
|
||||
$configuration = $mutation();
|
||||
if ($configuration === null) {
|
||||
return new JsonResponse(['error' => [
|
||||
'code' => 'tenant_not_found',
|
||||
'message' => 'Tenant not found.',
|
||||
]], JsonResponse::HTTP_NOT_FOUND);
|
||||
}
|
||||
|
||||
return new JsonResponse(['configuration' => $configuration]);
|
||||
} catch (\InvalidArgumentException $error) {
|
||||
return new JsonResponse(['error' => [
|
||||
'code' => 'invalid_firewall_configuration',
|
||||
'message' => $error->getMessage(),
|
||||
]], JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ class FirewallLogObject implements \JsonSerializable, JsonDeserializable
|
||||
public const EVENT_RULE_ENABLED = 'rule_enabled';
|
||||
public const EVENT_RULE_DISABLED = 'rule_disabled';
|
||||
public const EVENT_RULE_REMOVED = 'rule_removed';
|
||||
public const EVENT_SETTINGS_UPDATED = 'settings_updated';
|
||||
|
||||
private ?string $id = null;
|
||||
private ?string $eventId = null;
|
||||
|
||||
@@ -51,6 +51,7 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
||||
SecurityEvent::FIREWALL_RULE_ENABLED,
|
||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
||||
SecurityEvent::FIREWALL_SETTINGS_UPDATED,
|
||||
] as $event) {
|
||||
$this->events->listen(
|
||||
'core',
|
||||
@@ -150,6 +151,11 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
||||
'description' => 'View effective firewall settings for the current tenant',
|
||||
'group' => 'Firewall Management'
|
||||
],
|
||||
TenantFirewallStatusService::PERMISSION_SETTINGS_MANAGE => [
|
||||
'label' => 'Manage Tenant Firewall Settings',
|
||||
'description' => 'Update firewall settings for the current tenant',
|
||||
'group' => 'Firewall Management'
|
||||
],
|
||||
SystemFirewallRuleService::PERMISSION_READ => [
|
||||
'label' => 'View System Firewall Rules',
|
||||
'description' => 'View firewall rules that apply to every tenant',
|
||||
@@ -170,6 +176,11 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
||||
'description' => 'View the last firewall cleanup result and operational status',
|
||||
'group' => 'System Administration'
|
||||
],
|
||||
SystemFirewallStatusService::PERMISSION_SETTINGS_MANAGE => [
|
||||
'label' => 'Manage Tenant Firewall Settings System-Wide',
|
||||
'description' => 'Update firewall settings for any tenant',
|
||||
'group' => 'System Administration'
|
||||
],
|
||||
'system.admin' => [
|
||||
'label' => 'System Administrator',
|
||||
'description' => 'Full system access (superuser)',
|
||||
|
||||
@@ -24,6 +24,7 @@ final class FirewallLogService
|
||||
FirewallLogObject::EVENT_RULE_ENABLED,
|
||||
FirewallLogObject::EVENT_RULE_DISABLED,
|
||||
FirewallLogObject::EVENT_RULE_REMOVED,
|
||||
FirewallLogObject::EVENT_SETTINGS_UPDATED,
|
||||
];
|
||||
|
||||
public function __construct(private readonly FirewallStore $store)
|
||||
|
||||
@@ -272,6 +272,7 @@ class FirewallService
|
||||
SecurityEvent::FIREWALL_RULE_ENABLED => FirewallLogObject::EVENT_RULE_ENABLED,
|
||||
SecurityEvent::FIREWALL_RULE_DISABLED => FirewallLogObject::EVENT_RULE_DISABLED,
|
||||
SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::EVENT_RULE_REMOVED,
|
||||
SecurityEvent::FIREWALL_SETTINGS_UPDATED => FirewallLogObject::EVENT_SETTINGS_UPDATED,
|
||||
default => FirewallLogObject::EVENT_ACCESS_CHECK,
|
||||
};
|
||||
}
|
||||
@@ -288,7 +289,8 @@ class FirewallService
|
||||
SecurityEvent::FIREWALL_RULE_EXTENDED,
|
||||
SecurityEvent::FIREWALL_RULE_ENABLED,
|
||||
SecurityEvent::FIREWALL_RULE_DISABLED,
|
||||
SecurityEvent::FIREWALL_RULE_REMOVED => FirewallLogObject::RESULT_RECORDED,
|
||||
SecurityEvent::FIREWALL_RULE_REMOVED,
|
||||
SecurityEvent::FIREWALL_SETTINGS_UPDATED => FirewallLogObject::RESULT_RECORDED,
|
||||
default => FirewallLogObject::RESULT_BLOCKED,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\Models\Tenant\TenantConfiguration;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Event\SecurityEvent;
|
||||
|
||||
final class FirewallSettingsService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TenantService $tenants,
|
||||
private readonly EventDispatcherInterface $events,
|
||||
) {
|
||||
}
|
||||
|
||||
public function update(
|
||||
string $tenantId,
|
||||
bool $enabled,
|
||||
int $maxAuthFailures,
|
||||
int $authFailureWindow,
|
||||
int $autoBlockDuration,
|
||||
string $reason,
|
||||
?string $actorId
|
||||
): ?array {
|
||||
$reason = trim($reason);
|
||||
if ($reason === '' || strlen($reason) > 1000) {
|
||||
throw new \InvalidArgumentException('A change reason containing 1-1000 bytes is required.');
|
||||
}
|
||||
self::bounded($maxAuthFailures, 1, 1000, 'Maximum authentication failures');
|
||||
self::bounded($authFailureWindow, 1, 86400, 'Authentication failure window');
|
||||
self::bounded($autoBlockDuration, 1, 31536000, 'Automatic block duration');
|
||||
|
||||
$tenant = $this->tenants->fetchById($tenantId);
|
||||
if ($tenant === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$previous = $tenant->getConfiguration()->firewall()->jsonSerialize();
|
||||
$current = [
|
||||
'enabled' => $enabled,
|
||||
'maxAuthFailures' => $maxAuthFailures,
|
||||
'authFailureWindow' => $authFailureWindow,
|
||||
'autoBlockDuration' => $autoBlockDuration,
|
||||
];
|
||||
$configuration = (new TenantConfiguration())->jsonDeserialize([
|
||||
...$tenant->getConfiguration()->jsonSerialize(),
|
||||
'firewall' => $current,
|
||||
]);
|
||||
$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);
|
||||
$this->events->dispatch($event);
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
private static function bounded(int $value, int $minimum, int $maximum, string $label): void
|
||||
{
|
||||
if ($value < $minimum || $value > $maximum) {
|
||||
throw new \InvalidArgumentException(
|
||||
"{$label} must be between {$minimum} and {$maximum}."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,12 @@ use KTXC\Context\IdentityContextInterface;
|
||||
final class SystemFirewallStatusService
|
||||
{
|
||||
public const PERMISSION_MAINTENANCE_READ = 'firewall.system.maintenance.read';
|
||||
public const PERMISSION_SETTINGS_MANAGE = 'firewall.system.settings.manage';
|
||||
|
||||
public function __construct(
|
||||
private readonly FirewallStatusService $status,
|
||||
private readonly IdentityContextInterface $identity,
|
||||
private readonly FirewallSettingsService $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -28,6 +30,26 @@ final class SystemFirewallStatusService
|
||||
return $this->status->maintenanceStatus();
|
||||
}
|
||||
|
||||
public function updateTenantConfiguration(
|
||||
string $tenantId,
|
||||
bool $enabled,
|
||||
int $maxAuthFailures,
|
||||
int $authFailureWindow,
|
||||
int $autoBlockDuration,
|
||||
string $reason
|
||||
): ?array {
|
||||
$this->requirePermission(self::PERMISSION_SETTINGS_MANAGE);
|
||||
return $this->settings->update(
|
||||
$tenantId,
|
||||
$enabled,
|
||||
$maxAuthFailures,
|
||||
$authFailureWindow,
|
||||
$autoBlockDuration,
|
||||
$reason,
|
||||
$this->identity->identifier()
|
||||
);
|
||||
}
|
||||
|
||||
private function requirePermission(string $permission): void
|
||||
{
|
||||
if (!$this->identity->hasPermission($permission)) {
|
||||
|
||||
@@ -10,11 +10,13 @@ use KTXC\Context\TenantContextInterface;
|
||||
final class TenantFirewallStatusService
|
||||
{
|
||||
public const PERMISSION_SETTINGS_READ = 'firewall.tenant.settings.read';
|
||||
public const PERMISSION_SETTINGS_MANAGE = 'firewall.tenant.settings.manage';
|
||||
|
||||
public function __construct(
|
||||
private readonly FirewallStatusService $status,
|
||||
private readonly TenantContextInterface $tenant,
|
||||
private readonly IdentityContextInterface $identity,
|
||||
private readonly FirewallSettingsService $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -36,6 +38,25 @@ final class TenantFirewallStatusService
|
||||
];
|
||||
}
|
||||
|
||||
public function updateConfiguration(
|
||||
bool $enabled,
|
||||
int $maxAuthFailures,
|
||||
int $authFailureWindow,
|
||||
int $autoBlockDuration,
|
||||
string $reason
|
||||
): ?array {
|
||||
$this->requirePermission(self::PERMISSION_SETTINGS_MANAGE);
|
||||
return $this->settings->update(
|
||||
$this->tenant->requireIdentifier(),
|
||||
$enabled,
|
||||
$maxAuthFailures,
|
||||
$authFailureWindow,
|
||||
$autoBlockDuration,
|
||||
$reason,
|
||||
$this->identity->identifier()
|
||||
);
|
||||
}
|
||||
|
||||
private function requirePermission(string $permission): void
|
||||
{
|
||||
if (!$this->identity->hasPermission($permission)) {
|
||||
|
||||
Reference in New Issue
Block a user