feat(firewall): add tenant and system rule scopes
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXT\Unit\Service;
|
||||
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
use KTXC\Models\Firewall\FirewallRuleObject;
|
||||
use KTXC\Service\FirewallService;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
|
||||
use PHPUnit\Framework\Attributes\TestDox;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
#[AllowMockObjectsWithoutExpectations]
|
||||
class FirewallServiceTest extends TestCase
|
||||
{
|
||||
private FirewallStore&MockObject $store;
|
||||
private TenantContextInterface&MockObject $tenantContext;
|
||||
private EventDispatcherInterface&MockObject $events;
|
||||
private FirewallService $service;
|
||||
private string $currentTenant;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->store = $this->createMock(FirewallStore::class);
|
||||
$this->tenantContext = $this->createMock(TenantContextInterface::class);
|
||||
$this->events = $this->createMock(EventDispatcherInterface::class);
|
||||
$this->currentTenant = 'tenant-a';
|
||||
$this->tenantContext->method('identifier')->willReturnCallback(
|
||||
fn(): string => $this->currentTenant
|
||||
);
|
||||
$this->tenantContext->method('configuration')->willReturn(null);
|
||||
$this->service = new FirewallService($this->store, $this->tenantContext, $this->events);
|
||||
}
|
||||
|
||||
#[TestDox('System blocks cannot be overridden by tenant allows')]
|
||||
public function testSystemBlockPrecedence(): void
|
||||
{
|
||||
$systemBlock = $this->rule(
|
||||
'system-block',
|
||||
FirewallRuleObject::SCOPE_SYSTEM,
|
||||
FirewallRuleObject::ACTION_BLOCK,
|
||||
null
|
||||
);
|
||||
$tenantAllow = $this->rule(
|
||||
'tenant-allow',
|
||||
FirewallRuleObject::SCOPE_TENANT,
|
||||
FirewallRuleObject::ACTION_ALLOW,
|
||||
'tenant-a'
|
||||
);
|
||||
|
||||
$this->store->expects($this->once())
|
||||
->method('listApplicableRules')
|
||||
->with('tenant-a')
|
||||
->willReturn([$tenantAllow, $systemBlock]);
|
||||
$this->events->expects($this->once())->method('dispatch');
|
||||
|
||||
$result = $this->service->analyze('203.0.113.10');
|
||||
|
||||
self::assertTrue($result->isBlocked());
|
||||
self::assertSame('system-block', $result->ruleId);
|
||||
}
|
||||
|
||||
#[TestDox('Tenant blocks override system allows')]
|
||||
public function testTenantBlockPrecedence(): void
|
||||
{
|
||||
$systemAllow = $this->rule(
|
||||
'system-allow',
|
||||
FirewallRuleObject::SCOPE_SYSTEM,
|
||||
FirewallRuleObject::ACTION_ALLOW,
|
||||
null
|
||||
);
|
||||
$tenantBlock = $this->rule(
|
||||
'tenant-block',
|
||||
FirewallRuleObject::SCOPE_TENANT,
|
||||
FirewallRuleObject::ACTION_BLOCK,
|
||||
'tenant-a'
|
||||
);
|
||||
|
||||
$this->store->method('listApplicableRules')->willReturn([$systemAllow, $tenantBlock]);
|
||||
$this->events->expects($this->once())->method('dispatch');
|
||||
|
||||
$result = $this->service->analyze('203.0.113.10');
|
||||
|
||||
self::assertTrue($result->isBlocked());
|
||||
self::assertSame('tenant-block', $result->ruleId);
|
||||
}
|
||||
|
||||
#[TestDox('Rule caches are isolated by tenant')]
|
||||
public function testTenantCacheIsolation(): void
|
||||
{
|
||||
$this->store->expects($this->exactly(2))
|
||||
->method('listApplicableRules')
|
||||
->willReturnCallback(static fn(string $tenantId): array => [
|
||||
(new FirewallRuleObject())
|
||||
->setId($tenantId)
|
||||
->setScope(FirewallRuleObject::SCOPE_TENANT)
|
||||
->setTenantId($tenantId)
|
||||
->setType(FirewallRuleObject::TYPE_IP)
|
||||
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
||||
->setValue('203.0.113.10'),
|
||||
]);
|
||||
|
||||
self::assertSame('tenant-a', $this->service->analyze('203.0.113.10')->ruleId);
|
||||
$this->currentTenant = 'tenant-b';
|
||||
self::assertSame('tenant-b', $this->service->analyze('203.0.113.10')->ruleId);
|
||||
}
|
||||
|
||||
#[TestDox('New IP blocks are explicitly tenant-scoped')]
|
||||
public function testIpBlockScope(): void
|
||||
{
|
||||
$this->store->method('findExactIpRule')->willReturn(null);
|
||||
$this->store->expects($this->once())
|
||||
->method('depositRule')
|
||||
->with(self::callback(static function (FirewallRuleObject $rule): bool {
|
||||
return $rule->getScope() === FirewallRuleObject::SCOPE_TENANT
|
||||
&& $rule->getTenantId() === 'tenant-a';
|
||||
}))
|
||||
->willReturnArgument(0);
|
||||
|
||||
$rule = $this->service->blockIp('203.0.113.10');
|
||||
|
||||
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $rule->getScope());
|
||||
self::assertSame('tenant-a', $rule->getTenantId());
|
||||
}
|
||||
|
||||
private function rule(
|
||||
string $id,
|
||||
string $scope,
|
||||
string $action,
|
||||
?string $tenantId
|
||||
): FirewallRuleObject {
|
||||
return (new FirewallRuleObject())
|
||||
->setId($id)
|
||||
->setScope($scope)
|
||||
->setTenantId($tenantId)
|
||||
->setType(FirewallRuleObject::TYPE_IP)
|
||||
->setAction($action)
|
||||
->setValue('203.0.113.10')
|
||||
->setReason($id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user