266b364c93
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
165 lines
6.0 KiB
PHP
165 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXT\Integration\Stores;
|
|
|
|
use KTXC\Context\TenantContextInterface;
|
|
use KTXC\Db\DataStore;
|
|
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\TestCase;
|
|
|
|
#[AllowMockObjectsWithoutExpectations]
|
|
class FirewallStoreTest extends TestCase
|
|
{
|
|
private DataStore $dataStore;
|
|
private FirewallStore $store;
|
|
private bool $databaseAvailable = false;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$system = require dirname(__DIR__, 4).'/config/system.php';
|
|
$database = $system['database'];
|
|
$database['database'] = sprintf('ktrix_firewall_test_%d', getmypid());
|
|
|
|
$this->dataStore = new DataStore($database);
|
|
try {
|
|
$this->dataStore->getDatabase()->drop();
|
|
$this->databaseAvailable = true;
|
|
} catch (\MongoDB\Driver\Exception\Exception $error) {
|
|
self::markTestSkipped('MongoDB is unavailable: '.$error->getMessage());
|
|
}
|
|
$this->store = new FirewallStore($this->dataStore);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if ($this->databaseAvailable) {
|
|
$this->dataStore->getDatabase()->drop();
|
|
}
|
|
}
|
|
|
|
#[TestDox('Applicable rules contain system rules and only the requested tenant rules')]
|
|
public function testApplicableScopes(): void
|
|
{
|
|
$tenantA = $this->rule('tenant-a', FirewallRuleObject::SCOPE_TENANT, 'tenant-a');
|
|
$tenantB = $this->rule('tenant-b', FirewallRuleObject::SCOPE_TENANT, 'tenant-b');
|
|
$system = $this->rule('system', FirewallRuleObject::SCOPE_SYSTEM);
|
|
$expiredSystem = $this->rule('expired-system', FirewallRuleObject::SCOPE_SYSTEM)
|
|
->setExpiresAt(new \DateTimeImmutable('-1 minute'));
|
|
$disabledSystem = $this->rule('disabled-system', FirewallRuleObject::SCOPE_SYSTEM)
|
|
->setEnabled(false);
|
|
|
|
foreach ([$tenantA, $tenantB, $system, $expiredSystem, $disabledSystem] as $rule) {
|
|
$this->store->depositRule($rule);
|
|
}
|
|
|
|
$rules = $this->store->listApplicableRules('tenant-a');
|
|
$reasons = array_map(static fn(FirewallRuleObject $rule): ?string => $rule->getReason(), $rules);
|
|
sort($reasons);
|
|
|
|
self::assertSame(['system', 'tenant-a'], $reasons);
|
|
}
|
|
|
|
#[TestDox('Tenant rule listings cannot expose system or other tenant rules')]
|
|
public function testTenantListingIsolation(): void
|
|
{
|
|
$this->store->depositRule($this->rule('tenant-a', FirewallRuleObject::SCOPE_TENANT, 'tenant-a'));
|
|
$this->store->depositRule($this->rule('tenant-b', FirewallRuleObject::SCOPE_TENANT, 'tenant-b'));
|
|
$this->store->depositRule($this->rule('system', FirewallRuleObject::SCOPE_SYSTEM));
|
|
|
|
$rules = $this->store->listRules('tenant-a');
|
|
|
|
self::assertCount(1, $rules);
|
|
self::assertSame('tenant-a', $rules[0]->getReason());
|
|
self::assertSame(FirewallRuleObject::SCOPE_TENANT, $rules[0]->getScope());
|
|
}
|
|
|
|
#[TestDox('Expired exact IP rules do not suppress a replacement block')]
|
|
public function testExpiredBlockReplacement(): void
|
|
{
|
|
$expired = $this->rule('expired', FirewallRuleObject::SCOPE_TENANT, 'tenant-a')
|
|
->setExpiresAt(new \DateTimeImmutable('-1 minute'));
|
|
$this->store->depositRule($expired);
|
|
|
|
self::assertNull($this->store->findExactIpRule(
|
|
'tenant-a',
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK
|
|
));
|
|
|
|
$tenantContext = $this->createMock(TenantContextInterface::class);
|
|
$tenantContext->method('identifier')->willReturn('tenant-a');
|
|
$tenantContext->method('configuration')->willReturn(null);
|
|
$events = $this->createMock(EventDispatcherInterface::class);
|
|
$service = new FirewallService($this->store, $tenantContext, $events);
|
|
|
|
$replacement = $service->blockIp('203.0.113.10', durationSeconds: 300);
|
|
|
|
self::assertFalse($replacement->isExpired());
|
|
self::assertNotSame($expired->getId(), $replacement->getId());
|
|
self::assertSame(
|
|
$replacement->getId(),
|
|
$this->store->findExactIpRule(
|
|
'tenant-a',
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK
|
|
)?->getId()
|
|
);
|
|
self::assertCount(2, $this->store->listRules('tenant-a', false));
|
|
}
|
|
|
|
#[TestDox('Exact IP lookups respect tenant and system scope')]
|
|
public function testExactLookupScope(): void
|
|
{
|
|
$tenant = $this->rule('tenant', FirewallRuleObject::SCOPE_TENANT, 'tenant-a');
|
|
$system = $this->rule('system', FirewallRuleObject::SCOPE_SYSTEM);
|
|
$this->store->depositRule($tenant);
|
|
$this->store->depositRule($system);
|
|
|
|
self::assertSame(
|
|
$tenant->getId(),
|
|
$this->store->findExactIpRule(
|
|
'tenant-a',
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK
|
|
)?->getId()
|
|
);
|
|
self::assertSame(
|
|
$system->getId(),
|
|
$this->store->findExactIpRule(
|
|
null,
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK,
|
|
FirewallRuleObject::SCOPE_SYSTEM
|
|
)?->getId()
|
|
);
|
|
self::assertNull($this->store->findExactIpRule(
|
|
'tenant-b',
|
|
'203.0.113.10',
|
|
FirewallRuleObject::ACTION_BLOCK
|
|
));
|
|
}
|
|
|
|
private function rule(
|
|
string $reason,
|
|
string $scope,
|
|
?string $tenantId = null
|
|
): FirewallRuleObject {
|
|
return (new FirewallRuleObject())
|
|
->setScope($scope)
|
|
->setTenantId($tenantId)
|
|
->setType(FirewallRuleObject::TYPE_IP)
|
|
->setAction(FirewallRuleObject::ACTION_BLOCK)
|
|
->setValue('203.0.113.10')
|
|
->setReason($reason)
|
|
->setCreatedAt(new \DateTimeImmutable())
|
|
->setEnabled(true);
|
|
}
|
|
}
|