feat(firewall): add scoped log administration reads
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\Models\Firewall\FirewallLogObject;
|
||||
use KTXC\Models\Firewall\FirewallRuleObject;
|
||||
use KTXC\Stores\FirewallStore;
|
||||
|
||||
final class FirewallLogService
|
||||
{
|
||||
public const MAX_LIMIT = 100;
|
||||
|
||||
private const EVENT_TYPES = [
|
||||
FirewallLogObject::EVENT_AUTH_FAILURE,
|
||||
FirewallLogObject::EVENT_RATE_LIMIT,
|
||||
FirewallLogObject::EVENT_BRUTE_FORCE,
|
||||
FirewallLogObject::EVENT_SUSPICIOUS,
|
||||
FirewallLogObject::EVENT_RULE_MATCH,
|
||||
FirewallLogObject::EVENT_ACCESS_CHECK,
|
||||
FirewallLogObject::EVENT_RULE_CREATED,
|
||||
FirewallLogObject::EVENT_RULE_EXTENDED,
|
||||
FirewallLogObject::EVENT_RULE_DISABLED,
|
||||
FirewallLogObject::EVENT_RULE_REMOVED,
|
||||
];
|
||||
|
||||
public function __construct(private readonly FirewallStore $store)
|
||||
{
|
||||
}
|
||||
|
||||
public function tenant(string $tenantId, array $filters, int $limit, int $offset): array
|
||||
{
|
||||
return $this->store->queryTenantLogs(
|
||||
$tenantId,
|
||||
$this->validate($filters, $limit, $offset),
|
||||
$limit,
|
||||
$offset
|
||||
);
|
||||
}
|
||||
|
||||
public function system(?string $tenantId, array $filters, int $limit, int $offset): array
|
||||
{
|
||||
if ($tenantId !== null && ($tenantId === '' || strlen($tenantId) > 128)) {
|
||||
throw new \InvalidArgumentException('Invalid tenant filter.');
|
||||
}
|
||||
return $this->store->querySystemLogs(
|
||||
$tenantId,
|
||||
$this->validate($filters, $limit, $offset),
|
||||
$limit,
|
||||
$offset
|
||||
);
|
||||
}
|
||||
|
||||
private function validate(array $filters, int $limit, int $offset): array
|
||||
{
|
||||
if ($limit < 1 || $limit > self::MAX_LIMIT || $offset < 0) {
|
||||
throw new \InvalidArgumentException('Pagination requires limit 1-100 and offset 0 or greater.');
|
||||
}
|
||||
$ipAddress = self::nullableString($filters, 'ipAddress');
|
||||
if ($ipAddress !== null && filter_var($ipAddress, FILTER_VALIDATE_IP) === false) {
|
||||
throw new \InvalidArgumentException('Invalid IP address filter.');
|
||||
}
|
||||
$eventType = self::nullableString($filters, 'eventType');
|
||||
if ($eventType !== null && !in_array($eventType, self::EVENT_TYPES, true)) {
|
||||
throw new \InvalidArgumentException('Invalid firewall event type filter.');
|
||||
}
|
||||
$result = self::nullableString($filters, 'result');
|
||||
if ($result !== null && !in_array($result, [
|
||||
FirewallLogObject::RESULT_ALLOWED,
|
||||
FirewallLogObject::RESULT_BLOCKED,
|
||||
FirewallLogObject::RESULT_RECORDED,
|
||||
], true)) {
|
||||
throw new \InvalidArgumentException('Invalid firewall result filter.');
|
||||
}
|
||||
$ruleScope = self::nullableString($filters, 'ruleScope');
|
||||
if ($ruleScope !== null && !in_array($ruleScope, [
|
||||
FirewallRuleObject::SCOPE_TENANT,
|
||||
FirewallRuleObject::SCOPE_SYSTEM,
|
||||
], true)) {
|
||||
throw new \InvalidArgumentException('Invalid rule scope filter.');
|
||||
}
|
||||
$from = self::date($filters, 'from');
|
||||
$to = self::date($filters, 'to');
|
||||
if ($from !== null && $to !== null && $from > $to) {
|
||||
throw new \InvalidArgumentException('The from date must not be later than the to date.');
|
||||
}
|
||||
|
||||
return [
|
||||
'ipAddress' => $ipAddress,
|
||||
'eventType' => $eventType,
|
||||
'result' => $result,
|
||||
'ruleId' => self::nullableString($filters, 'ruleId'),
|
||||
'ruleScope' => $ruleScope,
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
];
|
||||
}
|
||||
|
||||
private static function nullableString(array $filters, string $key): ?string
|
||||
{
|
||||
$value = $filters[$key] ?? null;
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
if (!is_string($value) || $value === '' || strlen($value) > 255) {
|
||||
throw new \InvalidArgumentException("Invalid {$key} filter.");
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private static function date(array $filters, string $key): ?\DateTimeImmutable
|
||||
{
|
||||
$value = self::nullableString($filters, $key);
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return new \DateTimeImmutable($value);
|
||||
} catch (\Exception) {
|
||||
throw new \InvalidArgumentException("Invalid {$key} date filter.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
|
||||
final class SystemFirewallLogService
|
||||
{
|
||||
public const PERMISSION_READ = 'firewall.system.logs.read';
|
||||
|
||||
public function __construct(
|
||||
private readonly FirewallLogService $logs,
|
||||
private readonly IdentityContextInterface $identity,
|
||||
) {
|
||||
}
|
||||
|
||||
public function query(?string $tenantId, array $filters, int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
if (!$this->identity->hasPermission(self::PERMISSION_READ)) {
|
||||
throw new \RuntimeException('Missing required permission: '.self::PERMISSION_READ);
|
||||
}
|
||||
return $this->logs->system($tenantId, $filters, $limit, $offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Service;
|
||||
|
||||
use KTXC\Context\IdentityContextInterface;
|
||||
use KTXC\Context\TenantContextInterface;
|
||||
|
||||
final class TenantFirewallLogService
|
||||
{
|
||||
public const PERMISSION_READ = 'firewall.tenant.logs.read';
|
||||
|
||||
public function __construct(
|
||||
private readonly FirewallLogService $logs,
|
||||
private readonly TenantContextInterface $tenant,
|
||||
private readonly IdentityContextInterface $identity,
|
||||
) {
|
||||
}
|
||||
|
||||
public function query(array $filters, int $limit = 50, int $offset = 0): array
|
||||
{
|
||||
if (!$this->identity->hasPermission(self::PERMISSION_READ)) {
|
||||
throw new \RuntimeException('Missing required permission: '.self::PERMISSION_READ);
|
||||
}
|
||||
return $this->logs->tenant($this->tenant->requireIdentifier(), $filters, $limit, $offset);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user