diff --git a/application/Espo/Classes/Record/AppSecret/ValueInputFilter.php b/application/Espo/Classes/Record/AppSecret/ValueInputFilter.php new file mode 100644 index 0000000000..07f01f6b82 --- /dev/null +++ b/application/Espo/Classes/Record/AppSecret/ValueInputFilter.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\Classes\Record\AppSecret; + +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 ValueInputFilter implements Filter +{ + private const ATTR_VALUE = 'value'; + + public function __construct(private Crypt $crypt) {} + + /** + * @throws BadRequest + */ + public function filter(Data $data): void + { + $value = $data->get(self::ATTR_VALUE); + + if ($value !== null) { + if (!is_string($value)) { + throw new BadRequest(); + } + + $data->set(self::ATTR_VALUE, $this->crypt->encrypt($value)); + } + } +} diff --git a/application/Espo/Controllers/AppSecret.php b/application/Espo/Controllers/AppSecret.php new file mode 100644 index 0000000000..ff2cba2bde --- /dev/null +++ b/application/Espo/Controllers/AppSecret.php @@ -0,0 +1,43 @@ +. + * + * 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 AppSecret extends Record +{ + protected function checkAccess(): bool + { + return $this->user->isAdmin(); + } +} diff --git a/application/Espo/Entities/AppSecret.php b/application/Espo/Entities/AppSecret.php new file mode 100644 index 0000000000..286603207f --- /dev/null +++ b/application/Espo/Entities/AppSecret.php @@ -0,0 +1,65 @@ +. + * + * 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\Entities; + +use Espo\Core\ORM\Entity; + +class AppSecret extends Entity +{ + public const ENTITY_TYPE = 'AppSecret'; + + public function getName(): string + { + return $this->get('name'); + } + + public function getValue(): string + { + return (string) $this->get('value'); + } + + public function setName(string $name): self + { + $this->set('name', $name); + + return $this; + } + + /** + * @internal Do not use. + * @todo Rename to setValue in v9.0. + */ + public function setSecretValue(string $value): self + { + $this->set('value', $value); + + return $this; + } +} diff --git a/application/Espo/Resources/i18n/en_US/Admin.json b/application/Espo/Resources/i18n/en_US/Admin.json index 6c91c97151..88b9cec521 100644 --- a/application/Espo/Resources/i18n/en_US/Admin.json +++ b/application/Espo/Resources/i18n/en_US/Admin.json @@ -38,6 +38,7 @@ "Auth Tokens": "Auth Tokens", "Auth Log": "Auth Log", "App Log": "App Log", + "App Secrets": "App Secrets", "Authentication": "Authentication", "Currency": "Currency", "Integrations": "Integrations", @@ -297,6 +298,7 @@ "templateManager": "Customize message templates.", "authLog": "Login history.", "appLog": "Application log.", + "appSecrets": "Store sensitive information like API keys, passwords, and other secrets.", "leadCapture": "API entry points for Web-to-Lead.", "attachments": "All file attachments stored in the system.", "systemRequirements": "System Requirements for EspoCRM.", @@ -325,7 +327,8 @@ "entityManager": "fields,relations,relationships", "templateManager": "notifications", "jobs": "cron", - "labelManager": "language,translation" + "labelManager": "language,translation", + "appSecrets": "key,keys,password" }, "options": { "previewSize": { diff --git a/application/Espo/Resources/i18n/en_US/AppSecret.json b/application/Espo/Resources/i18n/en_US/AppSecret.json new file mode 100644 index 0000000000..47b8b9021e --- /dev/null +++ b/application/Espo/Resources/i18n/en_US/AppSecret.json @@ -0,0 +1,11 @@ +{ + "labels": { + "Create AppSecret": "Create Secret" + }, + "fields": { + "value": "Value" + }, + "tooltips": { + "name": "Allowed characters:\n* `[a-z]`\n* `[A-Z]`\n* `[0-9]`\n* `_`" + } +} diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 63fde131d9..655c72018a 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -63,7 +63,8 @@ "WorkingTimeRange": "Working Time Exception", "AuthenticationProvider": "Authentication Provider", "GlobalStream": "Global Stream", - "AddressCountry": "Address Country" + "AddressCountry": "Address Country", + "AppSecret": "App Secret" }, "scopeNamesPlural": { "Note": "Notes", @@ -117,7 +118,8 @@ "WorkingTimeRange": "Working Time Exceptions", "AuthenticationProvider": "Authentication Providers", "GlobalStream": "Global Stream", - "AddressCountry": "Address Countries" + "AddressCountry": "Address Countries", + "AppSecret": "App Secrets" }, "labels": { "Previous Page": "Previous Page", diff --git a/application/Espo/Resources/layouts/AppSecret/detail.json b/application/Espo/Resources/layouts/AppSecret/detail.json new file mode 100644 index 0000000000..6f775a997e --- /dev/null +++ b/application/Espo/Resources/layouts/AppSecret/detail.json @@ -0,0 +1,13 @@ +[ + { + "rows": [ + [{"name": "name"}, false], + [{"name": "value"}] + ] + }, + { + "rows": [ + [{"name": "description"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/AppSecret/detailSmall.json b/application/Espo/Resources/layouts/AppSecret/detailSmall.json new file mode 100644 index 0000000000..b26111e948 --- /dev/null +++ b/application/Espo/Resources/layouts/AppSecret/detailSmall.json @@ -0,0 +1,13 @@ +[ + { + "rows": [ + [{"name": "name"}], + [{"name": "value"}] + ] + }, + { + "rows": [ + [{"name": "description"}] + ] + } +] diff --git a/application/Espo/Resources/layouts/AppSecret/filters.json b/application/Espo/Resources/layouts/AppSecret/filters.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/application/Espo/Resources/layouts/AppSecret/filters.json @@ -0,0 +1 @@ +[] diff --git a/application/Espo/Resources/layouts/AppSecret/list.json b/application/Espo/Resources/layouts/AppSecret/list.json new file mode 100644 index 0000000000..5aaa33c292 --- /dev/null +++ b/application/Espo/Resources/layouts/AppSecret/list.json @@ -0,0 +1,10 @@ +[ + { + "name": "name", + "link": true + }, + { + "name": "description", + "width": 50 + } +] diff --git a/application/Espo/Resources/metadata/app/adminPanel.json b/application/Espo/Resources/metadata/app/adminPanel.json index e3edc69e2f..e1ff45c66e 100644 --- a/application/Espo/Resources/metadata/app/adminPanel.json +++ b/application/Espo/Resources/metadata/app/adminPanel.json @@ -338,6 +338,12 @@ "iconClass": "fas fa-phone", "description": "phoneNumbers" }, + { + "url": "#Admin/appSecrets", + "label": "App Secrets", + "iconClass": "fas fa-key", + "description": "appSecrets" + }, { "url": "#Admin/appLog", "label": "App Log", diff --git a/application/Espo/Resources/metadata/clientDefs/AppSecret.json b/application/Espo/Resources/metadata/clientDefs/AppSecret.json new file mode 100644 index 0000000000..d641374e08 --- /dev/null +++ b/application/Espo/Resources/metadata/clientDefs/AppSecret.json @@ -0,0 +1,6 @@ +{ + "controller": "controllers/record", + "mergeDisabled": true, + "exportDisabled": true, + "massUpdateDisabled": true +} diff --git a/application/Espo/Resources/metadata/entityAcl/AppSecret.json b/application/Espo/Resources/metadata/entityAcl/AppSecret.json new file mode 100644 index 0000000000..81a4e22caa --- /dev/null +++ b/application/Espo/Resources/metadata/entityAcl/AppSecret.json @@ -0,0 +1,7 @@ +{ + "fields": { + "value": { + "internal": true + } + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/AppSecret.json b/application/Espo/Resources/metadata/entityDefs/AppSecret.json new file mode 100644 index 0000000000..e57899d326 --- /dev/null +++ b/application/Espo/Resources/metadata/entityDefs/AppSecret.json @@ -0,0 +1,61 @@ +{ + "fields": { + "name": { + "type": "varchar", + "maxLength": 100, + "required": true, + "pattern": "[a-zA-Z]{1}[a-zA-Z0-9_]+", + "index": true, + "tooltip": true + }, + "value": { + "type": "text", + "required": true, + "view": "views/admin/app-secret/fields/value" + }, + "description": { + "type": "text" + }, + "createdAt": { + "type": "datetime", + "readOnly": true + }, + "modifiedAt": { + "type": "datetime", + "readOnly": true + }, + "createdBy": { + "type": "link", + "readOnly": true + }, + "modifiedBy": { + "type": "link", + "readOnly": true + } + }, + "links": { + "createdBy": { + "type": "belongsTo", + "entity": "User" + }, + "modifiedBy": { + "type": "belongsTo", + "entity": "User" + } + }, + "collection": { + "orderBy": "name", + "order": "asc", + "textFilterFields": ["name"] + }, + "indexes": { + "nameDeleteId": { + "type": "unique", + "columns": [ + "name", + "deleteId" + ] + } + }, + "deleteId": true +} diff --git a/application/Espo/Resources/metadata/recordDefs/AppSecret.json b/application/Espo/Resources/metadata/recordDefs/AppSecret.json new file mode 100644 index 0000000000..eef267a915 --- /dev/null +++ b/application/Espo/Resources/metadata/recordDefs/AppSecret.json @@ -0,0 +1,9 @@ +{ + "createInputFilterClassNameList": [ + "Espo\\Classes\\Record\\AppSecret\\ValueInputFilter" + ], + "updateInputFilterClassNameList": [ + "Espo\\Classes\\Record\\AppSecret\\ValueInputFilter" + ], + "duplicateWhereBuilderClassName": "Espo\\Classes\\DuplicateWhereBuilders\\General" +} diff --git a/application/Espo/Resources/metadata/scopes/AppSecret.json b/application/Espo/Resources/metadata/scopes/AppSecret.json new file mode 100644 index 0000000000..75fba6f3f4 --- /dev/null +++ b/application/Espo/Resources/metadata/scopes/AppSecret.json @@ -0,0 +1,8 @@ +{ + "entity": true, + "layouts": false, + "tab": false, + "acl": false, + "customizable": false, + "duplicateCheckFieldList": ["name"] +} diff --git a/application/Espo/Tools/AppSecret/SecretProvider.php b/application/Espo/Tools/AppSecret/SecretProvider.php new file mode 100644 index 0000000000..f6445fe103 --- /dev/null +++ b/application/Espo/Tools/AppSecret/SecretProvider.php @@ -0,0 +1,64 @@ +. + * + * 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\AppSecret; + +use Espo\Core\Utils\Crypt; +use Espo\Entities\AppSecret; +use Espo\ORM\EntityManager; + +/** + * @since 8.5.0 + * @noinspection PhpUnused + */ +class SecretProvider +{ + public function __construct( + private Crypt $crypt, + private EntityManager $entityManager, + ) {} + + /** + * Get an app secret value + * @param string $name A secret name. + */ + public function get(string $name): ?string + { + $secret = $this->entityManager + ->getRDBRepositoryByClass(AppSecret::class) + ->where(['name' => $name]) + ->findOne(); + + if (!$secret) { + return null; + } + + return $this->crypt->decrypt($secret->getValue()); + } +} diff --git a/client/src/controllers/admin.js b/client/src/controllers/admin.js index fb3812469c..6f384af727 100644 --- a/client/src/controllers/admin.js +++ b/client/src/controllers/admin.js @@ -394,6 +394,11 @@ class AdminController extends Controller { }); } + // noinspection JSUnusedGlobalSymbols + actionAppSecrets() { + this.getRouter().dispatch('AppSecret', 'list', {fromAdmin: true}); + } + // noinspection JSUnusedGlobalSymbols actionJobs() { this.collectionFactory.create('Job', collection => { diff --git a/client/src/views/admin/app-secret/fields/value.js b/client/src/views/admin/app-secret/fields/value.js new file mode 100644 index 0000000000..6eb2e71261 --- /dev/null +++ b/client/src/views/admin/app-secret/fields/value.js @@ -0,0 +1,80 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2024 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * 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 TextFieldView from 'views/fields/text'; + +export default class extends TextFieldView { + + detailTemplateContent = `**********` + + validations = ['required'] + + changingMode = false + + data() { + return { + isNew: this.model.isNew(), + ...super.data(), + }; + } + + afterRenderEdit() { + super.afterRenderEdit(); + + if (!this.model.isNew() && !this.changingMode) { + this.element.innerHTML = ''; + + const a = document.createElement('a'); + a.role = 'button'; + a.onclick = () => this.changePassword(); + a.textContent = this.translate('change'); + + this.element.appendChild(a); + } + } + + onDetailModeSet() { + this.changingMode = false; + + return super.onDetailModeSet(); + } + + fetch() { + if (!this.model.isNew() && !this.changingMode) { + return {}; + } + + return super.fetch(); + } + + async changePassword() { + this.changingMode = true; + + await this.reRender(); + } +}