mass link restriction, ref

This commit is contained in:
Yuri Kuznetsov
2024-02-26 13:22:08 +02:00
parent 392616bdd3
commit d80b8ce76b
10 changed files with 101 additions and 53 deletions
@@ -79,7 +79,6 @@ class Record extends RecordBase
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
* @throws Error
*/
public function postActionCreateLink(Request $request): bool
{
+27 -21
View File
@@ -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<string, string[]>
* @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++;
}
@@ -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"
}
}
}
@@ -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<TargetListEntity>
*/
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);
-19
View File
@@ -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);
@@ -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)) {
@@ -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();
}
+4
View File
@@ -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."
}
}
}
+4
View File
@@ -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."
}
}
}
+36
View File
@@ -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();
}
}