diff --git a/application/Espo/Classes/FieldProcessing/OAuthAccount/DataLoader.php b/application/Espo/Classes/FieldProcessing/OAuthAccount/DataLoader.php new file mode 100644 index 0000000000..461b739592 --- /dev/null +++ b/application/Espo/Classes/FieldProcessing/OAuthAccount/DataLoader.php @@ -0,0 +1,72 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldProcessing\OAuthAccount; + +use Espo\Core\FieldProcessing\Loader; +use Espo\Core\FieldProcessing\Loader\Params; +use Espo\Entities\OAuthAccount; +use Espo\ORM\Entity; +use Espo\Tools\OAuth\ConfigDataProvider; + +/** + * @implements Loader + */ +class DataLoader implements Loader +{ + public function __construct( + private ConfigDataProvider $configDataProvider, + ) {} + + public function process(Entity $entity, Params $params): void + { + if (!$entity->get('providerId')) { + return; + } + + $provider = $entity->getProvider(); + + $scope = null; + + if ($provider->getScopes()) { + $scope = implode($provider->getScopeSeparator() ?? ' ', $provider->getScopes()); + } + + $data = [ + 'endpoint' => $provider->getAuthorizationEndpoint(), + 'clientId' => $provider->getClientId(), + 'redirectUri' => $this->configDataProvider->getRedirectUri(), + 'scope' => $scope, + 'prompt' => $provider->getAuthorizationPrompt(), + 'params' => $provider->getAuthorizationParams(), + ]; + + $entity->set('data', $data); + } +} diff --git a/application/Espo/Classes/FieldProcessing/OAuthProvider/AuthorizationRedirectUriLoader.php b/application/Espo/Classes/FieldProcessing/OAuthProvider/AuthorizationRedirectUriLoader.php new file mode 100644 index 0000000000..b694ff0589 --- /dev/null +++ b/application/Espo/Classes/FieldProcessing/OAuthProvider/AuthorizationRedirectUriLoader.php @@ -0,0 +1,51 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\FieldProcessing\OAuthProvider; + +use Espo\Core\FieldProcessing\Loader; +use Espo\Core\FieldProcessing\Loader\Params; +use Espo\Entities\OAuthProvider; +use Espo\ORM\Entity; +use Espo\Tools\OAuth\ConfigDataProvider; + +/** + * @implements Loader + */ +class AuthorizationRedirectUriLoader implements Loader +{ + public function __construct( + private ConfigDataProvider $configDataProvider, + ) {} + + public function process(Entity $entity, Params $params): void + { + $entity->set('authorizationRedirectUri', $this->configDataProvider->getRedirectUri()); + } +} diff --git a/application/Espo/Classes/Record/OAuthProvider/GeneralFilter.php b/application/Espo/Classes/Record/OAuthProvider/GeneralFilter.php new file mode 100644 index 0000000000..7a53cacf27 --- /dev/null +++ b/application/Espo/Classes/Record/OAuthProvider/GeneralFilter.php @@ -0,0 +1,71 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\Record\OAuthProvider; + +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Record\Input\Data; +use Espo\Core\Record\Input\Filter; +use Espo\Core\Utils\Crypt; + +/** + * @noinspection PhpUnused + */ +class GeneralFilter implements Filter +{ + private const ATTR_CLIENT_SECRET = 'clientSecret'; + + public function __construct(private Crypt $crypt) {} + + /** + * @throws BadRequest + */ + public function filter(Data $data): void + { + $this->processClientSecret($data); + } + + /** + * @throws BadRequest + */ + private function processClientSecret(Data $data): void + { + $value = $data->get(self::ATTR_CLIENT_SECRET); + + if ($value === null) { + return; + } + + if (!is_string($value)) { + throw new BadRequest(); + } + + $data->set(self::ATTR_CLIENT_SECRET, $this->crypt->encrypt($value)); + } +} diff --git a/application/Espo/Controllers/OAuthAccount.php b/application/Espo/Controllers/OAuthAccount.php new file mode 100644 index 0000000000..73a0894b40 --- /dev/null +++ b/application/Espo/Controllers/OAuthAccount.php @@ -0,0 +1,47 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Controllers; + +use Espo\Core\Controllers\RecordBase; + +/** + * @noinspection PhpUnused + */ +class OAuthAccount extends RecordBase +{ + protected function checkAccess(): bool + { + if (!$this->user->isAdmin()) { + return false; + } + + return true; + } +} diff --git a/application/Espo/Controllers/OAuthProvider.php b/application/Espo/Controllers/OAuthProvider.php new file mode 100644 index 0000000000..d3073654cb --- /dev/null +++ b/application/Espo/Controllers/OAuthProvider.php @@ -0,0 +1,47 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Controllers; + +use Espo\Core\Controllers\Record; + +/** + * @noinspection PhpUnused + */ +class OAuthProvider extends Record +{ + protected function checkAccess(): bool + { + if (!$this->user->isAdmin()) { + return false; + } + + return true; + } +} diff --git a/application/Espo/Core/Formula/Functions/ExtGroup/OauthGroup/GetAccessTokenType.php b/application/Espo/Core/Formula/Functions/ExtGroup/OauthGroup/GetAccessTokenType.php new file mode 100644 index 0000000000..43710dea8d --- /dev/null +++ b/application/Espo/Core/Formula/Functions/ExtGroup/OauthGroup/GetAccessTokenType.php @@ -0,0 +1,80 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Formula\Functions\ExtGroup\OauthGroup; + +use Espo\Core\Formula\EvaluatedArgumentList; +use Espo\Core\Formula\Exceptions\BadArgumentType; +use Espo\Core\Formula\Exceptions\Error; +use Espo\Core\Formula\Exceptions\TooFewArguments; +use Espo\Core\Formula\Func; +use Espo\Tools\OAuth\Exceptions\AccountNotFound; +use Espo\Tools\OAuth\Exceptions\NoToken; +use Espo\Tools\OAuth\Exceptions\ProviderNotAvailable; +use Espo\Tools\OAuth\Exceptions\TokenObtainingFailure; +use Espo\Tools\OAuth\TokensProvider; + +/** + * @noinspection PhpUnused + */ +class GetAccessTokenType implements Func +{ + public function __construct( + private TokensProvider $provider, + ) {} + + public function process(EvaluatedArgumentList $arguments): string + { + if (count($arguments) < 1) { + throw TooFewArguments::create(1); + } + + $id = $arguments[0]; + + if (!is_string($id)) { + throw BadArgumentType::create(1, 'string'); + } + + try { + $tokens = $this->provider->get($arguments[0]); + } catch (AccountNotFound|NoToken|ProviderNotAvailable|TokenObtainingFailure $e) { + $message = "Could not obtain access token for OAuth account $id."; + + throw new Error($message, 500, $e); + } + + $accessToken = $tokens->getAccessToken(); + + if (!$accessToken) { + throw new Error("No access token."); + } + + return $accessToken; + } +} diff --git a/application/Espo/Entities/OAuthAccount.php b/application/Espo/Entities/OAuthAccount.php new file mode 100644 index 0000000000..1b2972c2ef --- /dev/null +++ b/application/Espo/Entities/OAuthAccount.php @@ -0,0 +1,83 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +/** @noinspection PhpMultipleClassDeclarationsInspection */ + +namespace Espo\Entities; + +use Espo\Core\Field\DateTime; +use Espo\Core\ORM\Entity; +use ValueError; + +class OAuthAccount extends Entity +{ + public const ENTITY_TYPE = 'OAuthAccount'; + + public function getProvider(): OAuthProvider + { + $provider = $this->relations->getOne('provider'); + + if (!$provider instanceof OAuthProvider) { + throw new ValueError("No provider."); + } + + return $provider; + } + + public function getAccessToken(): ?string + { + return $this->get('accessToken'); + } + + public function getRefreshToken(): ?string + { + return $this->get('refreshToken'); + } + + public function getExpiresAt(): ?DateTime + { + /** @var ?DateTime */ + return $this->getValueObject('expiresAt'); + } + + public function setAccessToken(?string $accessToken): self + { + return $this->set('accessToken', $accessToken); + } + + public function setRefreshToken(?string $refreshToken): self + { + return $this->set('refreshToken', $refreshToken); + } + + public function setExpiresAt(?DateTime $expiresAt): self + { + return $this->setValueObject('expiresAt', $expiresAt); + } +} diff --git a/application/Espo/Entities/OAuthProvider.php b/application/Espo/Entities/OAuthProvider.php new file mode 100644 index 0000000000..c1183b0d87 --- /dev/null +++ b/application/Espo/Entities/OAuthProvider.php @@ -0,0 +1,118 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +/** @noinspection PhpMultipleClassDeclarationsInspection */ + +namespace Espo\Entities; + +use Espo\Core\ORM\Entity; +use stdClass; +use ValueError; + +class OAuthProvider extends Entity +{ + public const ENTITY_TYPE = 'OAuthProvider'; + + public function isActive(): bool + { + return $this->get('isActive'); + } + + public function getClientId(): string + { + $value = $this->get('clientId'); + + if (!is_string($value)) { + throw new ValueError("No client ID."); + } + + return $value; + } + + public function getClientSecret(): string + { + $value = $this->get('clientSecret'); + + if (!is_string($value)) { + throw new ValueError("No client secret."); + } + + return $value; + } + + public function getTokenEndpoint(): string + { + $value = $this->get('tokenEndpoint'); + + if (!is_string($value)) { + throw new ValueError("No token endpoint."); + } + + return $value; + } + + public function getAuthorizationEndpoint(): string + { + $value = $this->get('authorizationEndpoint'); + + if (!is_string($value)) { + throw new ValueError("No authorization endpoint."); + } + + return $value; + } + + public function getResourceEndpoint(): ?string + { + return $this->get('resourceEndpoint'); + } + + /** + * @return string[] + */ + public function getScopes(): array + { + return $this->get('scopes') ?? []; + } + + public function getScopeSeparator(): ?string + { + return $this->get('scopeSeparator'); + } + + public function getAuthorizationPrompt(): string + { + return $this->get('authorizationPrompt'); + } + + public function getAuthorizationParams(): ?stdClass + { + return $this->get('authorizationParams') ?? null; + } +} diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 0b9940e1eb..490c63c7bf 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -82,6 +82,7 @@ "Group Email Folders": "Group Email Folders", "Authentication Providers": "Authentication Providers", "Address Countries": "Address Countries", + "OAuth Providers": "OAuth Providers", "Success": "Success", "Fail": "Fail", "Configuration Instructions": "Configuration Instructions", @@ -318,7 +319,8 @@ "sms": "SMS settings.", "pdfTemplates": "Templates for printing to PDF.", "formulaSandbox": "Write and test formula scripts.", - "addressCountries": "Countries available for address fields." + "addressCountries": "Countries available for address fields.", + "oAuthProviders": "OAuth providers for integrations." }, "keywords": { "settings": "system", diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 66c35994c5..aa754fcb36 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -64,7 +64,9 @@ "AuthenticationProvider": "Authentication Provider", "GlobalStream": "Global Stream", "AddressCountry": "Address Country", - "AppSecret": "App Secret" + "AppSecret": "App Secret", + "OAuthProvider": "OAuth Provider", + "OAuthAccount": "OAuth Account" }, "scopeNamesPlural": { "Note": "Notes", @@ -119,7 +121,9 @@ "AuthenticationProvider": "Authentication Providers", "GlobalStream": "Global Stream", "AddressCountry": "Address Countries", - "AppSecret": "App Secrets" + "AppSecret": "App Secrets", + "OAuthProvider": "OAuth Providers", + "OAuthAccount": "OAuth Accounts" }, "labels": { "Previous Page": "Previous Page", diff --git a/application/Espo/Resources/i18n/en_US/OAuthAccount.json b/application/Espo/Resources/i18n/en_US/OAuthAccount.json new file mode 100644 index 0000000000..ef8895c969 --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/OAuthAccount.json @@ -0,0 +1,16 @@ +{ + "labels": { + "Create OAuthAccount": "Create OAuth Account", + "Connection": "Connection" + }, + "fields": { + "provider": "Provider", + "hasAccessToken": "Has Access Token", + "user": "User", + "providerIsActive": "Provider is Active", + "data": "Data" + }, + "links": { + "provider": "Provider" + } +} diff --git a/application/Espo/Resources/i18n/en_US/OAuthProvider.json b/application/Espo/Resources/i18n/en_US/OAuthProvider.json new file mode 100644 index 0000000000..1b0d05ebc6 --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/OAuthProvider.json @@ -0,0 +1,25 @@ +{ + "labels": { + "Create OAuthProvider": "Create OAuth Provider" + }, + "fields": { + "isActive": "Is Active", + "clientId": "Client ID", + "clientSecret": "Client Secret", + "authorizationEndpoint": "Authorization Endpoint", + "tokenEndpoint": "Token Endpoint", + "resourceEndpoint": "Resource Endpoint", + "authorizationRedirectUri": "Authorization Redirect URI", + "scopes": "Scopes", + "scopeSeparator": "Scope Separator", + "hasAccessToken": "Has Access Token", + "authorizationPrompt": "Authorization Prompt", + "authorizationParams": "Authorization Params" + }, + "links": { + "accounts": "Accounts" + }, + "tooltips": { + "authorizationParams": "Additional query parameters to be sent to the authorization endpoint. Specified in JSON format." + } +} diff --git a/application/Espo/Resources/layouts/OAuthAccount/detail.json b/application/Espo/Resources/layouts/OAuthAccount/detail.json new file mode 100644 index 0000000000..a01dfd9857 --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthAccount/detail.json @@ -0,0 +1,13 @@ +[ + { + "rows": [ + [{"name": "name"}, {"name": "provider"}], + [{"name": "user"}, {"name": "hasAccessToken"}] + ] + }, + { + "rows": [ + [{"name": "description"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/OAuthAccount/detailSmall.json b/application/Espo/Resources/layouts/OAuthAccount/detailSmall.json new file mode 100644 index 0000000000..667bfbd734 --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthAccount/detailSmall.json @@ -0,0 +1,14 @@ +[ + { + "rows": [ + [{"name": "name"}], + [{"name": "provider"}], + [{"name": "user"}, {"name": "hasAccessToken"}] + ] + }, + { + "rows": [ + [{"name": "description"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/OAuthAccount/list.json b/application/Espo/Resources/layouts/OAuthAccount/list.json new file mode 100644 index 0000000000..2e8d7a0e7a --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthAccount/list.json @@ -0,0 +1,10 @@ +[ + { + "name": "name", + "link": true + }, + { + "name": "provider", + "width": 50 + } +] diff --git a/application/Espo/Resources/layouts/OAuthAccount/listForProvider.json b/application/Espo/Resources/layouts/OAuthAccount/listForProvider.json new file mode 100644 index 0000000000..2c6defa7ff --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthAccount/listForProvider.json @@ -0,0 +1,10 @@ +[ + { + "name": "name", + "link": true + }, + { + "name": "hasAccessToken", + "width": 50 + } +] diff --git a/application/Espo/Resources/layouts/OAuthAccount/listSmall.json b/application/Espo/Resources/layouts/OAuthAccount/listSmall.json new file mode 100644 index 0000000000..2e8d7a0e7a --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthAccount/listSmall.json @@ -0,0 +1,10 @@ +[ + { + "name": "name", + "link": true + }, + { + "name": "provider", + "width": 50 + } +] diff --git a/application/Espo/Resources/layouts/OAuthProvider/bottomPanelsDetail.json b/application/Espo/Resources/layouts/OAuthProvider/bottomPanelsDetail.json new file mode 100644 index 0000000000..b0417593f2 --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthProvider/bottomPanelsDetail.json @@ -0,0 +1,5 @@ +{ + "accounts": { + "index": 0 + } +} diff --git a/application/Espo/Resources/layouts/OAuthProvider/detail.json b/application/Espo/Resources/layouts/OAuthProvider/detail.json new file mode 100644 index 0000000000..470e317722 --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthProvider/detail.json @@ -0,0 +1,27 @@ +[ + { + "rows": [ + [{"name": "name"}, {"name": "isActive"}], + [{"name": "clientId"}, {"name": "clientSecret"}], + [{"name": "authorizationRedirectUri"}, false] + ] + }, + { + "rows": [ + [{"name": "authorizationEndpoint"}, {"name": "tokenEndpoint"}], + [{"name": "resourceEndpoint"}, false] + ] + }, + { + "rows": [ + [{"name": "scopes"}], + [{"name": "scopeSeparator"}, false], + [{"name": "authorizationParams"}, {"name": "authorizationPrompt"}] + ] + }, + { + "rows": [ + [{"name": "description"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/OAuthProvider/detailSmall.json b/application/Espo/Resources/layouts/OAuthProvider/detailSmall.json new file mode 100644 index 0000000000..4a58834761 --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthProvider/detailSmall.json @@ -0,0 +1,7 @@ +[ + { + "rows": [ + [{"name": "name"}, {"name": "isActive"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/OAuthProvider/list.json b/application/Espo/Resources/layouts/OAuthProvider/list.json new file mode 100644 index 0000000000..b9a72cd908 --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthProvider/list.json @@ -0,0 +1,10 @@ +[ + { + "name": "name", + "link": true + }, + { + "name": "isActive", + "width": 50 + } +] diff --git a/application/Espo/Resources/layouts/OAuthProvider/listSmall.json b/application/Espo/Resources/layouts/OAuthProvider/listSmall.json new file mode 100644 index 0000000000..b9a72cd908 --- /dev/null +++ b/application/Espo/Resources/layouts/OAuthProvider/listSmall.json @@ -0,0 +1,10 @@ +[ + { + "name": "name", + "link": true + }, + { + "name": "isActive", + "width": 50 + } +] diff --git a/application/Espo/Resources/metadata/app/adminPanel.json b/application/Espo/Resources/metadata/app/adminPanel.json index eeb1e2d421..eeb736ab83 100644 --- a/application/Espo/Resources/metadata/app/adminPanel.json +++ b/application/Espo/Resources/metadata/app/adminPanel.json @@ -350,6 +350,12 @@ "iconClass": "fas fa-key", "description": "appSecrets" }, + { + "url": "#Admin/oAuthProviders", + "label": "OAuth Providers", + "iconClass": "fas fa-sign-in-alt", + "description": "oAuthProviders" + }, { "url": "#Admin/appLog", "label": "App Log", diff --git a/application/Espo/Resources/metadata/app/formula.json b/application/Espo/Resources/metadata/app/formula.json index 3c8a6bf103..d2b2e28df8 100644 --- a/application/Espo/Resources/metadata/app/formula.json +++ b/application/Espo/Resources/metadata/app/formula.json @@ -625,6 +625,12 @@ "insertText": "ext\\acl\\getPermissionLevel(USER_ID, PERMISSION)", "returnType": "string", "unsafe": true + }, + { + "name": "ext\\oauth\\getAccessToken", + "insertText": "ext\\oauth\\getAccessToken(ID)", + "returnType": "string", + "unsafe": true } ], "functionClassNameMap": { @@ -637,6 +643,7 @@ "ext\\acl\\getLevel": "Espo\\Core\\Formula\\Functions\\ExtGroup\\AclGroup\\GetLevelType", "ext\\acl\\getPermissionLevel": "Espo\\Core\\Formula\\Functions\\ExtGroup\\AclGroup\\GetPermissionLevelType", "util\\base64Encode": "Espo\\Core\\Formula\\Functions\\UtilGroup\\Base64EncodeType", - "util\\base64Decode": "Espo\\Core\\Formula\\Functions\\UtilGroup\\Base64DecodeType" + "util\\base64Decode": "Espo\\Core\\Formula\\Functions\\UtilGroup\\Base64DecodeType", + "ext\\oauth\\getAccessToken": "Espo\\Core\\Formula\\Functions\\ExtGroup\\OauthGroup\\GetAccessTokenType" } } diff --git a/application/Espo/Resources/metadata/app/jsLibs.json b/application/Espo/Resources/metadata/app/jsLibs.json index 9142b3934e..4dc60a7e5b 100644 --- a/application/Espo/Resources/metadata/app/jsLibs.json +++ b/application/Espo/Resources/metadata/app/jsLibs.json @@ -158,6 +158,11 @@ "exportsTo": "ace.require.define.modules", "exportsAs": "ace/mode/javascript" }, + "ace-mode-json": { + "path": "client/lib/ace-mode-json.js", + "exportsTo": "ace.require.define.modules", + "exportsAs": "ace/mode/json" + }, "ace-ext-language_tools": { "path": "client/lib/ace-ext-language_tools.js", "exportsTo": "ace.require.define.modules", diff --git a/application/Espo/Resources/metadata/clientDefs/OAuthAccount.json b/application/Espo/Resources/metadata/clientDefs/OAuthAccount.json new file mode 100644 index 0000000000..231ddf57d3 --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/OAuthAccount.json @@ -0,0 +1,13 @@ +{ + "controller": "controllers/record", + "sidePanels": { + "detail": [ + { + "name": "connection", + "label": "Connection", + "view": "views/o-auth-account/records/panels/connection", + "notRefreshable": true + } + ] + } +} diff --git a/application/Espo/Resources/metadata/clientDefs/OAuthProvider.json b/application/Espo/Resources/metadata/clientDefs/OAuthProvider.json new file mode 100644 index 0000000000..035c56421c --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/OAuthProvider.json @@ -0,0 +1,64 @@ +{ + "controller": "controllers/record", + "relationshipPanels": { + "accounts": { + "layout": "listForProvider", + "selectDisabled": true, + "unlinkDisabled": true + } + }, + "dynamicLogic": { + "fields": { + "authorizationRedirectUri": { + "visible": { + "conditionGroup": [ + { + "type": "isNotEmpty", + "attribute": "id" + } + ] + } + }, + "clientId": { + "required": { + "conditionGroup": [ + { + "type": "isTrue", + "attribute": "isActive" + } + ] + } + }, + "clientSecret": { + "required": { + "conditionGroup": [ + { + "type": "isTrue", + "attribute": "isActive" + } + ] + } + }, + "authorizationEndpoint": { + "required": { + "conditionGroup": [ + { + "type": "isTrue", + "attribute": "isActive" + } + ] + } + }, + "tokenEndpoint": { + "required": { + "conditionGroup": [ + { + "type": "isTrue", + "attribute": "isActive" + } + ] + } + } + } + } +} diff --git a/application/Espo/Resources/metadata/entityAcl/OAuthAccount.json b/application/Espo/Resources/metadata/entityAcl/OAuthAccount.json new file mode 100644 index 0000000000..57f1755b3e --- /dev/null +++ b/application/Espo/Resources/metadata/entityAcl/OAuthAccount.json @@ -0,0 +1,13 @@ +{ + "fields": { + "accessToken": { + "forbidden": true + }, + "refreshToken": { + "forbidden": true + }, + "expiresAt": { + "forbidden": true + } + } +} diff --git a/application/Espo/Resources/metadata/entityAcl/OAuthProvider.json b/application/Espo/Resources/metadata/entityAcl/OAuthProvider.json new file mode 100644 index 0000000000..83d945dfe9 --- /dev/null +++ b/application/Espo/Resources/metadata/entityAcl/OAuthProvider.json @@ -0,0 +1,7 @@ +{ + "fields": { + "clientSecret": { + "internal": true + } + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/OAuthAccount.json b/application/Espo/Resources/metadata/entityDefs/OAuthAccount.json new file mode 100644 index 0000000000..f76a38b452 --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/OAuthAccount.json @@ -0,0 +1,91 @@ +{ + "fields": { + "name": { + "type": "varchar", + "required": true, + "maxLength": 100 + }, + "provider": { + "type": "link", + "required": true, + "readOnlyAfterCreate": true + }, + "user": { + "type": "link", + "readOnly": true + }, + "hasAccessToken": { + "type": "bool", + "readOnly": true, + "notStorable": true, + "orderDisabled": true, + "directAccessDisabled": true, + "select": { + "select": "IS_NOT_NULL:(accessToken)" + } + }, + "providerIsActive": { + "type": "foreign", + "link": "provider", + "field": "isActive" + }, + "data": { + "type": "jsonObject", + "notStorable": true, + "directAccessDisabled": true, + "readOnly": true + }, + "accessToken": { + "type": "password", + "readOnly": true, + "dbType": "text" + }, + "refreshToken": { + "type": "password", + "readOnly": true, + "dbType": "text" + }, + "description": { + "type": "text" + }, + "expiresAt": { + "type": "datetime", + "readOnly": true + }, + "createdAt": { + "type": "datetime", + "readOnly": true + }, + "modifiedAt": { + "type": "datetime", + "readOnly": true + }, + "createdBy": { + "type": "link", + "readOnly": true + }, + "modifiedBy": { + "type": "link", + "readOnly": true + } + }, + "links": { + "provider": { + "type": "belongsTo", + "entity": "OAuthProvider", + "foreign": "accounts" + }, + "user": { + "type": "belongsTo", + "entity": "User" + }, + "createdBy": { + "type": "belongsTo", + "entity": "User" + }, + "modifiedBy": { + "type": "belongsTo", + "entity": "User" + } + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/OAuthProvider.json b/application/Espo/Resources/metadata/entityDefs/OAuthProvider.json new file mode 100644 index 0000000000..cfa6c3e4d0 --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/OAuthProvider.json @@ -0,0 +1,109 @@ +{ + "fields": { + "name": { + "type": "varchar", + "required": true, + "maxLength": 100 + }, + "isActive": { + "type": "bool", + "default": true + }, + "clientId": { + "type": "varchar", + "maxLength": 150 + }, + "clientSecret": { + "type": "password", + "maxLength": 512, + "dbType": "text" + }, + "authorizationEndpoint": { + "type": "url", + "maxLength": 512, + "dbType": "text", + "strip": false + }, + "tokenEndpoint": { + "type": "url", + "maxLength": 512, + "dbType": "text", + "strip": false + }, + "resourceEndpoint": { + "type": "url", + "maxLength": 512, + "dbType": "text", + "strip": false + }, + "authorizationRedirectUri": { + "type": "url", + "notStorable": true, + "readOnly": true, + "copyToClipboard": true, + "directAccessDisabled": true + }, + "authorizationPrompt": { + "type": "enum", + "default": "none", + "options": [ + "none", + "consent", + "login", + "select_account" + ], + "maxLength": 14 + }, + "scopes": { + "type": "array", + "noEmptyString": true, + "allowCustomOptions": true, + "storeArrayValues": false, + "displayAsList": true + }, + "authorizationParams": { + "type": "jsonObject", + "view": "views/o-auth-provider/fields/authorization-params", + "tooltip": true + }, + "scopeSeparator": { + "type": "varchar", + "maxLength": 1 + }, + "description": { + "type": "text" + }, + "createdAt": { + "type": "datetime", + "readOnly": true + }, + "modifiedAt": { + "type": "datetime", + "readOnly": true + }, + "createdBy": { + "type": "link", + "readOnly": true + }, + "modifiedBy": { + "type": "link", + "readOnly": true + } + }, + "links": { + "accounts": { + "type": "hasMany", + "entity": "OAuthAccount", + "foreign": "provider", + "readOnly": true + }, + "createdBy": { + "type": "belongsTo", + "entity": "User" + }, + "modifiedBy": { + "type": "belongsTo", + "entity": "User" + } + } +} diff --git a/application/Espo/Resources/metadata/recordDefs/OAuthAccount.json b/application/Espo/Resources/metadata/recordDefs/OAuthAccount.json new file mode 100644 index 0000000000..4b34b8783f --- /dev/null +++ b/application/Espo/Resources/metadata/recordDefs/OAuthAccount.json @@ -0,0 +1,5 @@ +{ + "readLoaderClassNameList": [ + "Espo\\Classes\\FieldProcessing\\OAuthAccount\\DataLoader" + ] +} diff --git a/application/Espo/Resources/metadata/recordDefs/OAuthProvider.json b/application/Espo/Resources/metadata/recordDefs/OAuthProvider.json new file mode 100644 index 0000000000..25a28cca5b --- /dev/null +++ b/application/Espo/Resources/metadata/recordDefs/OAuthProvider.json @@ -0,0 +1,12 @@ +{ + "readLoaderClassNameList": [ + "Espo\\Classes\\FieldProcessing\\OAuthProvider\\AuthorizationRedirectUriLoader" + ], + "createInputFilterClassNameList": [ + "Espo\\Classes\\Record\\OAuthProvider\\GeneralFilter" + ], + "updateInputFilterClassNameList": [ + "Espo\\Classes\\Record\\OAuthProvider\\GeneralFilter" + ], + "duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General" +} diff --git a/application/Espo/Resources/metadata/scopes/OAuthAccount.json b/application/Espo/Resources/metadata/scopes/OAuthAccount.json new file mode 100644 index 0000000000..39c96e7ac2 --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/OAuthAccount.json @@ -0,0 +1,3 @@ +{ + "entity": true +} diff --git a/application/Espo/Resources/metadata/scopes/OAuthProvider.json b/application/Espo/Resources/metadata/scopes/OAuthProvider.json new file mode 100644 index 0000000000..63e92530ee --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/OAuthProvider.json @@ -0,0 +1,6 @@ +{ + "entity": true, + "duplicateCheckFieldList": [ + "name" + ] +} diff --git a/application/Espo/Resources/routes.json b/application/Espo/Resources/routes.json index 2d27606c16..4fdee26e9d 100644 --- a/application/Espo/Resources/routes.json +++ b/application/Espo/Resources/routes.json @@ -485,6 +485,16 @@ }, "noAuth": true }, + { + "method": "post", + "route": "/OAuth/:id/connection", + "actionClassName": "Espo\\Tools\\OAuth\\Api\\PostConnection" + }, + { + "method": "delete", + "route": "/OAuth/:id/connection", + "actionClassName": "Espo\\Tools\\OAuth\\Api\\DeleteConnection" + }, { "route": "/:controller/:id", "method": "get", diff --git a/application/Espo/Tools/OAuth/Api/DeleteConnection.php b/application/Espo/Tools/OAuth/Api/DeleteConnection.php new file mode 100644 index 0000000000..7753611c4c --- /dev/null +++ b/application/Espo/Tools/OAuth/Api/DeleteConnection.php @@ -0,0 +1,61 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth\Api; + +use Espo\Core\Api\Action; +use Espo\Core\Api\Request; +use Espo\Core\Api\Response; +use Espo\Core\Api\ResponseComposer; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Record\EntityProvider; +use Espo\Entities\OAuthAccount; +use Espo\Tools\OAuth\ConnectionService; + +/** + * @noinspection PhpUnused + */ +class DeleteConnection implements Action +{ + public function __construct( + private ConnectionService $service, + private EntityProvider $entityProvider, + ) {} + + public function process(Request $request): Response + { + $id = $request->getRouteParam('id') ?? throw new BadRequest(); + + $account = $this->entityProvider->getByClass(OAuthAccount::class, $id); + + $this->service->disconnect($account); + + return ResponseComposer::json(true); + } +} diff --git a/application/Espo/Tools/OAuth/Api/PostConnection.php b/application/Espo/Tools/OAuth/Api/PostConnection.php new file mode 100644 index 0000000000..6f56876a7d --- /dev/null +++ b/application/Espo/Tools/OAuth/Api/PostConnection.php @@ -0,0 +1,66 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth\Api; + +use Espo\Core\Api\Action; +use Espo\Core\Api\Request; +use Espo\Core\Api\Response; +use Espo\Core\Api\ResponseComposer; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Record\EntityProvider; +use Espo\Entities\OAuthAccount; +use Espo\Tools\OAuth\ConnectionService; + +/** + * @noinspection PhpUnused + */ +class PostConnection implements Action +{ + public function __construct( + private ConnectionService $service, + private EntityProvider $entityProvider, + ) {} + + public function process(Request $request): Response + { + $id = $request->getRouteParam('id') ?? throw new BadRequest(); + $code = $request->getParsedBody()->code ?? null; + + if (!is_string($code)) { + throw new BadRequest("No code."); + } + + $account = $this->entityProvider->getByClass(OAuthAccount::class, $id); + + $this->service->connect($account, $code); + + return ResponseComposer::json(true); + } +} diff --git a/application/Espo/Tools/OAuth/ConfigDataProvider.php b/application/Espo/Tools/OAuth/ConfigDataProvider.php new file mode 100644 index 0000000000..684fc0cdb5 --- /dev/null +++ b/application/Espo/Tools/OAuth/ConfigDataProvider.php @@ -0,0 +1,44 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth; + +use Espo\Core\Utils\Config\ApplicationConfig; + +class ConfigDataProvider +{ + public function __construct( + private ApplicationConfig $config, + ) {} + + public function getRedirectUri(): string + { + return $this->config->getSiteUrl() . '/oauth/callback'; + } +} diff --git a/application/Espo/Tools/OAuth/ConnectionService.php b/application/Espo/Tools/OAuth/ConnectionService.php new file mode 100644 index 0000000000..ab4cea50e3 --- /dev/null +++ b/application/Espo/Tools/OAuth/ConnectionService.php @@ -0,0 +1,82 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth; + +use Espo\Core\Exceptions\Error; +use Espo\Core\Exceptions\Forbidden; +use Espo\Entities\OAuthAccount; +use Espo\ORM\EntityManager; +use GuzzleHttp\Exception\GuzzleException; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; + +class ConnectionService +{ + public function __construct( + private EntityManager $entityManager, + private GenericProviderFactory $genericProviderFactory, + private TokenSetter $tokenSetter, + ) {} + + /** + * @throws Forbidden + * @throws Error + */ + public function connect(OAuthAccount $account, string $code): void + { + $provider = $account->getProvider(); + + if (!$provider->isActive()) { + throw new Forbidden("Provider is not active."); + } + + $genericProvider = $this->genericProviderFactory->create($provider); + + try { + $tokens = $genericProvider->getAccessToken('authorization_code', ['code' => $code]); + } catch (GuzzleException $e) { + throw new Error("Token request error.", 500, $e); + } catch (IdentityProviderException $e) { + throw new Error("Token request response error.", 500, $e); + } + + $this->tokenSetter->set($account, $tokens); + + $this->entityManager->saveEntity($account); + } + + public function disconnect(OAuthAccount $account): void + { + $account->setAccessToken(null); + $account->setRefreshToken(null); + $account->setExpiresAt(null); + + $this->entityManager->saveEntity($account); + } +} diff --git a/application/Espo/Tools/OAuth/Exceptions/AccountNotFound.php b/application/Espo/Tools/OAuth/Exceptions/AccountNotFound.php new file mode 100644 index 0000000000..2981e6a328 --- /dev/null +++ b/application/Espo/Tools/OAuth/Exceptions/AccountNotFound.php @@ -0,0 +1,33 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth\Exceptions; + +class AccountNotFound extends OAuthException +{} diff --git a/application/Espo/Tools/OAuth/Exceptions/NoToken.php b/application/Espo/Tools/OAuth/Exceptions/NoToken.php new file mode 100644 index 0000000000..de04601255 --- /dev/null +++ b/application/Espo/Tools/OAuth/Exceptions/NoToken.php @@ -0,0 +1,33 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth\Exceptions; + +class NoToken extends OAuthException +{} diff --git a/application/Espo/Tools/OAuth/Exceptions/OAuthException.php b/application/Espo/Tools/OAuth/Exceptions/OAuthException.php new file mode 100644 index 0000000000..b1d7123bbf --- /dev/null +++ b/application/Espo/Tools/OAuth/Exceptions/OAuthException.php @@ -0,0 +1,35 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth\Exceptions; + +use Exception; + +class OAuthException extends Exception +{} diff --git a/application/Espo/Tools/OAuth/Exceptions/ProviderNotAvailable.php b/application/Espo/Tools/OAuth/Exceptions/ProviderNotAvailable.php new file mode 100644 index 0000000000..3f2174142f --- /dev/null +++ b/application/Espo/Tools/OAuth/Exceptions/ProviderNotAvailable.php @@ -0,0 +1,33 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth\Exceptions; + +class ProviderNotAvailable extends OAuthException +{} diff --git a/application/Espo/Tools/OAuth/Exceptions/TokenObtainingFailure.php b/application/Espo/Tools/OAuth/Exceptions/TokenObtainingFailure.php new file mode 100644 index 0000000000..aecaa44c09 --- /dev/null +++ b/application/Espo/Tools/OAuth/Exceptions/TokenObtainingFailure.php @@ -0,0 +1,33 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth\Exceptions; + +class TokenObtainingFailure extends OAuthException +{} diff --git a/application/Espo/Tools/OAuth/GenericProviderFactory.php b/application/Espo/Tools/OAuth/GenericProviderFactory.php new file mode 100644 index 0000000000..9c0ea5bca5 --- /dev/null +++ b/application/Espo/Tools/OAuth/GenericProviderFactory.php @@ -0,0 +1,60 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth; + +use Espo\Core\Utils\Crypt; +use Espo\Entities\OAuthProvider; +use League\OAuth2\Client\Provider\GenericProvider; + +/** + * @internal + */ +class GenericProviderFactory +{ + public function __construct( + private ConfigDataProvider $configDataProvider, + private Crypt $crypt, + ) {} + + public function create(OAuthProvider $provider): GenericProvider + { + $secret = $this->crypt->decrypt($provider->getClientSecret()); + + return new GenericProvider([ + 'clientId' => $provider->getClientId(), + 'clientSecret' => $secret, + 'redirectUri' => $this->configDataProvider->getRedirectUri(), + 'urlAccessToken' => $provider->getTokenEndpoint(), + + 'urlAuthorize' => 'dummy', + 'urlResourceOwnerDetails' => 'dummy', + ]); + } +} diff --git a/application/Espo/Tools/OAuth/TokenSetter.php b/application/Espo/Tools/OAuth/TokenSetter.php new file mode 100644 index 0000000000..5872285d9f --- /dev/null +++ b/application/Espo/Tools/OAuth/TokenSetter.php @@ -0,0 +1,62 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth; + +use Espo\Core\Field\DateTime; +use Espo\Core\Utils\Crypt; +use Espo\Entities\OAuthAccount; +use League\OAuth2\Client\Token\AccessTokenInterface; + +/** + * @internal + */ +class TokenSetter +{ + public function __construct( + private Crypt $crypt, + ) {} + + public function set(OAuthAccount $account, AccessTokenInterface $tokens): void + { + $accessToken = $this->crypt->encrypt($tokens->getToken()); + + $refreshToken = $tokens->getRefreshToken() ? + $this->crypt->encrypt($tokens->getRefreshToken()) : + null; + + $expires = $tokens->getExpires() !== null ? + DateTime::fromTimestamp($tokens->getExpires()) : + null; + + $account->setAccessToken($accessToken); + $account->setRefreshToken($refreshToken); + $account->setExpiresAt($expires); + } +} diff --git a/application/Espo/Tools/OAuth/Tokens.php b/application/Espo/Tools/OAuth/Tokens.php new file mode 100644 index 0000000000..440ae88164 --- /dev/null +++ b/application/Espo/Tools/OAuth/Tokens.php @@ -0,0 +1,62 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth; + +use Espo\Core\Field\DateTime; +use SensitiveParameter; + +/** + * @immutable + */ +class Tokens +{ + public function __construct( + #[SensitiveParameter] + private string $accessToken, + #[SensitiveParameter] + private ?string $refreshToken, + private ?DateTime $expiresAt, + ) {} + + public function getAccessToken(): string + { + return $this->accessToken; + } + + public function getRefreshToken(): ?string + { + return $this->refreshToken; + } + + public function getExpiresAt(): ?DateTime + { + return $this->expiresAt; + } +} diff --git a/application/Espo/Tools/OAuth/TokensProvider.php b/application/Espo/Tools/OAuth/TokensProvider.php new file mode 100644 index 0000000000..8c6fb29d3d --- /dev/null +++ b/application/Espo/Tools/OAuth/TokensProvider.php @@ -0,0 +1,177 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\OAuth; + +use Espo\Core\Field\DateTime; +use Espo\Core\Utils\Crypt; +use Espo\Entities\OAuthAccount; +use Espo\ORM\EntityManager; +use Espo\ORM\Name\Attribute; +use Espo\ORM\Query\SelectBuilder; +use Espo\Tools\OAuth\Exceptions\NoToken; +use Espo\Tools\OAuth\Exceptions\ProviderNotAvailable; +use Espo\Tools\OAuth\Exceptions\AccountNotFound; +use Espo\Tools\OAuth\Exceptions\TokenObtainingFailure; +use GuzzleHttp\Exception\GuzzleException; +use League\OAuth2\Client\Provider\Exception\IdentityProviderException; +use LogicException; + +class TokensProvider +{ + private const EXPIRATION_LEAD_TIME = 60; + + public function __construct( + private EntityManager $entityManager, + private GenericProviderFactory $genericProviderFactory, + private TokenSetter $tokenSetter, + private Crypt $crypt, + ) {} + + /** + * @throws AccountNotFound + * @throws ProviderNotAvailable + * @throws NoToken + * @throws TokenObtainingFailure + */ + public function get(string $id): Tokens + { + $account = $this->fetch($id); + + if ( + $account->getRefreshToken() && + $account->getExpiresAt() && + $account->getExpiresAt()->isGreaterThan( + DateTime::createNow()->addSeconds(- self::EXPIRATION_LEAD_TIME) + ) + ) { + $this->refresh($account); + } + + if (!$account->getAccessToken()) { + throw new NoToken(); + } + + $accessToken = $this->crypt->decrypt($account->getAccessToken()); + + $refreshToken = $account->getRefreshToken() ? + $this->crypt->decrypt($account->getRefreshToken()) : + null; + + return new Tokens( + accessToken: $accessToken, + refreshToken: $refreshToken, + expiresAt: $account->getExpiresAt(), + ); + } + + /** + * @throws ProviderNotAvailable + * @throws AccountNotFound + * @throws NoToken + */ + private function fetch(string $id): OAuthAccount + { + // Ensuring the token is not being refreshed. + $this->entityManager->getTransactionManager()->start(); + + $account = $this->entityManager + ->getRDBRepositoryByClass(OAuthAccount::class) + ->clone( + SelectBuilder::create() + ->from(OAuthAccount::ENTITY_TYPE) + ->forShare() + ->build() + ) + ->where([Attribute::ID => $id]) + ->findOne(); + + $this->entityManager->getTransactionManager()->commit(); + + if (!$account) { + throw new AccountNotFound(); + } + + if (!$account->getProvider()->isActive()) { + throw new ProviderNotAvailable(); + } + + if (!$account->getAccessToken()) { + throw new NoToken(); + } + + return $account; + } + + /** + * @throws TokenObtainingFailure + * @noinspection PhpDocRedundantThrowsInspection + */ + private function refresh(OAuthAccount $account): void + { + $this->entityManager + ->getTransactionManager() + ->run(function () use ($account) { + $this->refreshInTransaction($account); + }); + } + + /** + * @throws TokenObtainingFailure + */ + private function refreshInTransaction(OAuthAccount $account): void + { + $refreshToken = $account->getRefreshToken(); + + if (!$refreshToken) { + throw new LogicException(); + } + + $refreshToken = $this->crypt->decrypt($refreshToken); + + $this->entityManager + ->getRDBRepositoryByClass(OAuthAccount::class) + ->forUpdate() + ->sth() + ->where([Attribute::ID => $account->getId()]) + ->find(); + + $genericProvider = $this->genericProviderFactory->create($account->getProvider()); + + try { + $tokens = $genericProvider->getAccessToken('refresh_token', ['refresh_token' => $refreshToken]); + } catch (GuzzleException|IdentityProviderException $e) { + throw new TokenObtainingFailure($e->getMessage(), $e->getCode(), $e); + } + + $this->tokenSetter->set($account, $tokens); + + $this->entityManager->saveEntity($account); + } +} diff --git a/client/src/controllers/admin.js b/client/src/controllers/admin.js index efe4cd7469..cfc3c8b722 100644 --- a/client/src/controllers/admin.js +++ b/client/src/controllers/admin.js @@ -398,6 +398,11 @@ class AdminController extends Controller { this.getRouter().dispatch('AppSecret', 'list', {fromAdmin: true}); } + // noinspection JSUnusedGlobalSymbols + actionOAuthProviders() { + this.getRouter().dispatch('OAuthProvider', 'list', {fromAdmin: true}); + } + // noinspection JSUnusedGlobalSymbols actionJobs() { this.collectionFactory.create('Job', collection => { diff --git a/client/src/views/o-auth-account/records/panels/connection.js b/client/src/views/o-auth-account/records/panels/connection.js new file mode 100644 index 0000000000..32fe7869df --- /dev/null +++ b/client/src/views/o-auth-account/records/panels/connection.js @@ -0,0 +1,307 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import SidePanelView from 'views/record/panels/side'; + +export default class OAuthAccountConnectionPanelView extends SidePanelView { + + // language=Handlebars + templateContent = ` + {{#if hasDisconnect}} +
+ {{translate 'Connected' scope='ExternalAccount'}} +
+ + {{/if}} + + {{#if hasConnect}} +
+ {{translate 'Disconnected' scope='ExternalAccount'}} +
+ + {{/if}} + ` + + /** + * @private + * @type {boolean} + */ + inProcess = false + + data() { + const isSet = this.model.attributes.hasAccessToken !== undefined; + + const hasDisconnect = !this.inProcess && + isSet && + this.model.attributes.hasAccessToken; + + const hasConnect = + !this.inProcess && + isSet && + !this.model.attributes.hasAccessToken && + this.model.attributes.providerIsActive; + + // noinspection JSValidateTypes + return { + hasDisconnect, + hasConnect, + } + } + + setup() { + super.setup(); + + this.listenTo(this.model, 'sync', () => this.reRender()); + + this.addActionHandler('connect', () => this.actionConnect()); + this.addActionHandler('disconnect', () => this.actionDisconnect()); + } + + /** + * @private + */ + async actionDisconnect() { + this.inProcess = true; + + await this.reRender(); + + Espo.Ui.notify(' ... '); + + await Espo.Ajax.deleteRequest(`OAuth/${this.model.id}/connection`); + + await this.model.fetch(); + + Espo.Ui.notify(); + + this.inProcess = false; + + await this.reRender(); + } + + /** + * @private + */ + async actionConnect() { + const data = this.model.attributes.data || {}; + + const endpoint = data.endpoint; + const redirectUri = data.redirectUri; + const clientId = data.clientId; + const scope = data.scope; + const prompt = data.prompt; + const params = data.params; + + const proxy = window.open('about:blank', 'ConnectWithOAuth', 'location=0,status=0,width=800,height=800'); + + const info = await this.processWithData({ + endpoint, + redirectUri, + clientId, + scope, + prompt, + params, + }, proxy); + + this.inProcess = true; + + await this.reRender() + + Espo.Ui.notify(' ... '); + + try { + await Espo.Ajax.postRequest(`OAuth/${this.model.id}/connection`, {code: info.code}); + } catch (e) { + this.inProcess = false; + + await this.reRender(); + + return; + } + + await this.model.fetch(); + + Espo.Ui.notify(); + + this.inProcess = false; + + await this.reRender(); + } + + /** + * @private + * @param {{ + * endpoint: string, + * clientId: string, + * redirectUri: string, + * scope: string|null, + * prompt: string, + * params: Record|null, + * }} data + * @param {WindowProxy} proxy + * @return {Promise<{code: string}>} + */ + processWithData(data, proxy) { + const state = undefined; + + const params = { + client_id: data.clientId, + redirect_uri: data.redirectUri, + response_type: 'code', + prompt: data.prompt, + }; + + if (data.scope) { + params.scope = data.scope; + } + + if (data.params) { + for (const name in data.params) { + params[name] = data.params[name]; + } + } + + const partList = Object.entries(params) + .map(([key, value]) => { + return key + '=' + encodeURIComponent(value); + }); + + const url = data.endpoint + '?' + partList.join('&'); + + return this.processWindow(url, state, proxy); + } + + /** + * @private + * @param {string} url + * @param {string} state + * @param {WindowProxy} proxy + * @return {Promise<{code: string}>} + */ + processWindow(url, state, proxy) { + proxy.location.href = url; + + return new Promise((resolve, reject) => { + const fail = () => { + window.clearInterval(interval); + + if (!proxy.closed) { + proxy.close(); + } + + reject(); + }; + + const interval = window.setInterval(() => { + if (proxy.closed) { + fail(); + + return; + } + + let url; + + try { + url = proxy.location.href; + } catch (e) { + return; + } + + if (!url) { + return; + } + + const parsedData = this.parseWindowUrl(url); + + if (!parsedData) { + fail(); + Espo.Ui.error('Could not parse URL', true); + + return; + } + + if ((parsedData.error || parsedData.code) && state && parsedData.state !== state) { + fail(); + Espo.Ui.error('State mismatch', true); + + return; + } + + if (parsedData.error) { + fail(); + Espo.Ui.error(parsedData.errorDescription || this.translate('Error'), true); + + return; + } + + if (parsedData.code) { + window.clearInterval(interval); + proxy.close(); + + resolve({ + code: parsedData.code, + }); + } + }, 300); + }); + } + + /** + * @private + * @param {string} url + * @return {?{ + * code: ?string, + * state: ?string, + * error: ?string, + * errorDescription: ?string, + * }} + */ + parseWindowUrl(url) { + try { + const params = new URL(url).searchParams; + + return { + code: params.get('code'), + state: params.get('state'), + error: params.get('error'), + errorDescription: params.get('errorDescription'), + }; + } catch (e) { + return null; + } + } +} diff --git a/client/src/views/o-auth-provider/fields/authorization-params.js b/client/src/views/o-auth-provider/fields/authorization-params.js new file mode 100644 index 0000000000..957474a27f --- /dev/null +++ b/client/src/views/o-auth-provider/fields/authorization-params.js @@ -0,0 +1,234 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import BaseFieldView from 'views/fields/base'; + +/** + * @type {{ + * edit: import('ace-builds').edit, + * require: import('ace-builds').require, + * }} + */ +let ace; + +export default class OAuthProviderAuthorizationParamsFieldView extends BaseFieldView { + + // language=Handlebars + detailTemplateContent = ` + {{#if isNotEmpty}} +
{{value}}
+ {{else}} + {{#if isSet}} + {{translate 'None'}} + {{else}} + + {{/if}} + {{/if}} + ` + + // language=Handlebars + editTemplateContent = ` +
{{value}}
+ ` + + height = 46 + maxLineDetailCount = 80 + maxLineEditCount = 200 + + data() { + const data = super.data(); + + const value = this.model.attributes[this.name]; + + data.containerId = this.containerId; + data.isNotEmpty = value != null; + data.isSet = value !== undefined; + + try { + data.value = value ? JSON.stringify(value, null, ' ') : null; + } catch (e) { + data.value = null; + } + + return data; + } + + setup() { + super.setup(); + + this.height = this.options.height || this.params.height || this.height; + + this.maxLineDetailCount = + this.options.maxLineDetailCount || + this.params.maxLineDetailCount || + this.maxLineDetailCount; + + this.maxLineEditCount = + this.options.maxLineEditCount || + this.params.maxLineEditCount || + this.maxLineEditCount; + + this.containerId = 'editor-' + Math.floor((Math.random() * 10000) + 1).toString(); + + if (this.mode === this.MODE_EDIT || this.mode === this.MODE_DETAIL) { + this.wait( + this.requireAce() + ); + } + + this.on('remove', () => { + if (this.editor) { + this.editor.destroy(); + } + }); + + this.validations.push(() => this.validateJson()); + } + + requireAce() { + return Espo.loader.requirePromise('lib!ace') + .then(lib => { + ace = lib; + + const list = [ + Espo.loader.requirePromise('lib!ace-ext-language_tools'), + Espo.loader.requirePromise('lib!ace-mode-json'), + ]; + + if (this.getThemeManager().getParam('isDark')) { + list.push( + Espo.loader.requirePromise('lib!ace-theme-tomorrow_night') + ); + } + + return Promise.all(list); + }); + } + + afterRender() { + super.afterRender(); + + this.$editor = this.$el.find('#' + this.containerId); + + if ( + this.$editor.length && + ( + this.mode === this.MODE_EDIT || + this.mode === this.MODE_DETAIL || + this.mode === this.MODE_LIST + ) + ) { + this.$editor.css('fontSize', 'var(--font-size-base)'); + + if (this.mode === this.MODE_EDIT) { + this.$editor.css('minHeight', this.height + 'px'); + } + + const editor = this.editor = ace.edit(this.containerId); + + editor.setOptions({fontFamily: 'var(--font-family-monospace)'}); + editor.setFontSize('var(--font-size-base)'); + editor.container.style.lineHeight = 'var(--line-height-computed)'; + editor.renderer.updateFontSize(); + + editor.setOptions({ + maxLines: this.mode === this.MODE_EDIT ? this.maxLineEditCount : this.maxLineDetailCount, + enableLiveAutocompletion: true, + }); + + if (this.getThemeManager().getParam('isDark')) { + editor.setOptions({ + theme: 'ace/theme/tomorrow_night', + }); + } + + if (this.isEditMode()) { + editor.getSession().on('change', () => { + this.trigger('change', {ui: true}); + }); + + editor.getSession().setUseWrapMode(true); + } + + if (this.isReadMode()) { + editor.setReadOnly(true); + editor.renderer.$cursorLayer.element.style.display = 'none'; + editor.renderer.setShowGutter(false); + } + + editor.setShowPrintMargin(false); + editor.getSession().setUseWorker(false); + editor.commands.removeCommand('find'); + editor.setHighlightActiveLine(false); + + const Mode = ace.require('ace/mode/json').Mode; + + editor.session.setMode(new Mode()); + } + } + + /** + * @private + * @return {boolean} + */ + validateJson() { + const raw = this.editor.getValue(); + + if (!raw) { + return false; + } + + try { + JSON.parse(raw); + } catch (e) { + const message = this.translate('Not valid'); + + this.showValidationMessage(message, '.ace_editor'); + + return true; + } + + return false; + } + + fetch() { + let value = null; + + const raw = this.editor.getValue(); + + if (!raw) { + return {[this.name]: null}; + } + + try { + value = JSON.parse(raw); + } catch (e) {} + + return {[this.name]: value}; + } +} diff --git a/composer.json b/composer.json index fd36a715e4..d6d6987e58 100644 --- a/composer.json +++ b/composer.json @@ -51,7 +51,8 @@ "chillerlan/php-qrcode": "^4.4", "ext-ctype": "*", "lasserafn/php-initial-avatar-generator": "^4.4", - "tholu/php-cidr-match": "^0.4" + "tholu/php-cidr-match": "^0.4", + "league/oauth2-client": "^2.8" }, "require-dev": { "phpunit/phpunit": "^9.5", diff --git a/composer.lock b/composer.lock index 7e558b2a21..ea9b581848 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c71fbf8961a0a000e8e902a0bf78bd6c", + "content-hash": "c9e886d22a6f5af3773701b37aa6cf97", "packages": [ { "name": "async-aws/core", @@ -1481,6 +1481,215 @@ }, "time": "2024-11-04T11:18:07+00:00" }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", + "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2024-07-24T11:22:20+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2024-10-17T10:06:22+00:00" + }, { "name": "guzzlehttp/psr7", "version": "2.7.0", @@ -2750,6 +2959,71 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "php": "^7.1 || >=8.0.0 <8.5.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1" + }, + "time": "2025-02-26T04:37:30+00:00" + }, { "name": "maennchen/zipstream-php", "version": "3.1.2", @@ -8988,5 +9262,5 @@ "ext-ctype": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/frontend/libs.json b/frontend/libs.json index d147a812ec..9da171db96 100644 --- a/frontend/libs.json +++ b/frontend/libs.json @@ -196,6 +196,10 @@ "src": "node_modules/ace-builds/src-noconflict/mode-html.js", "dest": "client/lib/ace-mode-html.js" }, + { + "src": "node_modules/ace-builds/src-noconflict/mode-json.js", + "dest": "client/lib/ace-mode-json.js" + }, { "src": "node_modules/ace-builds/src-noconflict/mode-handlebars.js", "dest": "client/lib/ace-mode-handlebars.js" diff --git a/public/oauth/callback/index.php b/public/oauth/callback/index.php new file mode 100644 index 0000000000..3b19b9861d --- /dev/null +++ b/public/oauth/callback/index.php @@ -0,0 +1,41 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +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') +);