Files
da-postgresql/exec/lib/NamePolicy.php
T
2026-07-04 16:00:04 +02:00

230 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
final class NamePolicy
{
public const MAX_IDENTIFIER_LEN = 63;
public const MAX_HOST_COMMENT_LEN = 180;
public const ALL_IPV4_HOST_PATTERN = '0.0.0.0/0';
/**
* @var array<string,string>
*/
public const PRIVILEGE_LABELS = [
'ALL' => 'Wszystkie uprawnienia',
'CONNECT' => 'CONNECT',
'CREATE' => 'CREATE (baza/schemat)',
'TEMPORARY' => 'TEMPORARY',
'SCHEMA_USAGE' => 'USAGE (schema public)',
'TABLE_SELECT' => 'SELECT (tabele)',
'TABLE_INSERT' => 'INSERT (tabele)',
'TABLE_UPDATE' => 'UPDATE (tabele)',
'TABLE_DELETE' => 'DELETE (tabele)',
'TABLE_TRUNCATE' => 'TRUNCATE (tabele)',
'TABLE_REFERENCES' => 'REFERENCES (tabele)',
'TABLE_TRIGGER' => 'TRIGGER (tabele)',
'SEQUENCE_USAGE' => 'USAGE (sekwencje)',
'SEQUENCE_SELECT' => 'SELECT (sekwencje)',
'SEQUENCE_UPDATE' => 'UPDATE (sekwencje)',
'FUNCTION_EXECUTE' => 'EXECUTE (funkcje)',
];
public static function defaultPrivileges(): array
{
return ['ALL'];
}
public static function normalizeDatabaseName(string $input, string $prefix): string
{
return self::normalizeIdentifier($input, $prefix, 'Nazwa bazy');
}
public static function normalizeRoleName(string $input, string $prefix): string
{
return self::normalizeIdentifier($input, $prefix, 'Nazwa użytkownika');
}
public static function assertBelongsToUser(string $name, string $prefix, string $entityLabel = 'Obiekt'): void
{
if (strpos($name, $prefix) !== 0) {
throw new InvalidArgumentException($entityLabel . ' musi zaczynać się od prefiksu: ' . $prefix);
}
}
public static function normalizeIdentifier(string $input, string $prefix, string $label): string
{
$value = strtolower(trim($input));
if ($value === '') {
throw new InvalidArgumentException($label . ' jest wymagana.');
}
if (!preg_match('/^[a-z0-9_]+$/', $value)) {
throw new InvalidArgumentException($label . ' może zawierać wyłącznie znaki a-z, 0-9 oraz _.');
}
if (strpos($value, $prefix) !== 0) {
$value = $prefix . $value;
}
if (strpos($value, $prefix) !== 0) {
throw new InvalidArgumentException($label . ' musi zaczynać się od prefiksu: ' . $prefix);
}
if (strlen($value) > self::MAX_IDENTIFIER_LEN) {
throw new InvalidArgumentException($label . ' przekracza limit 63 znaków PostgreSQL.');
}
if ($value === $prefix) {
throw new InvalidArgumentException($label . ' nie może być równa samemu prefiksowi.');
}
return $value;
}
/**
* @return string[]
*/
public static function parseHosts(string $raw): array
{
$raw = str_replace(["\r\n", "\r", ';'], ["\n", "\n", "\n"], $raw);
$raw = str_replace(',', "\n", $raw);
$parts = explode("\n", $raw);
$hosts = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
$hosts[] = $part;
}
return array_values(array_unique($hosts));
}
public static function normalizeHost(string $input, bool $allowWildcard): string
{
$host = strtolower(trim($input));
if ($host === '') {
throw new InvalidArgumentException('Pusty host.');
}
if (in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
return $host;
}
if (self::isStrictIpv4($host)) {
return $host;
}
throw new InvalidArgumentException('Nieobsługiwany format hosta: ' . $input);
}
public static function normalizeRemoteIpv4Host(string $input): string
{
$host = trim($input);
if ($host === '') {
throw new InvalidArgumentException('Adres IPv4 jest wymagany.');
}
if (!self::isStrictIpv4($host)) {
throw new InvalidArgumentException('Dozwolone są tylko pojedyncze adresy IPv4, np. 203.0.113.10.');
}
return $host;
}
public static function normalizeRemoteIpv4AccessPattern(string $input): string
{
$host = trim(strtolower($input));
if ($host === '') {
throw new InvalidArgumentException('Adres IPv4 albo CIDR jest wymagany.');
}
if (self::isStrictIpv4($host)) {
return $host;
}
if (preg_match('#^([^/]+)/([0-9]{1,2})$#', $host, $m)) {
$ip = $m[1];
$prefix = (int)$m[2];
if (!self::isStrictIpv4($ip) || $prefix < 0 || $prefix > 32) {
throw new InvalidArgumentException('Nieprawidłowy CIDR IPv4: ' . $input);
}
return self::normalizeIpv4Cidr($ip, $prefix);
}
throw new InvalidArgumentException('Dozwolone są pojedyncze adresy IPv4 albo CIDR, np. 203.0.113.10 lub 203.0.113.0/24.');
}
public static function normalizeHostComment(string $input): string
{
$comment = trim(preg_replace('/[\r\n\t]+/', ' ', $input) ?? '');
$comment = preg_replace('/\s+/', ' ', $comment) ?? '';
$comment = trim($comment);
if (strlen($comment) > self::MAX_HOST_COMMENT_LEN) {
throw new InvalidArgumentException('Komentarz hosta przekracza limit ' . self::MAX_HOST_COMMENT_LEN . ' znaków.');
}
return $comment;
}
/**
* @param array<int,string> $input
* @return string[]
*/
public static function sanitizePrivileges(array $input): array
{
$allowed = array_keys(self::PRIVILEGE_LABELS);
$result = [];
foreach ($input as $priv) {
$priv = strtoupper(trim((string)$priv));
if ($priv === '') {
continue;
}
if (!in_array($priv, $allowed, true)) {
continue;
}
$result[] = $priv;
}
$result = array_values(array_unique($result));
if (in_array('ALL', $result, true)) {
return ['ALL'];
}
return $result;
}
private static function isStrictIpv4(string $value): bool
{
if (!preg_match('/^(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$/', $value)) {
return false;
}
return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
}
private static function normalizeIpv4Cidr(string $ip, int $prefix): string
{
$long = ip2long($ip);
if ($long === false) {
throw new InvalidArgumentException('Nieprawidłowy adres IPv4: ' . $ip);
}
$unsigned = (int)sprintf('%u', $long);
$mask = $prefix === 0 ? 0 : ((0xFFFFFFFF << (32 - $prefix)) & 0xFFFFFFFF);
$network = $unsigned & $mask;
$networkIp = long2ip($network);
if ($networkIp === false) {
throw new InvalidArgumentException('Nieprawidłowy CIDR IPv4: ' . $ip . '/' . $prefix);
}
return $networkIp . '/' . $prefix;
}
}