diff --git a/application/Espo/Core/Acl/Table/DefaultRoleListProvider.php b/application/Espo/Core/Acl/Table/DefaultRoleListProvider.php index c8676f0031..fefc75dc40 100644 --- a/application/Espo/Core/Acl/Table/DefaultRoleListProvider.php +++ b/application/Espo/Core/Acl/Table/DefaultRoleListProvider.php @@ -30,6 +30,7 @@ namespace Espo\Core\Acl\Table; use Espo\Core\Name\Field; +use Espo\Core\Utils\Config; use Espo\Entities\Team; use Espo\ORM\EntityManager; use Espo\Entities\User; @@ -37,8 +38,13 @@ use Espo\Entities\Role as RoleEntity; class DefaultRoleListProvider implements RoleListProvider { - public function __construct(private User $user, private EntityManager $entityManager) - {} + private const PARAM_BASELINE_ROLE_ID = 'baselineRoleId'; + + public function __construct( + private User $user, + private EntityManager $entityManager, + private Config $config, + ) {} /** * @return Role[] @@ -47,10 +53,15 @@ class DefaultRoleListProvider implements RoleListProvider { $roleList = []; + $baselineRole = $this->getBaselineRole(); + + if ($baselineRole) { + $roleList[] = $baselineRole; + } + /** @var iterable $userRoleList */ $userRoleList = $this->entityManager - ->getRDBRepository(User::ENTITY_TYPE) - ->getRelation($this->user, 'roles') + ->getRelation($this->user, User::LINK_ROLES) ->find(); foreach ($userRoleList as $role) { @@ -59,15 +70,13 @@ class DefaultRoleListProvider implements RoleListProvider /** @var iterable $teamList */ $teamList = $this->entityManager - ->getRDBRepository(User::ENTITY_TYPE) ->getRelation($this->user, Field::TEAMS) ->find(); foreach ($teamList as $team) { /** @var iterable $teamRoleList */ $teamRoleList = $this->entityManager - ->getRDBRepository(Team::ENTITY_TYPE) - ->getRelation($team, 'roles') + ->getRelation($team, Team::LINK_ROLES) ->find(); foreach ($teamRoleList as $role) { @@ -76,10 +85,19 @@ class DefaultRoleListProvider implements RoleListProvider } return array_map( - function (RoleEntity $role): RoleEntityWrapper { - return new RoleEntityWrapper($role); - }, + fn (RoleEntity $role) => new RoleEntityWrapper($role), $roleList ); } + + private function getBaselineRole(): ?RoleEntity + { + $roleId = $this->config->get(self::PARAM_BASELINE_ROLE_ID); + + if (!$roleId) { + return null; + } + + return $this->entityManager->getRDBRepositoryByClass(RoleEntity::class)->getById($roleId); + } } diff --git a/application/Espo/Entities/Team.php b/application/Espo/Entities/Team.php index 448523f6ce..ce89d8ddf3 100644 --- a/application/Espo/Entities/Team.php +++ b/application/Espo/Entities/Team.php @@ -30,14 +30,17 @@ namespace Espo\Entities; use Espo\Core\Field\Link; +use Espo\Core\ORM\Entity; -class Team extends \Espo\Core\ORM\Entity +class Team extends Entity { public const ENTITY_TYPE = 'Team'; public const RELATIONSHIP_ENTITY_TEAM = 'EntityTeam'; public const RELATIONSHIP_TEAM_USER = 'TeamUser'; + public const LINK_ROLES = 'roles'; + public function getWorkingTimeCalendar(): ?Link { /** @var ?Link */ diff --git a/application/Espo/Resources/i18n/en_US/Role.json b/application/Espo/Resources/i18n/en_US/Role.json index 4e01245393..a155acd287 100644 --- a/application/Espo/Resources/i18n/en_US/Role.json +++ b/application/Espo/Resources/i18n/en_US/Role.json @@ -39,7 +39,8 @@ "Access": "Access", "Create Role": "Create Role", "Scope Level": "Scope Level", - "Field Level": "Field Level" + "Field Level": "Field Level", + "Baseline": "Baseline" }, "options": { "accessList": { diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index eccf7605d2..8a3387f414 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -171,7 +171,8 @@ "authIpAddressCheck": "Restrict access by IP address", "authIpAddressWhitelist": "IP Address Whitelist", "authIpAddressCheckExcludedUsers": "Users excluded from check", - "availableReactions": "Available Reactions" + "availableReactions": "Available Reactions", + "baselineRole": "Baseline Role" }, "options": { "authenticationMethod": { @@ -288,7 +289,8 @@ "oidcUsernameClaim": "A claim to use for a username (for user matching and creation).", "oidcTeams": "Espo teams mapped against groups/teams/roles of the identity provider. Teams with an empty mapping value will be always assigned to a user (when creating or syncing).", "oidcLogoutUrl": "An URL the browser will redirect to after logging out from Espo. Intended for clearing the session information in the browser and doing logging out on the provider side. Usually the URL contains a redirect-URL parameter, to return back to Espo.\n\nAvailable placeholders:\n* `{siteUrl}`\n* `{clientId}`", - "quickSearchFullTextAppendWildcard": "Append a wildcard to an autocomplete search query when Full-Text search is enabled. Reduces search performance." + "quickSearchFullTextAppendWildcard": "Append a wildcard to an autocomplete search query when Full-Text search is enabled. Reduces search performance.", + "baselineRole": "The default role applied to all users. Any additional roles assigned to a user grant permissions on top of this baseline.\n\n**Important**. Changing this role affects all users – review the change carefully to avoid granting unintended permissions." }, "labels": { "Group Tab": "Group Tab", diff --git a/application/Espo/Resources/layouts/Role/list.json b/application/Espo/Resources/layouts/Role/list.json index 5c1765d118..5d09f7f4b4 100644 --- a/application/Espo/Resources/layouts/Role/list.json +++ b/application/Espo/Resources/layouts/Role/list.json @@ -1,3 +1,11 @@ [ - {"name":"name", "link":true} + { + "name": "name", + "link": true + }, + { + "name": "info", + "width": 30, + "noLabel": true + } ] diff --git a/application/Espo/Resources/layouts/Role/listSmall.json b/application/Espo/Resources/layouts/Role/listSmall.json index 5c1765d118..5d09f7f4b4 100644 --- a/application/Espo/Resources/layouts/Role/listSmall.json +++ b/application/Espo/Resources/layouts/Role/listSmall.json @@ -1,3 +1,11 @@ [ - {"name":"name", "link":true} + { + "name": "name", + "link": true + }, + { + "name": "info", + "width": 30, + "noLabel": true + } ] diff --git a/application/Espo/Resources/layouts/Settings/settings.json b/application/Espo/Resources/layouts/Settings/settings.json index 7c6245331d..2decade203 100644 --- a/application/Espo/Resources/layouts/Settings/settings.json +++ b/application/Espo/Resources/layouts/Settings/settings.json @@ -46,12 +46,11 @@ "tabBreak": true, "tabLabel": "$label:General", "rows": [ - [{"name": "cleanupDeletedRecords"}, {"name": "emailAddressIsOptedOutByDefault"}], - [{"name": "aclAllowDeleteCreated"}, {"name": "b2cMode"}], - [{"name": "exportDisabled"}, false], + [{"name": "cleanupDeletedRecords"}, false], + [{"name": "b2cMode"}, false], + [{"name": "emailAddressIsOptedOutByDefault"}, false], [{"name": "pdfEngine"}, false] ] - }, { "label": "Search", @@ -87,5 +86,14 @@ [{"name": "availableReactions"}, {"name": "followCreatedEntities"}], [{"name": "streamEmailWithContentEntityTypeList"}, false] ] + }, + { + "tabBreak": true, + "tabLabel": "$label:Access", + "rows": [ + [{"name": "baselineRole"}, false], + [{"name": "aclAllowDeleteCreated"}, {"name": "exportDisabled"}] + ] + } ] diff --git a/application/Espo/Resources/metadata/app/config.json b/application/Espo/Resources/metadata/app/config.json index 46dbd468dd..a884d41076 100644 --- a/application/Espo/Resources/metadata/app/config.json +++ b/application/Espo/Resources/metadata/app/config.json @@ -165,6 +165,9 @@ }, "streamEmailWithContentEntityTypeList": { "level": "admin" + }, + "baselineRole": { + "level": "admin" } } } diff --git a/application/Espo/Resources/metadata/entityDefs/Role.json b/application/Espo/Resources/metadata/entityDefs/Role.json index 23bfeedbd9..718325e5a5 100644 --- a/application/Espo/Resources/metadata/entityDefs/Role.json +++ b/application/Espo/Resources/metadata/entityDefs/Role.json @@ -6,6 +6,13 @@ "type": "varchar", "pattern": "$noBadCharacters" }, + "info": { + "type": "base", + "orderDisabled": true, + "notStorable": true, + "readOnly": true, + "view": "views/role/fields/info" + }, "assignmentPermission": { "type": "enum", "options": ["not-set", "all", "team", "no"], diff --git a/application/Espo/Resources/metadata/entityDefs/Settings.json b/application/Espo/Resources/metadata/entityDefs/Settings.json index 6be233a89d..c7965406d5 100644 --- a/application/Espo/Resources/metadata/entityDefs/Settings.json +++ b/application/Espo/Resources/metadata/entityDefs/Settings.json @@ -945,6 +945,11 @@ "validatorClassNameList": [ "Espo\\Classes\\FieldValidators\\Settings\\AvailableReactions\\Valid" ] + }, + "baselineRole": { + "type": "link", + "entity": "Role", + "tooltip": true } } } diff --git a/application/Espo/Tools/App/SettingsService.php b/application/Espo/Tools/App/SettingsService.php index 4e40916499..6bc103b724 100644 --- a/application/Espo/Tools/App/SettingsService.php +++ b/application/Espo/Tools/App/SettingsService.php @@ -73,6 +73,7 @@ class SettingsService private ThemeManager $themeManager, private Config\SystemConfig $systemConfig, private EmailConfigDataProvider $emailConfigDataProvider, + private Acl\Cache\Clearer $aclCacheClearer, ) {} /** @@ -238,6 +239,10 @@ class SettingsService $this->dataManager->clearCache(); } + if (property_exists($data, 'baselineRoleId')) { + $this->aclCacheClearer->clearForAllInternalUsers(); + } + if (isset($data->defaultCurrency) || isset($data->baseCurrency) || isset($data->currencyRates)) { $this->populateDatabaseWithCurrencyRates(); } diff --git a/client/src/views/role/fields/info.js b/client/src/views/role/fields/info.js new file mode 100644 index 0000000000..a709701f12 --- /dev/null +++ b/client/src/views/role/fields/info.js @@ -0,0 +1,64 @@ +/************************************************************************ + * 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'; + +export default class extends BaseFieldView { + + // language=Handlebars + listTemplateContent = `` + + /** + * @private + * @type {string|null} + */ + baselineRoleId + + setup() { + super.setup(); + + this.baselineRoleId = this.getConfig().get('baselineRoleId'); + } + + afterRenderList() { + super.afterRenderList(); + + if (this.baselineRoleId && this.model.id === this.baselineRoleId) { + this.element?.append( + (() => { + const span = document.createElement('span'); + + span.className = 'label label-default'; + span.textContent = this.translate('Baseline', 'labels', 'Role'); + + return span; + })() + ) + } + } +}