fix(firewall): enforce system rules independently of tenant context

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-07-30 22:41:27 -04:00
parent da81f1ddf1
commit abc5bfcccc
6 changed files with 119 additions and 81 deletions
+14 -4
View File
@@ -10,20 +10,30 @@ use KTXC\Stores\FirewallStore;
final class FirewallRuleCache
{
/** @var array<string, FirewallRuleObject[]> */
private array $rules = [];
private array $tenantRules = [];
/** @var FirewallRuleObject[]|null */
private ?array $systemRules = null;
public function __construct(private readonly FirewallStore $store)
{
}
/** @return FirewallRuleObject[] */
public function applicable(string $tenantId): array
public function tenant(string $tenantId): array
{
return $this->rules[$tenantId] ??= $this->store->listApplicableRules($tenantId);
return $this->tenantRules[$tenantId] ??= $this->store->listRules($tenantId);
}
/** @return FirewallRuleObject[] */
public function system(): array
{
return $this->systemRules ??= $this->store->listSystemRules();
}
public function invalidate(): void
{
$this->rules = [];
$this->tenantRules = [];
$this->systemRules = null;
}
}
+11 -29
View File
@@ -73,26 +73,22 @@ class FirewallService
string $ipAddress,
?string $deviceFingerprint = null
): FirewallAnalyzeResult {
// Check if firewall is enabled for this tenant
if (!$this->isEnabled()) {
return new FirewallAnalyzeResult(true);
}
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
return new FirewallAnalyzeResult(true);
$ruleGroups = [
[$this->ruleCache->system(), FirewallRuleObject::ACTION_BLOCK],
];
if ($tenantId !== null && $this->isEnabled()) {
$tenantRules = $this->ruleCache->tenant($tenantId);
$ruleGroups[] = [$tenantRules, FirewallRuleObject::ACTION_ALLOW];
$ruleGroups[] = [$tenantRules, FirewallRuleObject::ACTION_BLOCK];
}
$rules = $this->getActiveRules();
$ruleGroups[] = [$this->ruleCache->system(), FirewallRuleObject::ACTION_ALLOW];
foreach ([
[FirewallRuleObject::SCOPE_SYSTEM, FirewallRuleObject::ACTION_BLOCK],
[FirewallRuleObject::SCOPE_TENANT, FirewallRuleObject::ACTION_ALLOW],
[FirewallRuleObject::SCOPE_TENANT, FirewallRuleObject::ACTION_BLOCK],
[FirewallRuleObject::SCOPE_SYSTEM, FirewallRuleObject::ACTION_ALLOW],
] as [$scope, $action]) {
foreach ($ruleGroups as [$rules, $action]) {
foreach ($rules as $rule) {
if ($rule->getScope() !== $scope || $rule->getAction() !== $action) {
if ($rule->getAction() !== $action) {
continue;
}
@@ -310,20 +306,6 @@ class FirewallService
return $value;
}
/**
* Get active rules (cached)
* @return FirewallRuleObject[]
*/
private function getActiveRules(): array
{
$tenantId = $this->tenantContext->identifier();
if (!$tenantId) {
return [];
}
return $this->ruleCache->applicable($tenantId);
}
/**
* Cleanup maintenance tasks
*/
-37
View File
@@ -55,43 +55,6 @@ class FirewallStore
return $list;
}
/**
* List active system-wide rules and active rules owned by a tenant.
*/
public function listApplicableRules(string $tenantId): array
{
$now = (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM);
$filter = [
'enabled' => true,
'$and' => [
[
'$or' => [
['scope' => FirewallRuleObject::SCOPE_SYSTEM],
[
'tenantId' => $tenantId,
'scope' => FirewallRuleObject::SCOPE_TENANT,
],
],
],
[
'$or' => [
['expiresAt' => null],
['expiresAt' => ['$gt' => $now]],
],
],
],
];
$cursor = $this->dataStore->selectCollection(self::RULES_COLLECTION)->find($filter);
$list = [];
foreach ($cursor as $entry) {
$list[] = (new FirewallRuleObject())->jsonDeserialize((array)$entry);
}
return $list;
}
public function listSystemRules(bool $activeOnly = true): array
{
$filter = ['scope' => FirewallRuleObject::SCOPE_SYSTEM, 'tenantId' => null];