oidc portal
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user