ip address whitelist

This commit is contained in:
Yuri Kuznetsov
2024-05-09 19:21:19 +03:00
parent 586e28bd26
commit e86c4a9b6b
23 changed files with 602 additions and 19 deletions
@@ -0,0 +1,90 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldValidators\Settings\AuthIpAddressWhitelist;
use Espo\Core\FieldValidation\Validator;
use Espo\Core\FieldValidation\Validator\Data;
use Espo\Core\FieldValidation\Validator\Failure;
use Espo\ORM\Entity;
/**
* @implements Validator<Entity>
*/
class Valid implements Validator
{
public function validate(Entity $entity, string $field, Data $data): ?Failure
{
$list = $entity->get($field);
if (!is_array($list)) {
return null;
}
foreach ($list as $item) {
if (!is_string($item)) {
continue;
}
if (!$this->isValid($item)) {
return Failure::create();
}
}
return null;
}
private function isValid(string $item): bool
{
$address = $item;
if (count(explode('/', $item)) > 1) {
[$address, $mask] = explode('/', $item, 2);
if (!is_numeric($mask)) {
return false;
}
$mask = (int) $mask;
if ($mask < 0 || $mask > 128) {
return false;
}
}
if (
filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false &&
filter_var($address, FILTER_VALIDATE_IP) === false
) {
return false;
}
return true;
}
}
+3 -1
View File
@@ -237,7 +237,9 @@ class Auth
$response->writeBody($e->getBody());
}
$this->log->notice("Auth exception: {message}", ['message' => $e->getMessage()]);
if ($e->getMessage()) {
$this->log->notice("Auth exception: {message}", ['message' => $e->getMessage()]);
}
return;
}
@@ -95,6 +95,7 @@ class Authentication
* Process logging in.
*
* @throws ServiceUnavailable
* @throws Forbidden
*/
public function login(AuthenticationData $data, Request $request, Response $response): Result
{
@@ -114,7 +115,11 @@ class Authentication
return $this->processFail(Result::fail(FailReason::METHOD_NOT_ALLOWED), $data, $request);
}
$this->hookManager->processBeforeLogin($data, $request);
try {
$this->hookManager->processBeforeLogin($data, $request);
} catch (Forbidden $e) {
$this->processForbidden($e);
}
if (!$method && $password === null) {
$this->log->error("Auth: Trying to login w/o password.");
@@ -240,6 +245,12 @@ class Authentication
}
}
try {
$this->hookManager->processOnLogin($result, $data, $request);
} catch (Forbidden $e) {
$this->processForbidden($e, $authLogRecord);
}
if (
!$result->isSecondStepRequired() &&
$request->getHeader(self::HEADER_ESPO_AUTHORIZATION)
@@ -786,4 +797,25 @@ class Authentication
$user->set('ipAddress', $this->util->obtainIpFromRequest($request));
}
/**
* @throws Forbidden
*/
private function processForbidden(Forbidden $exception, ?AuthLogRecord $authLogRecord = null): never
{
$this->log->warning('Auth: Forbidden. {message}', [
'message' => $exception->getMessage(),
'exception' => $exception,
]);
if ($authLogRecord) {
$authLogRecord
->setIsDenied()
->setDenialReason(AuthLogRecord::DENIAL_REASON_FORBIDDEN);
$this->entityManager->saveEntity($authLogRecord);
}
throw new Forbidden();
}
}
@@ -147,4 +147,25 @@ class ConfigDataProvider
return $list;
}
public function ipAddressCheck(): bool
{
return (bool) $this->config->get('authIpAddressCheck');
}
/**
* @return string[]
*/
public function getIpAddressWhitelist(): array
{
return $this->config->get('authIpAddressWhitelist') ?? [];
}
/**
* @return string[]
*/
public function getIpAddressCheckExcludedUserIdList(): array
{
return $this->config->get('authIpAddressCheckExcludedUsersIds') ?? [];
}
}
@@ -35,7 +35,6 @@ use Espo\Core\Authentication\AuthenticationData;
use Espo\Core\Api\Request;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Authentication\ConfigDataProvider;
use Espo\Core\Utils\Log;
use Espo\ORM\EntityManager;
use Espo\Entities\AuthLogRecord;
@@ -51,7 +50,6 @@ class FailedAttemptsLimit implements BeforeLogin
public function __construct(
private ConfigDataProvider $configDataProvider,
private EntityManager $entityManager,
private Log $log,
private Util $util
) {}
@@ -82,11 +80,11 @@ class FailedAttemptsLimit implements BeforeLogin
throw new RuntimeException($e->getMessage());
}
$ip = $this->util->obtainIpFromRequest($request);
$apAddress = $this->util->obtainIpFromRequest($request);
$where = [
'requestTime>' => $requestTimeFrom->format('U'),
'ipAddress' => $ip,
'ipAddress' => $apAddress,
'isDenied' => true,
];
@@ -109,8 +107,6 @@ class FailedAttemptsLimit implements BeforeLogin
return;
}
$this->log->warning("AUTH: Max failed login attempts exceeded for IP {ipAddress}.", ['ipAddress' => $ip]);
throw new Forbidden("Max failed login attempts exceeded.");
throw new Forbidden("Max failed login attempts exceeded for IP address $apAddress.");
}
}
@@ -0,0 +1,87 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\Hook\Hooks;
use Espo\Core\Api\Request;
use Espo\Core\Api\Util;
use Espo\Core\Authentication\AuthenticationData;
use Espo\Core\Authentication\ConfigDataProvider;
use Espo\Core\Authentication\Hook\OnLogin;
use Espo\Core\Authentication\Result;
use Espo\Core\Authentication\Util\IpAddressUtil;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Utils\Config;
class IpAddressWhitelist implements OnLogin
{
public function __construct(
private ConfigDataProvider $configDataProvider,
private Util $util,
private Config $config,
private IpAddressUtil $ipAddressUtil
) {}
public function process(Result $result, AuthenticationData $data, Request $request): void
{
if (!$this->configDataProvider->ipAddressCheck()) {
return;
}
$ipAddress = $this->util->obtainIpFromRequest($request);
if (
$ipAddress &&
$this->ipAddressUtil->isInWhitelist($ipAddress, $this->configDataProvider->getIpAddressWhitelist())
) {
return;
}
$user = $result->getUser();
if ($user && $user->isPortal()) {
return;
}
if ($user && $user->isSuperAdmin() && $this->config->get('restrictedMode')) {
return;
}
if (
$user &&
in_array($user->getId(), $this->configDataProvider->getIpAddressCheckExcludedUserIdList())
) {
return;
}
$username = $user ? $user->getUserName() : '?';
throw new Forbidden("Not allowed IP address $ipAddress, user: $username.");
}
}
@@ -29,6 +29,8 @@
namespace Espo\Core\Authentication\Hook;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ServiceUnavailable;
use Espo\Core\Utils\Metadata;
use Espo\Core\InjectableFactory;
use Espo\Core\Authentication\AuthenticationData;
@@ -41,6 +43,10 @@ class Manager
public function __construct(private Metadata $metadata, private InjectableFactory $injectableFactory)
{}
/**
* @throws ServiceUnavailable
* @throws Forbidden
*/
public function processBeforeLogin(AuthenticationData $data, Request $request): void
{
foreach ($this->getBeforeLoginHookList() as $hook) {
@@ -48,6 +54,16 @@ class Manager
}
}
/**
* @throws Forbidden
*/
public function processOnLogin(Result $result, AuthenticationData $data, Request $request): void
{
foreach ($this->getOnLoginHookList() as $hook) {
$hook->process($result, $data, $request);
}
}
public function processOnFail(Result $result, AuthenticationData $data, Request $request): void
{
foreach ($this->getOnFailHookList() as $hook) {
@@ -102,6 +118,21 @@ class Manager
return $list;
}
/**
* @return OnLogin[]
*/
private function getOnLoginHookList(): array
{
$list = [];
foreach ($this->getHookClassNameList('onLogin') as $className) {
/** @var class-string<OnLogin> $className */
$list[] = $this->injectableFactory->create($className);
}
return $list;
}
/**
* @return OnResult[]
*/
@@ -0,0 +1,46 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\Hook;
use Espo\Core\Authentication\AuthenticationData;
use Espo\Core\Api\Request;
use Espo\Core\Authentication\Result;
use Espo\Core\Exceptions\Forbidden;
/**
* Once a login result is ready.
*/
interface OnLogin
{
/**
* @throws Forbidden
*/
public function process(Result $result, AuthenticationData $data, Request $request): void;
}
@@ -0,0 +1,59 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\Util;
use CIDRmatch\CIDRmatch;
class IpAddressUtil
{
/**
* @param string $ipAddress An IP address.
* @param string[] $whitelist A whitelist. IPs or IP ranges in CIDR notation.
*/
public function isInWhitelist(string $ipAddress, array $whitelist): bool
{
$cidrMatch = new CIDRmatch();
foreach ($whitelist as $whiteIpAddress) {
if ($ipAddress === $whiteIpAddress) {
return true;
}
if (
str_contains($whiteIpAddress, '/') &&
$cidrMatch->match($ipAddress, $whiteIpAddress)
) {
return true;
}
}
return false;
}
}
@@ -41,6 +41,7 @@ class AuthLogRecord extends Entity
public const DENIAL_REASON_IS_NOT_PORTAL_USER = 'IS_NOT_PORTAL_USER';
public const DENIAL_REASON_USER_IS_NOT_IN_PORTAL = 'USER_IS_NOT_IN_PORTAL';
public const DENIAL_REASON_IS_SYSTEM_USER = 'IS_SYSTEM_USER';
public const DENIAL_REASON_FORBIDDEN = 'FORBIDDEN';
public function setUsername(?string $username): self
{
@@ -299,5 +299,9 @@ return [
'listPagination' => true,
'starsLimit' => 500,
'quickSearchFullTextAppendWildcard' => false,
'authIpAddressCheck' => false,
'authIpAddressWhitelist' => [],
'authIpAddressCheckExcludedUsersIds' => [],
'authIpAddressCheckExcludedUsersNames' => (object) [],
'isInstalled' => false,
];
@@ -30,7 +30,9 @@
"INACTIVE_USER": "Inactive user",
"IS_PORTAL_USER": "Portal user",
"IS_NOT_PORTAL_USER": "Not a portal user",
"USER_IS_NOT_IN_PORTAL": "User is not related to the portal"
"USER_IS_NOT_IN_PORTAL": "User is not related to the portal",
"IS_SYSTEM_USER": "Is system user",
"FORBIDDEN": "Forbidden"
}
}
}
@@ -169,7 +169,10 @@
"oidcAllowAdminUser": "OIDC Allow OIDC login for admin users",
"oidcLogoutUrl": "OIDC Logout URL",
"pdfEngine": "PDF Engine",
"quickSearchFullTextAppendWildcard": "Append wildcard in quick search"
"quickSearchFullTextAppendWildcard": "Append wildcard in quick search",
"authIpAddressCheck": "Restrict access by IP address",
"authIpAddressWhitelist": "IP Address Whitelist",
"authIpAddressCheckExcludedUsers": "Users excluded from check"
},
"options": {
"authenticationMethod": {
@@ -308,7 +311,8 @@
"Passwords": "Passwords",
"2-Factor Authentication": "2-Factor Authentication",
"Attachments": "Attachments",
"IdP Group": "IdP Group"
"IdP Group": "IdP Group",
"Access": "Access"
},
"messages": {
"ldapTestConnection": "The connection successfully established."
@@ -13,6 +13,13 @@
[{"name": "auth2FAForced"}, {"name": "auth2FAInPortal"}]
]
},
{
"label": "Access",
"rows": [
[{"name": "authIpAddressCheck"}, false],
[{"name": "authIpAddressWhitelist"}, {"name": "authIpAddressCheckExcludedUsers"}]
]
},
{
"label": "Passwords",
"rows": [
@@ -2,6 +2,9 @@
"beforeLoginHookClassNameList": [
"Espo\\Core\\Authentication\\Hook\\Hooks\\FailedAttemptsLimit"
],
"onLoginHookClassNameList": [
"Espo\\Core\\Authentication\\Hook\\Hooks\\IpAddressWhitelist"
],
"onFailHookClassNameList": [],
"onSuccessHookClassNameList": [],
"onSuccessByTokenHookClassNameList": [],
@@ -141,6 +141,15 @@
},
"starsLimit": {
"level": "admin"
},
"authIpAddressCheck": {
"level": "superAdmin"
},
"authIpAddressWhitelist": {
"level": "superAdmin"
},
"authIpAddressCheckExcludedUsers": {
"level": "superAdmin"
}
}
}
@@ -40,7 +40,8 @@
"IS_PORTAL_USER",
"IS_NOT_PORTAL_USER",
"USER_IS_NOT_IN_PORTAL",
"IS_SYSTEM_USER"
"IS_SYSTEM_USER",
"FORBIDDEN"
],
"readOnly": true
},
@@ -895,6 +895,21 @@
"quickSearchFullTextAppendWildcard": {
"type": "bool",
"tooltip": true
},
"authIpAddressCheck": {
"type": "bool"
},
"authIpAddressWhitelist": {
"type": "array",
"allowCustomOptions": true,
"noEmptyString": true,
"validatorClassNameList": [
"Espo\\Classes\\FieldValidators\\Settings\\AuthIpAddressWhitelist\\Valid"
]
},
"authIpAddressCheckExcludedUsers": {
"type": "linkMultiple",
"entity": "User"
}
}
}
+41 -4
View File
@@ -34,6 +34,40 @@ class AdminAuthenticationRecordView extends SettingsEditRecordView {
saveAndContinueEditingAction = false
dynamicLogicDefs = {
fields: {
authIpAddressWhitelist: {
visible: {
conditionGroup: [
{
attribute: 'authIpAddressCheck',
type: 'isTrue'
}
]
},
required: {
conditionGroup: [
{
attribute: 'authIpAddressCheck',
type: 'isTrue'
}
]
}
},
authIpAddressCheckExcludedUsers: {
visible: {
conditionGroup: [
{
attribute: 'authIpAddressCheck',
type: 'isTrue'
}
]
}
},
},
panels: {},
}
setup() {
this.methodList = [];
@@ -49,6 +83,12 @@ class AdminAuthenticationRecordView extends SettingsEditRecordView {
super.setup();
if (this.getHelper().getAppParam('isRestrictedMode') && !this.getUser().isSuperAdmin()) {
this.setFieldReadOnly('authIpAddressCheck', true);
this.setFieldReadOnly('authIpAddressWhitelist', true);
this.setFieldReadOnly('authIpAddressCheckExcludedUsers', true);
}
this.handlePanelsVisibility();
this.listenTo(this.model, 'change:authenticationMethod', () => {
@@ -69,10 +109,7 @@ class AdminAuthenticationRecordView extends SettingsEditRecordView {
}
setupBeforeFinal() {
this.dynamicLogicDefs = {
fields: {},
panels: {},
};
this.dynamicLogicDefs = Espo.Utils.cloneDeep(this.dynamicLogicDefs);
this.methodList.forEach(method => {
const fieldList = this.getMetadata().get(['authenticationMethods', method, 'settings', 'fieldList']);
+2 -1
View File
@@ -51,7 +51,8 @@
"picqer/php-barcode-generator": "^2.4",
"chillerlan/php-qrcode": "^4.3",
"ext-ctype": "*",
"lasserafn/php-initial-avatar-generator": "^4.3"
"lasserafn/php-initial-avatar-generator": "^4.3",
"tholu/php-cidr-match": "^0.4.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
Generated
+54 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a98b5f556eaf6120fad8b36c44d060bf",
"content-hash": "c9ba40700e3c077689a1b222e3b749c1",
"packages": [
{
"name": "async-aws/core",
@@ -6609,6 +6609,59 @@
],
"time": "2021-03-23T23:28:01+00:00"
},
{
"name": "tholu/php-cidr-match",
"version": "0.4",
"source": {
"type": "git",
"url": "https://github.com/tholu/php-cidr-match.git",
"reference": "50fadb1e43d865167b3ca705fbdbfff87ea5ab96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tholu/php-cidr-match/zipball/50fadb1e43d865167b3ca705fbdbfff87ea5ab96",
"reference": "50fadb1e43d865167b3ca705fbdbfff87ea5ab96",
"shasum": ""
},
"require": {
"php": ">=5.4"
},
"require-dev": {
"composer/composer": ">2",
"phpunit/phpunit": "4.8.36",
"squizlabs/php_codesniffer": "3.6.2"
},
"type": "library",
"autoload": {
"psr-0": {
"CIDRmatch\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Thomas Lutz",
"email": "thomaslutz.de@gmail.com",
"homepage": "https://github.com/tholu",
"role": "Creator, Developer, Maintainer"
}
],
"description": "CIDRmatch is a library to match an IP to an IP range in CIDR notation (IPv4 and IPv6).",
"keywords": [
"cidr",
"ipv4",
"ipv6",
"php"
],
"support": {
"issues": "https://github.com/tholu/php-cidr-match/issues",
"source": "https://github.com/tholu/php-cidr-match/tree/0.4"
},
"time": "2022-05-11T06:48:12+00:00"
},
{
"name": "webmozart/assert",
"version": "1.11.0",
+10
View File
@@ -15,6 +15,16 @@
},
"description": "Array of hook class names. Fired before logging in before credentials are checked. Should implement Espo\\Core\\Authentication\\Hook\\BeforeLogin interface. Important: Need to have __APPEND__ item in the beginning of the array when extending."
},
"onLoginHookClassNameList": {
"type": "array",
"items": {
"anyOf": [
{"type": "string"},
{"enum": ["__APPEND__"]}
]
},
"description": "Array of hook class names. Fired once logging in is about to success. Should implement Espo\\Core\\Authentication\\Hook\\OnLogin interface. As of v8.3. Important: Need to have __APPEND__ item in the beginning of the array when extending."
},
"onFailHookClassNameList": {
"type": "array",
"items": {
@@ -0,0 +1,72 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace tests\unit\Espo\Core\Authentication\Util;
use PHPUnit\Framework\TestCase;
class IpAddressUtilTest extends TestCase
{
public function testWhitelist(): void
{
$util = new \Espo\Core\Authentication\Util\IpAddressUtil();
$this->assertTrue(
$util->isInWhitelist('192.168.0.1', [
'192.168.0.1',
])
);
$this->assertFalse(
$util->isInWhitelist('192.168.0.1', [
'192.168.0.0',
])
);
$this->assertTrue(
$util->isInWhitelist('192.168.0.1', [
'192.168.0.1',
'192.168.0.1',
])
);
$this->assertTrue(
$util->isInWhitelist('192.168.0.5', [
'192.168.0.1',
'192.168.0.1/24',
])
);
$this->assertFalse(
$util->isInWhitelist('0.0.0.5', [
'192.168.0.1/24',
])
);
}
}