acl recactoring

This commit is contained in:
Yuri Kuznetsov
2021-04-15 09:18:06 +03:00
parent 781d9dd440
commit aa7959b91c
38 changed files with 2363 additions and 441 deletions

View File

@@ -54,7 +54,7 @@ class User extends \Espo\Core\Controllers\Record
throw new NotFound();
}
return $this->getAclManager()->getMap($user);
return $this->getAclManager()->getMapData($user);
}
public function postActionChangeOwnPassword($params, $data, $request)

View File

@@ -57,9 +57,9 @@ class Acl
/**
* Get a full access data map.
*/
public function getMap(): StdClass
public function getMapData(): StdClass
{
return $this->aclManager->getMap($this->user);
return $this->aclManager->getMapData($this->user);
}
/**

View File

@@ -29,16 +29,14 @@
namespace Espo\Core\Acl;
use Espo\Entities\User;
use Espo\Core\{
ORM\EntityManager,
Acl\Table\RoleListProvider,
Acl\Table\CacheKeyProvider,
Utils\Config,
Utils\Metadata,
Utils\FieldUtil,
Utils\DataCache,
Utils\ObjectUtil,
};
use StdClass;
@@ -50,17 +48,13 @@ use RuntimeException;
*/
class DefaultTable implements Table
{
protected const LEVEL_NOT_SET = 'not-set';
private const LEVEL_NOT_SET = 'not-set';
protected $type = 'acl';
protected $defaultAclType = 'recordAllTeamOwnNo';
private $data = null;
protected $cacheKey;
protected $actionList = [
private $actionList = [
self::ACTION_READ,
self::ACTION_STREAM,
self::ACTION_EDIT,
@@ -68,7 +62,7 @@ class DefaultTable implements Table
self::ACTION_CREATE,
];
protected $booleanActionList = [
private $booleanActionList = [
self::ACTION_CREATE,
];
@@ -80,7 +74,7 @@ class DefaultTable implements Table
self::LEVEL_NO,
];
protected $fieldActionList = [
private $fieldActionList = [
self::ACTION_READ,
self::ACTION_EDIT,
];
@@ -90,44 +84,36 @@ class DefaultTable implements Table
self::LEVEL_NO,
];
protected $valuePermissionHighestLevels = [];
protected $valuePermissionList = [];
protected $forbiddenAttributesCache = [];
protected $forbiddenFieldsCache = [];
protected $isStrictModeForced = false;
protected $isStrictMode = false;
private $isStrictMode = false;
protected $entityManager;
private $data = null;
private $cacheKey;
private $valuePermissionList = [];
private $roleListProvider;
protected $user;
protected $config;
protected $metadata;
protected $fieldUtil;
protected $dataCache;
public function __construct(
EntityManager $entityManager,
RoleListProvider $roleListProvider,
CacheKeyProvider $cacheKeyProvider,
User $user,
Config $config,
Metadata $metadata,
FieldUtil $fieldUtil,
DataCache $dataCache
) {
$this->entityManager = $entityManager;
$this->roleListProvider = $roleListProvider;
$this->data = (object) [
'table' => (object) [],
'fieldTable' => (object) [],
'fieldTableQuickAccess' => (object) [],
'scopes' => (object) [],
'fields' => (object) [],
'permissions' => (object) [],
];
if ($this->isStrictModeForced) {
@@ -139,8 +125,6 @@ class DefaultTable implements Table
$this->user = $user;
$this->metadata = $metadata;
$this->fieldUtil = $fieldUtil;
$this->dataCache = $dataCache;
if (!$this->user->isFetched()) {
throw new RuntimeException('User must be fetched before ACL check.');
@@ -149,46 +133,30 @@ class DefaultTable implements Table
$this->valuePermissionList = $this->metadata
->get(['app', $this->type, 'valuePermissionList'], []);
$this->valuePermissionHighestLevels = $this->metadata
->get(['app', $this->type, 'valuePermissionHighestLevels'], []);
$this->cacheKey = $cacheKeyProvider->get();
$this->initCacheKey();
if ($config && $config->get('useCache') && $this->dataCache->has($this->cacheKey)) {
$this->data = $this->dataCache->get($this->cacheKey);
if ($config->get('useCache') && $dataCache->has($this->cacheKey)) {
$this->data = $dataCache->get($this->cacheKey);
}
else {
$this->load();
if ($config && $config->get('useCache')) {
$this->buildCache();
if ($config->get('useCache')) {
$dataCache->store($this->cacheKey, $this->data);
}
}
}
protected function initCacheKey(): void
{
$this->cacheKey = 'acl/' . $this->user->id;
}
/**
* Get a full map.
*/
public function getMap(): StdClass
{
return ObjectUtil::clone($this->data);
}
/**
* Get scope data.
*/
public function getScopeData(string $scope): ScopeData
{
if (!isset($this->data->table->$scope)) {
if (!isset($this->data->scopes->$scope)) {
return ScopeData::fromRaw(false);
}
$data = $this->data->table->$scope;
$data = $this->data->scopes->$scope;
if (is_string($data)) {
return $this->getScopeData($data);
@@ -197,14 +165,32 @@ class DefaultTable implements Table
return ScopeData::fromRaw($data);
}
/**
* Get field data.
*/
public function getFieldData(string $scope, string $field): FieldData
{
if (!isset($this->data->fields->$scope)) {
return FieldData::fromRaw((object) [
self::ACTION_READ => self::LEVEL_YES,
self::ACTION_EDIT => self::LEVEL_YES,
]);
}
$data = $this->data->fields->$scope->$field ?? (object) [
self::ACTION_READ => self::LEVEL_YES,
self::ACTION_EDIT => self::LEVEL_YES,
];
return FieldData::fromRaw($data);
}
/**
* Get a permission level.
*/
public function getPermissionLevel(string $permission): string
{
$key = $permission . 'Permission';
return $this->data->$key ?? self::LEVEL_NO;
return $this->data->permissions->$permission ?? self::LEVEL_NO;
}
private function load(): void
@@ -219,14 +205,16 @@ class DefaultTable implements Table
$fieldTableList = [];
if (!$this->user->isAdmin()) {
$roleList = $this->getRoleList();
$roleList = $this->roleListProvider->get();
foreach ($roleList as $role) {
$aclTableList[] = $role->get('data');
$fieldTableList[] = $role->get('fieldData');
$aclTableList[] = $role->getScopeTableData();
$fieldTableList[] = $role->getFieldTableData();
foreach ($this->valuePermissionList as $permission) {
$valuePermissionLists->{$permission}[] = $role->get($permission);
foreach ($this->valuePermissionList as $permissionKey) {
$permission = $this->normilizePermissionName($permissionKey);
$valuePermissionLists->{$permissionKey}[] = $role->getPermissionLevel($permission);
}
}
@@ -252,10 +240,8 @@ class DefaultTable implements Table
}
}
$this->data->table = $aclTable;
$this->data->fieldTable = $fieldTable;
$this->fillFieldTableQuickAccess();
$this->data->scopes = $aclTable;
$this->data->fields = $fieldTable;
if (!$this->user->isAdmin()) {
$permissionsDefaultsGroupName = 'permissionsDefaults';
@@ -264,221 +250,53 @@ class DefaultTable implements Table
$permissionsDefaultsGroupName = 'permissionsStrictDefaults';
}
foreach ($this->valuePermissionList as $permission) {
$this->data->$permission = $this->mergeValueList(
$valuePermissionLists->$permission,
$this->metadata
->get(['app', $this->type, $permissionsDefaultsGroupName, $permission, self::LEVEL_YES])
foreach ($this->valuePermissionList as $permissionKey) {
$permission = $this->normilizePermissionName($permissionKey);
$defaultLevel = $this->metadata
->get(['app', $this->type, $permissionsDefaultsGroupName, $permissionKey]) ??
($this->isStrictMode ? self::LEVEL_NO : self::LEVEL_YES);
$this->data->permissions->$permission = $this->mergeValueList(
$valuePermissionLists->$permissionKey,
$defaultLevel
);
if ($this->metadata->get('app.'.$this->type.'.mandatory.' . $permission)) {
$this->data->$permission = $this->metadata
->get('app.'.$this->type.'.mandatory.' . $permission);
$mandatoryLevel = $this->metadata->get(['app', $this->type, 'mandatory', $permissionKey]);
if ($mandatoryLevel !== null) {
$this->data->permissions->$permission = $mandatoryLevel;
}
}
}
if ($this->user->isAdmin()) {
foreach ($this->valuePermissionList as $permission) {
if (isset($this->valuePermissionHighestLevels[$permission])) {
$this->data->$permission = $this->valuePermissionHighestLevels[$permission];
foreach ($this->valuePermissionList as $permissionKey) {
$permission = $this->normilizePermissionName($permissionKey);
$highestLevel = $this->metadata
->get(['app', $this->type, 'valuePermissionHighestLevels', $permissionKey]);
if ($highestLevel !== null) {
$this->data->permissions->$permission = $highestLevel;
continue;
}
$this->data->$permission = self::LEVEL_ALL;
$this->data->permissions->$permission = self::LEVEL_ALL;
}
}
}
protected function getRoleList(): array
private function normilizePermissionName(string $permissionKey): string
{
$roleList = [];
$permission = $permissionKey;
$userRoleList = $this->entityManager
->getRepository('User')
->getRelation($this->user, 'roles')
->find();
foreach ($userRoleList as $role) {
$roleList[] = $role;
if (substr($permissionKey, -10) === 'Permission') {
$permission = substr($permissionKey, 0, -10);
}
$teamList = $this->entityManager
->getRepository('User')
->getRelation($this->user, 'teams')
->find();
foreach ($teamList as $team) {
$teamRoleList = $this->entityManager
->getRepository('Team')
->getRelation($team, 'roles')
->find();
foreach ($teamRoleList as $role) {
$roleList[] = $role;
}
}
return $roleList;
}
public function getScopeForbiddenAttributeList(
string $scope,
string $action = self::ACTION_READ,
string $thresholdLevel = self::LEVEL_NO
): array {
if (!in_array($thresholdLevel, $this->fieldLevelList) || $thresholdLevel === self::LEVEL_YES) {
throw new RuntimeException("Bad threshold level.");
}
$key = $scope . '_'. $action . '_' . $thresholdLevel;
if (isset($this->forbiddenAttributesCache[$key])) {
return $this->forbiddenAttributesCache[$key];
}
$fieldTableQuickAccess = $this->data->fieldTableQuickAccess;
if (
!isset($fieldTableQuickAccess->$scope) || !isset($fieldTableQuickAccess->$scope->attributes) ||
!isset($fieldTableQuickAccess->$scope->attributes->$action)
) {
$this->forbiddenAttributesCache[$key] = [];
return [];
}
$levelList = [];
foreach ($this->fieldLevelList as $level) {
if (array_search($level, $this->fieldLevelList) >= array_search($thresholdLevel, $this->fieldLevelList)) {
$levelList[] = $level;
}
}
$attributeList = [];
foreach ($levelList as $level) {
if (!isset($fieldTableQuickAccess->$scope->attributes->$action->$level)) {
continue;
}
foreach ($fieldTableQuickAccess->$scope->attributes->$action->$level as $attribute) {
if (in_array($attribute, $attributeList)) {
continue;
}
$attributeList[] = $attribute;
}
}
$this->forbiddenAttributesCache[$key] = $attributeList;
return $attributeList;
}
public function getScopeForbiddenFieldList(
string $scope,
string $action = self::ACTION_READ,
string $thresholdLevel = self::LEVEL_NO
): array {
if (!in_array($thresholdLevel, $this->fieldLevelList) || $thresholdLevel === self::LEVEL_YES) {
throw new RuntimeException("Bad threshold level.");
}
$key = $scope . '_'. $action . '_' . $thresholdLevel;
if (isset($this->forbiddenFieldsCache[$key])) {
return $this->forbiddenFieldsCache[$key];
}
$fieldTableQuickAccess = $this->data->fieldTableQuickAccess;
if (
!isset($fieldTableQuickAccess->$scope) || !isset($fieldTableQuickAccess->$scope->fields) ||
!isset($fieldTableQuickAccess->$scope->fields->$action)
) {
$this->forbiddenFieldsCache[$key] = [];
return [];
}
$levelList = [];
foreach ($this->fieldLevelList as $level) {
if (array_search($level, $this->fieldLevelList) >= array_search($thresholdLevel, $this->fieldLevelList)) {
$levelList[] = $level;
}
}
$fieldList = [];
foreach ($levelList as $level) {
if (!isset($fieldTableQuickAccess->$scope->fields->$action->$level)) {
continue;
}
foreach ($fieldTableQuickAccess->$scope->fields->$action->$level as $field) {
if (in_array($field, $fieldList)) {
continue;
}
$fieldList[] = $field;
}
}
$this->forbiddenFieldsCache[$key] = $fieldList;
return $fieldList;
}
protected function fillFieldTableQuickAccess(): void
{
$fieldTable = $this->data->fieldTable;
$fieldTableQuickAccess = (object) [];
foreach (get_object_vars($fieldTable) as $scope => $scopeData) {
$fieldTableQuickAccess->$scope = (object) [
'attributes' => (object) [],
'fields' => (object) []
];
foreach ($this->fieldActionList as $action) {
$fieldTableQuickAccess->$scope->attributes->$action = (object) [];
$fieldTableQuickAccess->$scope->fields->$action = (object) [];
foreach ($this->fieldLevelList as $level) {
$fieldTableQuickAccess->$scope->attributes->$action->$level = [];
$fieldTableQuickAccess->$scope->fields->$action->$level = [];
}
}
foreach (get_object_vars($scopeData) as $field => $fieldData) {
$attributeList = $this->fieldUtil->getAttributeList($scope, $field);
foreach ($this->fieldActionList as $action) {
if (!isset($fieldData->$action)) {
continue;
}
foreach ($this->fieldLevelList as $level) {
if ($fieldData->$action === $level) {
$fieldTableQuickAccess->$scope->fields->$action->{$level}[] = $field;
foreach ($attributeList as $attribute) {
$fieldTableQuickAccess->$scope->attributes->$action->{$level}[] = $attribute;
}
}
}
}
}
}
$this->data->fieldTableQuickAccess = $fieldTableQuickAccess;
return $permission;
}
protected function applyHighest(StdClass &$table, StdClass &$fieldTable): void
@@ -544,21 +362,22 @@ class DefaultTable implements Table
$table->$scope = $value;
}
$defaultFieldData = $this->metadata->get(['app', $this->type, $defaultsGroupName, 'fieldLevel'], []);
$defaultFieldData = $this->metadata
->get(['app', $this->type, $defaultsGroupName, 'fieldLevel']) ?? [];
foreach ($this->getScopeList() as $scope) {
if (isset($table->$scope) && $table->$scope === false) {
continue;
}
if (!$this->metadata->get('scopes.' . $scope . '.entity')) {
if (!$this->metadata->get(['scopes', $scope, 'entity'])) {
continue;
}
$fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", []));
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
$defaultScopeFieldData = $this->metadata
->get('app.'.$this->type.'.'.$defaultsGroupName.'.scopeFieldLevel.' . $scope, []);
->get(['app', $this->type, $defaultsGroupName, 'scopeFieldLevel', $scope]) ?? [];
foreach (array_merge($defaultFieldData, $defaultScopeFieldData) as $field => $f) {
if (!in_array($field, $fieldList)) {
@@ -593,41 +412,43 @@ class DefaultTable implements Table
}
foreach ($this->getScopeWithAclList() as $scope) {
if (!isset($table->$scope)) {
$aclType = $this->metadata->get('scopes.' . $scope . '.' . $this->type);
if (isset($table->$scope)) {
continue;
}
if ($aclType === true) {
$aclType = $this->defaultAclType;
}
$aclType = $this->metadata->get(['scopes', $scope, $this->type]);
if (!empty($aclType)) {
$paramDefaultsName = 'scopeLevelTypesDefaults';
if ($aclType === true) {
$aclType = $this->defaultAclType;
}
if ($this->isStrictMode) {
$paramDefaultsName = 'scopeLevelTypesStrictDefaults';
}
if (empty($aclType)) {
continue;
}
$defaultValue = $this->metadata
->get(
['app', $this->type, $paramDefaultsName, $aclType],
$this->metadata->get(['app', $this->type, $paramDefaultsName, 'record'])
);
$paramDefaultsName = 'scopeLevelTypesDefaults';
if (is_array($defaultValue)) {
$defaultValue = (object) $defaultValue;
}
if ($this->isStrictMode) {
$paramDefaultsName = 'scopeLevelTypesStrictDefaults';
}
$table->$scope = $defaultValue;
$defaultValue =
$this->metadata->get(['app', $this->type, $paramDefaultsName, $aclType]) ??
$this->metadata->get(['app', $this->type, $paramDefaultsName, 'record']);
if (is_object($table->$scope)) {
$actionList = $this->metadata->get(['scopes', $scope, $this->type . 'ActionList']);
if (is_array($defaultValue)) {
$defaultValue = (object) $defaultValue;
}
if ($actionList) {
foreach (get_object_vars($table->$scope) as $action => $level) {
if (!in_array($action, $actionList)) {
unset($table->$scope->$action);
}
}
$table->$scope = $defaultValue;
if (is_object($table->$scope)) {
$actionList = $this->metadata->get(['scopes', $scope, $this->type . 'ActionList']);
if ($actionList) {
foreach (get_object_vars($table->$scope) as $action => $level) {
if (!in_array($action, $actionList)) {
unset($table->$scope->$action);
}
}
}
@@ -641,7 +462,7 @@ class DefaultTable implements Table
return;
}
$data = $this->metadata->get('app.'.$this->type.'.mandatory.scopeLevel', []);
$data = $this->metadata->get(['app', $this->type, 'mandatory', 'scopeLevel']) ?? [];
foreach ($data as $scope => $item) {
$value = $item;
@@ -653,23 +474,23 @@ class DefaultTable implements Table
$table->$scope = $value;
}
$mandatoryFieldData = $this->metadata->get('app.'.$this->type.'.mandatory.fieldLevel', []);
$mandatoryFieldData = $this->metadata->get(['app', $this->type, 'mandatory', 'fieldLevel']) ?? [];
foreach ($this->getScopeList() as $scope) {
if (isset($table->$scope) && $table->$scope === false) {
continue;
}
if (!$this->metadata->get('scopes.' . $scope . '.entity')) {
if (!$this->metadata->get(['scopes', $scope, 'entity'])) {
continue;
}
$fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", []));
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
$mandatoryScopeFieldData = $this->metadata
->get('app.'.$this->type.'.mandatory.scopeFieldLevel.' . $scope, []);
->get(['app', $this->type, 'mandatory', 'scopeFieldLevel', $scope]) ?? [];
foreach (array_merge($mandatoryFieldData, $mandatoryScopeFieldData) as $field => $f) {
foreach (array_merge($mandatoryFieldData, $mandatoryScopeFieldData) as $field => $item) {
if (!in_array($field, $fieldList)) {
continue;
}
@@ -683,12 +504,12 @@ class DefaultTable implements Table
foreach ($this->fieldActionList as $action) {
$level = self::LEVEL_NO;
if ($f === true) {
if ($item === true) {
$level = self::LEVEL_YES;
}
else {
if (is_array($f) && isset($f[$action])) {
$level = $f[$action];
if (is_array($item) && isset($item[$action])) {
$level = $item[$action];
}
}
@@ -705,7 +526,7 @@ class DefaultTable implements Table
}
foreach ($this->getScopeList() as $scope) {
if ($this->metadata->get('scopes.' . $scope . '.disabled')) {
if ($this->metadata->get(['scopes', $scope, 'disabled'])) {
$table->$scope = false;
unset($fieldTable->$scope);
@@ -713,6 +534,9 @@ class DefaultTable implements Table
}
}
/**
* @todo Revise usage of this method.
*/
protected function applyAdditional(&$table, &$fieldTable, &$valuePermissionLists): void
{
if ($this->user->isPortal()) {
@@ -836,7 +660,7 @@ class DefaultTable implements Table
}
$actionList = $this->metadata
->get(['scopes', $scope, $this->type . 'ActionList'], $this->actionList);
->get(['scopes', $scope, $this->type . 'ActionList']) ?? $this->actionList;
foreach ($actionList as $i => $action) {
if (isset($row->$action)) {
@@ -891,7 +715,7 @@ class DefaultTable implements Table
continue;
}
$fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", []));
$fieldList = array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
foreach (get_object_vars($table->$scope) as $field => $row) {
if (!is_object($row)) {
@@ -933,9 +757,4 @@ class DefaultTable implements Table
return $data;
}
private function buildCache(): void
{
$this->dataCache->store($this->cacheKey, $this->data);
}
}

View File

@@ -0,0 +1,96 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl;
use StdClass;
use RuntimeException;
/**
* Field data.
*/
class FieldData
{
private $raw;
private $actionData = [];
private function __construct()
{
}
public function __get(string $name)
{
throw new RuntimeException("Accessing ScopeData properties is not allowed.");
}
/**
* Get a level for an action.
*/
public function get(string $action): string
{
return $this->actionData[$action] ?? Table::LEVEL_NO;
}
/**
* Get a 'read' level.
*/
public function getRead(): string
{
return $this->get(Table::ACTION_READ);
}
/**
* Get an 'edit' level.
*/
public function getEdit(): string
{
return $this->get(Table::ACTION_EDIT);
}
/**
* Create from a raw table value.
*/
public static function fromRaw(StdClass $raw): self
{
$obj = new self();
$obj->actionData = get_object_vars($raw);
foreach ($obj->actionData as $item) {
if (!is_string($item)) {
throw new RuntimeException("Bad raw scope data.");
}
}
$obj->raw = $raw;
return $obj;
}
}

View File

@@ -0,0 +1,35 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Map;
interface CacheKeyProvider
{
public function get(): string;
}

View File

@@ -0,0 +1,194 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Map;
use Espo\Core\{
Acl\Table,
Utils\FieldUtil,
};
use StdClass;
class DataBuilder
{
private $actionList = [
Table::ACTION_READ,
Table::ACTION_STREAM,
Table::ACTION_EDIT,
Table::ACTION_DELETE,
Table::ACTION_CREATE,
];
private $fieldActionList = [
Table::ACTION_READ,
Table::ACTION_EDIT,
];
private $fieldLevelList = [
Table::LEVEL_YES,
Table::LEVEL_NO,
];
private $metadataProvider;
private $fieldUtil;
public function __construct(MetadataProvider $metadataProvider, FieldUtil $fieldUtil)
{
$this->metadataProvider = $metadataProvider;
$this->fieldUtil = $fieldUtil;
}
public function build(Table $table): StdClass
{
$data = (object) [
'table' => (object) [],
'fieldTable' => (object) [],
];
foreach ($this->metadataProvider->getScopeList() as $scope) {
$data->table->$scope = $this->getScopeRawData($table, $scope);
$fieldData = $this->getScopeFieldData($table, $scope);
if ($fieldData !== null) {
$data->fieldTable->$scope = $fieldData;
}
}
foreach ($this->metadataProvider->getPermissionList() as $permission) {
$data->{$permission . 'Permission'} = $table->getPermissionLevel($permission);
}
$data->fieldTableQuickAccess = $this->buildFieldTableQuickAccess($data->fieldTable);
return $data;
}
/**
* @return bool|StdClass
*/
private function getScopeRawData(Table $table, string $scope)
{
$data = $table->getScopeData($scope);
if ($data->isBoolean()) {
return $data->isTrue();
}
$rawData = (object) [];
foreach ($this->actionList as $action) {
$rawData->$action = $data->get($action);
}
return $rawData;
}
private function getScopeFieldData(Table $table, string $scope): ?StdClass
{
if (!$this->metadataProvider->isScopeEntity($scope)) {
return null;
}
$fieldList = $this->metadataProvider->getScopeFieldList($scope);
$rawData = (object) [];
foreach ($fieldList as $field) {
$data = $table->getFieldData($scope, $field);
if (
$data->getRead() === Table::LEVEL_YES &&
$data->getEdit() === Table::LEVEL_YES
) {
continue;
}
$rawData->$field = (object) [
Table::ACTION_READ => $data->getRead(),
Table::ACTION_EDIT => $data->getEdit(),
];
}
return $rawData;
}
protected function buildFieldTableQuickAccess(StdClass $fieldTable): StdClass
{
$quickAccess = (object) [];
foreach (get_object_vars($fieldTable) as $scope => $scopeData) {
$quickAccess->$scope = $this->buildFieldTableQuickAccessScope($scope, $scopeData);
}
return $quickAccess;
}
private function buildFieldTableQuickAccessScope(string $scope, StdClass $data): StdClass
{
$quickAccess = (object) [
'attributes' => (object) [],
'fields' => (object) [],
];
foreach ($this->fieldActionList as $action) {
$quickAccess->attributes->$action = (object) [];
$quickAccess->fields->$action = (object) [];
foreach ($this->fieldLevelList as $level) {
$quickAccess->attributes->$action->$level = [];
$quickAccess->fields->$action->$level = [];
}
}
foreach (get_object_vars($data) as $field => $fieldData) {
$attributeList = $this->fieldUtil->getAttributeList($scope, $field);
foreach ($this->fieldActionList as $action) {
if (!isset($fieldData->$action)) {
continue;
}
foreach ($this->fieldLevelList as $level) {
if ($fieldData->$action === $level) {
$quickAccess->fields->$action->{$level}[] = $field;
foreach ($attributeList as $attribute) {
$quickAccess->attributes->$action->{$level}[] = $attribute;
}
}
}
}
}
return $quickAccess;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Map;
use Espo\Entities\User;
class DefaultCacheKeyProvider implements CacheKeyProvider
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function get(): string
{
return 'aclMap/' . $this->user->getId();
}
}

View File

@@ -0,0 +1,251 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Map;
use Espo\Entities\User;
use Espo\Core\{
Acl\Table,
Utils\Config,
Utils\DataCache,
Utils\ObjectUtil,
};
use StdClass;
use RuntimeException;
/**
* Provides quick access to ACL data.
*/
class Map
{
private $data;
private $cacheKey;
private $forbiddenFieldsCache = [];
private $forbiddenAttributesCache;
private $fieldLevelList = [
Table::LEVEL_YES,
Table::LEVEL_NO,
];
private $user;
private $table;
private $config;
private $dataCache;
private $dataBuilder;
public function __construct(
User $user,
Table $table,
DataBuilder $dataBuilder,
Config $config,
DataCache $dataCache,
CacheKeyProvider $cacheKeyProvider
) {
$this->user = $user;
$this->table = $table;
$this->dataBuilder = $dataBuilder;
$this->config = $config;
$this->dataCache = $dataCache;
$this->cacheKey = $cacheKeyProvider->get();
if ($this->config->get('useCache') && $this->dataCache->has($this->cacheKey)) {
$this->data = $this->dataCache->get($this->cacheKey);
}
else {
$this->data = $this->dataBuilder->build($table);
if ($this->config->get('useCache')) {
$this->dataCache->store($this->cacheKey, $this->data);
}
}
}
/**
* Get raw data (for front-end).
*/
public function getData(): StdClass
{
return ObjectUtil::clone($this->data);
}
/**
* Get a list of forbidden attributes for a scope and action.
*
* @param $scope A scope.
* @param $action An action.
* @param $thresholdLevel An attribute will be treated as forbidden if the level is
* equal to or lower than the threshold.
* @return array<string>
*/
public function getScopeForbiddenAttributeList(
string $scope,
string $action = Table::ACTION_READ,
string $thresholdLevel = Table::LEVEL_NO
): array {
if (
!in_array($thresholdLevel, $this->fieldLevelList) ||
$thresholdLevel === Table::LEVEL_YES
) {
throw new RuntimeException("Bad threshold level.");
}
$key = $scope . '_'. $action . '_' . $thresholdLevel;
if (isset($this->forbiddenAttributesCache[$key])) {
return $this->forbiddenAttributesCache[$key];
}
$fieldTableQuickAccess = $this->data->fieldTableQuickAccess;
if (
!isset($fieldTableQuickAccess->$scope) ||
!isset($fieldTableQuickAccess->$scope->attributes) ||
!isset($fieldTableQuickAccess->$scope->attributes->$action)
) {
$this->forbiddenAttributesCache[$key] = [];
return [];
}
$levelList = [];
foreach ($this->fieldLevelList as $level) {
if (
array_search($level, $this->fieldLevelList) >=
array_search($thresholdLevel, $this->fieldLevelList)
) {
$levelList[] = $level;
}
}
$attributeList = [];
foreach ($levelList as $level) {
if (!isset($fieldTableQuickAccess->$scope->attributes->$action->$level)) {
continue;
}
foreach ($fieldTableQuickAccess->$scope->attributes->$action->$level as $attribute) {
if (in_array($attribute, $attributeList)) {
continue;
}
$attributeList[] = $attribute;
}
}
$this->forbiddenAttributesCache[$key] = $attributeList;
return $attributeList;
}
/**
* Get a list of forbidden fields for a scope and action.
*
* @param $scope A scope.
* @param $action An action.
* @param $thresholdLevel An attribute will be treated as forbidden if the level is
* equal to or lower than the threshold.
* @return array<string>
*/
public function getScopeForbiddenFieldList(
string $scope,
string $action = Table::ACTION_READ,
string $thresholdLevel = Table::LEVEL_NO
): array {
if (
!in_array($thresholdLevel, $this->fieldLevelList) ||
$thresholdLevel === Table::LEVEL_YES
) {
throw new RuntimeException("Bad threshold level.");
}
$key = $scope . '_'. $action . '_' . $thresholdLevel;
if (isset($this->forbiddenFieldsCache[$key])) {
return $this->forbiddenFieldsCache[$key];
}
$fieldTableQuickAccess = $this->data->fieldTableQuickAccess;
if (
!isset($fieldTableQuickAccess->$scope) ||
!isset($fieldTableQuickAccess->$scope->fields) ||
!isset($fieldTableQuickAccess->$scope->fields->$action)
) {
$this->forbiddenFieldsCache[$key] = [];
return [];
}
$levelList = [];
foreach ($this->fieldLevelList as $level) {
if (
array_search($level, $this->fieldLevelList) >=
array_search($thresholdLevel, $this->fieldLevelList)
) {
$levelList[] = $level;
}
}
$fieldList = [];
foreach ($levelList as $level) {
if (!isset($fieldTableQuickAccess->$scope->fields->$action->$level)) {
continue;
}
foreach ($fieldTableQuickAccess->$scope->fields->$action->$level as $field) {
if (in_array($field, $fieldList)) {
continue;
}
$fieldList[] = $field;
}
}
$this->forbiddenFieldsCache[$key] = $fieldList;
return $fieldList;
}
}

View File

@@ -0,0 +1,82 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Map;
use Espo\Entities\User;
use Espo\Core\{
InjectableFactory,
Acl\Table,
Acl\Map\Map,
Binding\BindingContainer,
Binding\Binder,
Binding\BindingData,
};
class MapFactory
{
private $injectableFactory;
public function __construct(InjectableFactory $injectableFactory)
{
$this->injectableFactory = $injectableFactory;
}
public function create(User $user, Table $table): Map
{
$bindingContainer = $this->createBindingContainer($user, $table);
return $this->injectableFactory->createWithBinding(Map::class, $bindingContainer);
}
private function createBindingContainer(User $user, Table $table): BindingContainer
{
$bindingData = new BindingData();
$binder = new Binder($bindingData);
$binder
->bindCallback(
User::class,
function () use ($user): User {
return $user;
}
)
->bindCallback(
Table::class,
function () use ($table): Table {
return $table;
}
)
->bindImplementation(CacheKeyProvider::class, DefaultCacheKeyProvider::class);
return new BindingContainer($bindingData);
}
}

View File

@@ -0,0 +1,86 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Map;
use Espo\Core\{
Utils\Metadata,
};
class MetadataProvider
{
protected $type = 'acl';
private $metadata;
public function __construct(Metadata $metadata)
{
$this->metadata = $metadata;
}
/**
* @return array<string>
*/
public function getScopeList(): array
{
return array_keys($this->metadata->get('scopes') ?? []);
}
public function isScopeEntity(string $scope): bool
{
return (bool) $this->metadata->get(['scopes', $scope, 'entity']);
}
/**
* @return array<string>
*/
public function getScopeFieldList(string $scope): array
{
return array_keys($this->metadata->get(['entityDefs', $scope, 'fields']) ?? []);
}
/**
* @return array<string>
*/
public function getPermissionList(): array
{
$itemList = $this->metadata->get(['app', $this->type, 'valuePermissionList']) ?? [];
return array_map(
function (string $item): string {
if (substr($item, -10) === 'Permission') {
return substr($item, 0, -10);
}
return $item;
},
$itemList
);
}
}

View File

@@ -29,10 +29,8 @@
namespace Espo\Core\Acl;
use StdClass;
/**
* Contains access levels for a user.
* Access levels for a user.
*/
interface Table
{
@@ -56,40 +54,18 @@ interface Table
public const ACTION_CREATE = 'create';
/**
* Get a full map.
*/
public function getMap(): StdClass;
/**
* Get scope data.
*/
public function getScopeData(string $scope): ScopeData;
/**
* Get field data.
*/
public function getFieldData(string $scope, string $field): FieldData;
/**
* Get a permission level.
*/
public function getPermissionLevel(string $permission): string;
/**
* Get a list of forbidden attributes for a scope and action.
*
* @param $scope A scope.
* $param $action An action.
* @param $thresholdLevel An attribute will be treated as forbidden if the level is
* equal to or lower than the threshold.
* @return array<string>
*/
public function getScopeForbiddenAttributeList(string $scope, string $action, string $thresholdLevel): array;
/**
* Get a list of forbidden fields for a scope and action.
*
* @param $scope A scope.
* $param $action An action.
* @param $thresholdLevel An attribute will be treated as forbidden if the level is
* equal to or lower than the threshold.
* @return array<string>
*/
public function getScopeForbiddenFieldList(string $scope, string $action, string $thresholdLevel): array;
}

View File

@@ -0,0 +1,35 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Table;
interface CacheKeyProvider
{
public function get(): string;
}

View File

@@ -0,0 +1,47 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Table;
use Espo\Entities\User;
class DefaultCacheKeyProvider implements CacheKeyProvider
{
private $user;
public function __construct(User $user)
{
$this->user = $user;
}
public function get(): string
{
return 'acl/' . $this->user->getId();
}
}

View File

@@ -0,0 +1,90 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Table;
use Espo\ORM\EntityManager;
use Espo\Entities\{
User,
Role as RoleEntity,
};
class DefaultRoleListProvider implements RoleListProvider
{
private $user;
private $entityManager;
public function __construct(User $user, EntityManager $entityManager)
{
$this->user = $user;
$this->entityManager = $entityManager;
}
/**
* @return array<Role>
*/
public function get(): array
{
$roleList = [];
$userRoleList = $this->entityManager
->getRepository('User')
->getRelation($this->user, 'roles')
->find();
foreach ($userRoleList as $role) {
$roleList[] = $role;
}
$teamList = $this->entityManager
->getRepository('User')
->getRelation($this->user, 'teams')
->find();
foreach ($teamList as $team) {
$teamRoleList = $this->entityManager
->getRepository('Team')
->getRelation($team, 'roles')
->find();
foreach ($teamRoleList as $role) {
$roleList[] = $role;
}
}
return array_map(
function (RoleEntity $role): RoleEntityWrapper {
return new RoleEntityWrapper($role);
},
$roleList
);
}
}

View File

@@ -0,0 +1,41 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Table;
use StdClass;
interface Role
{
public function getScopeTableData(): StdClass;
public function getFieldTableData(): StdClass;
public function getPermissionLevel(string $permission): ?string;
}

View File

@@ -0,0 +1,59 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Table;
use Espo\ORM\Entity;
use StdClass;
class RoleEntityWrapper implements Role
{
private $entity;
public function __construct(Entity $entity)
{
$this->entity = $entity;
}
public function getScopeTableData(): StdClass
{
return $this->entity->get('data') ?? (object) [];
}
public function getFieldTableData(): StdClass
{
return $this->entity->get('fieldData') ?? (object) [];
}
public function getPermissionLevel(string $permission): ?string
{
return $this->entity->get($permission . 'Permission');
}
}

View File

@@ -0,0 +1,38 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Acl\Table;
interface RoleListProvider
{
/**
* @return array<Role>
*/
public function get(): array;
}

View File

@@ -33,6 +33,13 @@ use Espo\Entities\User;
use Espo\Core\{
InjectableFactory,
Acl\Table\CacheKeyProvider,
Acl\Table\DefaultCacheKeyProvider,
Acl\Table\RoleListProvider,
Acl\Table\DefaultRoleListProvider,
Binding\BindingContainer,
Binding\Binder,
Binding\BindingData,
};
class TableFactory
@@ -51,8 +58,27 @@ class TableFactory
*/
public function create(User $user): Table
{
return $this->injectableFactory->createWith(DefaultTable::class, [
'user' => $user,
]);
$bindingContainer = $this->createBindingContainer($user);
return $this->injectableFactory->createWithBinding(DefaultTable::class, $bindingContainer);
}
private function createBindingContainer(User $user): BindingContainer
{
$bindingData = new BindingData();
$binder = new Binder($bindingData);
$binder
->bindCallback(
User::class,
function () use ($user): User {
return $user;
}
)
->bindImplementation(RoleListProvider::class, DefaultRoleListProvider::class)
->bindImplementation(CacheKeyProvider::class, DefaultCacheKeyProvider::class);
return new BindingContainer($bindingData);
}
}

View File

@@ -40,6 +40,8 @@ use Espo\Core\{
Acl\OwnerUserFieldProvider,
Acl\TableFactory,
Acl\Table,
Acl\Map\Map,
Acl\Map\MapFactory,
Acl\OwnershipCheckerFactory,
Acl\OwnershipChecker,
Acl\OwnershipOwnChecker,
@@ -73,6 +75,8 @@ class AclManager
protected $tableHashMap = [];
protected $mapHashMap = [];
protected $userAclClassName = Acl::class;
protected const PERMISSION_ASSIGNMENT = 'assignment';
@@ -99,6 +103,8 @@ class AclManager
protected $tableFactory;
protected $mapFactory;
protected $globalRestricton;
protected $ownerUserFieldProvider;
@@ -109,6 +115,7 @@ class AclManager
AccessCheckerFactory $accessCheckerFactory,
OwnershipCheckerFactory $ownershipCheckerFactory,
TableFactory $tableFactory,
MapFactory $mapFactory,
GlobalRestricton $globalRestricton,
OwnerUserFieldProvider $ownerUserFieldProvider,
EntityManager $entityManager
@@ -116,6 +123,7 @@ class AclManager
$this->accessCheckerFactory = $accessCheckerFactory;
$this->ownershipCheckerFactory = $ownershipCheckerFactory;
$this->tableFactory = $tableFactory;
$this->mapFactory = $mapFactory;
$this->globalRestricton = $globalRestricton;
$this->ownerUserFieldProvider = $ownerUserFieldProvider;
$this->entityManager = $entityManager;
@@ -160,12 +168,27 @@ class AclManager
return $this->tableHashMap[$key];
}
/**
* Get a full access data map.
*/
public function getMap(User $user): StdClass
protected function getMap(User $user): Map
{
return $this->getTable($user)->getMap();
$key = $user->getId();
if (!$key) {
$key = spl_object_hash($user);
}
if (!array_key_exists($key, $this->mapHashMap)) {
$this->mapHashMap[$key] = $this->mapFactory->create($user, $this->getTable($user));
}
return $this->mapHashMap[$key];
}
/**
* Get a full access data map (for front-end).
*/
public function getMapData(User $user): StdClass
{
return $this->getMap($user)->getData();
}
/**
@@ -437,7 +460,7 @@ class AclManager
): array {
$list = array_merge(
$this->getTable($user)->getScopeForbiddenAttributeList(
$this->getMap($user)->getScopeForbiddenAttributeList(
$scope,
$action,
$thresholdLevel
@@ -465,7 +488,7 @@ class AclManager
): array {
$list = array_merge(
$this->getTable($user)->getScopeForbiddenFieldList(
$this->getMap($user)->getScopeForbiddenFieldList(
$scope,
$action,
$thresholdLevel

View File

@@ -0,0 +1,57 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Portal\Acl\Map;
use Espo\Entities\{
User,
Portal,
};
use Espo\Core\{
Acl\Map\CacheKeyProvider as CacheKeyProviderInterface,
};
class CacheKeyProvider implements CacheKeyProviderInterface
{
private $user;
private $portal;
public function __construct(User $user, Portal $portal)
{
$this->user = $user;
$this->portal = $portal;
}
public function get(): string
{
return 'aclPortalMap/' . $this->portal->getId() . '/' . $this->user->getId();
}
}

View File

@@ -0,0 +1,97 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Portal\Acl\Map;
use Espo\Entities\{
User,
Portal,
};
use Espo\Core\{
InjectableFactory,
Portal\Acl\Table as PortalTable,
Portal\Acl\Map\MetadataProvider as PortalMetadataProvider,
Portal\Acl\Map\CacheKeyProvider as PortalCacheKeyProvider,
Acl\Map\MetadataProvider,
Acl\Map\CacheKeyProvider,
Acl\Map\Map,
Acl\Table,
Binding\BindingContainer,
Binding\Binder,
Binding\BindingData,
};
class MapFactory
{
private $injectableFactory;
public function __construct(InjectableFactory $injectableFactory)
{
$this->injectableFactory = $injectableFactory;
}
public function create(User $user, PortalTable $table, Portal $portal): Map
{
$bindingContainer = $this->createBindingContainer($user, $table, $portal);
return $this->injectableFactory->createWithBinding(Map::class, $bindingContainer);
}
private function createBindingContainer(User $user, PortalTable $table, Portal $portal): BindingContainer
{
$bindingData = new BindingData();
$binder = new Binder($bindingData);
$binder
->bindCallback(
User::class,
function () use ($user): User {
return $user;
}
)
->bindCallback(
Table::class,
function () use ($table): PortalTable {
return $table;
}
)
->bindCallback(
Portal::class,
function () use ($portal): Portal {
return $portal;
}
)
->bindImplementation(MetadataProvider::class, PortalMetadataProvider::class)
->bindImplementation(CacheKeyProvider::class, PortalCacheKeyProvider::class);
return new BindingContainer($bindingData);
}
}

View File

@@ -0,0 +1,39 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Portal\Acl\Map;
use Espo\Core\{
Acl\Map\MetadataProvider as BaseMetadataProvider,
};
class MetadataProvider extends BaseMetadataProvider
{
protected $type = 'aclPortal';
}

View File

@@ -29,22 +29,10 @@
namespace Espo\Core\Portal\Acl;
use Espo\Entities\{
User,
Portal,
};
use Espo\Core\{
Acl\DefaultTable as BaseTable,
ORM\EntityManager,
Utils\Config,
Utils\Metadata,
Utils\FieldUtil,
Utils\DataCache,
};
use RuntimeException;
class Table extends BaseTable
{
public const LEVEL_ACCOUNT = 'account';
@@ -53,8 +41,6 @@ class Table extends BaseTable
protected $type = 'aclPortal';
protected $portal;
protected $defaultAclType = 'recordAllOwnNo';
protected $levelList = [
@@ -68,54 +54,6 @@ class Table extends BaseTable
protected $isStrictModeForced = true;
public function __construct(
EntityManager $entityManager,
User $user,
Portal $portal,
Config $config,
Metadata $metadata,
FieldUtil $fieldUtil,
DataCache $dataCache
) {
if (empty($portal)) {
throw new RuntimeException("No portal was passed to AclPortal\\Table constructor.");
}
$this->portal = $portal;
parent::__construct($entityManager, $user, $config, $metadata, $fieldUtil, $dataCache);
}
protected function initCacheKey(): void
{
$this->cacheKey = 'aclPortal/' . $this->portal->getId() . '/' . $this->user->getId();
}
protected function getRoleList(): array
{
$roleList = [];
$userRoleList = $this->entityManager
->getRepository('User')
->getRelation($this->user, 'portalRoles')
->find();
foreach ($userRoleList as $role) {
$roleList[] = $role;
}
$portalRoleList = $this->entityManager
->getRepository('Portal')
->getRelation($this->portal, 'portalRoles')
->find();
foreach ($portalRoleList as $role) {
$roleList[] = $role;
}
return $roleList;
}
protected function getScopeWithAclList(): array
{
$scopeList = [];
@@ -151,9 +89,9 @@ class Table extends BaseTable
protected function applyDisabled(&$table, &$fieldTable): void
{
foreach ($this->getScopeList() as $scope) {
$d = $this->metadata->get('scopes.' . $scope);
$item = $this->metadata->get(['scopes', $scope]) ?? [];
if (!empty($d['disabled']) || !empty($d['portalDisabled'])) {
if (!empty($item['disabled']) || !empty($item['portalDisabled'])) {
$table->$scope = false;
unset($fieldTable->$scope);

View File

@@ -0,0 +1,57 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Portal\Acl\Table;
use Espo\Entities\{
User,
Portal,
};
use Espo\Core\{
Acl\Table\CacheKeyProvider as CacheKeyProviderInterface,
};
class CacheKeyProvider implements CacheKeyProviderInterface
{
private $user;
private $portal;
public function __construct(User $user, Portal $portal)
{
$this->user = $user;
$this->portal = $portal;
}
public function get(): string
{
return 'aclPortal/' . $this->portal->getId() . '/' . $this->user->getId();
}
}

View File

@@ -0,0 +1,93 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Portal\Acl\Table;
use Espo\ORM\EntityManager;
use Espo\Entities\{
User,
Portal,
PortalRole,
};
use Espo\Core\{
Acl\Table\RoleListProvider as RoleListProviderInterface,
Acl\Table\RoleEntityWrapper,
Acl\Table\Role,
};
class RoleListProvider implements RoleListProviderInterface
{
private $user;
private $portal;
private $entityManager;
public function __construct(User $user, Portal $portal, EntityManager $entityManager)
{
$this->user = $user;
$this->portal = $portal;
$this->entityManager = $entityManager;
}
/**
* @return array<Role>
*/
public function get(): array
{
$roleList = [];
$userRoleList = $this->entityManager
->getRepository('User')
->getRelation($this->user, 'portalRoles')
->find();
foreach ($userRoleList as $role) {
$roleList[] = $role;
}
$portalRoleList = $this->entityManager
->getRepository('Portal')
->getRelation($this->portal, 'portalRoles')
->find();
foreach ($portalRoleList as $role) {
$roleList[] = $role;
}
return array_map(
function (PortalRole $role): RoleEntityWrapper {
return new RoleEntityWrapper($role);
},
$roleList
);
}
}

View File

@@ -36,6 +36,13 @@ use Espo\Entities\{
use Espo\Core\{
InjectableFactory,
Acl\Table\CacheKeyProvider,
Portal\Acl\Table\CacheKeyProvider as PortalCacheKeyProvider,
Acl\Table\RoleListProvider,
Portal\Acl\Table\RoleListProvider as PortalRoleListProvider,
Binding\BindingContainer,
Binding\Binder,
Binding\BindingData,
};
class TableFactory
@@ -52,9 +59,33 @@ class TableFactory
*/
public function create(User $user, Portal $portal): Table
{
return $this->injectableFactory->createWith(Table::class, [
'user' => $user,
'portal' => $portal,
]);
$bindingContainer = $this->createBindingContainer($user, $portal);
return $this->injectableFactory->createWithBinding(Table::class, $bindingContainer);
}
private function createBindingContainer(User $user, Portal $portal): BindingContainer
{
$bindingData = new BindingData();
$binder = new Binder($bindingData);
$binder
->bindCallback(
User::class,
function () use ($user): User {
return $user;
}
)
->bindCallback(
Portal::class,
function () use ($portal): Portal {
return $portal;
}
)
->bindImplementation(RoleListProvider::class, PortalRoleListProvider::class)
->bindImplementation(CacheKeyProvider::class, PortalCacheKeyProvider::class);
return new BindingContainer($bindingData);
}
}

View File

@@ -47,9 +47,11 @@ use Espo\Core\{
Portal\Acl\OwnershipContactChecker,
Portal\Acl\TableFactory,
Portal\Acl,
Portal\Acl\Map\MapFactory,
Acl\GlobalRestricton,
Acl\OwnerUserFieldProvider,
Acl\Table as TableBase,
Acl\Map\Map,
AclManager as InternalAclManager,
};
@@ -68,6 +70,7 @@ class AclManager extends InternalAclManager
AccessCheckerFactory $accessCheckerFactory,
OwnershipCheckerFactory $ownershipCheckerFactory,
TableFactory $tableFactory,
MapFactory $mapFactory,
GlobalRestricton $globalRestricton,
OwnerUserFieldProvider $ownerUserFieldProvider,
EntityManager $entityManager,
@@ -76,6 +79,7 @@ class AclManager extends InternalAclManager
$this->accessCheckerFactory = $accessCheckerFactory;
$this->ownershipCheckerFactory = $ownershipCheckerFactory;
$this->tableFactory = $tableFactory;
$this->mapFactory = $mapFactory;
$this->globalRestricton = $globalRestricton;
$this->ownerUserFieldProvider = $ownerUserFieldProvider;
$this->entityManager = $entityManager;
@@ -111,13 +115,29 @@ class AclManager extends InternalAclManager
return $this->tableHashMap[$key];
}
public function getMap(User $user): StdClass
protected function getMap(User $user): Map
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->getMap($user);
$key = $user->getId();
if (!$key) {
$key = spl_object_hash($user);
}
return parent::getMap($user);
if (!array_key_exists($key, $this->mapHashMap)) {
$this->mapHashMap[$key] = $this->mapFactory
->create($user, $this->getTable($user), $this->getPortal());
}
return $this->mapHashMap[$key];
}
public function getMapData(User $user): StdClass
{
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->getMapData($user);
}
return parent::getMapData($user);
}
public function getLevel(User $user, string $scope, string $action): string
@@ -285,7 +305,8 @@ class AclManager extends InternalAclManager
): array {
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel);
return $this->internalAclManager
->getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel);
}
return parent::getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel);
@@ -299,7 +320,8 @@ class AclManager extends InternalAclManager
): array {
if ($this->checkUserIsNotPortal($user)) {
return $this->internalAclManager->getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel);
return $this->internalAclManager
->getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel);
}
return parent::getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel);

View File

@@ -214,7 +214,7 @@ class App
protected function getAclDataForFrontend()
{
$data = $this->acl->getMap();
$data = $this->acl->getMapData();
if (!$this->user->isAdmin()) {
$data = unserialize(serialize($data));

View File

@@ -77,6 +77,8 @@ class Portal extends Record implements
protected function clearRolesCache()
{
$this->fileManager->removeInDir('data/cache/application/aclPortal');
$this->fileManager->removeInDir('data/cache/application/aclPortalMap');
$this->dataManager->updateCacheTimestamp();
}
}

View File

@@ -58,6 +58,8 @@ class PortalRole extends Record implements
protected function clearRolesCache()
{
$this->fileManager->removeInDir('data/cache/application/aclPortal');
$this->fileManager->removeInDir('data/cache/application/aclPortalMap');
$this->dataManager->updateCacheTimestamp();
}
}

View File

@@ -58,6 +58,8 @@ class Role extends Record implements
protected function clearRolesCache()
{
$this->fileManager->removeInDir('data/cache/application/acl');
$this->fileManager->removeInDir('data/cache/application/aclMap');
$this->dataManager->updateCacheTimestamp();
}
}

View File

@@ -44,6 +44,7 @@ class Team extends Record implements
public function afterUpdateEntity(Entity $entity, $data)
{
parent::afterUpdateEntity($entity, $data);
if (property_exists($data, 'rolesIds')) {
$this->clearRolesCache();
}
@@ -52,6 +53,8 @@ class Team extends Record implements
protected function clearRolesCache()
{
$this->fileManager->removeInDir('data/cache/application/acl');
$this->fileManager->removeInDir('data/cache/application/aclMap');
$this->dataManager->updateCacheTimestamp();
}
@@ -61,6 +64,8 @@ class Team extends Record implements
if ($link === 'users') {
$this->fileManager->removeFile('data/cache/application/acl/' . $foreignId . '.php');
$this->fileManager->removeFile('data/cache/application/aclMap/' . $foreignId . '.php');
$this->dataManager->updateCacheTimestamp();
}
}
@@ -71,6 +76,8 @@ class Team extends Record implements
if ($link === 'users') {
$this->fileManager->removeFile('data/cache/application/acl/' . $foreignId . '.php');
$this->fileManager->removeFile('data/cache/application/aclMap/' . $foreignId . '.php');
$this->dataManager->updateCacheTimestamp();
}
}

View File

@@ -771,6 +771,7 @@ class User extends Record implements
protected function clearRoleCache(string $id)
{
$this->fileManager->removeFile('data/cache/application/acl/' . $id . '.php');
$this->fileManager->removeFile('data/cache/application/aclMap/' . $id . '.php');
$this->dataManager->updateCacheTimestamp();
}
@@ -778,11 +779,11 @@ class User extends Record implements
protected function clearPortalRolesCache()
{
$this->fileManager->removeInDir('data/cache/application/aclPortal');
$this->fileManager->removeInDir('data/cache/application/aclPortalMap');
$this->dataManager->updateCacheTimestamp();
}
public function loadAdditionalFields(Entity $entity)
{
parent::loadAdditionalFields($entity);

View File

@@ -96,13 +96,36 @@ define('acl-manager', ['acl'], function (Acl) {
this.data.attributeTable = this.data.attributeTable || {};
},
/**
* @deprecated Use `getPermissionLevel`.
* @returns string|null
*/
get: function (name) {
return this.data[name] || null;
},
/**
* @param string permission
* @returns string
*/
getPermissionLevel: function (permission) {
var permissionKey = permission;
if (permission.substr(-10) !== 'Permission') {
permissionKey = permission + 'Permission';
}
return this.data[permissionKey] || 'no';
},
getLevel: function (scope, action) {
if (!(scope in this.data.table)) return;
if (typeof this.data.table[scope] !== 'object' || !(action in this.data.table[scope])) return;
if (!(scope in this.data.table)) {
return;
}
if (typeof this.data.table[scope] !== 'object' || !(action in this.data.table[scope])) {
return;
}
return this.data.table[scope][action];
},
@@ -113,17 +136,21 @@ define('acl-manager', ['acl'], function (Acl) {
checkScopeHasAcl: function (scope) {
var data = (this.data.table || {})[scope];
if (typeof data === 'undefined') {
return false;
}
return true;
},
checkScope: function (scope, action, precise) {
var data = (this.data.table || {})[scope];
if (typeof data === 'undefined') {
data = null;
}
return this.getImplementation(scope).checkScope(data, action, precise);
},
@@ -143,6 +170,7 @@ define('acl-manager', ['acl'], function (Acl) {
}
var data = (this.data.table || {})[scope];
if (typeof data === 'undefined') {
data = null;
}
@@ -160,7 +188,8 @@ define('acl-manager', ['acl'], function (Acl) {
check: function (subject, action, precise) {
if (typeof subject === 'string') {
return this.checkScope(subject, action, precise);
} else {
}
else {
return this.checkModel(subject, action, precise);
}
},
@@ -230,6 +259,7 @@ define('acl-manager', ['acl'], function (Acl) {
thresholdLevel = thresholdLevel || 'no';
var key = scope + '_' + action + '_' + thresholdLevel;
if (key in this.forbiddenFieldsCache) {
return this.forbiddenFieldsCache[key];
}
@@ -242,10 +272,15 @@ define('acl-manager', ['acl'], function (Acl) {
var actionData = fieldsData[action] || {};
var fieldList = [];
levelList.forEach(function (level) {
var list = actionData[level] || [];
list.forEach(function (field) {
if (~fieldList.indexOf(field)) return;
if (~fieldList.indexOf(field)) {
return;
}
fieldList.push(field);
}, this);
}, this);
@@ -260,6 +295,7 @@ define('acl-manager', ['acl'], function (Acl) {
thresholdLevel = thresholdLevel || 'no';
var key = scope + '_' + action + '_' + thresholdLevel;
if (key in this.forbiddenAttributesCache) {
return this.forbiddenAttributesCache[key];
}
@@ -273,10 +309,15 @@ define('acl-manager', ['acl'], function (Acl) {
var actionData = attributesData[action] || {};
var attributeList = [];
levelList.forEach(function (level) {
var list = actionData[level] || [];
list.forEach(function (attribute) {
if (~attributeList.indexOf(attribute)) return;
if (~attributeList.indexOf(attribute)) {
return;
}
attributeList.push(attribute);
}, this);
}, this);
@@ -287,7 +328,10 @@ define('acl-manager', ['acl'], function (Acl) {
},
checkTeamAssignmentPermission: function (teamId) {
if (this.get('assignmentPermission') === 'all') return true;
if (this.get('assignmentPermission') === 'all') {
return true;
}
return ~this.getUser().getLinkMultipleIdList('teams').indexOf(teamId);
},

View File

@@ -0,0 +1,79 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace tests\unit\Espo\Core\Acl;
use Espo\Core\{
Acl\FieldData,
Acl\Table,
};
class FieldDataTest extends \PHPUnit\Framework\TestCase
{
protected function setUp() : void
{
}
public function testGet1(): void
{
$raw = (object) [
Table::ACTION_EDIT => Table::LEVEL_YES,
Table::ACTION_READ => Table::LEVEL_NO,
];
$data = FieldData::fromRaw($raw);
$this->assertEquals(Table::LEVEL_NO, $data->getRead());
$this->assertEquals(Table::LEVEL_YES, $data->getEdit());
}
public function testGet2(): void
{
$raw = (object) [
Table::ACTION_EDIT => Table::LEVEL_NO,
Table::ACTION_READ => Table::LEVEL_YES,
];
$data = FieldData::fromRaw($raw);
$this->assertEquals(Table::LEVEL_YES, $data->getRead());
$this->assertEquals(Table::LEVEL_NO, $data->getEdit());
}
public function testGetEmpty(): void
{
$raw = (object) [
];
$data = FieldData::fromRaw($raw);
$this->assertEquals(Table::LEVEL_NO, $data->getRead());
$this->assertEquals(Table::LEVEL_NO, $data->getEdit());
}
}

View File

@@ -0,0 +1,442 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace tests\unit\Espo\Core\Acl;
use Espo\Entities\User;
use Espo\Core\{
Acl\Map\Map,
Acl\Map\DataBuilder,
Acl\Map\MetadataProvider,
Acl\Map\CacheKeyProvider,
Acl\Table,
Acl\ScopeData,
Acl\FieldData,
Utils\Config,
Utils\FieldUtil,
Utils\DataCache,
};
use StdClass;
class MapTest extends \PHPUnit\Framework\TestCase
{
private $fieldUtil;
private $config;
private $table;
private $user;
private $metadataProvider;
private $cacheKeyProvider;
protected function setUp(): void
{
$this->config = $this->createMock(Config::class);
$this->fieldUtil = $this->createMock(FieldUtil::class);
$this->table = $this->createMock(Table::class);
$this->user = $this->createMock(User::class);
$this->dataCache = $this->createMock(DataCache::class);
$this->metadataProvider = $this->createMock(MetadataProvider::class);
$this->cacheKeyProvider = $this->createMock(CacheKeyProvider::class);
$this->config
->expects($this->any())
->method('get')
->willReturnMap([
['useCache', false]
]);
$this->user
->expects($this->any())
->method('getId')
->willReturn('user-id');
}
private function mockTableData(array $scopeData, array $fieldData, array $permissionData): void
{
$returnMap1 = [];
foreach ($scopeData as $scope => $item) {
$returnMap1[] = [$scope, ScopeData::fromRaw($item)];
}
$this->table
->expects($this->any())
->method('getScopeData')
->willReturnMap($returnMap1);
$returnMap2 = [];
foreach ($fieldData as $scope => $item1) {
foreach ($item1 as $field => $item2) {
$returnMap2[] = [$scope, $field, FieldData::fromRaw($item2)];
}
}
$this->table
->expects($this->any())
->method('getFieldData')
->willReturnMap($returnMap2);
$returnMap3 = [];
foreach ($permissionData as $permission => $level) {
$returnMap3[] = [$permission, $level];
}
$this->table
->expects($this->any())
->method('getPermissionLevel')
->willReturnMap($returnMap3);
}
public function testMap1(): void
{
$dataBuilder = new DataBuilder($this->metadataProvider, $this->fieldUtil);
$this->metadataProvider
->expects($this->any())
->method('getScopeList')
->willReturn(['Test1', 'Test2', 'Test3', 'Test4', 'Test5']);
$this->metadataProvider
->expects($this->any())
->method('getPermissionList')
->willReturn(['assignment', 'portal']);
$this->metadataProvider
->expects($this->any())
->method('isScopeEntity')
->willReturnMap([
['Test1', true],
['Test2', true],
['Test3', true],
['Test4', true],
['Test5', false],
]);
$this->metadataProvider
->expects($this->any())
->method('getScopeFieldList')
->willReturnMap([
['Test1', ['field1', 'field2', 'field3', 'field4']],
['Test2', ['field1']],
['Test3', []],
['Test4', []],
['Test5', []],
]);
$this->fieldUtil
->expects($this->any())
->method('getAttributeList')
->willReturnMap([
['Test1', 'field1', ['attr1a', 'attr1b']],
['Test1', 'field2', ['field2']],
['Test1', 'field3', ['field3']],
['Test1', 'field4', ['field4']],
['Test2', 'field1', ['field1']],
]);
$this->mockTableData(
[
'Test1' => (object) [
Table::ACTION_CREATE => Table::LEVEL_YES,
Table::ACTION_READ => Table::LEVEL_TEAM,
],
'Test2' => (object) [
Table::ACTION_CREATE => Table::LEVEL_YES,
Table::ACTION_READ => Table::LEVEL_TEAM,
Table::ACTION_EDIT => Table::LEVEL_OWN,
],
'Test3' => false,
'Test4' => true,
'Test5' => true,
],
[
'Test1' => [
'field1' => (object) [
Table::ACTION_READ => Table::LEVEL_YES,
Table::ACTION_EDIT => Table::LEVEL_NO,
],
'field2' => (object) [
Table::ACTION_READ => Table::LEVEL_NO,
Table::ACTION_EDIT => Table::LEVEL_YES,
],
'field3' => (object) [
Table::ACTION_READ => Table::LEVEL_NO,
Table::ACTION_EDIT => Table::LEVEL_NO,
],
'field4' => (object) [
Table::ACTION_READ => Table::LEVEL_YES,
Table::ACTION_EDIT => Table::LEVEL_YES,
],
],
'Test2' => [
'field1' => (object) [
Table::ACTION_READ => Table::LEVEL_NO,
Table::ACTION_EDIT => Table::LEVEL_NO,
],
],
],
[
'assignment' => Table::LEVEL_YES,
'portal' => Table::LEVEL_NO,
],
);
$expectedData = $this->getExpectedRawData();
$map = new Map(
$this->user,
$this->table,
$dataBuilder,
$this->config,
$this->dataCache,
$this->cacheKeyProvider
);
$this->assertEquals($expectedData, $map->getData());
$this->assertEquals(
['field2', 'field3'],
$map->getScopeForbiddenFieldList('Test1', Table::ACTION_READ)
);
$this->assertEquals(
['field1', 'field3'],
$map->getScopeForbiddenFieldList('Test1', Table::ACTION_EDIT)
);
$this->assertEquals(
['field2', 'field3'],
$map->getScopeForbiddenAttributeList('Test1', Table::ACTION_READ)
);
$this->assertEquals(
['attr1a', 'attr1b', 'field3'],
$map->getScopeForbiddenAttributeList('Test1', Table::ACTION_EDIT)
);
$this->assertEquals(
['attr1a', 'attr1b', 'field3'],
$map->getScopeForbiddenAttributeList('Test1', Table::ACTION_EDIT)
);
$this->assertEquals(
['field1'],
$map->getScopeForbiddenFieldList('Test2', Table::ACTION_READ)
);
$this->assertEquals(
['field1'],
$map->getScopeForbiddenFieldList('Test2', Table::ACTION_READ)
);
$this->assertEquals(
[],
$map->getScopeForbiddenFieldList('Test3', Table::ACTION_READ)
);
}
private function getExpectedRawData(): StdClass
{
return (object) [
'table' => (object) [
'Test1' => (object) [
'read' => 'team',
'stream' => 'no',
'edit' => 'no',
'delete' => 'no',
'create' => 'yes'
],
'Test2' => (object) [
'read' => 'team',
'stream' => 'no',
'edit' => 'own',
'delete' => 'no',
'create' => 'yes'
],
'Test3' => false,
'Test4' => true,
'Test5' => true
],
'fieldTable' => (object) [
'Test1' => (object) [
'field1' => (object) [
'read' => 'yes',
'edit' => 'no'
],
'field2' => (object) [
'read' => 'no',
'edit' => 'yes'
],
'field3' => (object) [
'read' => 'no',
'edit' => 'no'
]
],
'Test2' => (object) [
'field1' => (object) [
'read' => 'no',
'edit' => 'no'
]
],
'Test3' => (object) [],
'Test4' => (object) []
],
'assignmentPermission' => 'yes',
'portalPermission' => 'no',
'fieldTableQuickAccess' => (object) [
'Test1' => (object) [
'attributes' => (object) [
'read' => (object) [
'yes' => [
'attr1a',
'attr1b'
],
'no' => [
'field2',
'field3'
]
],
'edit' => (object) [
'yes' => [
'field2'
],
'no' => [
'attr1a',
'attr1b',
'field3'
]
]
],
'fields' => (object) [
'read' => (object) [
'yes' => [
'field1'
],
'no' => [
'field2',
'field3'
]
],
'edit' => (object) [
'yes' => [
'field2'
],
'no' => [
'field1',
'field3'
]
]
]
],
'Test2' => (object) [
'attributes' => (object) [
'read' => (object) [
'yes' => [],
'no' => [
'field1'
]
],
'edit' => (object) [
'yes' => [],
'no' => [
'field1'
]
]
],
'fields' => (object) [
'read' => (object) [
'yes' => [],
'no' => [
'field1'
]
],
'edit' => (object) [
'yes' => [],
'no' => [
'field1'
]
]
]
],
'Test3' => (object) [
'attributes' => (object) [
'read' => (object) [
'yes' => [],
'no' => []
],
'edit' => (object) [
'yes' => [],
'no' => []
]
],
'fields' => (object) [
'read' => (object) [
'yes' => [],
'no' => []
],
'edit' => (object) [
'yes' => [],
'no' => []
]
]
],
'Test4' => (object) [
'attributes' => (object) [
'read' => (object) [
'yes' => [],
'no' => []
],
'edit' => (object) [
'yes' => [],
'no' => []
]
],
'fields' => (object) [
'read' => (object) [
'yes' => [],
'no' => []
],
'edit' => (object) [
'yes' => [],
'no' => []
]
]
]
]
];
}
}

View File

@@ -0,0 +1,59 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace tests\unit\Espo\Core\Acl;
use Espo\Core\{
Acl\Map\MetadataProvider,
Utils\Metadata,
};
class MetadataProviderTest extends \PHPUnit\Framework\TestCase
{
private $metadata;
protected function setUp(): void
{
$this->metadata = $this->createMock(Metadata::class);
}
public function testGetPermissionList(): void
{
$provider = new MetadataProvider($this->metadata);
$this->metadata
->expects($this->once())
->method('get')
->with(['app', 'acl', 'valuePermissionList'])
->willReturn(['assignmentPermission', 'portalPermission']);
$this->assertEquals(['assignment', 'portal'], $provider->getPermissionList());
}
}

View File

@@ -37,6 +37,7 @@ use Espo\Core\{
Acl\OwnerUserFieldProvider,
Acl\TableFactory,
Acl\GlobalRestricton,
Acl\Map\MapFactory,
ORM\EntityManager,
};
@@ -72,10 +73,12 @@ class AclManagerTest extends \PHPUnit\Framework\TestCase
private $globalRestriction;
/**
* @var USer
* @var User
*/
private $user;
private $mapFactory;
protected function setUp(): void
{
$this->user = $this->createMock(User::class);
@@ -84,12 +87,14 @@ class AclManagerTest extends \PHPUnit\Framework\TestCase
$this->accessCheckerFactory = $this->createMock(AccessCheckerFactory::class);
$this->ownershipCheckerFactory = $this->createMock(OwnershipCheckerFactory::class);
$this->tableFactory = $this->createMock(TableFactory::class);
$this->mapFactory = $this->createMock(MapFactory::class);
$this->globalRestriction = $this->createMock(GlobalRestricton::class);
$this->aclManager = new AclManager(
$this->accessCheckerFactory,
$this->ownershipCheckerFactory,
$this->tableFactory,
$this->mapFactory,
$this->globalRestriction,
$this->createMock(OwnerUserFieldProvider::class),
$this->createMock(EntityManager::class)