From d80b8ce76bc95f2660e8508494bef063ae14419f Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 26 Feb 2024 13:22:08 +0200 Subject: [PATCH] mass link restriction, ref --- application/Espo/Core/Controllers/Record.php | 1 - application/Espo/Core/Record/Service.php | 48 +++++++++++-------- .../metadata/recordDefs/TargetList.json | 20 +++++++- .../Espo/Modules/Crm/Services/TargetList.php | 10 ---- application/Espo/Services/Team.php | 19 -------- .../Hook/Hooks/TargetListCreate.php | 9 ++++ .../Hook/Hooks/TargetListDelete.php | 3 +- schema/metadata/clientDefs.json | 4 ++ schema/metadata/recordDefs.json | 4 ++ upgrades/8.2/scripts/AfterUpgrade.php | 36 ++++++++++++++ 10 files changed, 101 insertions(+), 53 deletions(-) diff --git a/application/Espo/Core/Controllers/Record.php b/application/Espo/Core/Controllers/Record.php index 2f33c21354..37d4a4d22f 100644 --- a/application/Espo/Core/Controllers/Record.php +++ b/application/Espo/Core/Controllers/Record.php @@ -79,7 +79,6 @@ class Record extends RecordBase * @throws BadRequest * @throws Forbidden * @throws NotFound - * @throws Error */ public function postActionCreateLink(Request $request): bool { diff --git a/application/Espo/Core/Record/Service.php b/application/Espo/Core/Record/Service.php index baccdb3595..e3de94a892 100644 --- a/application/Espo/Core/Record/Service.php +++ b/application/Espo/Core/Record/Service.php @@ -32,7 +32,6 @@ namespace Espo\Core\Record; use Espo\Core\Binding\BindingContainerBuilder; use Espo\Core\Binding\ContextualBinder; use Espo\Core\Exceptions\Conflict; -use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\BadRequest; use Espo\Core\Exceptions\ConflictSilent; use Espo\Core\Exceptions\Forbidden; @@ -114,15 +113,25 @@ class Service implements Crud, protected string $entityType; protected bool $getEntityBeforeUpdate = false; - protected bool $noEditAccessRequiredForLink = false; + protected bool $maxSelectTextAttributeLengthDisabled = false; protected ?int $maxSelectTextAttributeLength = null; - /** @var string[] */ - protected array $noEditAccessRequiredLinkList = []; - private ?StreamService $streamService = null; + /** + * @deprecated As of v8.2. Use metadata > recordDefs > relationships > {link} > linkRequiredAccess. + * @todo Remove in v9.0. + */ + protected bool $noEditAccessRequiredForLink = false; + + /** + * @var string[] + * @deprecated As of v8.2. Use metadata > recordDefs > relationships > {link} > linkRequiredAccess. + * @todo Remove in v9.0. + */ + protected array $noEditAccessRequiredLinkList = []; + /** * @var array * @deprecated As of v8.2. Use metadata > recordDefs > relationships > {link} > mandatoryAttributeList. @@ -495,6 +504,7 @@ class Service implements Crud, ->bindInstance(Acl::class, $this->acl) ->bindInstance(User::class, $this->user) ->inContext(LinkCheck::class, function (ContextualBinder $binder) { + /** @noinspection PhpDeprecationInspection */ $binder ->bindValue('$noEditAccessRequiredLinkList', $this->noEditAccessRequiredLinkList) ->bindValue('$noEditAccessRequiredForLink', $this->noEditAccessRequiredForLink); @@ -1372,7 +1382,6 @@ class Service implements Crud, * @throws BadRequest * @throws Forbidden * @throws NotFound - * @throws Error */ public function massLink(string $id, string $link, SearchParams $searchParams): bool { @@ -1380,8 +1389,8 @@ class Service implements Crud, throw new Forbidden(); } - if (!$id || !$link) { - throw new BadRequest(); + if (!$this->metadata->get("recordDefs.$this->entityType.relationships.$link.massLink")) { + throw new Forbidden("Mass link is not allowed."); } $this->processForbiddenLinkEditCheck($link); @@ -1392,32 +1401,26 @@ class Service implements Crud, throw new NotFound(); } + $this->getLinkCheck()->processLink($entity, $link); + // Not used link-check deliberately. Only edit access. if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) { throw new Forbidden(); } - $methodName = 'massLink' . ucfirst($link); - - if (method_exists($this, $methodName)) { - return $this->$methodName($id, $searchParams); - } - if (!$entity instanceof CoreEntity) { throw new LogicException("Only core entities are supported."); } $foreignEntityType = $entity->getRelationParam($link, 'entity'); - if (empty($foreignEntityType)) { + if (!$foreignEntityType) { throw new LogicException("Link '$link' has no 'entity'."); } - $accessActionRequired = AclTable::ACTION_EDIT; - - if (in_array($link, $this->noEditAccessRequiredLinkList)) { - $accessActionRequired = AclTable::ACTION_READ; - } + $accessActionRequired = $this->metadata + ->get("recordDefs.$this->entityType.relationships.$link.linkRequiredForeignAccess") ?? + AclTable::ACTION_EDIT; if (!$this->acl->check($foreignEntityType, $accessActionRequired)) { throw new Forbidden(); @@ -1437,11 +1440,14 @@ class Service implements Crud, return true; } + // @todo Apply access control filter if $accessActionRequired === 'read'. For better performance. + $countRelated = 0; $foreignCollection = $this->entityManager ->getRDBRepository($foreignEntityType) ->clone($query) + ->sth() ->find(); foreach ($foreignCollection as $foreignEntity) { @@ -1451,7 +1457,7 @@ class Service implements Crud, $this->getRepository() ->getRelation($entity, $link) - ->relate($foreignEntity); + ->relate($foreignEntity, [SaveOption::API => true]); $countRelated++; } diff --git a/application/Espo/Modules/Crm/Resources/metadata/recordDefs/TargetList.json b/application/Espo/Modules/Crm/Resources/metadata/recordDefs/TargetList.json index ae3ffb6511..02bb84e7d0 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/recordDefs/TargetList.json +++ b/application/Espo/Modules/Crm/Resources/metadata/recordDefs/TargetList.json @@ -10,5 +10,23 @@ "afterCreateHookClassNameList": [ "Espo\\Modules\\Crm\\Classes\\RecordHooks\\TargetList\\AfterCreate", "Espo\\Modules\\Crm\\Classes\\RecordHooks\\TargetList\\AfterCreateDuplicate" - ] + ], + "relationships": { + "users": { + "massLink": true, + "linkRequiredForeignAccess": "read" + }, + "leads": { + "massLink": true, + "linkRequiredForeignAccess": "read" + }, + "contacts": { + "massLink": true, + "linkRequiredForeignAccess": "read" + }, + "accounts": { + "massLink": true, + "linkRequiredForeignAccess": "read" + } + } } diff --git a/application/Espo/Modules/Crm/Services/TargetList.php b/application/Espo/Modules/Crm/Services/TargetList.php index 2614ac1e64..3a11368ecd 100644 --- a/application/Espo/Modules/Crm/Services/TargetList.php +++ b/application/Espo/Modules/Crm/Services/TargetList.php @@ -33,22 +33,12 @@ use Espo\Core\Select\SearchParams; use Espo\Modules\Crm\Entities\TargetList as TargetListEntity; use Espo\Modules\Crm\Tools\TargetList\MetadataProvider; use Espo\Services\Record; -use Espo\Core\Utils\Metadata; /** * @extends Record */ class TargetList extends Record { - public function setMetadata(Metadata $metadata): void - { - parent::setMetadata($metadata); - - $targetLinkList = $this->metadata->get(['scopes', 'TargetList', 'targetLinkList']) ?? []; - - $this->noEditAccessRequiredLinkList = $targetLinkList; - } - protected function prepareLinkSearchParams(SearchParams $searchParams, string $link): SearchParams { $searchParams = parent::prepareLinkSearchParams($searchParams, $link); diff --git a/application/Espo/Services/Team.php b/application/Espo/Services/Team.php index 5ee0e59da9..ee54a763ef 100644 --- a/application/Espo/Services/Team.php +++ b/application/Espo/Services/Team.php @@ -31,7 +31,6 @@ namespace Espo\Services; use Espo\Core\Acl\Cache\Clearer as AclCacheClearer; use Espo\Entities\User as UserEntity; -use Espo\Core\Select\SearchParams; use Espo\Core\Di; /** @@ -43,13 +42,6 @@ class Team extends Record implements { use Di\DataManagerSetter; - protected function clearRolesCache(): void - { - $this->createAclCacheClearer()->clearForAllInternalUsers(); - - $this->dataManager->updateCacheTimestamp(); - } - public function link(string $id, string $link, string $foreignId): void { parent::link($id, $link, $foreignId); @@ -82,17 +74,6 @@ class Team extends Record implements } } - public function massLink(string $id, string $link, SearchParams $searchParams): bool - { - $result = parent::massLink($id, $link, $searchParams); - - if ($link === 'users') { - $this->clearRolesCache(); - } - - return $result; - } - private function createAclCacheClearer(): AclCacheClearer { return $this->injectableFactory->create(AclCacheClearer::class); diff --git a/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListCreate.php b/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListCreate.php index 41b037ea4e..f3267a1865 100644 --- a/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListCreate.php +++ b/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListCreate.php @@ -140,6 +140,15 @@ class TargetListCreate implements CreateHook ], ]); + $this->metadata->set('recordDefs', TargetList::ENTITY_TYPE, [ + 'relationships' => [ + $foreignLink => [ + 'massLink' => true, + 'linkRequiredForeignAccess' => 'read', + ], + ], + ]); + $targetLinkList = $this->metadata->get(['scopes', TargetList::ENTITY_TYPE, 'targetLinkList']) ?? []; if (!in_array($foreignLink, $targetLinkList)) { diff --git a/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListDelete.php b/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListDelete.php index ab0bc7ebc8..5de9e2e10e 100644 --- a/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListDelete.php +++ b/application/Espo/Tools/LinkManager/Hook/Hooks/TargetListDelete.php @@ -99,7 +99,8 @@ class TargetListDelete implements DeleteHook ]); } - $this->metadata->delete('clientDefs', TargetList::ENTITY_TYPE, ['relationshipPanels.' . $foreignLink]); + $this->metadata->delete('clientDefs', TargetList::ENTITY_TYPE, ["relationshipPanels.$foreignLink"]); + $this->metadata->delete('recordDefs', TargetList::ENTITY_TYPE, ["relationships.$foreignLink"]); $this->metadata->save(); } diff --git a/schema/metadata/clientDefs.json b/schema/metadata/clientDefs.json index 40ca05712c..eded8266ac 100644 --- a/schema/metadata/clientDefs.json +++ b/schema/metadata/clientDefs.json @@ -480,6 +480,10 @@ "syncWithModel": { "type": "boolean", "description": "Re-fetch records when the parent model is saved or refreshed by WebSocket." + }, + "massSelect": { + "type": "boolean", + "description": "Allow mass select." } } } diff --git a/schema/metadata/recordDefs.json b/schema/metadata/recordDefs.json index 60108ded17..e8dca395a6 100644 --- a/schema/metadata/recordDefs.json +++ b/schema/metadata/recordDefs.json @@ -150,6 +150,10 @@ "description": "Mandatory attributes to be selected when find related. As of v8.2.", "type": "array", "items": {"type": "string"} + }, + "massLink": { + "type": "boolean", + "default": "Allow mass link. As of v8.2." } } } diff --git a/upgrades/8.2/scripts/AfterUpgrade.php b/upgrades/8.2/scripts/AfterUpgrade.php index 5c8ced4a07..56ed440fe5 100644 --- a/upgrades/8.2/scripts/AfterUpgrade.php +++ b/upgrades/8.2/scripts/AfterUpgrade.php @@ -31,6 +31,7 @@ use Espo\Core\Container; use Espo\Core\InjectableFactory; use Espo\Core\Utils\Config; use Espo\Core\Utils\Config\ConfigWriter; +use Espo\Core\Utils\Metadata; use Espo\Entities\Template; use Espo\ORM\EntityManager; use Espo\Tools\Pdf\Template as PdfTemplate; @@ -53,6 +54,7 @@ class AfterUpgrade $config = $container->getByClass(Config::class); $this->updateTemplates($em, $config); + $this->updateTargetList($container->getByClass(Metadata::class)); } private function updateTemplates(EntityManager $entityManager, Config $config): void @@ -79,4 +81,38 @@ class AfterUpgrade $entityManager->saveEntity($template); } } + + private function updateTargetList(Metadata $metadata): void + { + $links = $metadata->get('entityDefs.TargetList.links') ?? []; + + $toSave = false; + + foreach ($links as $link => $defs) { + if (empty($defs['isCustom'])) { + continue; + } + + if (!$metadata->get("clientDefs.TargetList.relationshipPanels.$link.massSelect")) { + continue; + } + + $metadata->set('recordDefs', 'TargetList', [ + 'relationships' => [ + $link => [ + 'massLink' => true, + 'linkRequiredForeignAccess' => 'read', + ] + ] + ]); + + $toSave = true; + } + + if (!$toSave) { + return; + } + + $metadata->save(); + } }