feat(firewall): add scoped log administration reads
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -488,6 +488,52 @@ class FirewallStoreTest extends TestCase
|
||||
self::assertSame('tenant-disabled', $disabled['items'][0]->getReason());
|
||||
}
|
||||
|
||||
#[TestDox('Administrative log queries enforce tenant scope and supported filters')]
|
||||
public function testAdministrativeLogQuery(): void
|
||||
{
|
||||
$matching = (new FirewallLogObject())
|
||||
->setTenantId('tenant-a')
|
||||
->setIpAddress('203.0.113.10')
|
||||
->setEventType(FirewallLogObject::EVENT_RULE_MATCH)
|
||||
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
||||
->setRuleId('rule-123')
|
||||
->setRuleScope(FirewallRuleObject::SCOPE_TENANT)
|
||||
->setTimestamp(new \DateTimeImmutable('-1 hour'));
|
||||
$otherTenant = (new FirewallLogObject())
|
||||
->setTenantId('tenant-b')
|
||||
->setIpAddress('203.0.113.10')
|
||||
->setEventType(FirewallLogObject::EVENT_RULE_MATCH)
|
||||
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
||||
->setRuleId('rule-123')
|
||||
->setRuleScope(FirewallRuleObject::SCOPE_TENANT)
|
||||
->setTimestamp(new \DateTimeImmutable('-1 hour'));
|
||||
$tooOld = (new FirewallLogObject())
|
||||
->setTenantId('tenant-a')
|
||||
->setEventType(FirewallLogObject::EVENT_RULE_MATCH)
|
||||
->setResult(FirewallLogObject::RESULT_BLOCKED)
|
||||
->setRuleId('rule-123')
|
||||
->setTimestamp(new \DateTimeImmutable('-3 days'));
|
||||
foreach ([$matching, $otherTenant, $tooOld] as $log) {
|
||||
$this->store->createLog($log);
|
||||
}
|
||||
|
||||
$tenant = $this->store->queryTenantLogs('tenant-a', [
|
||||
'ipAddress' => '203.0.113.10',
|
||||
'eventType' => FirewallLogObject::EVENT_RULE_MATCH,
|
||||
'result' => FirewallLogObject::RESULT_BLOCKED,
|
||||
'ruleId' => 'rule-123',
|
||||
'ruleScope' => FirewallRuleObject::SCOPE_TENANT,
|
||||
'from' => new \DateTimeImmutable('-1 day'),
|
||||
'to' => new \DateTimeImmutable(),
|
||||
], 50, 0);
|
||||
$system = $this->store->querySystemLogs(null, [], 2, 0);
|
||||
|
||||
self::assertSame(1, $tenant['total']);
|
||||
self::assertSame('tenant-a', $tenant['items'][0]->getTenantId());
|
||||
self::assertSame(3, $system['total']);
|
||||
self::assertCount(2, $system['items']);
|
||||
}
|
||||
|
||||
private function rule(
|
||||
string $reason,
|
||||
string $scope,
|
||||
|
||||
@@ -9,7 +9,10 @@ use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Controllers\FirewallController;
|
||||
use KTXC\Service\FirewallRuleCache;
|
||||
use KTXC\Service\FirewallRuleManager;
|
||||
use KTXC\Service\FirewallLogService;
|
||||
use KTXC\Service\SystemFirewallLogService;
|
||||
use KTXC\Service\SystemFirewallRuleService;
|
||||
use KTXC\Service\TenantFirewallLogService;
|
||||
use KTXC\Service\TenantFirewallRuleService;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
@@ -38,7 +41,9 @@ final class FirewallControllerTest extends TestCase
|
||||
);
|
||||
$this->controller = new FirewallController(
|
||||
new TenantFirewallRuleService($manager, $tenant, $identity),
|
||||
new SystemFirewallRuleService($manager, $identity)
|
||||
new SystemFirewallRuleService($manager, $identity),
|
||||
new TenantFirewallLogService(new FirewallLogService($this->store), $tenant, $identity),
|
||||
new SystemFirewallLogService(new FirewallLogService($this->store), $identity)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,6 +81,8 @@ final class FirewallControllerTest extends TestCase
|
||||
'effectivePolicy' => TenantFirewallRuleService::PERMISSION_READ,
|
||||
'systemRules' => SystemFirewallRuleService::PERMISSION_READ,
|
||||
'systemRule' => SystemFirewallRuleService::PERMISSION_READ,
|
||||
'tenantLogs' => TenantFirewallLogService::PERMISSION_READ,
|
||||
'systemLogs' => SystemFirewallLogService::PERMISSION_READ,
|
||||
];
|
||||
|
||||
foreach ($expected as $method => $permission) {
|
||||
|
||||
@@ -10,6 +10,8 @@ use KTXC\Console\Event\EventsDebugCommand;
|
||||
use KTXC\Module\Module;
|
||||
use KTXC\Service\FirewallService;
|
||||
use KTXC\Service\SystemFirewallRuleService;
|
||||
use KTXC\Service\SystemFirewallLogService;
|
||||
use KTXC\Service\TenantFirewallLogService;
|
||||
use KTXC\Service\TenantFirewallRuleService;
|
||||
use KTXF\Event\DeliveryMode;
|
||||
use KTXF\Event\EventListenerRegistry;
|
||||
@@ -74,5 +76,7 @@ final class CoreModuleTest extends TestCase
|
||||
self::assertArrayHasKey(SystemFirewallRuleService::PERMISSION_MANAGE, $permissions);
|
||||
self::assertArrayHasKey(TenantFirewallRuleService::PERMISSION_READ, $permissions);
|
||||
self::assertArrayHasKey(TenantFirewallRuleService::PERMISSION_MANAGE, $permissions);
|
||||
self::assertArrayHasKey(TenantFirewallLogService::PERMISSION_READ, $permissions);
|
||||
self::assertArrayHasKey(SystemFirewallLogService::PERMISSION_READ, $permissions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXT\Unit\Service;
|
||||
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Models\Firewall\FirewallLogObject;
|
||||
use KTXC\Service\FirewallLogService;
|
||||
use KTXC\Service\SystemFirewallLogService;
|
||||
use KTXC\Service\TenantFirewallLogService;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[AllowMockObjectsWithoutExpectations]
|
||||
final class FirewallLogServicesTest extends TestCase
|
||||
{
|
||||
#[TestDox('Tenant log reads derive tenant ownership and validated filters')]
|
||||
public function testTenantQuery(): void
|
||||
{
|
||||
$store = $this->createMock(FirewallStore::class);
|
||||
$tenant = $this->createStub(TenantContextInterface::class);
|
||||
$tenant->method('requireIdentifier')->willReturn('tenant-a');
|
||||
$identity = $this->createStub(IdentityContextInterface::class);
|
||||
$identity->method('hasPermission')->willReturn(true);
|
||||
$store->expects(self::once())
|
||||
->method('queryTenantLogs')
|
||||
->with(
|
||||
'tenant-a',
|
||||
self::callback(static fn(array $filters): bool =>
|
||||
$filters['eventType'] === FirewallLogObject::EVENT_AUTH_FAILURE
|
||||
&& $filters['from'] instanceof \DateTimeImmutable
|
||||
),
|
||||
25,
|
||||
10
|
||||
)
|
||||
->willReturn(['items' => [], 'total' => 0, 'limit' => 25, 'offset' => 10]);
|
||||
$service = new TenantFirewallLogService(new FirewallLogService($store), $tenant, $identity);
|
||||
|
||||
self::assertSame(0, $service->query([
|
||||
'eventType' => FirewallLogObject::EVENT_AUTH_FAILURE,
|
||||
'from' => '2026-08-01T00:00:00+00:00',
|
||||
], 25, 10)['total']);
|
||||
}
|
||||
|
||||
#[TestDox('System log reads may filter one tenant without changing ownership')]
|
||||
public function testSystemQuery(): void
|
||||
{
|
||||
$store = $this->createMock(FirewallStore::class);
|
||||
$identity = $this->createStub(IdentityContextInterface::class);
|
||||
$identity->method('hasPermission')->willReturn(true);
|
||||
$store->expects(self::once())
|
||||
->method('querySystemLogs')
|
||||
->with('tenant-a', self::isArray(), 50, 0)
|
||||
->willReturn(['items' => [], 'total' => 0, 'limit' => 50, 'offset' => 0]);
|
||||
$service = new SystemFirewallLogService(new FirewallLogService($store), $identity);
|
||||
|
||||
self::assertSame(0, $service->query('tenant-a', [], 50, 0)['total']);
|
||||
}
|
||||
|
||||
#[TestDox('Log queries reject invalid filters before database access')]
|
||||
public function testValidation(): void
|
||||
{
|
||||
$store = $this->createMock(FirewallStore::class);
|
||||
$store->expects(self::never())->method('queryTenantLogs');
|
||||
$query = new FirewallLogService($store);
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
|
||||
$query->tenant('tenant-a', ['ipAddress' => 'not-an-ip'], 50, 0);
|
||||
}
|
||||
|
||||
#[TestDox('Log boundaries enforce dedicated read 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('queryTenantLogs');
|
||||
$store->expects(self::never())->method('querySystemLogs');
|
||||
|
||||
try {
|
||||
(new TenantFirewallLogService(new FirewallLogService($store), $tenant, $identity))->query([]);
|
||||
self::fail('Tenant log read should be rejected.');
|
||||
} catch (\RuntimeException $error) {
|
||||
self::assertStringContainsString(TenantFirewallLogService::PERMISSION_READ, $error->getMessage());
|
||||
}
|
||||
|
||||
$this->expectExceptionMessage(SystemFirewallLogService::PERMISSION_READ);
|
||||
(new SystemFirewallLogService(new FirewallLogService($store), $identity))->query(null, []);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user