baseline role

This commit is contained in:
Yuri Kuznetsov
2025-08-21 20:15:50 +03:00
parent 852345e004
commit bb285bf69c
12 changed files with 152 additions and 20 deletions
@@ -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<RoleEntity> $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<Team> $teamList */
$teamList = $this->entityManager
->getRDBRepository(User::ENTITY_TYPE)
->getRelation($this->user, Field::TEAMS)
->find();
foreach ($teamList as $team) {
/** @var iterable<RoleEntity> $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);
}
}
+4 -1
View File
@@ -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 */
@@ -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": {
@@ -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",
@@ -1,3 +1,11 @@
[
{"name":"name", "link":true}
{
"name": "name",
"link": true
},
{
"name": "info",
"width": 30,
"noLabel": true
}
]
@@ -1,3 +1,11 @@
[
{"name":"name", "link":true}
{
"name": "name",
"link": true
},
{
"name": "info",
"width": 30,
"noLabel": true
}
]
@@ -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"}]
]
}
]
@@ -165,6 +165,9 @@
},
"streamEmailWithContentEntityTypeList": {
"level": "admin"
},
"baselineRole": {
"level": "admin"
}
}
}
@@ -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"],
@@ -945,6 +945,11 @@
"validatorClassNameList": [
"Espo\\Classes\\FieldValidators\\Settings\\AvailableReactions\\Valid"
]
},
"baselineRole": {
"type": "link",
"entity": "Role",
"tooltip": true
}
}
}
@@ -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();
}
+64
View File
@@ -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 <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU 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;
})()
)
}
}
}