From e86c4a9b6b23dba49ffe3be002f3c7e7e41c892e Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 9 May 2024 19:21:19 +0300 Subject: [PATCH] ip address whitelist --- .../Settings/AuthIpAddressWhitelist/Valid.php | 90 +++++++++++++++++++ application/Espo/Core/Api/Auth.php | 4 +- .../Core/Authentication/Authentication.php | 34 ++++++- .../Authentication/ConfigDataProvider.php | 21 +++++ .../Hook/Hooks/FailedAttemptsLimit.php | 10 +-- .../Hook/Hooks/IpAddressWhitelist.php | 87 ++++++++++++++++++ .../Espo/Core/Authentication/Hook/Manager.php | 31 +++++++ .../Espo/Core/Authentication/Hook/OnLogin.php | 46 ++++++++++ .../Authentication/Util/IpAddressUtil.php | 59 ++++++++++++ application/Espo/Entities/AuthLogRecord.php | 1 + .../Espo/Resources/defaults/config.php | 4 + .../Resources/i18n/en_US/AuthLogRecord.json | 4 +- .../Espo/Resources/i18n/en_US/Settings.json | 8 +- .../layouts/Settings/authentication.json | 7 ++ .../metadata/app/authentication.json | 3 + .../Espo/Resources/metadata/app/config.json | 9 ++ .../metadata/entityDefs/AuthLogRecord.json | 3 +- .../metadata/entityDefs/Settings.json | 15 ++++ client/src/views/admin/authentication.js | 45 +++++++++- composer.json | 3 +- composer.lock | 55 +++++++++++- schema/metadata/app/authentication.json | 10 +++ .../Authentication/Util/IpAddressUtilTest.php | 72 +++++++++++++++ 23 files changed, 602 insertions(+), 19 deletions(-) create mode 100644 application/Espo/Classes/FieldValidators/Settings/AuthIpAddressWhitelist/Valid.php create mode 100644 application/Espo/Core/Authentication/Hook/Hooks/IpAddressWhitelist.php create mode 100644 application/Espo/Core/Authentication/Hook/OnLogin.php create mode 100644 application/Espo/Core/Authentication/Util/IpAddressUtil.php create mode 100644 tests/unit/Espo/Core/Authentication/Util/IpAddressUtilTest.php diff --git a/application/Espo/Classes/FieldValidators/Settings/AuthIpAddressWhitelist/Valid.php b/application/Espo/Classes/FieldValidators/Settings/AuthIpAddressWhitelist/Valid.php new file mode 100644 index 0000000000..1a96762db6 --- /dev/null +++ b/application/Espo/Classes/FieldValidators/Settings/AuthIpAddressWhitelist/Valid.php @@ -0,0 +1,90 @@ +. + * + * 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 + */ +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; + } +} diff --git a/application/Espo/Core/Api/Auth.php b/application/Espo/Core/Api/Auth.php index ed0ba09470..0fb3c8cab0 100644 --- a/application/Espo/Core/Api/Auth.php +++ b/application/Espo/Core/Api/Auth.php @@ -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; } diff --git a/application/Espo/Core/Authentication/Authentication.php b/application/Espo/Core/Authentication/Authentication.php index 1d3c48c1b0..3fb598bcc1 100644 --- a/application/Espo/Core/Authentication/Authentication.php +++ b/application/Espo/Core/Authentication/Authentication.php @@ -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(); + } } diff --git a/application/Espo/Core/Authentication/ConfigDataProvider.php b/application/Espo/Core/Authentication/ConfigDataProvider.php index 0a9498d4e1..aef91043fc 100644 --- a/application/Espo/Core/Authentication/ConfigDataProvider.php +++ b/application/Espo/Core/Authentication/ConfigDataProvider.php @@ -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') ?? []; + } } diff --git a/application/Espo/Core/Authentication/Hook/Hooks/FailedAttemptsLimit.php b/application/Espo/Core/Authentication/Hook/Hooks/FailedAttemptsLimit.php index 156e3b564b..1b6cd4e4e1 100644 --- a/application/Espo/Core/Authentication/Hook/Hooks/FailedAttemptsLimit.php +++ b/application/Espo/Core/Authentication/Hook/Hooks/FailedAttemptsLimit.php @@ -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."); } } diff --git a/application/Espo/Core/Authentication/Hook/Hooks/IpAddressWhitelist.php b/application/Espo/Core/Authentication/Hook/Hooks/IpAddressWhitelist.php new file mode 100644 index 0000000000..aeca5386fd --- /dev/null +++ b/application/Espo/Core/Authentication/Hook/Hooks/IpAddressWhitelist.php @@ -0,0 +1,87 @@ +. + * + * 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."); + } +} diff --git a/application/Espo/Core/Authentication/Hook/Manager.php b/application/Espo/Core/Authentication/Hook/Manager.php index 7adf5ebd68..05972ee5b0 100644 --- a/application/Espo/Core/Authentication/Hook/Manager.php +++ b/application/Espo/Core/Authentication/Hook/Manager.php @@ -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 $className */ + $list[] = $this->injectableFactory->create($className); + } + + return $list; + } + /** * @return OnResult[] */ diff --git a/application/Espo/Core/Authentication/Hook/OnLogin.php b/application/Espo/Core/Authentication/Hook/OnLogin.php new file mode 100644 index 0000000000..f31e6e8aa0 --- /dev/null +++ b/application/Espo/Core/Authentication/Hook/OnLogin.php @@ -0,0 +1,46 @@ +. + * + * 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; +} diff --git a/application/Espo/Core/Authentication/Util/IpAddressUtil.php b/application/Espo/Core/Authentication/Util/IpAddressUtil.php new file mode 100644 index 0000000000..4b9f4fa6bd --- /dev/null +++ b/application/Espo/Core/Authentication/Util/IpAddressUtil.php @@ -0,0 +1,59 @@ +. + * + * 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; + } +} diff --git a/application/Espo/Entities/AuthLogRecord.php b/application/Espo/Entities/AuthLogRecord.php index c880fff5cf..e37f1ead55 100644 --- a/application/Espo/Entities/AuthLogRecord.php +++ b/application/Espo/Entities/AuthLogRecord.php @@ -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 { diff --git a/application/Espo/Resources/defaults/config.php b/application/Espo/Resources/defaults/config.php index ba41e93030..f4f3f3862b 100644 --- a/application/Espo/Resources/defaults/config.php +++ b/application/Espo/Resources/defaults/config.php @@ -299,5 +299,9 @@ return [ 'listPagination' => true, 'starsLimit' => 500, 'quickSearchFullTextAppendWildcard' => false, + 'authIpAddressCheck' => false, + 'authIpAddressWhitelist' => [], + 'authIpAddressCheckExcludedUsersIds' => [], + 'authIpAddressCheckExcludedUsersNames' => (object) [], 'isInstalled' => false, ]; diff --git a/application/Espo/Resources/i18n/en_US/AuthLogRecord.json b/application/Espo/Resources/i18n/en_US/AuthLogRecord.json index a2b2cd1dda..05a6bde696 100644 --- a/application/Espo/Resources/i18n/en_US/AuthLogRecord.json +++ b/application/Espo/Resources/i18n/en_US/AuthLogRecord.json @@ -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" } } } diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index b25e3ce698..da8e56e05a 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -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." diff --git a/application/Espo/Resources/layouts/Settings/authentication.json b/application/Espo/Resources/layouts/Settings/authentication.json index 08e8854ce2..8e5f720cd7 100644 --- a/application/Espo/Resources/layouts/Settings/authentication.json +++ b/application/Espo/Resources/layouts/Settings/authentication.json @@ -13,6 +13,13 @@ [{"name": "auth2FAForced"}, {"name": "auth2FAInPortal"}] ] }, + { + "label": "Access", + "rows": [ + [{"name": "authIpAddressCheck"}, false], + [{"name": "authIpAddressWhitelist"}, {"name": "authIpAddressCheckExcludedUsers"}] + ] + }, { "label": "Passwords", "rows": [ diff --git a/application/Espo/Resources/metadata/app/authentication.json b/application/Espo/Resources/metadata/app/authentication.json index 8d0dcebb83..3f1f43b313 100644 --- a/application/Espo/Resources/metadata/app/authentication.json +++ b/application/Espo/Resources/metadata/app/authentication.json @@ -2,6 +2,9 @@ "beforeLoginHookClassNameList": [ "Espo\\Core\\Authentication\\Hook\\Hooks\\FailedAttemptsLimit" ], + "onLoginHookClassNameList": [ + "Espo\\Core\\Authentication\\Hook\\Hooks\\IpAddressWhitelist" + ], "onFailHookClassNameList": [], "onSuccessHookClassNameList": [], "onSuccessByTokenHookClassNameList": [], diff --git a/application/Espo/Resources/metadata/app/config.json b/application/Espo/Resources/metadata/app/config.json index f2bdf83ba4..570d238eb0 100644 --- a/application/Espo/Resources/metadata/app/config.json +++ b/application/Espo/Resources/metadata/app/config.json @@ -141,6 +141,15 @@ }, "starsLimit": { "level": "admin" + }, + "authIpAddressCheck": { + "level": "superAdmin" + }, + "authIpAddressWhitelist": { + "level": "superAdmin" + }, + "authIpAddressCheckExcludedUsers": { + "level": "superAdmin" } } } diff --git a/application/Espo/Resources/metadata/entityDefs/AuthLogRecord.json b/application/Espo/Resources/metadata/entityDefs/AuthLogRecord.json index 3842dee773..f1df7eef9d 100644 --- a/application/Espo/Resources/metadata/entityDefs/AuthLogRecord.json +++ b/application/Espo/Resources/metadata/entityDefs/AuthLogRecord.json @@ -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 }, diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index 2a8b5656dc..c76ddb5812 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -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" } } } diff --git a/client/src/views/admin/authentication.js b/client/src/views/admin/authentication.js index f532b64dcf..d38be39452 100644 --- a/client/src/views/admin/authentication.js +++ b/client/src/views/admin/authentication.js @@ -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']); diff --git a/composer.json b/composer.json index 42cad2e725..a92514cd26 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index 99f582d8fb..7933d280ce 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/schema/metadata/app/authentication.json b/schema/metadata/app/authentication.json index 8766119b1c..4f4468d26a 100644 --- a/schema/metadata/app/authentication.json +++ b/schema/metadata/app/authentication.json @@ -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": { diff --git a/tests/unit/Espo/Core/Authentication/Util/IpAddressUtilTest.php b/tests/unit/Espo/Core/Authentication/Util/IpAddressUtilTest.php new file mode 100644 index 0000000000..98920d675e --- /dev/null +++ b/tests/unit/Espo/Core/Authentication/Util/IpAddressUtilTest.php @@ -0,0 +1,72 @@ +. + * + * 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', + ]) + ); + } +}