chore: prepare for PR
This commit is contained in:
parent
fb186841f4
commit
4f39cf6dc8
4 changed files with 311 additions and 21 deletions
|
|
@ -95,7 +95,9 @@ public function submit()
|
||||||
// Check if it's valid CIDR notation
|
// Check if it's valid CIDR notation
|
||||||
if (str_contains($entry, '/')) {
|
if (str_contains($entry, '/')) {
|
||||||
[$ip, $mask] = explode('/', $entry);
|
[$ip, $mask] = explode('/', $entry);
|
||||||
if (filter_var($ip, FILTER_VALIDATE_IP) && is_numeric($mask) && $mask >= 0 && $mask <= 32) {
|
$isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||||
|
$maxMask = $isIpv6 ? 128 : 32;
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP) && is_numeric($mask) && $mask >= 0 && $mask <= $maxMask) {
|
||||||
return $entry;
|
return $entry;
|
||||||
}
|
}
|
||||||
$invalidEntries[] = $entry;
|
$invalidEntries[] = $entry;
|
||||||
|
|
@ -111,7 +113,7 @@ public function submit()
|
||||||
$invalidEntries[] = $entry;
|
$invalidEntries[] = $entry;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
})->filter()->unique();
|
})->filter()->values()->all();
|
||||||
|
|
||||||
if (! empty($invalidEntries)) {
|
if (! empty($invalidEntries)) {
|
||||||
$this->dispatch('error', 'Invalid IP addresses or subnets: '.implode(', ', $invalidEntries));
|
$this->dispatch('error', 'Invalid IP addresses or subnets: '.implode(', ', $invalidEntries));
|
||||||
|
|
@ -119,13 +121,15 @@ public function submit()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($validEntries->isEmpty()) {
|
if (empty($validEntries)) {
|
||||||
$this->dispatch('error', 'No valid IP addresses or subnets provided');
|
$this->dispatch('error', 'No valid IP addresses or subnets provided');
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->allowed_ips = $validEntries->implode(',');
|
$validEntries = deduplicateAllowlist($validEntries);
|
||||||
|
|
||||||
|
$this->allowed_ips = implode(',', $validEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->instantSave();
|
$this->instantSave();
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,10 @@ public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
|
||||||
[$ip, $mask] = $parts;
|
[$ip, $mask] = $parts;
|
||||||
|
|
||||||
if (! filter_var($ip, FILTER_VALIDATE_IP) || ! is_numeric($mask) || $mask < 0 || $mask > 32) {
|
$isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||||
|
$maxMask = $isIpv6 ? 128 : 32;
|
||||||
|
|
||||||
|
if (! filter_var($ip, FILTER_VALIDATE_IP) || ! is_numeric($mask) || $mask < 0 || $mask > $maxMask) {
|
||||||
$invalidEntries[] = $entry;
|
$invalidEntries[] = $entry;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -1416,24 +1416,48 @@ function checkIPAgainstAllowlist($ip, $allowlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
$mask = (int) $mask;
|
$mask = (int) $mask;
|
||||||
|
$isIpv6Subnet = filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||||
|
$maxMask = $isIpv6Subnet ? 128 : 32;
|
||||||
|
|
||||||
// Validate mask
|
// Validate mask for address family
|
||||||
if ($mask < 0 || $mask > 32) {
|
if ($mask < 0 || $mask > $maxMask) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate network addresses
|
if ($isIpv6Subnet) {
|
||||||
$ip_long = ip2long($ip);
|
// IPv6 CIDR matching using binary string comparison
|
||||||
$subnet_long = ip2long($subnet);
|
$ipBin = inet_pton($ip);
|
||||||
|
$subnetBin = inet_pton($subnet);
|
||||||
|
|
||||||
if ($ip_long === false || $subnet_long === false) {
|
if ($ipBin === false || $subnetBin === false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mask_long = ~((1 << (32 - $mask)) - 1);
|
// Build a 128-bit mask from $mask prefix bits
|
||||||
|
$maskBin = str_repeat("\xff", (int) ($mask / 8));
|
||||||
|
$remainder = $mask % 8;
|
||||||
|
if ($remainder > 0) {
|
||||||
|
$maskBin .= chr(0xFF & (0xFF << (8 - $remainder)));
|
||||||
|
}
|
||||||
|
$maskBin = str_pad($maskBin, 16, "\x00");
|
||||||
|
|
||||||
if (($ip_long & $mask_long) == ($subnet_long & $mask_long)) {
|
if (($ipBin & $maskBin) === ($subnetBin & $maskBin)) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// IPv4 CIDR matching
|
||||||
|
$ip_long = ip2long($ip);
|
||||||
|
$subnet_long = ip2long($subnet);
|
||||||
|
|
||||||
|
if ($ip_long === false || $subnet_long === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mask_long = ~((1 << (32 - $mask)) - 1);
|
||||||
|
|
||||||
|
if (($ip_long & $mask_long) == ($subnet_long & $mask_long)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Special case: 0.0.0.0 means allow all
|
// Special case: 0.0.0.0 means allow all
|
||||||
|
|
@ -1451,6 +1475,67 @@ function checkIPAgainstAllowlist($ip, $allowlist)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deduplicateAllowlist(array $entries): array
|
||||||
|
{
|
||||||
|
if (count($entries) <= 1) {
|
||||||
|
return array_values($entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize each entry into [original, ip, mask]
|
||||||
|
$parsed = [];
|
||||||
|
foreach ($entries as $entry) {
|
||||||
|
$entry = trim($entry);
|
||||||
|
if (empty($entry)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entry === '0.0.0.0') {
|
||||||
|
// Special case: bare 0.0.0.0 means "allow all" — treat as /0
|
||||||
|
$parsed[] = ['original' => $entry, 'ip' => '0.0.0.0', 'mask' => 0];
|
||||||
|
} elseif (str_contains($entry, '/')) {
|
||||||
|
[$ip, $mask] = explode('/', $entry);
|
||||||
|
$parsed[] = ['original' => $entry, 'ip' => $ip, 'mask' => (int) $mask];
|
||||||
|
} else {
|
||||||
|
$ip = $entry;
|
||||||
|
$isIpv6 = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
||||||
|
$parsed[] = ['original' => $entry, 'ip' => $ip, 'mask' => $isIpv6 ? 128 : 32];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$count = count($parsed);
|
||||||
|
$redundant = array_fill(0, $count, false);
|
||||||
|
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
if ($redundant[$i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($j = 0; $j < $count; $j++) {
|
||||||
|
if ($i === $j || $redundant[$j]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry $j is redundant if its mask is narrower/equal (>=) than $i's mask
|
||||||
|
// AND $j's network IP falls within $i's CIDR range
|
||||||
|
if ($parsed[$j]['mask'] >= $parsed[$i]['mask']) {
|
||||||
|
$cidr = $parsed[$i]['ip'].'/'.$parsed[$i]['mask'];
|
||||||
|
if (checkIPAgainstAllowlist($parsed[$j]['ip'], [$cidr])) {
|
||||||
|
$redundant[$j] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
if (! $redundant[$i]) {
|
||||||
|
$result[] = $parsed[$i]['original'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
function get_public_ips()
|
function get_public_ips()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@
|
||||||
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/-1']))->toBeFalse(); // Invalid mask
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/-1']))->toBeFalse(); // Invalid mask
|
||||||
});
|
});
|
||||||
|
|
||||||
test('IP allowlist with various subnet sizes', function () {
|
test('IP allowlist with various IPv4 subnet sizes', function () {
|
||||||
// /32 - single host
|
// /32 - single host
|
||||||
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.1/32']))->toBeTrue();
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.1/32']))->toBeTrue();
|
||||||
expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.1/32']))->toBeFalse();
|
expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.1/32']))->toBeFalse();
|
||||||
|
|
@ -96,16 +96,98 @@
|
||||||
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/31']))->toBeTrue();
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/31']))->toBeTrue();
|
||||||
expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.0/31']))->toBeFalse();
|
expect(checkIPAgainstAllowlist('192.168.1.2', ['192.168.1.0/31']))->toBeFalse();
|
||||||
|
|
||||||
// /16 - class B
|
// /25 - half a /24
|
||||||
|
expect(checkIPAgainstAllowlist('192.168.1.1', ['192.168.1.0/25']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('192.168.1.127', ['192.168.1.0/25']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('192.168.1.128', ['192.168.1.0/25']))->toBeFalse();
|
||||||
|
|
||||||
|
// /16
|
||||||
expect(checkIPAgainstAllowlist('172.16.0.1', ['172.16.0.0/16']))->toBeTrue();
|
expect(checkIPAgainstAllowlist('172.16.0.1', ['172.16.0.0/16']))->toBeTrue();
|
||||||
expect(checkIPAgainstAllowlist('172.16.255.255', ['172.16.0.0/16']))->toBeTrue();
|
expect(checkIPAgainstAllowlist('172.16.255.255', ['172.16.0.0/16']))->toBeTrue();
|
||||||
expect(checkIPAgainstAllowlist('172.17.0.1', ['172.16.0.0/16']))->toBeFalse();
|
expect(checkIPAgainstAllowlist('172.17.0.1', ['172.16.0.0/16']))->toBeFalse();
|
||||||
|
|
||||||
|
// /12
|
||||||
|
expect(checkIPAgainstAllowlist('172.16.0.1', ['172.16.0.0/12']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('172.31.255.255', ['172.16.0.0/12']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('172.32.0.1', ['172.16.0.0/12']))->toBeFalse();
|
||||||
|
|
||||||
|
// /8
|
||||||
|
expect(checkIPAgainstAllowlist('10.255.255.255', ['10.0.0.0/8']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('11.0.0.1', ['10.0.0.0/8']))->toBeFalse();
|
||||||
|
|
||||||
// /0 - all addresses
|
// /0 - all addresses
|
||||||
expect(checkIPAgainstAllowlist('1.1.1.1', ['0.0.0.0/0']))->toBeTrue();
|
expect(checkIPAgainstAllowlist('1.1.1.1', ['0.0.0.0/0']))->toBeTrue();
|
||||||
expect(checkIPAgainstAllowlist('255.255.255.255', ['0.0.0.0/0']))->toBeTrue();
|
expect(checkIPAgainstAllowlist('255.255.255.255', ['0.0.0.0/0']))->toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('IP allowlist with various IPv6 subnet sizes', function () {
|
||||||
|
// /128 - single host
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::1/128']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::2', ['2001:db8::1/128']))->toBeFalse();
|
||||||
|
|
||||||
|
// /127 - point-to-point link
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::0', ['2001:db8::/127']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::/127']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::2', ['2001:db8::/127']))->toBeFalse();
|
||||||
|
|
||||||
|
// /64 - standard subnet
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:abcd:1234::1', ['2001:db8:abcd:1234::/64']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:abcd:1234:ffff:ffff:ffff:ffff', ['2001:db8:abcd:1234::/64']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:abcd:1235::1', ['2001:db8:abcd:1234::/64']))->toBeFalse();
|
||||||
|
|
||||||
|
// /48 - site prefix
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:1234::1', ['2001:db8:1234::/48']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:1234:ffff::1', ['2001:db8:1234::/48']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:1235::1', ['2001:db8:1234::/48']))->toBeFalse();
|
||||||
|
|
||||||
|
// /32 - ISP allocation
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::/32']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:ffff:ffff::1', ['2001:db8::/32']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db9::1', ['2001:db8::/32']))->toBeFalse();
|
||||||
|
|
||||||
|
// /16
|
||||||
|
expect(checkIPAgainstAllowlist('2001:0000::1', ['2001::/16']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:ffff:ffff::1', ['2001::/16']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2002::1', ['2001::/16']))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('IP allowlist with bare IPv6 addresses', function () {
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::1']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::2', ['2001:db8::1']))->toBeFalse();
|
||||||
|
expect(checkIPAgainstAllowlist('::1', ['::1']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('::1', ['::2']))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('IP allowlist with IPv6 CIDR notation', function () {
|
||||||
|
// /64 prefix — issue #8729 exact case
|
||||||
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8230::1', ['2a01:e0a:21d:8230::/64']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8230:abcd:ef01:2345:6789', ['2a01:e0a:21d:8230::/64']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8231::1', ['2a01:e0a:21d:8230::/64']))->toBeFalse();
|
||||||
|
|
||||||
|
// /128 — single host
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::1/128']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::2', ['2001:db8::1/128']))->toBeFalse();
|
||||||
|
|
||||||
|
// /48 prefix
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:1234::1', ['2001:db8:1234::/48']))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8:1235::1', ['2001:db8:1234::/48']))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('IP allowlist with mixed IPv4 and IPv6', function () {
|
||||||
|
$allowlist = ['192.168.1.100', '10.0.0.0/8', '2a01:e0a:21d:8230::/64'];
|
||||||
|
|
||||||
|
expect(checkIPAgainstAllowlist('192.168.1.100', $allowlist))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('10.5.5.5', $allowlist))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8230::cafe', $allowlist))->toBeTrue();
|
||||||
|
expect(checkIPAgainstAllowlist('2a01:e0a:21d:8231::1', $allowlist))->toBeFalse();
|
||||||
|
expect(checkIPAgainstAllowlist('8.8.8.8', $allowlist))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('IP allowlist handles invalid IPv6 masks', function () {
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::/129']))->toBeFalse(); // mask > 128
|
||||||
|
expect(checkIPAgainstAllowlist('2001:db8::1', ['2001:db8::/-1']))->toBeFalse(); // negative mask
|
||||||
|
});
|
||||||
|
|
||||||
test('IP allowlist comma-separated string input', function () {
|
test('IP allowlist comma-separated string input', function () {
|
||||||
// Test with comma-separated string (as it would come from the settings)
|
// Test with comma-separated string (as it would come from the settings)
|
||||||
$allowlistString = '192.168.1.100,10.0.0.0/8,172.16.0.0/16';
|
$allowlistString = '192.168.1.100,10.0.0.0/8,172.16.0.0/16';
|
||||||
|
|
@ -134,14 +216,21 @@
|
||||||
// Valid cases - should pass
|
// Valid cases - should pass
|
||||||
expect($validate(''))->toBeTrue(); // Empty is allowed
|
expect($validate(''))->toBeTrue(); // Empty is allowed
|
||||||
expect($validate('0.0.0.0'))->toBeTrue(); // 0.0.0.0 is allowed
|
expect($validate('0.0.0.0'))->toBeTrue(); // 0.0.0.0 is allowed
|
||||||
expect($validate('192.168.1.1'))->toBeTrue(); // Valid IP
|
expect($validate('192.168.1.1'))->toBeTrue(); // Valid IPv4
|
||||||
expect($validate('192.168.1.0/24'))->toBeTrue(); // Valid CIDR
|
expect($validate('192.168.1.0/24'))->toBeTrue(); // Valid IPv4 CIDR
|
||||||
expect($validate('10.0.0.0/8'))->toBeTrue(); // Valid CIDR
|
expect($validate('10.0.0.0/8'))->toBeTrue(); // Valid IPv4 CIDR
|
||||||
expect($validate('192.168.1.1,10.0.0.1'))->toBeTrue(); // Multiple valid IPs
|
expect($validate('192.168.1.1,10.0.0.1'))->toBeTrue(); // Multiple valid IPs
|
||||||
expect($validate('192.168.1.0/24,10.0.0.0/8'))->toBeTrue(); // Multiple CIDRs
|
expect($validate('192.168.1.0/24,10.0.0.0/8'))->toBeTrue(); // Multiple CIDRs
|
||||||
expect($validate('0.0.0.0/0'))->toBeTrue(); // 0.0.0.0 with subnet
|
expect($validate('0.0.0.0/0'))->toBeTrue(); // 0.0.0.0 with subnet
|
||||||
expect($validate('0.0.0.0/24'))->toBeTrue(); // 0.0.0.0 with any subnet
|
expect($validate('0.0.0.0/24'))->toBeTrue(); // 0.0.0.0 with any subnet
|
||||||
expect($validate(' 192.168.1.1 '))->toBeTrue(); // With spaces
|
expect($validate(' 192.168.1.1 '))->toBeTrue(); // With spaces
|
||||||
|
// IPv6 valid cases — issue #8729
|
||||||
|
expect($validate('2001:db8::1'))->toBeTrue(); // Valid bare IPv6
|
||||||
|
expect($validate('::1'))->toBeTrue(); // Loopback IPv6
|
||||||
|
expect($validate('2a01:e0a:21d:8230::/64'))->toBeTrue(); // IPv6 /64 CIDR
|
||||||
|
expect($validate('2001:db8::/48'))->toBeTrue(); // IPv6 /48 CIDR
|
||||||
|
expect($validate('2001:db8::1/128'))->toBeTrue(); // IPv6 /128 CIDR
|
||||||
|
expect($validate('192.168.1.1,2a01:e0a:21d:8230::/64'))->toBeTrue(); // Mixed IPv4 + IPv6 CIDR
|
||||||
|
|
||||||
// Invalid cases - should fail
|
// Invalid cases - should fail
|
||||||
expect($validate('1'))->toBeFalse(); // Single digit
|
expect($validate('1'))->toBeFalse(); // Single digit
|
||||||
|
|
@ -155,6 +244,7 @@
|
||||||
expect($validate('not.an.ip.address'))->toBeFalse(); // Invalid format
|
expect($validate('not.an.ip.address'))->toBeFalse(); // Invalid format
|
||||||
expect($validate('192.168'))->toBeFalse(); // Incomplete IP
|
expect($validate('192.168'))->toBeFalse(); // Incomplete IP
|
||||||
expect($validate('192.168.1.1.1'))->toBeFalse(); // Too many octets
|
expect($validate('192.168.1.1.1'))->toBeFalse(); // Too many octets
|
||||||
|
expect($validate('2001:db8::/129'))->toBeFalse(); // IPv6 mask > 128
|
||||||
});
|
});
|
||||||
|
|
||||||
test('ValidIpOrCidr validation rule error messages', function () {
|
test('ValidIpOrCidr validation rule error messages', function () {
|
||||||
|
|
@ -181,3 +271,111 @@
|
||||||
expect($error)->toContain('10.0.0.256');
|
expect($error)->toContain('10.0.0.256');
|
||||||
expect($error)->not->toContain('192.168.1.1'); // Valid IP should not be in error
|
expect($error)->not->toContain('192.168.1.1'); // Valid IP should not be in error
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist removes bare IPv4 covered by various subnets', function () {
|
||||||
|
// /24
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.5', '192.168.1.0/24']))->toBe(['192.168.1.0/24']);
|
||||||
|
// /16
|
||||||
|
expect(deduplicateAllowlist(['172.16.5.10', '172.16.0.0/16']))->toBe(['172.16.0.0/16']);
|
||||||
|
// /8
|
||||||
|
expect(deduplicateAllowlist(['10.50.100.200', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
||||||
|
// /32 — same host, first entry wins (both equivalent)
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.1', '192.168.1.1/32']))->toBe(['192.168.1.1']);
|
||||||
|
// /31 — point-to-point
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.0', '192.168.1.0/31']))->toBe(['192.168.1.0/31']);
|
||||||
|
// IP outside subnet — both preserved
|
||||||
|
expect(deduplicateAllowlist(['172.17.0.1', '172.16.0.0/16']))->toBe(['172.17.0.1', '172.16.0.0/16']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist removes narrow IPv4 CIDR covered by broader CIDR', function () {
|
||||||
|
// /32 inside /24
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.1/32', '192.168.1.0/24']))->toBe(['192.168.1.0/24']);
|
||||||
|
// /25 inside /24
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.0/25', '192.168.1.0/24']))->toBe(['192.168.1.0/24']);
|
||||||
|
// /24 inside /16
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.0/24', '192.168.0.0/16']))->toBe(['192.168.0.0/16']);
|
||||||
|
// /16 inside /12
|
||||||
|
expect(deduplicateAllowlist(['172.16.0.0/16', '172.16.0.0/12']))->toBe(['172.16.0.0/12']);
|
||||||
|
// /16 inside /8
|
||||||
|
expect(deduplicateAllowlist(['10.1.0.0/16', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
||||||
|
// /24 inside /8
|
||||||
|
expect(deduplicateAllowlist(['10.1.2.0/24', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
||||||
|
// /12 inside /8
|
||||||
|
expect(deduplicateAllowlist(['172.16.0.0/12', '172.0.0.0/8']))->toBe(['172.0.0.0/8']);
|
||||||
|
// /31 inside /24
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.0/31', '192.168.1.0/24']))->toBe(['192.168.1.0/24']);
|
||||||
|
// Non-overlapping CIDRs — both preserved
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.0/24', '10.0.0.0/8']))->toBe(['192.168.1.0/24', '10.0.0.0/8']);
|
||||||
|
expect(deduplicateAllowlist(['172.16.0.0/16', '192.168.0.0/16']))->toBe(['172.16.0.0/16', '192.168.0.0/16']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist removes bare IPv6 covered by various prefixes', function () {
|
||||||
|
// /64 — issue #8729 exact scenario
|
||||||
|
expect(deduplicateAllowlist(['2a01:e0a:21d:8230::', '127.0.0.1', '2a01:e0a:21d:8230::/64']))
|
||||||
|
->toBe(['127.0.0.1', '2a01:e0a:21d:8230::/64']);
|
||||||
|
// /48
|
||||||
|
expect(deduplicateAllowlist(['2001:db8:1234::1', '2001:db8:1234::/48']))->toBe(['2001:db8:1234::/48']);
|
||||||
|
// /128 — same host, first entry wins (both equivalent)
|
||||||
|
expect(deduplicateAllowlist(['2001:db8::1', '2001:db8::1/128']))->toBe(['2001:db8::1']);
|
||||||
|
// IP outside prefix — both preserved
|
||||||
|
expect(deduplicateAllowlist(['2001:db8:1235::1', '2001:db8:1234::/48']))
|
||||||
|
->toBe(['2001:db8:1235::1', '2001:db8:1234::/48']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist removes narrow IPv6 CIDR covered by broader prefix', function () {
|
||||||
|
// /128 inside /64
|
||||||
|
expect(deduplicateAllowlist(['2a01:e0a:21d:8230::5/128', '2a01:e0a:21d:8230::/64']))->toBe(['2a01:e0a:21d:8230::/64']);
|
||||||
|
// /127 inside /64
|
||||||
|
expect(deduplicateAllowlist(['2001:db8:1234:5678::/127', '2001:db8:1234:5678::/64']))->toBe(['2001:db8:1234:5678::/64']);
|
||||||
|
// /64 inside /48
|
||||||
|
expect(deduplicateAllowlist(['2001:db8:1234:5678::/64', '2001:db8:1234::/48']))->toBe(['2001:db8:1234::/48']);
|
||||||
|
// /48 inside /32
|
||||||
|
expect(deduplicateAllowlist(['2001:db8:abcd::/48', '2001:db8::/32']))->toBe(['2001:db8::/32']);
|
||||||
|
// /32 inside /16
|
||||||
|
expect(deduplicateAllowlist(['2001:db8::/32', '2001::/16']))->toBe(['2001::/16']);
|
||||||
|
// /64 inside /32
|
||||||
|
expect(deduplicateAllowlist(['2001:db8:1234:5678::/64', '2001:db8::/32']))->toBe(['2001:db8::/32']);
|
||||||
|
// Non-overlapping IPv6 — both preserved
|
||||||
|
expect(deduplicateAllowlist(['2001:db8::/32', 'fd00::/8']))->toBe(['2001:db8::/32', 'fd00::/8']);
|
||||||
|
expect(deduplicateAllowlist(['2001:db8:1234::/48', '2001:db8:5678::/48']))->toBe(['2001:db8:1234::/48', '2001:db8:5678::/48']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist mixed IPv4 and IPv6 subnets', function () {
|
||||||
|
$result = deduplicateAllowlist([
|
||||||
|
'192.168.1.5', // covered by 192.168.0.0/16
|
||||||
|
'192.168.0.0/16',
|
||||||
|
'2a01:e0a:21d:8230::1', // covered by ::/64
|
||||||
|
'2a01:e0a:21d:8230::/64',
|
||||||
|
'10.0.0.1', // not covered by anything
|
||||||
|
'::1', // not covered by anything
|
||||||
|
]);
|
||||||
|
expect($result)->toBe(['192.168.0.0/16', '2a01:e0a:21d:8230::/64', '10.0.0.1', '::1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist preserves non-overlapping entries', function () {
|
||||||
|
$result = deduplicateAllowlist(['192.168.1.1', '10.0.0.1', '172.16.0.0/16']);
|
||||||
|
expect($result)->toBe(['192.168.1.1', '10.0.0.1', '172.16.0.0/16']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist handles exact duplicates', function () {
|
||||||
|
expect(deduplicateAllowlist(['192.168.1.1', '192.168.1.1']))->toBe(['192.168.1.1']);
|
||||||
|
expect(deduplicateAllowlist(['10.0.0.0/8', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
||||||
|
expect(deduplicateAllowlist(['2001:db8::1', '2001:db8::1']))->toBe(['2001:db8::1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist handles single entry and empty array', function () {
|
||||||
|
expect(deduplicateAllowlist(['10.0.0.1']))->toBe(['10.0.0.1']);
|
||||||
|
expect(deduplicateAllowlist([]))->toBe([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist with 0.0.0.0 removes everything else', function () {
|
||||||
|
$result = deduplicateAllowlist(['192.168.1.1', '0.0.0.0', '10.0.0.0/8']);
|
||||||
|
expect($result)->toBe(['0.0.0.0']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('deduplicateAllowlist multiple nested CIDRs keeps only broadest', function () {
|
||||||
|
// IPv4: three levels of nesting
|
||||||
|
expect(deduplicateAllowlist(['10.1.2.0/24', '10.1.0.0/16', '10.0.0.0/8']))->toBe(['10.0.0.0/8']);
|
||||||
|
// IPv6: three levels of nesting
|
||||||
|
expect(deduplicateAllowlist(['2001:db8:1234:5678::/64', '2001:db8:1234::/48', '2001:db8::/32']))->toBe(['2001:db8::/32']);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue