From a19ada57fa0ad8ebdaafb979d1ea9977dbf31fd0 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 14 Nov 2022 12:25:28 +0200 Subject: [PATCH] linkRequiredAccess --- application/Espo/Core/Record/Service.php | 111 +++++++++++------- .../Resources/metadata/recordDefs/Case.json | 8 ++ .../Espo/Modules/Crm/Services/CaseObj.php | 4 - .../Espo/Resources/i18n/en_US/Global.json | 2 + 4 files changed, 79 insertions(+), 46 deletions(-) create mode 100644 application/Espo/Modules/Crm/Resources/metadata/recordDefs/Case.json diff --git a/application/Espo/Core/Record/Service.php b/application/Espo/Core/Record/Service.php index 1bdd78c2b5..8df6a903f7 100644 --- a/application/Espo/Core/Record/Service.php +++ b/application/Espo/Core/Record/Service.php @@ -62,6 +62,7 @@ use Espo\Core\Di; use stdClass; use InvalidArgumentException; use LogicException; +use tests\unit\testData\DB\TEntity; /** * The layer between a controller and ORM repository. For CRUD and other operations with records. @@ -1001,7 +1002,7 @@ class Service implements Crud, } if (empty($id) || empty($link) || empty($foreignId)) { - throw new BadRequest; + throw new BadRequest(); } $this->processForbiddenLinkEditCheck($link); @@ -1013,17 +1014,10 @@ class Service implements Crud, } if (!$entity instanceof CoreEntity) { - throw new LogicException("Only core entities are supported"); + throw new LogicException("Only core entities are supported."); } - if ($this->noEditAccessRequiredForLink) { - if (!$this->acl->check($entity, AclTable::ACTION_READ)) { - throw new Forbidden(); - } - } - else if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) { - throw new Forbidden(); - } + $this->linkAccessCheck($entity, $link); $methodName = 'link' . ucfirst($link); @@ -1039,25 +1033,19 @@ class Service implements Crud, throw new LogicException("Entity '{$this->entityType}' has not relation '{$link}'."); } - $foreignEntity = $this->entityManager->getEntity($foreignEntityType, $foreignId); + $foreignEntity = $this->entityManager->getEntityById($foreignEntityType, $foreignId); if (!$foreignEntity) { throw new NotFound(); } - $accessActionRequired = AclTable::ACTION_EDIT; - - if (in_array($link, $this->noEditAccessRequiredLinkList)) { - $accessActionRequired = AclTable::ACTION_READ; - } - - if (!$this->acl->check($foreignEntity, $accessActionRequired)) { - throw new Forbidden(); - } + $this->linkForeignAccessCheck($link, $foreignEntity); $this->recordHookManager->processBeforeLink($entity, $link, $foreignEntity); - $this->getRepository()->relate($entity, $link, $foreignEntity); + $this->getRepository() + ->getRelation($entity, $link) + ->relate($foreignEntity); } /** @@ -1074,7 +1062,7 @@ class Service implements Crud, } if (empty($id) || empty($link) || empty($foreignId)) { - throw new BadRequest; + throw new BadRequest(); } $this->processForbiddenLinkEditCheck($link); @@ -1086,17 +1074,10 @@ class Service implements Crud, } if (!$entity instanceof CoreEntity) { - throw new LogicException("Only core entities are supported"); + throw new LogicException("Only core entities are supported."); } - if ($this->noEditAccessRequiredForLink) { - if (!$this->acl->check($entity, AclTable::ACTION_READ)) { - throw new Forbidden(); - } - } - else if (!$this->acl->check($entity, AclTable::ACTION_EDIT)) { - throw new Forbidden(); - } + $this->linkAccessCheck($entity, $link); $methodName = 'unlink' . ucfirst($link); @@ -1112,25 +1093,71 @@ class Service implements Crud, throw new LogicException("Entity '{$this->entityType}' has not relation '{$link}'."); } - $foreignEntity = $this->entityManager->getEntity($foreignEntityType, $foreignId); + $foreignEntity = $this->entityManager->getEntityById($foreignEntityType, $foreignId); if (!$foreignEntity) { throw new NotFound(); } - $accessActionRequired = AclTable::ACTION_EDIT; - - if (in_array($link, $this->noEditAccessRequiredLinkList)) { - $accessActionRequired = AclTable::ACTION_READ; - } - - if (!$this->acl->check($foreignEntity, $accessActionRequired)) { - throw new Forbidden(); - } + $this->linkForeignAccessCheck($link, $foreignEntity); $this->recordHookManager->processBeforeUnlink($entity, $link, $foreignEntity); - $this->getRepository()->unrelate($entity, $link, $foreignEntity); + $this->getRepository() + ->getRelation($entity, $link) + ->unrelate($foreignEntity); + } + + /** + * @param TEntity $entity + * @throws Forbidden + */ + private function linkAccessCheck(Entity $entity, string $link): void + { + /** @var AclTable::ACTION_*|null $action */ + $action = $this->metadata + ->get(['recordDefs', $this->entityType, 'relationships', $link, 'linkRequiredAccess']) ?? + null; + + if (!$action) { + $action = $this->noEditAccessRequiredForLink ? + AclTable::ACTION_READ : + AclTable::ACTION_EDIT; + } + + if (!$this->acl->check($entity, $action)) { + throw ForbiddenSilent::createWithBody( + "No record access for link operation ({$this->entityType}:{$link}).", + ErrorBody::create() + ->withMessageTranslation('noAccessToRecord', null, ['action' => $action]) + ->encode() + ); + } + } + + /** + * @throws Forbidden + */ + private function linkForeignAccessCheck(string $link, Entity $foreignEntity): void + { + $action = in_array($link, $this->noEditAccessRequiredLinkList) ? + AclTable::ACTION_READ : null; + + if (!$action) { + /** @var AclTable::ACTION_* $action */ + $action = $this->metadata + ->get(['recordDefs', $this->entityType, 'relationships', $link, 'linkRequiredForeignAccess']) ?? + AclTable::ACTION_EDIT; + } + + if (!$this->acl->check($foreignEntity, $action)) { + throw ForbiddenSilent::createWithBody( + "No foreign record access for link operation ({$this->entityType}:{$link}).", + ErrorBody::create() + ->withMessageTranslation('noAccessToForeignRecord', null, ['action' => $action]) + ->encode() + ); + } } /** diff --git a/application/Espo/Modules/Crm/Resources/metadata/recordDefs/Case.json b/application/Espo/Modules/Crm/Resources/metadata/recordDefs/Case.json new file mode 100644 index 0000000000..9ef352bc91 --- /dev/null +++ b/application/Espo/Modules/Crm/Resources/metadata/recordDefs/Case.json @@ -0,0 +1,8 @@ +{ + "relationships": { + "articles": { + "linkRequiredAccess": "edit", + "linkRequiredForeignAccess": "read" + } + } +} diff --git a/application/Espo/Modules/Crm/Services/CaseObj.php b/application/Espo/Modules/Crm/Services/CaseObj.php index 40ab060f60..c27bc7000e 100644 --- a/application/Espo/Modules/Crm/Services/CaseObj.php +++ b/application/Espo/Modules/Crm/Services/CaseObj.php @@ -40,10 +40,6 @@ use Espo\Services\Record; */ class CaseObj extends Record { - protected $noEditAccessRequiredLinkList = [ - 'articles', - ]; - /** * @param CaseEntity $entity */ diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index 638808d6e1..1e3ad12297 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -366,6 +366,8 @@ "confirmAppRefresh": "The application has been updated. It is recommended to refresh the page to ensure the proper functioning.", "loggedOutLeaveOut": "Logged out. The session is inactive. You may lose unsaved form data after page refresh. You may need to make a copy.", "moreThanOnceInstances": "More than one Espo instances under the same domain are opened in the browser. Use the incognito mode for this instance or log out from the other instance.", + "noAccessToRecord": "Operation requires `{action}` access to record.", + "noAccessToForeignRecord": "Operation requires `{action}` access to foreign record.", "error404": "The url you requested can't be handled.", "error403": "You don't have an access to this area." },