oidc portal

This commit is contained in:
Yuri Kuznetsov
2023-01-22 12:54:34 +02:00
parent dd0deeb967
commit 15f337a319
48 changed files with 1454 additions and 374 deletions
@@ -0,0 +1,62 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Classes\FieldValidators\AuthenticationProvider;
use Espo\Core\FieldValidation\Validator;
use Espo\Core\FieldValidation\Validator\Data;
use Espo\Core\FieldValidation\Validator\Failure;
use Espo\Core\Utils\Metadata;
use Espo\Entities\AuthenticationProvider;
use Espo\ORM\Entity;
/**
* @implements Validator<AuthenticationProvider>
*/
class MethodValid implements Validator
{
public function __construct(private Metadata $metadata) {}
public function validate(Entity $entity, string $field, Data $data): ?Failure
{
$value = $entity->get($field);
if (!$value) {
return Failure::create();
}
$isAvailable = $this->metadata->get(['authenticationMethods', $value, 'provider', 'isAvailable']);
if (!$isAvailable) {
return Failure::create();
}
return null;
}
}
+11 -1
View File
@@ -33,6 +33,8 @@ use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Authentication\Authentication;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\InjectableFactory;
use Espo\Tools\App\AppService as Service;
@@ -56,6 +58,7 @@ class App
/**
* @throws BadRequest
* @throws Forbidden
*/
public function postActionDestroyAuthToken(Request $request, Response $response): bool
{
@@ -67,7 +70,14 @@ class App
$auth = $this->injectableFactory->create(Authentication::class);
return $auth->destroyAuthToken($data->token, $request, $response);
try {
$auth->destroyAuthToken($data->token, $request, $response);
return true;
}
catch (NotFound) {
return false;
}
}
private function getService(): Service
@@ -0,0 +1,44 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Controllers;
use Espo\Core\Controllers\RecordBase;
class AuthenticationProvider extends RecordBase
{
protected function checkAccess(): bool
{
if (!$this->user->isAdmin()) {
return false;
}
return true;
}
}
@@ -29,8 +29,8 @@
namespace Espo\Core\Authentication;
use Espo\Core\Authentication\Logout\Params as LogoutParams;
use Espo\Core\Exceptions\Error\Body;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Repositories\UserData as UserDataRepository;
use Espo\Entities\Portal;
use Espo\Entities\User;
@@ -38,6 +38,9 @@ use Espo\Entities\AuthLogRecord;
use Espo\Entities\AuthToken as AuthTokenEntity;
use Espo\Entities\UserData;
use Espo\Core\Exceptions\Error\Body;
use Espo\Core\Authentication\Logout\Params as LogoutParams;
use Espo\Core\Authentication\Util\MethodProvider;
use Espo\Core\Authentication\Result\FailReason;
use Espo\Core\Authentication\TwoFactor\LoginFactory as TwoFactorLoginFactory;
use Espo\Core\Authentication\AuthToken\Manager as AuthTokenManager;
@@ -45,7 +48,6 @@ use Espo\Core\Authentication\AuthToken\Data as AuthTokenData;
use Espo\Core\Authentication\AuthToken\AuthToken;
use Espo\Core\Authentication\Hook\Manager as HookManager;
use Espo\Core\Authentication\Login\Data as LoginData;
use Espo\Core\ApplicationUser;
use Espo\Core\ApplicationState;
use Espo\Core\Api\Request;
@@ -54,6 +56,7 @@ use Espo\Core\Utils\Log;
use Espo\Core\ORM\EntityManagerProxy;
use Espo\Core\Exceptions\ServiceUnavailable;
use LogicException;
use RuntimeException;
/**
@@ -71,50 +74,22 @@ class Authentication
private const COOKIE_AUTH_TOKEN_SECRET = 'auth-token-secret';
private bool $allowAnyAccess;
private ?Portal $portal = null;
private ApplicationUser $applicationUser;
private ApplicationState $applicationState;
private ConfigDataProvider $configDataProvider;
private EntityManagerProxy $entityManager;
private LoginFactory $loginFactory;
private TwoFactorLoginFactory $twoFactorLoginFactory;
private AuthTokenManager $authTokenManager;
private HookManager $hookManager;
private LogoutFactory $logoutFactory;
private Log $log;
public function __construct(
ApplicationUser $applicationUser,
ApplicationState $applicationState,
ConfigDataProvider $configDataProvider,
EntityManagerProxy $entityManagerProxy,
LoginFactory $loginFactory,
TwoFactorLoginFactory $twoFactorLoginFactory,
AuthTokenManager $authTokenManager,
HookManager $hookManager,
Log $log,
LogoutFactory $logoutFactory,
bool $allowAnyAccess = false
) {
$this->allowAnyAccess = $allowAnyAccess;
$this->applicationUser = $applicationUser;
$this->applicationState = $applicationState;
$this->configDataProvider = $configDataProvider;
$this->entityManager = $entityManagerProxy;
$this->loginFactory = $loginFactory;
$this->twoFactorLoginFactory = $twoFactorLoginFactory;
$this->authTokenManager = $authTokenManager;
$this->hookManager = $hookManager;
$this->logoutFactory = $logoutFactory;
$this->log = $log;
}
private ApplicationUser $applicationUser,
private ApplicationState $applicationState,
private ConfigDataProvider $configDataProvider,
private EntityManagerProxy $entityManager,
private LoginFactory $loginFactory,
private TwoFactorLoginFactory $twoFactorLoginFactory,
private AuthTokenManager $authTokenManager,
private HookManager $hookManager,
private Log $log,
private LogoutFactory $logoutFactory,
private MethodProvider $methodProvider
) {}
/**
* Process logging in.
* Note: This method can change the state of the object (by setting the `portal` property.).
*
* @throws ServiceUnavailable
*/
@@ -122,22 +97,22 @@ class Authentication
{
$username = $data->getUsername();
$password = $data->getPassword();
$authenticationMethod = $data->getMethod();
$method = $data->getMethod();
$byTokenOnly = $data->byTokenOnly();
if (
$authenticationMethod &&
!$this->configDataProvider->authenticationMethodIsApi($authenticationMethod)
$method &&
!$this->configDataProvider->authenticationMethodIsApi($method)
) {
$this->log
->warning("AUTH: Trying to use not allowed authentication method '{$authenticationMethod}'.");
->warning("AUTH: Trying to use not allowed authentication method '{$method}'.");
return $this->processFail(Result::fail(FailReason::METHOD_NOT_ALLOWED), $data, $request);
}
$this->hookManager->processBeforeLogin($data, $request);
if (!$authenticationMethod && $password === null) {
if (!$method && $password === null) {
$this->log->error("AUTH: Trying to login w/o password.");
return Result::fail(FailReason::NO_PASSWORD);
@@ -145,7 +120,7 @@ class Authentication
$authToken = null;
if (!$authenticationMethod) {
if (!$method) {
$authToken = $this->authTokenManager->get($password);
}
@@ -173,7 +148,7 @@ class Authentication
$byTokenAndUsername = $request->getHeader(self::HEADER_BY_TOKEN) === 'true';
if ($authenticationMethod && $byTokenAndUsername) {
if ($method && $byTokenAndUsername) {
return Result::fail(FailReason::DISCREPANT_DATA);
}
@@ -195,9 +170,9 @@ class Authentication
}
}
$authenticationMethod ??= $this->configDataProvider->getDefaultAuthenticationMethod();
$method ??= $this->methodProvider->get();
$login = $this->loginFactory->create($authenticationMethod, $this->isPortal());
$login = $this->loginFactory->create($method, $this->isPortal());
$loginData = LoginData
::createBuilder()
@@ -211,7 +186,7 @@ class Authentication
$user = $result->getUser();
$authLogRecord = !$authTokenIsFound ?
$this->createAuthLogRecord($username, $user, $request, $authenticationMethod) :
$this->createAuthLogRecord($username, $user, $request, $method) :
null;
if ($result->isFail()) {
@@ -356,40 +331,18 @@ class Authentication
}
}
private function setPortal(Portal $portal): void
{
$this->portal = $portal;
}
private function isPortal(): bool
{
return $this->portal || $this->applicationState->isPortal();
return $this->applicationState->isPortal();
}
private function getPortal(): Portal
{
if ($this->portal) {
return $this->portal;
}
return $this->applicationState->getPortal();
}
private function processAuthTokenCheck(AuthToken $authToken): bool
{
if ($this->allowAnyAccess && $authToken->getPortalId() && !$this->isPortal()) {
/** @var ?Portal $portal */
$portal = $this->entityManager->getEntity('Portal', $authToken->getPortalId());
if ($portal) {
$this->setPortal($portal);
}
}
if ($this->allowAnyAccess) {
return true;
}
if ($this->isPortal() && $authToken->getPortalId() !== $this->getPortal()->getId()) {
$this->log->info("AUTH: Trying to login to portal with a token not related to portal.");
@@ -552,12 +505,31 @@ class Authentication
return $authToken;
}
public function destroyAuthToken(string $token, Request $request, Response $response): bool
/**
* Destroy an auth token.
*
* @param string $token A token to destroy.
* @param Request $request A request.
* @param Response $response A response.
* @throws Forbidden
* @throws NotFound
*/
public function destroyAuthToken(string $token, Request $request, Response $response): void
{
$authToken = $this->authTokenManager->get($token);
if (!$authToken) {
return false;
throw new NotFound("Auth token not found.");
}
if (!$this->applicationState->hasUser()) {
throw new LogicException("No logged user.");
}
$user = $this->applicationState->getUser();
if ($authToken->getUserId() !== $user->getId()) {
throw new Forbidden("Auth token for another user.");
}
$this->authTokenManager->inactivate($authToken);
@@ -570,12 +542,15 @@ class Authentication
}
}
$method = $this->configDataProvider->getDefaultAuthenticationMethod();
$method = $this->methodProvider->get();
if ($this->logoutFactory->isCreatable($method)) {
$logout = $this->logoutFactory->create($method);
if (!$this->logoutFactory->isCreatable($method)) {
return;
}
$result = $logout->logout($authToken, LogoutParams::create());
$result = $this->logoutFactory
->create($method)
->logout($authToken, LogoutParams::create());
$redirectUrl = $result->getRedirectUrl();
@@ -584,14 +559,11 @@ class Authentication
}
}
return true;
}
private function createAuthLogRecord(
?string $username,
?User $user,
Request $request,
?string $authenticationMethod = null
?string $method = null
): ?AuthLogRecord {
if ($username === self::LOGOUT_USERNAME) {
@@ -616,7 +588,7 @@ class Authentication
'requestTime' => $request->getServerParam('REQUEST_TIME_FLOAT'),
'requestMethod' => $request->getMethod(),
'requestUrl' => $requestUrl,
'authenticationMethod' => $authenticationMethod,
'authenticationMethod' => $method,
]);
if ($this->isPortal()) {
@@ -33,22 +33,11 @@ use Espo\Core\InjectableFactory;
class AuthenticationFactory
{
private InjectableFactory $injectableFactory;
public function __construct(InjectableFactory $injectableFactory)
{
$this->injectableFactory = $injectableFactory;
}
public function __construct(private InjectableFactory $injectableFactory)
{}
public function create(): Authentication
{
return $this->injectableFactory->create(Authentication::class);
}
public function createWithAnyAccessAllowed(): Authentication
{
return $this->injectableFactory->createWith(Authentication::class, [
'allowAnyAccess' => true,
]);
}
}
@@ -35,7 +35,6 @@ use Espo\Core\Authentication\Jwt\Exceptions\Invalid;
use Espo\Core\Authentication\Jwt\Exceptions\SignatureNotVerified;
use Espo\Core\Authentication\Jwt\Token;
use Espo\Core\Authentication\Jwt\Validator;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use Espo\Entities\AuthToken as AuthTokenEntity;
use Espo\Entities\User;
@@ -48,28 +47,14 @@ use Espo\ORM\EntityManager;
*/
class BackchannelLogout
{
private Log $log;
private Validator $validator;
private TokenValidator $tokenValidator;
private Config $config;
private EntityManager $entityManager;
private AuthTokenManager $authTokenManger;
public function __construct(
Log $log,
Validator $validator,
TokenValidator $tokenValidator,
Config $config,
EntityManager $entityManager,
AuthTokenManager $authTokenManger
) {
$this->log = $log;
$this->validator = $validator;
$this->tokenValidator = $tokenValidator;
$this->config = $config;
$this->entityManager = $entityManager;
$this->authTokenManger = $authTokenManger;
}
private Log $log,
private Validator $validator,
private TokenValidator $tokenValidator,
private ConfigDataProvider $configDataProvider,
private EntityManager $entityManager,
private AuthTokenManager $authTokenManger
) {}
/**
* @throws SignatureNotVerified
@@ -86,7 +71,7 @@ class BackchannelLogout
$this->tokenValidator->validateSignature($token);
$this->tokenValidator->validateFields($token);
$usernameClaim = $this->config->get('oidcUsernameClaim');
$usernameClaim = $this->configDataProvider->getUsernameClaim();
if (!$usernameClaim) {
throw new Invalid("No username claim in config.");
@@ -0,0 +1,220 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\Oidc;
use Espo\Core\ApplicationState;
use Espo\Core\ORM\EntityManagerProxy;
use Espo\Core\Utils\Config;
use Espo\Entities\AuthenticationProvider;
use stdClass;
class ConfigDataProvider
{
private const JWKS_CACHE_PERIOD = '10 minutes';
private Config|AuthenticationProvider $object;
public function __construct(
private Config $config,
private ApplicationState $applicationState,
private EntityManagerProxy $entityManager
) {
$this->object = $this->getAuthenticationProvider() ?? $this->config;
}
private function isAuthenticationProvider(): bool
{
return $this->object instanceof AuthenticationProvider;
}
private function getAuthenticationProvider(): ?AuthenticationProvider
{
if (!$this->applicationState->isPortal()) {
return null;
}
$link = $this->applicationState->getPortal()->getAuthenticationProvider();
if (!$link) {
return null;
}
/** @var ?AuthenticationProvider */
return $this->entityManager->getEntityById(AuthenticationProvider::ENTITY_TYPE, $link->getId());
}
public function getRedirectUri(): string
{
return rtrim($this->config->get('siteUrl'), '/') . '/oauth-callback.php';
}
public function getClientId(): ?string
{
return $this->object->get('oidcClientId');
}
public function getClientSecret(): ?string
{
return $this->object->get('oidcClientSecret');
}
public function getAuthorizationEndpoint(): ?string
{
return $this->object->get('oidcAuthorizationEndpoint');
}
public function getTokenEndpoint(): ?string
{
return $this->object->get('oidcTokenEndpoint');
}
public function getJwksEndpoint(): ?string
{
return $this->object->get('oidcJwksEndpoint');
}
/**
* @return string[]
*/
public function getJwtSignatureAlgorithmList(): array
{
return $this->object->get('oidcJwtSignatureAlgorithmList') ?? [];
}
/**
* @return string[]
*/
public function getScopes(): array
{
/** @var string[] */
return $this->object->get('oidcScopes') ?? [];
}
public function getLogoutUrl(): ?string
{
return $this->object->get('oidcLogoutUrl');
}
public function getUsernameClaim(): ?string
{
return $this->object->get('oidcUsernameClaim');
}
public function createUser(): bool
{
return (bool) $this->object->get('oidcCreateUser');
}
public function sync(): bool
{
return (bool) $this->object->get('oidcSync');
}
public function syncTeams(): bool
{
if ($this->isAuthenticationProvider()) {
return false;
}
return (bool) $this->config->get('oidcSyncTeams');
}
public function fallback(): bool
{
if ($this->isAuthenticationProvider()) {
return false;
}
return (bool) $this->config->get('oidcFallback');
}
public function allowRegularUserFallback(): bool
{
if ($this->isAuthenticationProvider()) {
return false;
}
return (bool) $this->config->get('oidcAllowRegularUserFallback');
}
public function allowAdminUser(): bool
{
if ($this->isAuthenticationProvider()) {
return false;
}
return (bool) $this->config->get('oidcAllowAdminUser');
}
public function getGroupClaim(): ?string
{
if ($this->isAuthenticationProvider()) {
return null;
}
return $this->config->get('oidcGroupClaim');
}
/**
* @return ?string[]
*/
public function getTeamIds(): ?array
{
if ($this->isAuthenticationProvider()) {
return null;
}
return $this->config->get('oidcTeamsIds') ?? [];
}
public function getTeamColumns(): ?stdClass
{
if ($this->isAuthenticationProvider()) {
return null;
}
return $this->config->get('oidcTeamsColumns') ?? (object) [];
}
public function getAuthorizationPrompt(): string
{
return $this->config->get('oidcAuthorizationPrompt') ?? 'consent';
}
public function getAuthorizationMaxAge(): ?int
{
return $this->config->get('oidcAuthorizationMaxAge');
}
public function getJwksCachePeriod(): string
{
return $this->config->get('oidcJwksCachePeriod') ?? self::JWKS_CACHE_PERIOD;
}
}
@@ -33,7 +33,6 @@ use Espo\Core\Authentication\Jwt\SignatureVerifier;
use Espo\Core\Authentication\Jwt\SignatureVerifierFactory;
use Espo\Core\Authentication\Jwt\SignatureVerifiers\Hmac;
use Espo\Core\Authentication\Jwt\SignatureVerifiers\Rsa;
use Espo\Core\Utils\Config;
use RuntimeException;
class DefaultSignatureVerifierFactory implements SignatureVerifierFactory
@@ -54,14 +53,10 @@ class DefaultSignatureVerifierFactory implements SignatureVerifierFactory
self::HS512 => Hmac::class,
];
private Config $config;
private KeysProvider $keysProvider;
public function __construct(Config $config, KeysProvider $keysProvider)
{
$this->config = $config;
$this->keysProvider = $keysProvider;
}
public function __construct(
private KeysProvider $keysProvider,
private ConfigDataProvider $configDataProvider
) {}
public function create(string $algorithm): SignatureVerifier
{
@@ -79,7 +74,7 @@ class DefaultSignatureVerifierFactory implements SignatureVerifierFactory
}
if ($className === Hmac::class) {
$key = $this->config->get('oidcClientSecret');
$key = $this->configDataProvider->getClientSecret();
if (!$key) {
throw new RuntimeException("No client secret.");
@@ -29,8 +29,9 @@
namespace Espo\Core\Authentication\Oidc;
use Espo\Core\ApplicationState;
use Espo\Core\Authentication\Jwt\Token\Payload;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Log;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use RuntimeException;
@@ -38,9 +39,11 @@ use RuntimeException;
class DefaultUserProvider implements UserProvider
{
public function __construct(
private Config $config,
private ConfigDataProvider $configDataProvider,
private Sync $sync,
private EntityManager $entityManager
private EntityManager $entityManager,
private ApplicationState $applicationState,
private Log $log
) {}
public function get(Payload $payload): ?User
@@ -58,7 +61,7 @@ class DefaultUserProvider implements UserProvider
private function findUser(Payload $payload): ?User
{
$usernameClaim = $this->config->get('oidcUsernameClaim');
$usernameClaim = $this->configDataProvider->getUsernameClaim();
if (!$usernameClaim) {
throw new RuntimeException("No username claim in config.");
@@ -86,15 +89,41 @@ class DefaultUserProvider implements UserProvider
return null;
}
if (!$user->isRegular() && !$user->isAdmin()) {
$userId = $user->getId();
$isPortal = $this->applicationState->isPortal();
if (!$isPortal && !$user->isRegular() && !$user->isAdmin()) {
$this->log->info("Oidc: User {$userId} found but it's neither regular user not admin.");
return null;
}
if ($isPortal && !$user->isPortal()) {
$this->log->info("Oidc: User {$userId} found but it's not portal user.");
return null;
}
if ($isPortal) {
$portalId = $this->applicationState->getPortalId();
if (!$user->getPortals()->hasId($portalId)) {
$this->log->info("Oidc: User {$userId} found but it's not related to current portal.");
return null;
}
}
if ($user->isSuperAdmin()) {
$this->log->info("Oidc: User {$userId} found but it's super-admin, not allowed.");
return null;
}
if ($user->isAdmin() && !$this->config->get('oidcAllowAdminUser')) {
if ($user->isAdmin() && !$this->configDataProvider->allowAdminUser()) {
$this->log->info("Oidc: User {$userId} found but it's admin, not allowed.");
return null;
}
@@ -103,11 +132,11 @@ class DefaultUserProvider implements UserProvider
private function tryToCreateUser(Payload $payload): ?User
{
if (!$this->config->get('oidcCreateUser')) {
if (!$this->configDataProvider->createUser()) {
return null;
}
$usernameClaim = $this->config->get('oidcUsernameClaim');
$usernameClaim = $this->configDataProvider->getUsernameClaim();
if (!$usernameClaim) {
throw new RuntimeException("Could not create a user. No OIDC username claim in config.");
@@ -124,7 +153,10 @@ class DefaultUserProvider implements UserProvider
private function syncUser(User $user, Payload $payload): void
{
if (!$this->config->get('oidcSync') && !$this->config->get('oidcSyncTeams')) {
if (
!$this->configDataProvider->sync() &&
!$this->configDataProvider->syncTeams()
) {
return;
}
@@ -44,25 +44,15 @@ use stdClass;
class KeysProvider
{
private const CACHE_KEY = 'oidcJwks';
private const CACHE_PERIOD = '10 minutes';
private const REQUEST_TIMEOUT = 10;
private DataCache $dataCache;
private Config $config;
private KeyFactory $factory;
private Log $log;
public function __construct(
DataCache $dataCache,
Config $config,
KeyFactory $factory,
Log $log
) {
$this->dataCache = $dataCache;
$this->config = $config;
$this->factory = $factory;
$this->log = $log;
}
private DataCache $dataCache,
private Config $config,
private ConfigDataProvider $configDataProvider,
private KeyFactory $factory,
private Log $log
) {}
/**
* @return Key[]
@@ -106,8 +96,7 @@ class KeysProvider
*/
private function load(): array
{
/** @var ?string $endpoint */
$endpoint = $this->config->get('oidcJwksEndpoint');
$endpoint = $this->configDataProvider->getJwksEndpoint();
if (!$endpoint) {
throw new RuntimeException("JSON Web Key Set endpoint not specified in settings.");
@@ -145,7 +134,7 @@ class KeysProvider
try {
$parsedResponse = Json::decode($response);
}
catch (JsonException $e) {}
catch (JsonException) {}
if (!$parsedResponse instanceof stdClass || !isset($parsedResponse->keys)) {
throw new RuntimeException("OIDC: JWKS bad response.");
@@ -180,7 +169,7 @@ class KeysProvider
return null;
}
$period = '-' . ($this->config->get('oidcJwksCachePeriod') ?? self::CACHE_PERIOD);
$period = '-' . $this->configDataProvider->getJwksCachePeriod();
if ($timestamp < DateTime::createNow()->modify($period)->getTimestamp()) {
return null;
@@ -30,6 +30,7 @@
namespace Espo\Core\Authentication\Oidc;
use Espo\Core\Api\Request;
use Espo\Core\ApplicationState;
use Espo\Core\Authentication\Login as LoginInterface;
use Espo\Core\Authentication\Login\Data;
use Espo\Core\Authentication\Jwt\Token;
@@ -39,7 +40,6 @@ use Espo\Core\Authentication\Jwt\Exceptions\SignatureNotVerified;
use Espo\Core\Authentication\Jwt\Validator;
use Espo\Core\Authentication\Result;
use Espo\Core\Authentication\Result\FailReason;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Json;
use Espo\Core\Utils\Log;
use JsonException;
@@ -57,11 +57,12 @@ class Login implements LoginInterface
public function __construct(
private Espo $espoLogin,
private Config $config,
private Log $log,
private ConfigDataProvider $configDataProvider,
private Validator $validator,
private TokenValidator $tokenValidator,
private UserProvider $userProvider
private UserProvider $userProvider,
private ApplicationState $applicationState
) {}
public function login(Data $data, Request $request): Result
@@ -81,13 +82,10 @@ class Login implements LoginInterface
private function loginWithCode(string $code, Request $request): Result
{
/** @var ?string $endpoint */
$endpoint = $this->config->get('oidcTokenEndpoint');
/** @var ?string $clientId */
$clientId = $this->config->get('oidcClientId');
/** @var ?string $clientSecret */
$clientSecret = $this->config->get('oidcClientSecret');
$redirectUri = rtrim($this->config->get('siteUrl'), '/') . '/oauth-callback.php';
$endpoint = $this->configDataProvider->getTokenEndpoint();
$clientId = $this->configDataProvider->getClientId();
$clientSecret = $this->configDataProvider->getClientSecret();
$redirectUri = $this->configDataProvider->getRedirectUri();
if (!$endpoint) {
throw new RuntimeException("No token endpoint.");
@@ -161,7 +159,14 @@ class Login implements LoginInterface
{
if (
!$data->getAuthToken() &&
!$this->config->get('oidcFallback')
!$this->configDataProvider->fallback()
) {
return Result::fail(FailReason::METHOD_NOT_ALLOWED);
}
if (
!$data->getAuthToken() &&
$this->applicationState->isPortal()
) {
return Result::fail(FailReason::METHOD_NOT_ALLOWED);
}
@@ -170,16 +175,27 @@ class Login implements LoginInterface
$user = $result->getUser();
if (!$user) {
return $result;
}
if ($data->getAuthToken()) {
// Allow fallback when logged by auth token.
return $result;
}
if (
!$data->getAuthToken() &&
$user &&
$user->isRegular() &&
!$this->config->get('oidcAllowRegularUserFallback')
!$this->configDataProvider->allowRegularUserFallback()
// Portal users are allowed.
) {
return Result::fail(FailReason::METHOD_NOT_ALLOWED);
}
if ($user->isPortal()) {
return Result::fail(FailReason::METHOD_NOT_ALLOWED);
}
return $result;
}
@@ -243,7 +259,7 @@ class Login implements LoginInterface
try {
$parsedResponse = Json::decode($response);
}
catch (JsonException $e) {}
catch (JsonException) {}
if (!$parsedResponse instanceof stdClass) {
$this->log->error(self::composeLogMessage('Bad token response.', $status, $response));
@@ -37,31 +37,23 @@ use Espo\Core\Utils\Config;
class Logout implements LogoutInterface
{
private Config $config;
public function __construct(Config $config)
{
$this->config = $config;
}
public function __construct(
private Config $config,
private ConfigDataProvider $configDataProvider
) {}
public function logout(AuthToken $authToken, Params $params): Result
{
if ($authToken->getPortalId()) {
return Result::create();
}
/** @var ?string $url */
$url = $this->config->get('oidcLogoutUrl');
/** @var string $oidcClientId */
$oidcClientId = $this->config->get('oidcClientId') ?? '';
$url = $this->configDataProvider->getLogoutUrl();
$clientId = $this->configDataProvider->getClientId() ?? '';
$siteUrl = rtrim($this->config->get('siteUrl') ?? '', '/');
if ($url) {
$url = str_replace('{clientId}', urlencode($oidcClientId), $url);
$url = str_replace('{clientId}', urlencode($clientId), $url);
$url = str_replace('{siteUrl}', urlencode($siteUrl), $url);
}
// @todo Check session is set if auth token to bypass fallback logins.
// @todo Check session is set in auth token to bypass fallback logins.
return Result::create()->withRedirectUrl($url);
}
@@ -30,7 +30,9 @@
namespace Espo\Core\Authentication\Oidc;
use Espo\Core\Acl\Cache\Clearer as AclCacheClearer;
use Espo\Core\ApplicationState;
use Espo\Core\Authentication\Jwt\Token\Payload;
use Espo\Core\Field\LinkMultiple;
use Espo\Core\FieldProcessing\EmailAddress\Saver as EmailAddressSaver;
use Espo\Core\FieldProcessing\PhoneNumber\Saver as PhoneNumberSaver;
use Espo\Core\FieldProcessing\Relation\LinkMultipleSaver;
@@ -42,35 +44,20 @@ use Espo\Core\Utils\Util;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use RuntimeException;
use stdClass;
class Sync
{
private EntityManager $entityManager;
private Config $config;
private LinkMultipleSaver $linkMultipleSaver;
private EmailAddressSaver $emailAddressSaver;
private PhoneNumberSaver $phoneNumberSaver;
private PasswordHash $passwordHash;
private AclCacheClearer $aclCacheClearer;
public function __construct(
EntityManager $entityManager,
Config $config,
LinkMultipleSaver $linkMultipleSaver,
EmailAddressSaver $emailAddressSaver,
PhoneNumberSaver $phoneNumberSaver,
PasswordHash $passwordHash,
AclCacheClearer $aclCacheClearer
) {
$this->entityManager = $entityManager;
$this->config = $config;
$this->linkMultipleSaver = $linkMultipleSaver;
$this->emailAddressSaver = $emailAddressSaver;
$this->phoneNumberSaver = $phoneNumberSaver;
$this->passwordHash = $passwordHash;
$this->aclCacheClearer = $aclCacheClearer;
}
private EntityManager $entityManager,
private Config $config,
private ConfigDataProvider $configDataProvider,
private LinkMultipleSaver $linkMultipleSaver,
private EmailAddressSaver $emailAddressSaver,
private PhoneNumberSaver $phoneNumberSaver,
private PasswordHash $passwordHash,
private AclCacheClearer $aclCacheClearer,
private ApplicationState $applicationState
) {}
public function createUser(Payload $payload): User
{
@@ -78,6 +65,7 @@ class Sync
$this->validateUsername($username);
/** @var User $user */
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getNew();
$user->set([
@@ -89,6 +77,13 @@ class Sync
$user->set($this->getUserDataFromToken($payload));
$user->set($this->getUserTeamsDataFromToken($payload));
if ($this->applicationState->isPortal()) {
$portalId = $this->applicationState->getPortalId();
$user->set('type', User::TYPE_PORTAL);
$user->setPortals(LinkMultiple::create()->withAddedId($portalId));
}
$this->saveUser($user);
return $user;
@@ -104,13 +99,13 @@ class Sync
throw new RuntimeException("Could not sync user. Username mismatch.");
}
if ($this->config->get('oidcSync')) {
if ($this->configDataProvider->sync()) {
$user->set($this->getUserDataFromToken($payload));
}
$clearAclCache = false;
if ($this->config->get('oidcSyncTeams')) {
if ($this->configDataProvider->syncTeams()) {
$user->loadLinkMultipleField('teams');
$user->set($this->getUserTeamsDataFromToken($payload));
@@ -180,7 +175,7 @@ class Sync
private function getUsernameFromToken(Payload $payload): string
{
$usernameClaim = $this->config->get('oidcUsernameClaim');
$usernameClaim = $this->configDataProvider->getUsernameClaim();
if (!$usernameClaim) {
throw new RuntimeException("No OIDC username claim in config.");
@@ -204,10 +199,8 @@ class Sync
*/
private function getTeamIdList(Payload $payload): array
{
/** @var string[] $idList */
$idList = $this->config->get('oidcTeamsIds') ?? [];
/** @var stdClass $columns */
$columns = $this->config->get('oidcTeamsColumns') ?? (object) [];
$idList = $this->configDataProvider->getTeamIds() ?? [];
$columns = $this->configDataProvider->getTeamColumns() ?? (object) [];
if ($idList === []) {
return [];
@@ -233,8 +226,7 @@ class Sync
*/
private function getGroups(Payload $payload): array
{
/** @var ?string $groupClaim */
$groupClaim = $this->config->get('oidcGroupClaim');
$groupClaim = $this->configDataProvider->getGroupClaim();
if (!$groupClaim) {
return [];
@@ -33,21 +33,14 @@ use Espo\Core\Authentication\Jwt\Exceptions\Invalid;
use Espo\Core\Authentication\Jwt\Exceptions\SignatureNotVerified;
use Espo\Core\Authentication\Jwt\SignatureVerifierFactory;
use Espo\Core\Authentication\Jwt\Token;
use Espo\Core\Utils\Config;
use RuntimeException;
class TokenValidator
{
private Config $config;
private SignatureVerifierFactory $signatureVerifierFactory;
public function __construct(
Config $config,
SignatureVerifierFactory $signatureVerifierFactory
) {
$this->config = $config;
$this->signatureVerifierFactory = $signatureVerifierFactory;
}
private ConfigDataProvider $configDataProvider,
private SignatureVerifierFactory $signatureVerifierFactory
) {}
/**
* @throws SignatureNotVerified
@@ -57,8 +50,7 @@ class TokenValidator
{
$algorithm = $token->getHeader()->getAlg();
/** @var string[] $allowedAlgorithmList */
$allowedAlgorithmList = $this->config->get('oidcJwtSignatureAlgorithmList') ?? [];
$allowedAlgorithmList = $this->configDataProvider->getJwtSignatureAlgorithmList();
if (!in_array($algorithm, $allowedAlgorithmList)) {
throw new Invalid("JWT signing algorithm `{$algorithm}` not allowed.");
@@ -76,8 +68,7 @@ class TokenValidator
*/
public function validateFields(Token $token): void
{
/** @var ?string $oidcClientId */
$oidcClientId = $this->config->get('oidcClientId');
$oidcClientId = $this->configDataProvider->getClientId();
if (!$oidcClientId) {
throw new RuntimeException("OIDC: No client ID.");
@@ -0,0 +1,115 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Authentication\Util;
use Espo\Core\ApplicationState;
use Espo\Core\Authentication\ConfigDataProvider;
use Espo\Core\Authentication\Logins\Espo;
use Espo\Core\ORM\EntityManagerProxy;
use Espo\Core\Utils\Metadata;
use Espo\Entities\AuthenticationProvider;
use Espo\Entities\Portal;
use RuntimeException;
/**
* An authentication method provider.
*/
class MethodProvider
{
public function __construct(
private EntityManagerProxy $entityManager,
private ApplicationState $applicationState,
private ConfigDataProvider $configDataProvider,
private Metadata $metadata
) {}
/**
* Get an authentication method.
*/
public function get(): string
{
if ($this->applicationState->isPortal()) {
$method = $this->getForPortal($this->applicationState->getPortal());
if ($method) {
return $method;
}
return $this->getDefaultForPortal();
}
return $this->configDataProvider->getDefaultAuthenticationMethod();
}
/**
* Get an authentication method for portals. The method that is applied via the authentication provider link.
* If no provider, then returns null.
*/
public function getForPortal(Portal $portal): ?string
{
$providerId = $portal->getAuthenticationProvider()?->getId();
if (!$providerId) {
return null;
}
/** @var ?AuthenticationProvider $provider */
$provider = $this->entityManager->getEntityById(AuthenticationProvider::ENTITY_TYPE, $providerId);
if (!$provider) {
throw new RuntimeException("No authentication provider for portal.");
}
$method = $provider->getMethod();
if (!$method) {
throw new RuntimeException("No method in authentication provider.");
}
return $method;
}
/**
* Get a default authentication method for portals. Should be used if a portal does not have
* an authentication provider.
*/
private function getDefaultForPortal(): string
{
$method = $this->configDataProvider->getDefaultAuthenticationMethod();
$allow = $this->metadata->get(['authenticationMethods', $method, 'portalDefault']);
if (!$allow) {
return Espo::NAME;
}
return $method;
}
}
@@ -30,28 +30,21 @@
namespace Espo\Core\EntryPoint;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\{
Exceptions\NotFoundSilent,
InjectableFactory,
Utils\ClassFinder,
Api\Request,
Api\Response,
};
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\NotFoundSilent;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\ClassFinder;
/**
* Runs entry points.
*/
class EntryPointManager
{
private InjectableFactory $injectableFactory;
private ClassFinder $classFinder;
public function __construct(InjectableFactory $injectableFactory, ClassFinder $classFinder)
{
$this->injectableFactory = $injectableFactory;
$this->classFinder = $classFinder;
}
public function __construct(
private InjectableFactory $injectableFactory,
private ClassFinder $classFinder
) {}
/**
* @throws NotFound
@@ -78,20 +71,6 @@ class EntryPointManager
return $className::$authRequired ?? true;
}
/**
* @throws NotFound
*/
public function checkNotStrictAuth(string $name): bool
{
$className = $this->getClassName($name);
if (!$className) {
throw new NotFoundSilent("Entry point '{$name}' not found.");
}
return $className::$notStrictAuth ?? false;
}
/**
* @throws NotFound
*/
+8 -15
View File
@@ -67,7 +67,6 @@ class Starter
/**
* @throws BadRequest
* @throws NotFound
*/
public function start(?string $entryPoint = null, bool $final = false): void
{
@@ -92,7 +91,6 @@ class Starter
try {
$authRequired = $this->entryPointManager->checkAuthRequired($entryPoint);
$authNotStrict = $this->entryPointManager->checkNotStrictAuth($entryPoint);
}
catch (NotFound $exception) {
$this->errorOutput->processWithBodyPrinting($requestWrapped, $responseWrapped, $exception);
@@ -102,7 +100,7 @@ class Starter
return;
}
if ($authRequired && !$authNotStrict && !$final) {
if ($authRequired && !$final) {
$portalId = $this->detectPortalId($requestWrapped);
if ($portalId) {
@@ -116,8 +114,7 @@ class Starter
$entryPoint,
$requestWrapped,
$responseWrapped,
$authRequired,
$authNotStrict
$authRequired
);
(new ResponseEmitter())->emit($responseWrapped->getResponse());
@@ -127,8 +124,7 @@ class Starter
string $entryPoint,
RequestWrapper $requestWrapped,
ResponseWrapper $responseWrapped,
bool $authRequired,
bool $authNotStrict
bool $authRequired
): void {
try {
@@ -136,8 +132,7 @@ class Starter
$entryPoint,
$requestWrapped,
$responseWrapped,
$authRequired,
$authNotStrict
$authRequired
);
}
catch (Exception $exception) {
@@ -146,19 +141,17 @@ class Starter
}
/**
* @throws \Espo\Core\Exceptions\NotFound
* @throws NotFound
* @throws BadRequest
*/
private function processRequestInternal(
string $entryPoint,
RequestWrapper $request,
ResponseWrapper $response,
bool $authRequired,
bool $authNotStrict
bool $authRequired
): void {
$authentication = $authNotStrict ?
$this->authenticationFactory->createWithAnyAccessAllowed() :
$this->authenticationFactory->create();
$authentication = $this->authenticationFactory->create();
$apiAuth = $this->authBuilderFactory
->create()
@@ -30,7 +30,7 @@
namespace Espo\Core\EntryPoint\Traits;
/**
* To be used with EntryPoint interface.
* @deprecated
*/
trait NotStrictAuth
{
@@ -30,7 +30,7 @@
namespace Espo\Core\EntryPoints;
/**
* @deprecated Use `Espo\Core\EntryPoint\Traits\NotStrictAuth` instead.
* @deprecated
*/
trait NotStrictAuth
{
@@ -0,0 +1,42 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Entities;
use Espo\Core\ORM\Entity;
class AuthenticationProvider extends Entity
{
public const ENTITY_TYPE = 'AuthenticationProvider';
public function getMethod(): ?string
{
return $this->get('method');
}
}
+8
View File
@@ -29,6 +29,8 @@
namespace Espo\Entities;
use Espo\Core\Field\Link;
class Portal extends \Espo\Core\ORM\Entity
{
public const ENTITY_TYPE = 'Portal';
@@ -64,4 +66,10 @@ class Portal extends \Espo\Core\ORM\Entity
{
return $this->get('url');
}
public function getAuthenticationProvider(): ?Link
{
/** @var ?Link */
return $this->getValueObject('authenticationProvider');
}
}
+13
View File
@@ -172,6 +172,19 @@ class User extends Person
return $this;
}
public function getPortals(): LinkMultiple
{
/** @var LinkMultiple */
return $this->getValueObject('portals');
}
public function setPortals(LinkMultiple $portals): self
{
$this->setValueObject('portals', $portals);
return $this;
}
public function setRoles(LinkMultiple $roles): self
{
$this->setValueObject('roles', $roles);
-3
View File
@@ -32,7 +32,6 @@ namespace Espo\EntryPoints;
use Espo\Core\ApplicationUser;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\EntryPoints\NotStrictAuth;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Exceptions\ForbiddenSilent;
@@ -44,8 +43,6 @@ use Identicon\Identicon;
class Avatar extends Image
{
use NotStrictAuth;
protected string $systemColor = '#a4b5bd';
/** @var array<int, string|array{int, int, int}> */
@@ -77,6 +77,7 @@
"Layout Sets": "Layout Sets",
"Working Time Calendars": "Working Time Calendars",
"Group Email Folders": "Group Email Folders",
"Authentication Providers": "Authentication Providers",
"Success": "Success",
"Fail": "Fail",
"Configuration Instructions": "Configuration Instructions",
@@ -284,6 +285,7 @@
"systemRequirements": "System Requirements for EspoCRM.",
"apiUsers": "Separate users for integration purposes.",
"webhooks": "Manage webhooks.",
"authenticationProviders": "Additional authentication providers for portals.",
"emailAddresses": "All email addresses stored in the system.",
"phoneNumbers": "All phone numbers stored in the system.",
"dashboardTemplates": "Deploy dashboards to users.",
@@ -0,0 +1,8 @@
{
"fields": {
"method": "Method"
},
"labels": {
"Create AuthenticationProvider": "Create Provider"
}
}
@@ -58,7 +58,8 @@
"Webhook": "Webhook",
"Mass Action": "Mass Action",
"WorkingTimeCalendar": "Working Time Calendar",
"WorkingTimeRange": "Working Time Range"
"WorkingTimeRange": "Working Time Range",
"AuthenticationProvider": "Authentication Provider"
},
"scopeNamesPlural": {
"Note": "Notes",
@@ -107,7 +108,8 @@
"LayoutSet": "Layout Sets",
"Webhook": "Webhooks",
"WorkingTimeCalendar": "Working Time Calendars",
"WorkingTimeRange": "Working Time Ranges"
"WorkingTimeRange": "Working Time Ranges",
"AuthenticationProvider": "Authentication Providers"
},
"labels": {
"Sort": "Sort",
@@ -18,6 +18,7 @@
"weekStart": "First Day of Week",
"defaultCurrency": "Default Currency",
"layoutSet": "Layout Set",
"authenticationProvider": "Authentication Provider",
"customUrl": "Custom URL",
"customId": "Custom ID"
},
@@ -25,6 +26,7 @@
"users": "Users",
"portalRoles": "Roles",
"layoutSet": "Layout Set",
"authenticationProvider": "Authentication Provider",
"notes": "Notes"
},
"tooltips": {
@@ -0,0 +1,14 @@
[
{
"rows": [
[
{"name": "name"},
false
],
[
{"name": "method"},
false
]
]
}
]
@@ -0,0 +1,10 @@
[
{
"name":"name",
"link": true
},
{
"name":"method",
"width": 45
}
]
@@ -5,7 +5,7 @@
[{"name": "name"}, {"name": "isActive"}],
[{"name": "url"}, {"name": "isDefault"}],
[{"name": "portalRoles"}, {"name": "customId"}],
[false, {"name": "customUrl"}]
[{"name": "authenticationProvider"}, {"name": "customUrl"}]
]
},
{
@@ -310,6 +310,12 @@
"iconClass": "fas fa-list-ul",
"description": "jobs"
},
{
"url": "#Admin/authenticationProviders",
"label": "Authentication Providers",
"iconClass": "fas fa-sign-in-alt",
"description": "authenticationProviders"
},
{
"url": "#Admin/emailAddresses",
"label": "Email Addresses",
@@ -1,4 +1,5 @@
{
"portalDefault": true,
"settings": {
"isAvailable": true
}
@@ -1,5 +1,6 @@
{
"implementationClassName": "Espo\\Core\\Authentication\\Ldap\\LdapLogin",
"portalDefault": true,
"settings": {
"isAvailable": true,
"layout": {
@@ -5,6 +5,9 @@
"handler": "handlers/login/oidc",
"fallbackConfigParam": "oidcFallback"
},
"provider": {
"isAvailable": true
},
"settings": {
"isAvailable": true,
"layout": {
@@ -0,0 +1,13 @@
{
"controller": "controllers/record",
"recordViews": {
"detail": "views/authentication-provider/record/detail",
"edit": "views/authentication-provider/record/edit"
},
"searchPanelDisabled": true,
"inlineEditDisabled": true,
"duplicateDisabled": true,
"massUpdateDisabled": true,
"massRemoveDisabled": true,
"mergeDisabled": true
}
@@ -0,0 +1,75 @@
{
"fields": {
"name": {
"type": "varchar",
"required": true
},
"method": {
"type": "enum",
"view": "views/authentication-provider/fields/method",
"translation": "Settings.options.authenticationMethod",
"required": true,
"validatorClassNameMap": {
"valid": "Espo\\Classes\\FieldValidators\\AuthenticationProvider\\MethodValid"
}
},
"oidcAuthorizationRedirectUri": {
"type": "varchar",
"notStorable": true,
"readOnly": true
},
"oidcClientId": {
"type": "varchar"
},
"oidcClientSecret": {
"type": "password"
},
"oidcAuthorizationEndpoint": {
"type": "url",
"strip": false
},
"oidcTokenEndpoint": {
"type": "url",
"strip": false
},
"oidcJwksEndpoint": {
"type": "url",
"strip": false
},
"oidcJwtSignatureAlgorithmList": {
"type": "multiEnum",
"optionsPath": "entityDefs.Settings.fields.oidcJwtSignatureAlgorithmList.options",
"default": [
"RS256"
]
},
"oidcScopes": {
"type": "multiEnum",
"allowCustomOptions": true,
"optionsPath": "entityDefs.Settings.fields.oidcScopes.options",
"default": [
"profile",
"email",
"phone"
]
},
"oidcCreateUser": {
"type": "bool",
"tooltip": true
},
"oidcUsernameClaim": {
"type": "varchar",
"optionsPath": "entityDefs.Settings.fields.oidcUsernameClaim.options",
"tooltip": true,
"default": "sub"
},
"oidcSync": {
"type": "bool",
"tooltip": true
},
"oidcLogoutUrl": {
"type": "varchar",
"tooltip": true
}
}
}
@@ -98,6 +98,9 @@
"type": "link",
"tooltip": true
},
"authenticationProvider": {
"type": "link"
},
"modifiedAt": {
"type": "datetime",
"readOnly": true
@@ -145,6 +148,10 @@
"type": "belongsTo",
"entity": "LayoutSet",
"foreign": "portals"
},
"authenticationProvider": {
"type": "belongsTo",
"entity": "AuthenticationProvider"
}
},
"collection": {
@@ -0,0 +1,4 @@
{
"entity": true,
"exportFormatList": ["csv"]
}
+37 -41
View File
@@ -29,19 +29,19 @@
namespace Espo\Tools\App;
use Espo\Core\Authentication\Logins\Espo;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Authentication\Util\MethodProvider as AuthenticationMethodProvider;
use Espo\Core\ApplicationState;
use Espo\Core\Acl;
use Espo\Core\InjectableFactory;
use Espo\Core\DataManager;
use Espo\Core\FieldValidation\FieldValidationManager;
use Espo\Core\Utils\Currency\DatabasePopulator as CurrencyDatabasePopulator;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Config\ConfigWriter;
@@ -54,40 +54,19 @@ use stdClass;
class SettingsService
{
private ApplicationState $applicationState;
private Config $config;
private ConfigWriter $configWriter;
private Metadata $metadata;
private Acl $acl;
private EntityManager $entityManager;
private DataManager $dataManager;
private FieldValidationManager $fieldValidationManager;
private InjectableFactory $injectableFactory;
private Access $access;
public function __construct(
ApplicationState $applicationState,
Config $config,
ConfigWriter $configWriter,
Metadata $metadata,
Acl $acl,
EntityManager $entityManager,
DataManager $dataManager,
FieldValidationManager $fieldValidationManager,
InjectableFactory $injectableFactory,
Access $access
) {
$this->applicationState = $applicationState;
$this->config = $config;
$this->configWriter = $configWriter;
$this->metadata = $metadata;
$this->acl = $acl;
$this->entityManager = $entityManager;
$this->dataManager = $dataManager;
$this->fieldValidationManager = $fieldValidationManager;
$this->injectableFactory = $injectableFactory;
$this->access = $access;
}
private ApplicationState $applicationState,
private Config $config,
private ConfigWriter $configWriter,
private Metadata $metadata,
private Acl $acl,
private EntityManager $entityManager,
private DataManager $dataManager,
private FieldValidationManager $fieldValidationManager,
private InjectableFactory $injectableFactory,
private Access $access,
private AuthenticationMethodProvider $authenticationMethodProvider
) {}
public function getConfigData(): stdClass
{
@@ -133,7 +112,7 @@ class SettingsService
*/
private function getLoginData(): ?array
{
$method = $this->config->get('authenticationMethod') ?? Espo::NAME;
$method = $this->authenticationMethodProvider->get();
/** @var array<string, mixed> $mData */
$mData = $this->metadata->get(['authenticationMethods', $method, 'login']) ?? [];
@@ -145,7 +124,9 @@ class SettingsService
return null;
}
if ($this->applicationState->isPortal()) {
$isProvider = $this->isPortalWithAuthenticationProvider();
if (!$isProvider && $this->applicationState->isPortal()) {
/** @var ?bool $portal */
$portal = $mData['portal'] ?? null;
@@ -173,6 +154,10 @@ class SettingsService
$fallback = $fallbackConfigParam && $this->config->get($fallbackConfigParam);
}
if ($isProvider) {
$fallback = false;
}
/** @var stdClass $data */
$data = (object) ($mData['data'] ?? []);
@@ -184,10 +169,21 @@ class SettingsService
];
}
private function isPortalWithAuthenticationProvider(): bool
{
if (!$this->applicationState->isPortal()) {
return false;
}
$portal = $this->applicationState->getPortal();
return (bool) $this->authenticationMethodProvider->getForPortal($portal);
}
/**
* @throws \Espo\Core\Exceptions\BadRequest
* @throws BadRequest
* @throws Forbidden
* @throws \Espo\Core\Exceptions\Error
* @throws Error
*/
public function setConfigData(stdClass $data): void
{
@@ -372,7 +368,7 @@ class SettingsService
}
/**
* @throws \Espo\Core\Exceptions\BadRequest
* @throws BadRequest
*/
private function processValidation(Entity $entity, stdClass $data): void
{
+21 -31
View File
@@ -30,30 +30,27 @@
namespace Espo\Tools\Oidc;
use Espo\Core\Authentication\Jwt\Exceptions\Invalid;
use Espo\Core\Authentication\Oidc\ConfigDataProvider;
use Espo\Core\Authentication\Oidc\Login as OidcLogin;
use Espo\Core\Authentication\Oidc\BackchannelLogout;
use Espo\Core\Authentication\Util\MethodProvider;
use Espo\Core\Exceptions\Error;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\ForbiddenSilent;
use Espo\Core\Utils\Config;
use Espo\Core\Utils\Json;
class Service
{
private Config $config;
private BackchannelLogout $backchannelLogout;
public function __construct(Config $config, BackchannelLogout $backchannelLogout)
{
$this->config = $config;
$this->backchannelLogout = $backchannelLogout;
}
public function __construct(
private BackchannelLogout $backchannelLogout,
private MethodProvider $methodProvider,
private ConfigDataProvider $configDataProvider
) {}
/**
* @return array{
* clientId: non-empty-string,
* endpoint: non-empty-string,
* redirectUri: non-empty-string,
* redirectUri: string,
* scopes: non-empty-array<string>,
* claims: ?string,
* prompt: 'login'|'consent'|'select_account',
@@ -64,19 +61,15 @@ class Service
*/
public function getAuthorizationData(): array
{
if ($this->config->get('authenticationMethod') !== OidcLogin::NAME) {
throw new ForbiddenSilent();
if ($this->methodProvider->get() !== OidcLogin::NAME) {
throw new Forbidden();
}
/** @var ?string $clientId */
$clientId = $this->config->get('oidcClientId');
/** @var ?string $endpoint */
$endpoint = $this->config->get('oidcAuthorizationEndpoint');
/** @var string[] $scopes */
$scopes = $this->config->get('oidcScopes') ?? [];
/** @var ?string $groupClaim */
$groupClaim = $this->config->get('oidcGroupClaim');
$clientId = $this->configDataProvider->getClientId();
$endpoint = $this->configDataProvider->getAuthorizationEndpoint();
$scopes = $this->configDataProvider->getScopes();
$groupClaim = $this->configDataProvider->getGroupClaim();
$redirectUri = $this->configDataProvider->getRedirectUri();
if (!$clientId) {
throw new Error("No client ID.");
@@ -86,8 +79,6 @@ class Service
throw new Error("No authorization endpoint.");
}
$redirectUri = rtrim($this->config->get('siteUrl') ?? '', '/') . '/oauth-callback.php';
array_unshift($scopes, 'openid');
$claims = null;
@@ -101,9 +92,8 @@ class Service
}
/** @var 'login'|'consent'|'select_account' $prompt */
$prompt = $this->config->get('oidcAuthorizationPrompt') ?? 'consent';
/** @var ?int $maxAge */
$maxAge = $this->config->get('oidcAuthorizationMaxAge');
$prompt = $this->configDataProvider->getAuthorizationPrompt();
$maxAge = $this->configDataProvider->getAuthorizationMaxAge();
return [
'clientId' => $clientId,
@@ -117,19 +107,19 @@ class Service
}
/**
* @throws ForbiddenSilent
* @throws Forbidden
*/
public function backchannelLogout(string $rawToken): void
{
if ($this->config->get('authenticationMethod') !== OidcLogin::NAME) {
throw new ForbiddenSilent();
if ($this->methodProvider->get() !== OidcLogin::NAME) {
throw new Forbidden();
}
try {
$this->backchannelLogout->logout($rawToken);
}
catch (Invalid $e) {
throw new ForbiddenSilent("OIDC logout: Invalid JWT. " . $e->getMessage());
throw new Forbidden("OIDC logout: Invalid JWT. " . $e->getMessage());
}
}
}
+4
View File
@@ -197,6 +197,10 @@ function (Dep, /** typeof module:search-manager.Class */SearchManager, _) {
this.getRouter().dispatch('Attachment', 'list', {fromAdmin: true});
},
actionAuthenticationProviders: function () {
this.getRouter().dispatch('AuthenticationProvider', 'list', {fromAdmin: true});
},
actionEmailAddresses: function () {
this.getRouter().dispatch('EmailAddress', 'list', {fromAdmin: true});
},
@@ -0,0 +1,247 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
define('helpers/misc/authentication-provider', [], function () {
/**
* @memberOf module:helpers/misc/authentication-provider
*/
class Class {
/**
* @param {module:views/record/detail.Class} view A view.
*/
constructor(view) {
/**
* @private
* @type {module:views/record/detail.Class}
*/
this.view = view;
this.metadata = view.getMetadata();
/**
* @private
* @type {module:model.Class}
*/
this.model = view.model;
/** @var {Object.<string, Object.<string, *>>} defs */
let defs = view.getMetadata().get(['authenticationMethods']) || {};
/**
* @private
* @type {string[]}
*/
this.methodList = Object.keys(defs).filter(item => {
/** @var {Object.<string, *>} */
let data = defs[item].provider || {};
return data.isAvailable;
});
/** @private */
this.authFields = {};
/** @private */
this.dynamicLogicDefs = {
fields: {},
panels: {},
};
}
/**
*
* @param {function(): void} callback
*/
setupPanelsVisibility(callback) {
this.handlePanelsVisibility(callback);
this.view.listenTo(this.model, 'change:method', () => this.handlePanelsVisibility(callback));
}
/**
* @private
* @param {string} method
* @param {string} param
* @return {*}
*/
getFromMetadata(method, param) {
return this.metadata
.get(['authenticationMethods', method, 'provider', param]) ||
this.metadata
.get(['authenticationMethods', method, 'settings', param]);
}
/**
* @return {Object}
*/
setupMethods() {
this.methodList.forEach(method => this.setupMethod(method));
return this.dynamicLogicDefs;
}
/**
* @private
*/
setupMethod(method) {
/** @var {string[]} */
let fieldList = this.getFromMetadata(method, 'fieldList') || [];
fieldList = fieldList.filter(item => this.model.hasField(item));
this.authFields[method] = fieldList;
let mDynamicLogicFieldsDefs = (this.getFromMetadata(method, 'dynamicLogic') || {}).fields || {};
for (let f in mDynamicLogicFieldsDefs) {
if (!fieldList.includes(f)) {
continue;
}
let defs = this.modifyDynamicLogic(mDynamicLogicFieldsDefs[f]);
this.dynamicLogicDefs.fields[f] = Espo.Utils.cloneDeep(defs);
}
}
/**
* @private
*/
modifyDynamicLogic(defs) {
defs = Espo.Utils.clone(defs);
if (Array.isArray(defs)) {
return defs.map(item => this.modifyDynamicLogic(item));
}
if (typeof defs === 'object') {
let o = {};
for (let property in defs) {
let value = defs[property];
if (property === 'attribute' && value === 'authenticationMethod') {
value = 'method';
}
o[property] = this.modifyDynamicLogic(value);
}
return o;
}
return defs;
}
modifyDetailLayout(layout) {
this.methodList.forEach(method => {
let mLayout = this.getFromMetadata(method, 'layout');
if (!mLayout) {
return;
}
mLayout = Espo.Utils.cloneDeep(mLayout);
mLayout.name = method;
this.prepareLayout(mLayout, method);
layout.push(mLayout);
});
}
prepareLayout(layout, method) {
layout.rows.forEach(row => {
row
.filter(item => !item.noLabel && !item.labelText && item.name)
.forEach(item => {
if (item === null) {
return;
}
let labelText = this.view.translate(item.name, 'fields', 'Settings');
item.options = item.options || {};
if (labelText && labelText.toLowerCase().indexOf(method.toLowerCase() + ' ') === 0) {
item.labelText = labelText.substring(method.length + 1);
}
item.options.tooltipText = this.view.translate(item.name, 'tooltips', 'Settings');
});
});
layout.rows = layout.rows.map(row => {
row = row.map(cell => {
if (
cell &&
cell.name &&
!this.model.hasField(cell.name)
) {
return false;
}
return cell;
})
return row;
});
}
/**
* @private
* @param {function(): void} callback
*/
handlePanelsVisibility(callback) {
let authenticationMethod = this.model.get('method');
this.methodList.forEach(method => {
let fieldList = (this.authFields[method] || []);
if (method !== authenticationMethod) {
this.view.hidePanel(method);
fieldList.forEach(field => {
this.view.hideField(field);
});
return;
}
this.view.showPanel(method);
fieldList.forEach(field => this.view.showField(field));
callback();
});
}
}
return Class;
});
@@ -0,0 +1,50 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of tтhe "EspoCRM" word.
************************************************************************/
define('views/authentication-provider/fields/method', ['views/fields/enum'], function (Dep) {
return Dep.extend({
setupOptions: function () {
/** @var {Object.<string, Object.<string, *>>} defs */
let defs = this.getMetadata().get(['authenticationMethods']) || {};
let options = Object.keys(defs)
.filter(item => {
/** @var {Object.<string, *>} */
let data = defs[item].provider || {};
return data.isAvailable;
});
options.unshift('');
this.params.options = options;
},
});
});
@@ -0,0 +1,62 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
define('views/authentication-provider/record/detail', ['views/record/detail', 'helpers/misc/authentication-provider'],
function (Dep, Helper) {
return Dep.extend({
editModeDisabled: true,
/**
* @private
* @type {module:helpers/misc/authentication-provider.Class}
*/
helper: null,
setup: function () {
this.helper = new Helper(this);
Dep.prototype.setup.call(this);
},
setupBeforeFinal: function () {
this.dynamicLogicDefs = this.helper.setupMethods();
Dep.prototype.setupBeforeFinal.call(this);
this.helper.setupPanelsVisibility(() => {
this.processDynamicLogic();
});
},
modifyDetailLayout: function (layout) {
this.helper.modifyDetailLayout(layout);
},
});
});
@@ -0,0 +1,62 @@
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
define('views/authentication-provider/record/edit', ['views/record/edit', 'helpers/misc/authentication-provider'],
function (Dep, Helper) {
return Dep.extend({
saveAndNewAction: false,
/**
* @private
* @type {module:helpers/misc/authentication-provider.Class}
*/
helper: null,
setup: function () {
this.helper = new Helper(this);
Dep.prototype.setup.call(this);
},
setupBeforeFinal: function () {
this.dynamicLogicDefs = this.helper.setupMethods();
Dep.prototype.setupBeforeFinal.call(this);
this.helper.setupPanelsVisibility(() => {
this.processDynamicLogic();
});
},
modifyDetailLayout: function (layout) {
this.helper.modifyDetailLayout(layout);
},
});
});
@@ -32,16 +32,54 @@ define('views/settings/fields/oidc-redirect-uri', ['views/fields/varchar'], func
detailTemplateContent: `{{value}}`,
portalCollection: null,
data: function () {
let valueIsSet = this.model.entityType !== 'AuthenticationProvider' ||
this.portalCollection;
return {
value: this.getValueForDisplay(),
valueIsSet: valueIsSet,
};
},
getValueForDisplay: function () {
if (this.model.entityType === 'AuthenticationProvider') {
if (!this.portalCollection) {
return null;
}
return this.portalCollection.models
.map(model => {
let url = (model.get('url') || '').replace(/\/+$/, '');
return url + '/oauth-callback.php';
})
.join('\n');
}
let siteUrl = (this.getConfig().get('siteUrl') || '').replace(/\/+$/, '');
return siteUrl + '/oauth-callback.php';
},
setup: function () {
Dep.prototype.setup.call(this);
if (this.model.entityType === 'AuthenticationProvider') {
this.getCollectionFactory()
.create('Portal')
.then(collection => {
collection.data.select = ['url', 'isDefault'];
collection.fetch().then(() => {
this.portalCollection = collection;
this.reRender();
})
});
}
},
});
});
+41
View File
@@ -0,0 +1,41 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
include "../../bootstrap.php";
use Espo\Core\Application;
use Espo\Core\Application\Runner\Params;
use Espo\Core\ApplicationRunners\EntryPoint;
$app = new Application();
$app->run(
EntryPoint::class,
Params::create()->with('entryPoint', 'oauthCallback')
);
@@ -30,6 +30,8 @@
namespace tests\unit\Espo\Core\Authentication\Logins\Oidc;
use Espo\Core\Acl\Cache\Clearer;
use Espo\Core\ApplicationState;
use Espo\Core\Authentication\Oidc\ConfigDataProvider;
use Espo\Core\Authentication\Oidc\Sync;
use Espo\Core\FieldProcessing\EmailAddress\Saver as EmailAddressSaver;
use Espo\Core\FieldProcessing\PhoneNumber\Saver as PhoneNumberSaver;
@@ -43,19 +45,23 @@ class SyncTest extends TestCase
{
private ?Sync $sync = null;
private ?Config $config = null;
private ?ConfigDataProvider $configDataProvider = null;
protected function setUp(): void
{
$this->config = $this->createMock(Config::class);
$this->configDataProvider = $this->createMock(ConfigDataProvider::class);
$this->sync = new Sync(
$this->createMock(EntityManager::class),
$this->config,
$this->configDataProvider,
$this->createMock(LinkMultipleSaver::class),
$this->createMock(EmailAddressSaver::class),
$this->createMock(PhoneNumberSaver::class),
$this->createMock(PasswordHash::class),
$this->createMock(Clearer::class)
$this->createMock(Clearer::class),
$this->createMock(ApplicationState::class)
);
}