feat(firewall): add operational status reads
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -7,8 +7,10 @@ namespace KTXC\Controllers;
|
|||||||
use KTXC\Http\Response\JsonResponse;
|
use KTXC\Http\Response\JsonResponse;
|
||||||
use KTXC\Service\SystemFirewallLogService;
|
use KTXC\Service\SystemFirewallLogService;
|
||||||
use KTXC\Service\SystemFirewallRuleService;
|
use KTXC\Service\SystemFirewallRuleService;
|
||||||
|
use KTXC\Service\SystemFirewallStatusService;
|
||||||
use KTXC\Service\TenantFirewallLogService;
|
use KTXC\Service\TenantFirewallLogService;
|
||||||
use KTXC\Service\TenantFirewallRuleService;
|
use KTXC\Service\TenantFirewallRuleService;
|
||||||
|
use KTXC\Service\TenantFirewallStatusService;
|
||||||
use KTXF\Controller\ControllerAbstract;
|
use KTXF\Controller\ControllerAbstract;
|
||||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||||
|
|
||||||
@@ -19,6 +21,8 @@ final class FirewallController extends ControllerAbstract
|
|||||||
private readonly SystemFirewallRuleService $systemRules,
|
private readonly SystemFirewallRuleService $systemRules,
|
||||||
private readonly TenantFirewallLogService $tenantLogs,
|
private readonly TenantFirewallLogService $tenantLogs,
|
||||||
private readonly SystemFirewallLogService $systemLogs,
|
private readonly SystemFirewallLogService $systemLogs,
|
||||||
|
private readonly TenantFirewallStatusService $tenantStatus,
|
||||||
|
private readonly SystemFirewallStatusService $systemStatus,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +162,46 @@ final class FirewallController extends ControllerAbstract
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[AuthenticatedRoute(
|
||||||
|
'/firewall/metrics',
|
||||||
|
name: 'firewall.tenant.metrics.read',
|
||||||
|
permissions: [TenantFirewallLogService::PERMISSION_READ],
|
||||||
|
)]
|
||||||
|
public function tenantMetrics(?string $since = null): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->readResponse(fn(): array => $this->tenantStatus->metrics($since));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AuthenticatedRoute(
|
||||||
|
'/firewall/configuration',
|
||||||
|
name: 'firewall.tenant.configuration.read',
|
||||||
|
permissions: [TenantFirewallStatusService::PERMISSION_SETTINGS_READ],
|
||||||
|
)]
|
||||||
|
public function tenantConfiguration(): JsonResponse
|
||||||
|
{
|
||||||
|
return new JsonResponse($this->tenantStatus->configuration());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AuthenticatedRoute(
|
||||||
|
'/firewall/system/metrics',
|
||||||
|
name: 'firewall.system.metrics.read',
|
||||||
|
permissions: [SystemFirewallLogService::PERMISSION_READ],
|
||||||
|
)]
|
||||||
|
public function systemMetrics(?string $tenantId = null, ?string $since = null): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->readResponse(fn(): array => $this->systemStatus->metrics($tenantId, $since));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[AuthenticatedRoute(
|
||||||
|
'/firewall/system/maintenance',
|
||||||
|
name: 'firewall.system.maintenance.read',
|
||||||
|
permissions: [SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ],
|
||||||
|
)]
|
||||||
|
public function maintenanceStatus(): JsonResponse
|
||||||
|
{
|
||||||
|
return new JsonResponse($this->systemStatus->maintenanceStatus());
|
||||||
|
}
|
||||||
|
|
||||||
private function queryResponse(callable $query, string $limit, string $offset): JsonResponse
|
private function queryResponse(callable $query, string $limit, string $offset): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
@@ -178,4 +222,13 @@ final class FirewallController extends ControllerAbstract
|
|||||||
|
|
||||||
return new JsonResponse($rule);
|
return new JsonResponse($rule);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function readResponse(callable $read): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return new JsonResponse($read());
|
||||||
|
} catch (\InvalidArgumentException $error) {
|
||||||
|
return new JsonResponse(['error' => $error->getMessage()], JsonResponse::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ use KTXC\Console\Firewall\FirewallSetupCommand;
|
|||||||
use KTXC\Service\FirewallService;
|
use KTXC\Service\FirewallService;
|
||||||
use KTXC\Service\SystemFirewallLogService;
|
use KTXC\Service\SystemFirewallLogService;
|
||||||
use KTXC\Service\SystemFirewallRuleService;
|
use KTXC\Service\SystemFirewallRuleService;
|
||||||
|
use KTXC\Service\SystemFirewallStatusService;
|
||||||
use KTXC\Service\TenantFirewallLogService;
|
use KTXC\Service\TenantFirewallLogService;
|
||||||
use KTXC\Service\TenantFirewallRuleService;
|
use KTXC\Service\TenantFirewallRuleService;
|
||||||
|
use KTXC\Service\TenantFirewallStatusService;
|
||||||
use KTXF\Event\DeliveryMode;
|
use KTXF\Event\DeliveryMode;
|
||||||
use KTXF\Event\EventListenerRegistry;
|
use KTXF\Event\EventListenerRegistry;
|
||||||
use KTXF\Event\SecurityEvent;
|
use KTXF\Event\SecurityEvent;
|
||||||
@@ -142,6 +144,11 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
|||||||
'description' => 'View firewall security and audit logs owned by the current tenant',
|
'description' => 'View firewall security and audit logs owned by the current tenant',
|
||||||
'group' => 'Firewall Management'
|
'group' => 'Firewall Management'
|
||||||
],
|
],
|
||||||
|
TenantFirewallStatusService::PERMISSION_SETTINGS_READ => [
|
||||||
|
'label' => 'View Tenant Firewall Settings',
|
||||||
|
'description' => 'View effective firewall settings for the current tenant',
|
||||||
|
'group' => 'Firewall Management'
|
||||||
|
],
|
||||||
SystemFirewallRuleService::PERMISSION_READ => [
|
SystemFirewallRuleService::PERMISSION_READ => [
|
||||||
'label' => 'View System Firewall Rules',
|
'label' => 'View System Firewall Rules',
|
||||||
'description' => 'View firewall rules that apply to every tenant',
|
'description' => 'View firewall rules that apply to every tenant',
|
||||||
@@ -157,6 +164,11 @@ class Module extends ModuleInstanceAbstract implements ModuleConsoleInterface, M
|
|||||||
'description' => 'View firewall security and audit logs across tenants',
|
'description' => 'View firewall security and audit logs across tenants',
|
||||||
'group' => 'System Administration'
|
'group' => 'System Administration'
|
||||||
],
|
],
|
||||||
|
SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ => [
|
||||||
|
'label' => 'View Firewall Maintenance Status',
|
||||||
|
'description' => 'View the last firewall cleanup result and operational status',
|
||||||
|
'group' => 'System Administration'
|
||||||
|
],
|
||||||
'system.admin' => [
|
'system.admin' => [
|
||||||
'label' => 'System Administrator',
|
'label' => 'System Administrator',
|
||||||
'description' => 'Full system access (superuser)',
|
'description' => 'Full system access (superuser)',
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Service;
|
||||||
|
|
||||||
|
use KTXC\Stores\FirewallStore;
|
||||||
|
|
||||||
|
final class FirewallStatusService
|
||||||
|
{
|
||||||
|
public function __construct(private readonly FirewallStore $store)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tenantMetrics(string $tenantId, ?string $since = null): array
|
||||||
|
{
|
||||||
|
$sinceDate = self::date($since);
|
||||||
|
return [
|
||||||
|
'tenantId' => $tenantId,
|
||||||
|
'blockedRequests' => $this->store->countBlockedRequests($tenantId, $sinceDate),
|
||||||
|
'since' => $sinceDate?->format(\DateTimeInterface::ATOM),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function systemMetrics(?string $tenantId = null, ?string $since = null): array
|
||||||
|
{
|
||||||
|
if ($tenantId !== null && ($tenantId === '' || strlen($tenantId) > 128)) {
|
||||||
|
throw new \InvalidArgumentException('Invalid tenant filter.');
|
||||||
|
}
|
||||||
|
$sinceDate = self::date($since);
|
||||||
|
return [
|
||||||
|
'tenantId' => $tenantId,
|
||||||
|
'blockedRequests' => $this->store->countSystemBlockedRequests($tenantId, $sinceDate),
|
||||||
|
'since' => $sinceDate?->format(\DateTimeInterface::ATOM),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function maintenanceStatus(): array
|
||||||
|
{
|
||||||
|
return $this->store->maintenanceStatus() ?? [
|
||||||
|
'status' => 'never_run',
|
||||||
|
'startedAt' => null,
|
||||||
|
'completedAt' => null,
|
||||||
|
'result' => null,
|
||||||
|
'error' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function date(?string $value): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ($value === '' || strlen($value) > 255) {
|
||||||
|
throw new \InvalidArgumentException('Invalid since date filter.');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new \DateTimeImmutable($value);
|
||||||
|
} catch (\Exception) {
|
||||||
|
throw new \InvalidArgumentException('Invalid since date filter.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Service;
|
||||||
|
|
||||||
|
use KTXC\Context\IdentityContextInterface;
|
||||||
|
|
||||||
|
final class SystemFirewallStatusService
|
||||||
|
{
|
||||||
|
public const PERMISSION_MAINTENANCE_READ = 'firewall.system.maintenance.read';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly FirewallStatusService $status,
|
||||||
|
private readonly IdentityContextInterface $identity,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function metrics(?string $tenantId = null, ?string $since = null): array
|
||||||
|
{
|
||||||
|
$this->requirePermission(SystemFirewallLogService::PERMISSION_READ);
|
||||||
|
return $this->status->systemMetrics($tenantId, $since);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function maintenanceStatus(): array
|
||||||
|
{
|
||||||
|
$this->requirePermission(self::PERMISSION_MAINTENANCE_READ);
|
||||||
|
return $this->status->maintenanceStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requirePermission(string $permission): void
|
||||||
|
{
|
||||||
|
if (!$this->identity->hasPermission($permission)) {
|
||||||
|
throw new \RuntimeException("Missing required permission: {$permission}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXC\Service;
|
||||||
|
|
||||||
|
use KTXC\Context\IdentityContextInterface;
|
||||||
|
use KTXC\Context\TenantContextInterface;
|
||||||
|
|
||||||
|
final class TenantFirewallStatusService
|
||||||
|
{
|
||||||
|
public const PERMISSION_SETTINGS_READ = 'firewall.tenant.settings.read';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly FirewallStatusService $status,
|
||||||
|
private readonly TenantContextInterface $tenant,
|
||||||
|
private readonly IdentityContextInterface $identity,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function metrics(?string $since = null): array
|
||||||
|
{
|
||||||
|
$this->requirePermission(TenantFirewallLogService::PERMISSION_READ);
|
||||||
|
return $this->status->tenantMetrics($this->tenant->requireIdentifier(), $since);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configuration(): array
|
||||||
|
{
|
||||||
|
$this->requirePermission(self::PERMISSION_SETTINGS_READ);
|
||||||
|
return $this->tenant->configuration()?->firewall()->jsonSerialize()
|
||||||
|
?? [
|
||||||
|
'enabled' => true,
|
||||||
|
'maxAuthFailures' => 5,
|
||||||
|
'authFailureWindow' => 300,
|
||||||
|
'autoBlockDuration' => 3600,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function requirePermission(string $permission): void
|
||||||
|
{
|
||||||
|
if (!$this->identity->hasPermission($permission)) {
|
||||||
|
throw new \RuntimeException("Missing required permission: {$permission}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -576,6 +576,21 @@ class FirewallStore
|
|||||||
return $this->dataStore->selectCollection(self::LOGS_COLLECTION)->countDocuments($filter);
|
return $this->dataStore->selectCollection(self::LOGS_COLLECTION)->countDocuments($filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countSystemBlockedRequests(
|
||||||
|
?string $tenantId = null,
|
||||||
|
?\DateTimeImmutable $since = null
|
||||||
|
): int {
|
||||||
|
$filter = ['result' => FirewallLogObject::RESULT_BLOCKED];
|
||||||
|
if ($tenantId !== null) {
|
||||||
|
$filter['tenantId'] = $tenantId;
|
||||||
|
}
|
||||||
|
if ($since !== null) {
|
||||||
|
$filter['timestamp'] = ['$gte' => self::bsonDate($since)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dataStore->selectCollection(self::LOGS_COLLECTION)->countDocuments($filter);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up old logs
|
* Clean up old logs
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -532,6 +532,9 @@ class FirewallStoreTest extends TestCase
|
|||||||
self::assertSame('tenant-a', $tenant['items'][0]->getTenantId());
|
self::assertSame('tenant-a', $tenant['items'][0]->getTenantId());
|
||||||
self::assertSame(3, $system['total']);
|
self::assertSame(3, $system['total']);
|
||||||
self::assertCount(2, $system['items']);
|
self::assertCount(2, $system['items']);
|
||||||
|
self::assertSame(2, $this->store->countSystemBlockedRequests('tenant-a'));
|
||||||
|
self::assertSame(3, $this->store->countSystemBlockedRequests());
|
||||||
|
self::assertSame(2, $this->store->countSystemBlockedRequests(null, new \DateTimeImmutable('-1 day')));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function rule(
|
private function rule(
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ use KTXC\Context\TenantContextInterface;
|
|||||||
use KTXC\Controllers\FirewallController;
|
use KTXC\Controllers\FirewallController;
|
||||||
use KTXC\Service\FirewallRuleCache;
|
use KTXC\Service\FirewallRuleCache;
|
||||||
use KTXC\Service\FirewallRuleManager;
|
use KTXC\Service\FirewallRuleManager;
|
||||||
|
use KTXC\Service\FirewallStatusService;
|
||||||
use KTXC\Service\FirewallLogService;
|
use KTXC\Service\FirewallLogService;
|
||||||
use KTXC\Service\SystemFirewallLogService;
|
use KTXC\Service\SystemFirewallLogService;
|
||||||
use KTXC\Service\SystemFirewallRuleService;
|
use KTXC\Service\SystemFirewallRuleService;
|
||||||
|
use KTXC\Service\SystemFirewallStatusService;
|
||||||
use KTXC\Service\TenantFirewallLogService;
|
use KTXC\Service\TenantFirewallLogService;
|
||||||
use KTXC\Service\TenantFirewallRuleService;
|
use KTXC\Service\TenantFirewallRuleService;
|
||||||
|
use KTXC\Service\TenantFirewallStatusService;
|
||||||
use KTXC\Stores\FirewallStore;
|
use KTXC\Stores\FirewallStore;
|
||||||
use KTXF\Event\EventDispatcherInterface;
|
use KTXF\Event\EventDispatcherInterface;
|
||||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||||
@@ -43,7 +46,9 @@ final class FirewallControllerTest extends TestCase
|
|||||||
new TenantFirewallRuleService($manager, $tenant, $identity),
|
new TenantFirewallRuleService($manager, $tenant, $identity),
|
||||||
new SystemFirewallRuleService($manager, $identity),
|
new SystemFirewallRuleService($manager, $identity),
|
||||||
new TenantFirewallLogService(new FirewallLogService($this->store), $tenant, $identity),
|
new TenantFirewallLogService(new FirewallLogService($this->store), $tenant, $identity),
|
||||||
new SystemFirewallLogService(new FirewallLogService($this->store), $identity)
|
new SystemFirewallLogService(new FirewallLogService($this->store), $identity),
|
||||||
|
new TenantFirewallStatusService(new FirewallStatusService($this->store), $tenant, $identity),
|
||||||
|
new SystemFirewallStatusService(new FirewallStatusService($this->store), $identity)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +77,18 @@ final class FirewallControllerTest extends TestCase
|
|||||||
self::assertSame(400, $this->controller->systemRules(limit: '101')->getStatusCode());
|
self::assertSame(400, $this->controller->systemRules(limit: '101')->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[TestDox('Metric endpoints return scoped counts and stable validation errors')]
|
||||||
|
public function testMetrics(): void
|
||||||
|
{
|
||||||
|
$this->store->method('countBlockedRequests')->willReturn(4);
|
||||||
|
|
||||||
|
$response = $this->controller->tenantMetrics();
|
||||||
|
$data = json_decode($response->getContent(), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
self::assertSame(4, $data['blockedRequests']);
|
||||||
|
self::assertSame(400, $this->controller->systemMetrics(since: 'not-a-date')->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
#[TestDox('Every rule endpoint declares its scope-specific read permission')]
|
#[TestDox('Every rule endpoint declares its scope-specific read permission')]
|
||||||
public function testRoutePermissions(): void
|
public function testRoutePermissions(): void
|
||||||
{
|
{
|
||||||
@@ -83,6 +100,10 @@ final class FirewallControllerTest extends TestCase
|
|||||||
'systemRule' => SystemFirewallRuleService::PERMISSION_READ,
|
'systemRule' => SystemFirewallRuleService::PERMISSION_READ,
|
||||||
'tenantLogs' => TenantFirewallLogService::PERMISSION_READ,
|
'tenantLogs' => TenantFirewallLogService::PERMISSION_READ,
|
||||||
'systemLogs' => SystemFirewallLogService::PERMISSION_READ,
|
'systemLogs' => SystemFirewallLogService::PERMISSION_READ,
|
||||||
|
'tenantMetrics' => TenantFirewallLogService::PERMISSION_READ,
|
||||||
|
'tenantConfiguration' => TenantFirewallStatusService::PERMISSION_SETTINGS_READ,
|
||||||
|
'systemMetrics' => SystemFirewallLogService::PERMISSION_READ,
|
||||||
|
'maintenanceStatus' => SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($expected as $method => $permission) {
|
foreach ($expected as $method => $permission) {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ use KTXC\Service\FirewallService;
|
|||||||
use KTXC\Service\SystemFirewallRuleService;
|
use KTXC\Service\SystemFirewallRuleService;
|
||||||
use KTXC\Service\SystemFirewallLogService;
|
use KTXC\Service\SystemFirewallLogService;
|
||||||
use KTXC\Service\TenantFirewallLogService;
|
use KTXC\Service\TenantFirewallLogService;
|
||||||
|
use KTXC\Service\TenantFirewallStatusService;
|
||||||
|
use KTXC\Service\SystemFirewallStatusService;
|
||||||
use KTXC\Service\TenantFirewallRuleService;
|
use KTXC\Service\TenantFirewallRuleService;
|
||||||
use KTXF\Event\DeliveryMode;
|
use KTXF\Event\DeliveryMode;
|
||||||
use KTXF\Event\EventListenerRegistry;
|
use KTXF\Event\EventListenerRegistry;
|
||||||
@@ -78,5 +80,7 @@ final class CoreModuleTest extends TestCase
|
|||||||
self::assertArrayHasKey(TenantFirewallRuleService::PERMISSION_MANAGE, $permissions);
|
self::assertArrayHasKey(TenantFirewallRuleService::PERMISSION_MANAGE, $permissions);
|
||||||
self::assertArrayHasKey(TenantFirewallLogService::PERMISSION_READ, $permissions);
|
self::assertArrayHasKey(TenantFirewallLogService::PERMISSION_READ, $permissions);
|
||||||
self::assertArrayHasKey(SystemFirewallLogService::PERMISSION_READ, $permissions);
|
self::assertArrayHasKey(SystemFirewallLogService::PERMISSION_READ, $permissions);
|
||||||
|
self::assertArrayHasKey(TenantFirewallStatusService::PERMISSION_SETTINGS_READ, $permissions);
|
||||||
|
self::assertArrayHasKey(SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ, $permissions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace KTXT\Unit\Service;
|
||||||
|
|
||||||
|
use KTXC\Context\IdentityContextInterface;
|
||||||
|
use KTXC\Context\TenantContextInterface;
|
||||||
|
use KTXC\Models\Tenant\TenantConfiguration;
|
||||||
|
use KTXC\Service\FirewallStatusService;
|
||||||
|
use KTXC\Service\SystemFirewallLogService;
|
||||||
|
use KTXC\Service\SystemFirewallStatusService;
|
||||||
|
use KTXC\Service\TenantFirewallLogService;
|
||||||
|
use KTXC\Service\TenantFirewallStatusService;
|
||||||
|
use KTXC\Stores\FirewallStore;
|
||||||
|
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
|
||||||
|
use PHPUnit\Framework\Attributes\TestDox;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
#[AllowMockObjectsWithoutExpectations]
|
||||||
|
final class FirewallStatusServicesTest extends TestCase
|
||||||
|
{
|
||||||
|
#[TestDox('Tenant metrics and configuration derive their tenant context')]
|
||||||
|
public function testTenantStatus(): void
|
||||||
|
{
|
||||||
|
$store = $this->createMock(FirewallStore::class);
|
||||||
|
$tenant = $this->createStub(TenantContextInterface::class);
|
||||||
|
$tenant->method('requireIdentifier')->willReturn('tenant-a');
|
||||||
|
$tenant->method('configuration')->willReturn(
|
||||||
|
(new TenantConfiguration())->jsonDeserialize([
|
||||||
|
'firewall' => ['enabled' => false, 'maxAuthFailures' => 8],
|
||||||
|
])
|
||||||
|
);
|
||||||
|
$identity = $this->createStub(IdentityContextInterface::class);
|
||||||
|
$identity->method('hasPermission')->willReturn(true);
|
||||||
|
$store->expects(self::once())
|
||||||
|
->method('countBlockedRequests')
|
||||||
|
->with('tenant-a', self::isInstanceOf(\DateTimeImmutable::class))
|
||||||
|
->willReturn(7);
|
||||||
|
$service = new TenantFirewallStatusService(new FirewallStatusService($store), $tenant, $identity);
|
||||||
|
|
||||||
|
self::assertSame(7, $service->metrics('2026-08-01T00:00:00+00:00')['blockedRequests']);
|
||||||
|
self::assertFalse($service->configuration()['enabled']);
|
||||||
|
self::assertSame(8, $service->configuration()['maxAuthFailures']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[TestDox('System metrics and maintenance status expose operational state')]
|
||||||
|
public function testSystemStatus(): void
|
||||||
|
{
|
||||||
|
$store = $this->createMock(FirewallStore::class);
|
||||||
|
$identity = $this->createStub(IdentityContextInterface::class);
|
||||||
|
$identity->method('hasPermission')->willReturn(true);
|
||||||
|
$store->expects(self::once())
|
||||||
|
->method('countSystemBlockedRequests')
|
||||||
|
->with('tenant-a', null)
|
||||||
|
->willReturn(12);
|
||||||
|
$store->method('maintenanceStatus')->willReturn(['status' => 'success']);
|
||||||
|
$service = new SystemFirewallStatusService(new FirewallStatusService($store), $identity);
|
||||||
|
|
||||||
|
self::assertSame(12, $service->metrics('tenant-a')['blockedRequests']);
|
||||||
|
self::assertSame('success', $service->maintenanceStatus()['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[TestDox('Maintenance reports an explicit state before its first run')]
|
||||||
|
public function testNeverRunStatus(): void
|
||||||
|
{
|
||||||
|
$store = $this->createStub(FirewallStore::class);
|
||||||
|
|
||||||
|
self::assertSame('never_run', (new FirewallStatusService($store))->maintenanceStatus()['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[TestDox('Status reads require their dedicated permissions')]
|
||||||
|
public function testPermissions(): void
|
||||||
|
{
|
||||||
|
$store = $this->createMock(FirewallStore::class);
|
||||||
|
$identity = $this->createStub(IdentityContextInterface::class);
|
||||||
|
$identity->method('hasPermission')->willReturn(false);
|
||||||
|
$tenant = $this->createStub(TenantContextInterface::class);
|
||||||
|
$store->expects(self::never())->method('countBlockedRequests');
|
||||||
|
$store->expects(self::never())->method('maintenanceStatus');
|
||||||
|
|
||||||
|
try {
|
||||||
|
(new TenantFirewallStatusService(new FirewallStatusService($store), $tenant, $identity))->metrics();
|
||||||
|
self::fail('Tenant metrics should be rejected.');
|
||||||
|
} catch (\RuntimeException $error) {
|
||||||
|
self::assertStringContainsString(TenantFirewallLogService::PERMISSION_READ, $error->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->expectExceptionMessage(SystemFirewallStatusService::PERMISSION_MAINTENANCE_READ);
|
||||||
|
(new SystemFirewallStatusService(new FirewallStatusService($store), $identity))->maintenanceStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[TestDox('Metrics reject invalid dates before querying storage')]
|
||||||
|
public function testMetricValidation(): void
|
||||||
|
{
|
||||||
|
$store = $this->createMock(FirewallStore::class);
|
||||||
|
$store->expects(self::never())->method('countSystemBlockedRequests');
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
|
||||||
|
(new FirewallStatusService($store))->systemMetrics(null, 'not-a-date');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user