feat(firewall): add safeguarded rule creation
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Service\FirewallRuleConflictException;
|
||||
use KTXC\Service\SystemFirewallLogService;
|
||||
use KTXC\Service\SystemFirewallRuleService;
|
||||
use KTXC\Service\SystemFirewallStatusService;
|
||||
@@ -106,6 +108,58 @@ final class FirewallController extends ControllerAbstract
|
||||
return $this->ruleResponse($this->systemRules->fetchRule($ruleId));
|
||||
}
|
||||
|
||||
#[AuthenticatedRoute(
|
||||
'/firewall/rules',
|
||||
name: 'firewall.tenant.rules.create',
|
||||
methods: ['POST'],
|
||||
permissions: [TenantFirewallRuleService::PERMISSION_MANAGE],
|
||||
)]
|
||||
public function createTenantRule(
|
||||
Request $request,
|
||||
string $type,
|
||||
string $action,
|
||||
string $value,
|
||||
string $reason,
|
||||
?int $durationSeconds = null,
|
||||
bool $confirmCurrentIp = false
|
||||
): JsonResponse {
|
||||
return $this->mutationResponse(fn() => $this->tenantRules->createRule(
|
||||
$type,
|
||||
$action,
|
||||
$value,
|
||||
$reason,
|
||||
$durationSeconds,
|
||||
$request->getClientIp(),
|
||||
$confirmCurrentIp
|
||||
));
|
||||
}
|
||||
|
||||
#[AuthenticatedRoute(
|
||||
'/firewall/system/rules',
|
||||
name: 'firewall.system.rules.create',
|
||||
methods: ['POST'],
|
||||
permissions: [SystemFirewallRuleService::PERMISSION_MANAGE],
|
||||
)]
|
||||
public function createSystemRule(
|
||||
Request $request,
|
||||
string $type,
|
||||
string $action,
|
||||
string $value,
|
||||
string $reason,
|
||||
?int $durationSeconds = null,
|
||||
bool $confirmCurrentIp = false
|
||||
): JsonResponse {
|
||||
return $this->mutationResponse(fn() => $this->systemRules->createRule(
|
||||
$type,
|
||||
$action,
|
||||
$value,
|
||||
$reason,
|
||||
$durationSeconds,
|
||||
$request->getClientIp(),
|
||||
$confirmCurrentIp
|
||||
));
|
||||
}
|
||||
|
||||
#[AuthenticatedRoute(
|
||||
'/firewall/logs',
|
||||
name: 'firewall.tenant.logs.list',
|
||||
@@ -231,4 +285,21 @@ final class FirewallController extends ControllerAbstract
|
||||
return new JsonResponse(['error' => $error->getMessage()], JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private function mutationResponse(callable $mutation): JsonResponse
|
||||
{
|
||||
try {
|
||||
return new JsonResponse(['rule' => $mutation()], JsonResponse::HTTP_CREATED);
|
||||
} catch (FirewallRuleConflictException $error) {
|
||||
return new JsonResponse(['error' => [
|
||||
'code' => $error->conflictCode,
|
||||
'message' => $error->getMessage(),
|
||||
]], JsonResponse::HTTP_CONFLICT);
|
||||
} catch (\InvalidArgumentException $error) {
|
||||
return new JsonResponse(['error' => [
|
||||
'code' => 'invalid_firewall_rule',
|
||||
'message' => $error->getMessage(),
|
||||
]], JsonResponse::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Service;
|
||||
|
||||
final class FirewallRuleConflictException extends \RuntimeException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $conflictCode,
|
||||
string $message
|
||||
) {
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use KTXC\Models\Firewall\FirewallRuleObject;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Event\SecurityEvent;
|
||||
use KTXF\IpUtils;
|
||||
|
||||
final class FirewallRuleManager
|
||||
{
|
||||
@@ -84,6 +85,66 @@ final class FirewallRuleManager
|
||||
];
|
||||
}
|
||||
|
||||
public function createManualRule(
|
||||
FirewallRuleScope $scope,
|
||||
string $type,
|
||||
string $action,
|
||||
string $value,
|
||||
string $reason,
|
||||
?string $createdBy,
|
||||
?int $durationSeconds = null,
|
||||
?string $currentIp = null,
|
||||
bool $confirmCurrentIp = false
|
||||
): FirewallRuleObject {
|
||||
$reason = trim($reason);
|
||||
if ($reason === '' || strlen($reason) > 1000) {
|
||||
throw new \InvalidArgumentException('A rule reason containing 1-1000 bytes is required.');
|
||||
}
|
||||
if ($currentIp !== null) {
|
||||
$currentIp = FirewallRuleValidator::ipAddress($currentIp);
|
||||
}
|
||||
if (
|
||||
!$confirmCurrentIp
|
||||
&& $currentIp !== null
|
||||
&& $action === FirewallRuleObject::ACTION_BLOCK
|
||||
&& $this->matchesIp($type, $value, $currentIp)
|
||||
) {
|
||||
throw new FirewallRuleConflictException(
|
||||
'current_ip_confirmation_required',
|
||||
'This rule would block your current IP address. Explicit confirmation is required.'
|
||||
);
|
||||
}
|
||||
|
||||
return match ([$type, $action]) {
|
||||
[FirewallRuleObject::TYPE_IP, FirewallRuleObject::ACTION_BLOCK] =>
|
||||
$this->blockIp($scope, $value, $reason, $createdBy, $durationSeconds),
|
||||
[FirewallRuleObject::TYPE_IP, FirewallRuleObject::ACTION_ALLOW] =>
|
||||
$durationSeconds === null
|
||||
? $this->allowIp($scope, $value, $reason, $createdBy)
|
||||
: throw new \InvalidArgumentException('Temporary allow rules are not supported.'),
|
||||
[FirewallRuleObject::TYPE_IP_RANGE, FirewallRuleObject::ACTION_BLOCK] =>
|
||||
$durationSeconds === null
|
||||
? $this->blockIpRange($scope, $value, $reason, $createdBy)
|
||||
: throw new \InvalidArgumentException('Temporary CIDR rules are not supported.'),
|
||||
[FirewallRuleObject::TYPE_DEVICE, FirewallRuleObject::ACTION_BLOCK] =>
|
||||
$this->blockDevice($scope, $value, $reason, $createdBy, $durationSeconds),
|
||||
default => throw new \InvalidArgumentException('Unsupported firewall rule type and action combination.'),
|
||||
};
|
||||
}
|
||||
|
||||
private function matchesIp(string $type, string $value, string $currentIp): bool
|
||||
{
|
||||
if ($type === FirewallRuleObject::TYPE_IP) {
|
||||
$value = FirewallRuleValidator::ipAddress($value);
|
||||
return inet_pton($value) === inet_pton($currentIp);
|
||||
}
|
||||
if ($type === FirewallRuleObject::TYPE_IP_RANGE) {
|
||||
return IpUtils::checkIp($currentIp, FirewallRuleValidator::cidr($value));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function blockIp(
|
||||
FirewallRuleScope $scope,
|
||||
string $ipAddress,
|
||||
|
||||
@@ -41,6 +41,29 @@ final class SystemFirewallRuleService
|
||||
return $this->rules->fetch(FirewallRuleScope::system(), $ruleId);
|
||||
}
|
||||
|
||||
public function createRule(
|
||||
string $type,
|
||||
string $action,
|
||||
string $value,
|
||||
string $reason,
|
||||
?int $durationSeconds = null,
|
||||
?string $currentIp = null,
|
||||
bool $confirmCurrentIp = false
|
||||
): FirewallRuleObject {
|
||||
$this->requirePermission(self::PERMISSION_MANAGE);
|
||||
return $this->rules->createManualRule(
|
||||
FirewallRuleScope::system(),
|
||||
$type,
|
||||
$action,
|
||||
$value,
|
||||
$reason,
|
||||
$this->identity->identifier(),
|
||||
$durationSeconds,
|
||||
$currentIp,
|
||||
$confirmCurrentIp
|
||||
);
|
||||
}
|
||||
|
||||
public function blockIp(string $ip, ?string $reason = null, ?int $durationSeconds = null): FirewallRuleObject
|
||||
{
|
||||
$this->requirePermission(self::PERMISSION_MANAGE);
|
||||
|
||||
@@ -43,6 +43,29 @@ final class TenantFirewallRuleService
|
||||
return $this->rules->fetch($this->scope(), $ruleId);
|
||||
}
|
||||
|
||||
public function createRule(
|
||||
string $type,
|
||||
string $action,
|
||||
string $value,
|
||||
string $reason,
|
||||
?int $durationSeconds = null,
|
||||
?string $currentIp = null,
|
||||
bool $confirmCurrentIp = false
|
||||
): FirewallRuleObject {
|
||||
$this->requirePermission(self::PERMISSION_MANAGE);
|
||||
return $this->rules->createManualRule(
|
||||
$this->scope(),
|
||||
$type,
|
||||
$action,
|
||||
$value,
|
||||
$reason,
|
||||
$this->identity->identifier(),
|
||||
$durationSeconds,
|
||||
$currentIp,
|
||||
$confirmCurrentIp
|
||||
);
|
||||
}
|
||||
|
||||
public function effectivePolicy(): array
|
||||
{
|
||||
$this->requirePermission(self::PERMISSION_READ);
|
||||
|
||||
Reference in New Issue
Block a user