mass link restriction, ref
This commit is contained in:
@@ -79,7 +79,6 @@ class Record extends RecordBase
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionCreateLink(Request $request): bool
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user