From f7c8b62f6cb6da8ffd044993d0b12bbfc719f103 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 10 Apr 2021 16:25:40 +0300 Subject: [PATCH] grand acl refactoring --- application/Espo/Acl/Email.php | 3 +- application/Espo/Acl/EmailAddress.php | 5 +- application/Espo/Acl/Import.php | 3 +- application/Espo/Acl/Note.php | 7 +- application/Espo/Acl/PhoneNumber.php | 5 +- application/Espo/Acl/ScheduledJob.php | 5 +- application/Espo/Acl/User.php | 7 +- application/Espo/Acl/Webhook.php | 5 +- application/Espo/AclPortal/Attachment.php | 3 +- application/Espo/AclPortal/Email.php | 3 +- application/Espo/AclPortal/EmailAddress.php | 7 +- application/Espo/AclPortal/Note.php | 7 +- application/Espo/AclPortal/PhoneNumber.php | 7 +- .../Acl/Attachment/AccessChecker.php} | 51 +- .../Acl/Attachment/OwnershipChecker.php | 52 + application/Espo/Core/Acl.php | 75 +- application/Espo/Core/Acl/AccessChecker.php | 40 + .../Espo/Core/Acl/AccessCheckerFactory.php | 114 +++ ...elProvider.php => AccessCreateChecker.php} | 6 +- .../Espo/Core/Acl/AccessDeleteChecker.php | 40 + .../{ScopeAcl.php => AccessEditChecker.php} | 6 +- .../Espo/Core/Acl/AccessEntityCREDChecker.php | 40 + .../Core/Acl/AccessEntityCREDSChecker.php | 41 + ...eAcl.php => AccessEntityCreateChecker.php} | 4 +- ...eAcl.php => AccessEntityDeleteChecker.php} | 4 +- ...ditAcl.php => AccessEntityEditChecker.php} | 4 +- ...eadAcl.php => AccessEntityReadChecker.php} | 4 +- ...mAcl.php => AccessEntityStreamChecker.php} | 4 +- .../Espo/Core/Acl/AccessReadChecker.php | 40 + .../Espo/Core/Acl/AccessStreamChecker.php | 40 + application/Espo/Core/Acl/Acl.php | 275 +---- application/Espo/Core/Acl/Base.php | 109 +- .../Espo/Core/Acl/DefaultAccessChecker.php | 211 ++++ .../Espo/Core/Acl/DefaultOwnershipChecker.php | 104 ++ application/Espo/Core/Acl/DefaultTable.php | 939 ++++++++++++++++++ .../Espo/Core/Acl/GlobalRestricton.php | 37 +- .../Espo/Core/Acl/OwnerUserFieldProvider.php | 2 +- .../OwnershipChecker.php} | 6 +- .../Espo/Core/Acl/OwnershipCheckerFactory.php | 102 ++ ...{EntityAcl.php => OwnershipOwnChecker.php} | 6 +- .../Espo/Core/Acl/OwnershipTeamChecker.php | 42 + application/Espo/Core/Acl/ScopeChecker.php | 87 ++ .../Espo/Core/Acl/ScopeCheckerData.php | 61 ++ .../Espo/Core/Acl/ScopeCheckerDataBuilder.php | 104 ++ application/Espo/Core/Acl/ScopeData.php | 20 +- application/Espo/Core/Acl/Table.php | 902 +---------------- ...RestrictonFactory.php => TableFactory.php} | 21 +- .../Traits/DefaultAccessCheckerDependency.php | 102 ++ application/Espo/Core/AclManager.php | 445 +++++---- application/Espo/Core/AclPortal/Acl.php | 9 +- application/Espo/Core/AclPortal/Base.php | 174 +++- application/Espo/Core/AclPortal/Portal.php | 222 ----- application/Espo/Core/Portal/Acl.php | 41 +- .../Core/Portal/Acl/AccessCheckerFactory.php | 115 +++ .../Core/Portal/Acl/DefaultAccessChecker.php | 164 +++ .../Portal/Acl/DefaultOwnershipChecker.php | 178 ++++ .../Portal/Acl/OwnershipAccountChecker.php | 44 + .../Portal/Acl/OwnershipCheckerFactory.php | 103 ++ .../Portal/Acl/OwnershipContactChecker.php | 44 + .../Espo/Core/Portal/Acl/ScopeChecker.php | 98 ++ .../Acl/ScopeCheckerData.php} | 48 +- .../Portal/Acl/ScopeCheckerDataBuilder.php | 134 +++ .../Core/{AclPortal => Portal/Acl}/Table.php | 47 +- .../Acl/TableFactory.php} | 33 +- application/Espo/Core/Portal/AclManager.php | 224 +++-- .../Espo/Core/Portal/Loaders/AclManager.php | 4 +- application/Espo/Modules/Crm/Acl/Meeting.php | 3 +- .../Crm/AclPortal/KnowledgeBaseArticle.php | 3 +- .../metadata/{entityAcl => aclDefs}/Call.json | 0 .../{entityAcl => aclDefs}/Meeting.json | 0 .../metadata/aclDefs/Attachment.json | 4 + .../Resources/metadata/aclDefs/Email.json | 3 + .../Espo/Resources/metadata/app/metadata.json | 1 + .../Resources/metadata/entityAcl/Email.json | 3 +- .../Espo/Core/Acl/ScopeCheckerDataTest.php | 99 ++ tests/unit/Espo/Core/Acl/ScopeCheckerTest.php | 435 ++++++++ tests/unit/Espo/Core/Acl/ScopeDataTest.php | 18 +- .../Core/AclPortal/ScopeCheckerDataTest.php | 108 ++ .../Espo/Core/AclPortal/ScopeCheckerTest.php | 529 ++++++++++ 79 files changed, 5220 insertions(+), 1875 deletions(-) rename application/Espo/{Acl/Attachment.php => Classes/Acl/Attachment/AccessChecker.php} (78%) create mode 100644 application/Espo/Classes/Acl/Attachment/OwnershipChecker.php create mode 100644 application/Espo/Core/Acl/AccessChecker.php create mode 100644 application/Espo/Core/Acl/AccessCheckerFactory.php rename application/Espo/Core/Acl/{LevelProvider.php => AccessCreateChecker.php} (90%) create mode 100644 application/Espo/Core/Acl/AccessDeleteChecker.php rename application/Espo/Core/Acl/{ScopeAcl.php => AccessEditChecker.php} (90%) create mode 100644 application/Espo/Core/Acl/AccessEntityCREDChecker.php create mode 100644 application/Espo/Core/Acl/AccessEntityCREDSChecker.php rename application/Espo/Core/Acl/{EntityCreateAcl.php => AccessEntityCreateChecker.php} (93%) rename application/Espo/Core/Acl/{EntityDeleteAcl.php => AccessEntityDeleteChecker.php} (93%) rename application/Espo/Core/Acl/{EntityEditAcl.php => AccessEntityEditChecker.php} (93%) rename application/Espo/Core/Acl/{EntityReadAcl.php => AccessEntityReadChecker.php} (93%) rename application/Espo/Core/Acl/{EntityStreamAcl.php => AccessEntityStreamChecker.php} (93%) create mode 100644 application/Espo/Core/Acl/AccessReadChecker.php create mode 100644 application/Espo/Core/Acl/AccessStreamChecker.php create mode 100644 application/Espo/Core/Acl/DefaultAccessChecker.php create mode 100644 application/Espo/Core/Acl/DefaultOwnershipChecker.php create mode 100644 application/Espo/Core/Acl/DefaultTable.php rename application/Espo/Core/{AclPortal/PortalScopeAcl.php => Acl/OwnershipChecker.php} (93%) create mode 100644 application/Espo/Core/Acl/OwnershipCheckerFactory.php rename application/Espo/Core/Acl/{EntityAcl.php => OwnershipOwnChecker.php} (89%) create mode 100644 application/Espo/Core/Acl/OwnershipTeamChecker.php create mode 100644 application/Espo/Core/Acl/ScopeChecker.php create mode 100644 application/Espo/Core/Acl/ScopeCheckerData.php create mode 100644 application/Espo/Core/Acl/ScopeCheckerDataBuilder.php rename application/Espo/Core/Acl/{GlobalRestrictonFactory.php => TableFactory.php} (80%) create mode 100644 application/Espo/Core/Acl/Traits/DefaultAccessCheckerDependency.php delete mode 100644 application/Espo/Core/AclPortal/Portal.php create mode 100644 application/Espo/Core/Portal/Acl/AccessCheckerFactory.php create mode 100644 application/Espo/Core/Portal/Acl/DefaultAccessChecker.php create mode 100644 application/Espo/Core/Portal/Acl/DefaultOwnershipChecker.php create mode 100644 application/Espo/Core/Portal/Acl/OwnershipAccountChecker.php create mode 100644 application/Espo/Core/Portal/Acl/OwnershipCheckerFactory.php create mode 100644 application/Espo/Core/Portal/Acl/OwnershipContactChecker.php create mode 100644 application/Espo/Core/Portal/Acl/ScopeChecker.php rename application/Espo/Core/{AclPortal/AclFactory.php => Portal/Acl/ScopeCheckerData.php} (62%) create mode 100644 application/Espo/Core/Portal/Acl/ScopeCheckerDataBuilder.php rename application/Espo/Core/{AclPortal => Portal/Acl}/Table.php (78%) rename application/Espo/Core/{Acl/AclFactory.php => Portal/Acl/TableFactory.php} (72%) rename application/Espo/Modules/Crm/Resources/metadata/{entityAcl => aclDefs}/Call.json (100%) rename application/Espo/Modules/Crm/Resources/metadata/{entityAcl => aclDefs}/Meeting.json (100%) create mode 100644 application/Espo/Resources/metadata/aclDefs/Attachment.json create mode 100644 application/Espo/Resources/metadata/aclDefs/Email.json create mode 100644 tests/unit/Espo/Core/Acl/ScopeCheckerDataTest.php create mode 100644 tests/unit/Espo/Core/Acl/ScopeCheckerTest.php create mode 100644 tests/unit/Espo/Core/AclPortal/ScopeCheckerDataTest.php create mode 100644 tests/unit/Espo/Core/AclPortal/ScopeCheckerTest.php diff --git a/application/Espo/Acl/Email.php b/application/Espo/Acl/Email.php index f0be0a5a52..5053f2a709 100644 --- a/application/Espo/Acl/Email.php +++ b/application/Espo/Acl/Email.php @@ -37,10 +37,9 @@ use Espo\Core\{ Acl\Acl, Acl\ScopeData, Acl\Table, - Acl\EntityReadAcl, }; -class Email extends Acl implements EntityReadAcl +class Email extends Acl { public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { diff --git a/application/Espo/Acl/EmailAddress.php b/application/Espo/Acl/EmailAddress.php index 352b9fe1d8..bdb825a62f 100644 --- a/application/Espo/Acl/EmailAddress.php +++ b/application/Espo/Acl/EmailAddress.php @@ -37,6 +37,9 @@ use Espo\Core\{ Acl\Table, }; +/** + * @todo Move to another class. + */ class EmailAddress extends Acl { /** @@ -53,7 +56,7 @@ class EmailAddress extends Acl $entityWithSameAddressList = $repository->getEntityListByAddressId($entity->getId(), $excludeEntity); foreach ($entityWithSameAddressList as $e) { - if ($this->getAclManager()->check($user, $e, Table::ACTION_EDIT)) { + if ($this->aclManager->check($user, $e, Table::ACTION_EDIT)) { continue; } diff --git a/application/Espo/Acl/Import.php b/application/Espo/Acl/Import.php index 32da111770..b2cca57ff0 100644 --- a/application/Espo/Acl/Import.php +++ b/application/Espo/Acl/Import.php @@ -36,10 +36,9 @@ use Espo\ORM\Entity; use Espo\Core\{ Acl\Acl, Acl\ScopeData, - Acl\EntityReadAcl, }; -class Import extends Acl implements EntityReadAcl +class Import extends Acl { public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { diff --git a/application/Espo/Acl/Note.php b/application/Espo/Acl/Note.php index 49b9a55b89..d5ac22f288 100644 --- a/application/Espo/Acl/Note.php +++ b/application/Espo/Acl/Note.php @@ -37,13 +37,12 @@ use Espo\Core\{ Acl\Acl, Acl\ScopeData, Acl\Table, - Acl\EntityEditAcl, }; use Exception; use DateTime; -class Note extends Acl implements EntityEditAcl +class Note extends Acl { protected $deleteThresholdPeriod = '1 month'; @@ -66,7 +65,7 @@ class Note extends Acl implements EntityEditAcl $parent = $this->entityManager->getEntity($entity->get('parentType'), $entity->get('parentId')); - if ($parent && $this->getAclManager()->checkEntity($user, $parent, 'stream')) { + if ($parent && $this->aclManager->checkEntity($user, $parent, 'stream')) { return true; } @@ -133,7 +132,7 @@ class Note extends Acl implements EntityEditAcl } $deleteThresholdPeriod = - '-' . $this->getConfig()->get('noteDeleteThresholdPeriod', $this->deleteThresholdPeriod); + '-' . $this->config->get('noteDeleteThresholdPeriod', $this->deleteThresholdPeriod); $dt = new DateTime(); diff --git a/application/Espo/Acl/PhoneNumber.php b/application/Espo/Acl/PhoneNumber.php index 4357525319..948b6242ef 100644 --- a/application/Espo/Acl/PhoneNumber.php +++ b/application/Espo/Acl/PhoneNumber.php @@ -38,6 +38,9 @@ use Espo\Core\{ Acl\Table, }; +/** + * @todo Move to another class. + */ class PhoneNumber extends Acl { /** @@ -54,7 +57,7 @@ class PhoneNumber extends Acl $entityWithSameNumberList = $repository->getEntityListByPhoneNumberId($entity->getId(), $excludeEntity); foreach ($entityWithSameNumberList as $e) { - if ($this->getAclManager()->check($user, $e, Table::ACTION_EDIT)) { + if ($this->aclManager->check($user, $e, Table::ACTION_EDIT)) { continue; } diff --git a/application/Espo/Acl/ScheduledJob.php b/application/Espo/Acl/ScheduledJob.php index 9bc492f1ec..3a6861994a 100644 --- a/application/Espo/Acl/ScheduledJob.php +++ b/application/Espo/Acl/ScheduledJob.php @@ -37,12 +37,9 @@ use Espo\Core\{ Acl\Acl, Acl\ScopeData, Acl\Table, - Acl\EntityCreateAcl, - Acl\EntityReadAcl, - Acl\EntityEditAcl, }; -class ScheduledJob extends Acl implements EntityCreateAcl, EntityReadAcl, EntityEditAcl +class ScheduledJob extends Acl { public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { diff --git a/application/Espo/Acl/User.php b/application/Espo/Acl/User.php index 7bde9ac963..9d333fd3bf 100644 --- a/application/Espo/Acl/User.php +++ b/application/Espo/Acl/User.php @@ -37,12 +37,9 @@ use Espo\Core\{ Acl\Acl, Acl\ScopeData, Acl\Table, - Acl\EntityCreateAcl, - Acl\EntityReadAcl, - Acl\EntityEditAcl, }; -class User extends Acl implements EntityCreateAcl, EntityReadAcl, EntityEditAcl +class User extends Acl { public function checkIsOwner(EntityUser $user, Entity $entity) { @@ -52,7 +49,7 @@ class User extends Acl implements EntityCreateAcl, EntityReadAcl, EntityEditAcl public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { if (!$user->isAdmin() && $entity->isPortal()) { - if ($this->getAclManager()->get($user, 'portal') === Table::LEVEL_YES) { + if ($this->aclManager->get($user, 'portal') === Table::LEVEL_YES) { return true; } } diff --git a/application/Espo/Acl/Webhook.php b/application/Espo/Acl/Webhook.php index 910f27678e..2911a4551c 100644 --- a/application/Espo/Acl/Webhook.php +++ b/application/Espo/Acl/Webhook.php @@ -36,12 +36,9 @@ use Espo\ORM\Entity; use Espo\Core\{ Acl\Acl, Acl\ScopeData, - Acl\EntityCreateAcl, - Acl\EntityReadAcl, - Acl\EntityEditAcl, }; -class Webhook extends Acl implements EntityCreateAcl, EntityReadAcl, EntityEditAcl +class Webhook extends Acl { public function checkIsOwner(EntityUser $user, Entity $entity) { diff --git a/application/Espo/AclPortal/Attachment.php b/application/Espo/AclPortal/Attachment.php index 5f71daa5d1..d250d19cb7 100644 --- a/application/Espo/AclPortal/Attachment.php +++ b/application/Espo/AclPortal/Attachment.php @@ -37,11 +37,10 @@ use Espo\ORM\Entity; use Espo\Core\{ Acl\ScopeData, Acl\Table, - Acl\EntityReadAcl, AclPortal\Acl as Acl, }; -class Attachment extends Acl implements EntityReadAcl +class Attachment extends Acl { public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { diff --git a/application/Espo/AclPortal/Email.php b/application/Espo/AclPortal/Email.php index eb3d005247..9f29d02bc0 100644 --- a/application/Espo/AclPortal/Email.php +++ b/application/Espo/AclPortal/Email.php @@ -36,11 +36,10 @@ use Espo\ORM\Entity; use Espo\Core\{ Acl\ScopeData, Acl\Table, - Acl\EntityReadAcl, AclPortal\Acl as Acl, }; -class Email extends Acl implements EntityReadAcl +class Email extends Acl { public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { diff --git a/application/Espo/AclPortal/EmailAddress.php b/application/Espo/AclPortal/EmailAddress.php index 909d94e449..01866321b7 100644 --- a/application/Espo/AclPortal/EmailAddress.php +++ b/application/Espo/AclPortal/EmailAddress.php @@ -37,6 +37,9 @@ use Espo\Core\{ AclPortal\Acl as Acl, }; +/** + * @todo Move to another place. + */ class EmailAddress extends Acl { public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity) : bool @@ -45,13 +48,13 @@ class EmailAddress extends Acl $isFobidden = false; - $repository = $this->getEntityManager()->getRepository('EmailAddress'); + $repository = $this->entityManager->getRepository('EmailAddress'); if (!$user->isAdmin()) { $entityWithSameAddressList = $repository->getEntityListByAddressId($id, $excludeEntity); foreach ($entityWithSameAddressList as $e) { - if (!$this->getAclManager()->check($user, $e, Table::ACTION_EDIT)) { + if (!$this->aclManager->check($user, $e, Table::ACTION_EDIT)) { $isFobidden = true; break; diff --git a/application/Espo/AclPortal/Note.php b/application/Espo/AclPortal/Note.php index cda6557d3b..f7781efb1d 100644 --- a/application/Espo/AclPortal/Note.php +++ b/application/Espo/AclPortal/Note.php @@ -35,11 +35,10 @@ use Espo\ORM\Entity; use Espo\Core\{ Acl\ScopeData, Acl\Table, - Acl\EntityCreateAcl, AclPortal\Acl as Acl, }; -class Note extends Acl implements EntityCreateAcl +class Note extends Acl { public function checkIsOwner(EntityUser $user, Entity $entity) { @@ -64,10 +63,10 @@ class Note extends Acl implements EntityCreateAcl return false; } - $parent = $this->getEntityManager()->getEntity($entity->get('parentType'), $entity->get('parentId')); + $parent = $this->entityManager->getEntity($entity->get('parentType'), $entity->get('parentId')); if ($parent) { - if ($this->getAclManager()->checkEntity($user, $parent, Table::ACTION_STREAM)) { + if ($this->aclManager->checkEntity($user, $parent, Table::ACTION_STREAM)) { return true; } } diff --git a/application/Espo/AclPortal/PhoneNumber.php b/application/Espo/AclPortal/PhoneNumber.php index 8e8ba39763..ad85657931 100644 --- a/application/Espo/AclPortal/PhoneNumber.php +++ b/application/Espo/AclPortal/PhoneNumber.php @@ -37,6 +37,9 @@ use Espo\Core\{ AclPortal\Acl as Acl, }; +/** + * @todo Move to another place. + */ class PhoneNumber extends Acl { public function checkEditInEntity(EntityUser $user, Entity $entity, Entity $excludeEntity) : bool @@ -45,13 +48,13 @@ class PhoneNumber extends Acl $isFobidden = false; - $repository = $this->getEntityManager()->getRepository('PhoneNumber'); + $repository = $this->entityManager->getRepository('PhoneNumber'); if (!$user->isAdmin()) { $entityWithSameNumberList = $repository->getEntityListByPhoneNumberId($id, $excludeEntity); foreach ($entityWithSameNumberList as $e) { - if (!$this->getAclManager()->check($user, $e, Table::ACTION_EDIT)) { + if (!$this->aclManager->check($user, $e, Table::ACTION_EDIT)) { $isFobidden = true; break; diff --git a/application/Espo/Acl/Attachment.php b/application/Espo/Classes/Acl/Attachment/AccessChecker.php similarity index 78% rename from application/Espo/Acl/Attachment.php rename to application/Espo/Classes/Acl/Attachment/AccessChecker.php index 23cd4eed37..c74cad6ff3 100644 --- a/application/Espo/Acl/Attachment.php +++ b/application/Espo/Classes/Acl/Attachment/AccessChecker.php @@ -27,30 +27,44 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Acl; +namespace Espo\Classes\Acl\Attachment; use Espo\Entities\{ - User as EntityUser, + User, Note, }; use Espo\ORM\Entity; use Espo\Core\{ - Acl\Acl, + ORM\EntityManager, + AclManager, Acl\ScopeData, - Acl\Table, - Acl\EntityReadAcl, + Acl\DefaultAccessChecker, + Acl\AccessEntityCREDChecker, + Acl\Traits\DefaultAccessCheckerDependency, }; -class Attachment extends Acl implements EntityReadAcl +class AccessChecker implements AccessEntityCREDChecker { - public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool - { - if ($user->isAdmin()) { - return true; - } + use DefaultAccessCheckerDependency; + private $aclManager; + + private $entityManager; + + public function __construct( + DefaultAccessChecker $defaultAccessChecker, + AclManager $aclManager, + EntityManager $entityManager + ) { + $this->defaultAccessChecker = $defaultAccessChecker; + $this->aclManager = $aclManager; + $this->entityManager = $entityManager; + } + + public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool + { if ($entity->get('parentType') === 'Settings') { return true; } @@ -71,7 +85,7 @@ class Attachment extends Acl implements EntityReadAcl } if (!$parent || !$hasParent) { - if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { + if ($this->defaultAccessChecker->checkEntityRead($user, $entity, $data)) { return true; } @@ -89,14 +103,14 @@ class Attachment extends Acl implements EntityReadAcl return true; } - if ($this->checkEntity($user, $entity, $data, Table::ACTION_READ)) { + if ($this->defaultAccessChecker->checkEntityRead($user, $entity, $data)) { return true; } return false; } - protected function checkEntityReadNoteParent(EntityUser $user, Note $note): ?bool + private function checkEntityReadNoteParent(User $user, Note $note): ?bool { if ($note->getTargetType() === Note::TARGET_TEAMS) { $intersect = array_intersect( @@ -136,13 +150,4 @@ class Attachment extends Acl implements EntityReadAcl return null; } - - public function checkIsOwner(EntityUser $user, Entity $entity) - { - if ($user->getId() === $entity->get(self::ATTR_CREATED_BY_ID)) { - return true; - } - - return false; - } } diff --git a/application/Espo/Classes/Acl/Attachment/OwnershipChecker.php b/application/Espo/Classes/Acl/Attachment/OwnershipChecker.php new file mode 100644 index 0000000000..b0bbb709b0 --- /dev/null +++ b/application/Espo/Classes/Acl/Attachment/OwnershipChecker.php @@ -0,0 +1,52 @@ +getId() === $entity->get(self::ATTR_CREATED_BY_ID)) { + return true; + } + + return false; + } +} diff --git a/application/Espo/Core/Acl.php b/application/Espo/Core/Acl.php index 09971f0339..cce0b23d83 100644 --- a/application/Espo/Core/Acl.php +++ b/application/Espo/Core/Acl.php @@ -31,6 +31,7 @@ namespace Espo\Core; use Espo\Core\{ Acl\Table, + Acl\Exceptions\NotImplemented, }; use Espo\ORM\Entity; @@ -39,7 +40,7 @@ use Espo\Entities\User; use StdClass; /** - * A wrapper for AclManager. To check access for a current user. + * A wrapper for `AclManager` for a current user. A central access point for access checking. */ class Acl { @@ -53,6 +54,9 @@ class Acl $this->user = $user; } + /** + * Get a full access data map. + */ public function getMap(): StdClass { return $this->aclManager->getMap($this->user); @@ -69,9 +73,9 @@ class Acl /** * Get a permission. E.g. 'assignment' permission. */ - public function get(string $permission): ?string + public function getPermissionLevel(string $permission): string { - return $this->aclManager->get($this->user, $permission); + return $this->aclManager->getPermissionLevel($this->user, $permission); } /** @@ -107,7 +111,10 @@ class Acl } /** - * Check a scope or entity. If $action is omitted, it will check whether a scope level is set to 'enabled'. + * Check a scope or entity. If $action is omitted, it will check + * whether a scope level is set to 'enabled'. + * + * @throws NotImplemented */ public function check($subject, ?string $action = null): bool { @@ -115,7 +122,10 @@ class Acl } /** - * Check access to scope. If $action is omitted, it will check whether a scope level is set to 'enabled'. + * Check access to scope. If $action is omitted, it will check + * whether a scope level is set to 'enabled'. + * + * @throws NotImplemented */ public function checkScope(string $scope, ?string $action = null): bool { @@ -171,32 +181,25 @@ class Acl } /** - * @deprecated + * Check whether a user is an owner of an entity. */ - public function checkUser(string $permission, User $entity): bool + public function checkOwnershipOwn(Entity $entity): bool { - return $this->aclManager->checkUser($this->user, $permission, $entity); + return $this->aclManager->checkOwnershipOwn($this->user, $entity); } /** - * Whether a user is owned of an entity (record). Usually 'assignedUser' field is used for checking. + * Check whether an entity belongs to a user team. */ - public function checkIsOwner(Entity $entity): bool + public function checkOwnershipTeam(Entity $entity): bool { - return $this->aclManager->checkIsOwner($this->user, $entity); - } - - /** - * Whether a user team list overlaps with teams set in an entity. - */ - public function checkInTeam(Entity $entity): bool - { - return $this->aclManager->checkInTeam($this->user, $entity); + return $this->aclManager->checkOwnershipTeam($this->user, $entity); } /** * Get attributes forbidden for a user. * + * @param $thresholdLevel Should not be used. Stands for possible future enhancements. * * @return array */ public function getScopeForbiddenAttributeList( @@ -212,6 +215,7 @@ class Acl /** * Get fields forbidden for a user. * + * @param $thresholdLevel Should not be used. Stands for possible future enhancements. * @return array */ public function getScopeForbiddenFieldList( @@ -227,6 +231,7 @@ class Acl /** * Get links forbidden for a user. * + * @param $thresholdLevel Should not be used. Stands for possible future enhancements. * @return array */ public function getScopeForbiddenLinkList( @@ -290,4 +295,36 @@ class Acl { return $this->aclManager->getScopeRestrictedLinkList($scope, $type); } + + /** + * @deprecated Use `getPermissionLevel` instead. + */ + public function get(string $permission): string + { + return $this->getPermissionLevel($permission); + } + + /** + * @deprecated Use `checkOwnershipOwn` instead. + */ + public function checkIsOwner(Entity $entity): bool + { + return $this->aclManager->checkIsOwner($this->user, $entity); + } + + /** + * @deprecated Use `checkOwnershipTeam` instead. + */ + public function checkInTeam(Entity $entity): bool + { + return $this->aclManager->checkInTeam($this->user, $entity); + } + + /** + * @deprecated + */ + public function checkUser(string $permission, User $entity): bool + { + return $this->aclManager->checkUser($this->user, $permission, $entity); + } } diff --git a/application/Espo/Core/Acl/AccessChecker.php b/application/Espo/Core/Acl/AccessChecker.php new file mode 100644 index 0000000000..9a2c5cac77 --- /dev/null +++ b/application/Espo/Core/Acl/AccessChecker.php @@ -0,0 +1,40 @@ +classFinder = $classFinder; + $this->metadata = $metadata; + $this->injectableFactory = $injectableFactory; + } + + /** + * Create an access checker. + * + * @throws NotImplemented + */ + public function create(string $scope, AclManager $aclManager): AccessChecker + { + $className = $this->getClassName($scope); + + $bindingContainer = $this->createBindingContainer($aclManager); + + return $this->injectableFactory->createWithBinding($className, $bindingContainer); + } + + private function getClassName(string $scope): string + { + $className1 = $this->metadata->get(['aclDefs', $scope, 'accessCheckerClassName']); + + if ($className1) { + return $className1; + } + + if (!$this->metadata->get(['scopes', $scope])) { + throw new NotImplemented(); + } + + // For backward compatibility. + $className2 = $this->classFinder->find('Acl', $scope); + + if ($className2) { + return $className2; + } + + return $this->defaultClassName; + } + + private function createBindingContainer(AclManager $aclManager): BindingContainer + { + $bindingData = new BindingData(); + + $binder = new Binder($bindingData); + + $binder->bindCallback( + AclManager::class, + function () use ($aclManager): AclManager { + return $aclManager; + } + ); + + return new BindingContainer($bindingData); + } +} diff --git a/application/Espo/Core/Acl/LevelProvider.php b/application/Espo/Core/Acl/AccessCreateChecker.php similarity index 90% rename from application/Espo/Core/Acl/LevelProvider.php rename to application/Espo/Core/Acl/AccessCreateChecker.php index 7db6ff1dd3..adca03dedc 100644 --- a/application/Espo/Core/Acl/LevelProvider.php +++ b/application/Espo/Core/Acl/AccessCreateChecker.php @@ -31,10 +31,10 @@ namespace Espo\Core\Acl; use Espo\Entities\User; -interface LevelProvider +interface AccessCreateChecker extends AccessChecker { /** - * Get an access level for a specific action. + * Check 'create' access. */ - public function getLevel(User $user, ScopeData $data, string $action) : string; + public function checkCreate(User $user, ScopeData $data): bool; } diff --git a/application/Espo/Core/Acl/AccessDeleteChecker.php b/application/Espo/Core/Acl/AccessDeleteChecker.php new file mode 100644 index 0000000000..8b7cc994cb --- /dev/null +++ b/application/Espo/Core/Acl/AccessDeleteChecker.php @@ -0,0 +1,40 @@ +entityManager = $entityManager; - $this->aclManager = $aclManager; - $this->config = $config; - - $this->scope = $scope; - } - - public function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool - { - return $this->checkScopeInternal($user, $data, $action, $entity); - } - - public function checkScope(User $user, ScopeData $data, ?string $action = null): bool - { - return $this->checkScopeInternal($user, $data, $action); - } - - protected function checkScopeInternal( - User $user, - ScopeData $data, - ?string $action = null, - ?Entity $entity = null, - array $entityAccessData = [] - ): bool { - - if ($data->isFalse()) { - return false; - } - - if ($data->isTrue()) { - return true; - } - - $isOwner = null; - - if (isset($entityAccessData['isOwner'])) { - $isOwner = $entityAccessData['isOwner']; - } - - $inTeam = null; - - if (isset($entityAccessData['inTeam'])) { - $inTeam = $entityAccessData['inTeam']; - } - - if (is_null($action)) { - if ($data->hasNotNo()) { - return true; - } - - return false; - } - - $value = $data->get($action); - - if ($value === Table::LEVEL_ALL || $value === Table::LEVEL_YES) { - return true; - } - - if ($value === Table::LEVEL_NO) { - return false; - } - - if (is_null($isOwner)) { - if ($entity) { - $isOwner = $this->checkIsOwner($user, $entity); - } - else { - return true; - } - } - - if ($isOwner) { - if ($value === Table::LEVEL_OWN || $value === Table::LEVEL_TEAM) { - return true; - } - } - - if (is_null($inTeam) && $entity) { - $inTeam = $this->checkInTeam($user, $entity); - } - - if ($inTeam) { - if ($value === Table::LEVEL_TEAM) { - return true; - } - } - - return false; - } - - /** - * @return bool - */ - public function checkIsOwner(User $user, Entity $entity) - { - if ($entity->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) { - if ( - $entity->has(self::ATTR_ASSIGNED_USER_ID) && - $user->getId() === $entity->get(self::ATTR_ASSIGNED_USER_ID) - ) { - return true; - } - } - else if ($entity->hasAttribute(self::ATTR_CREATED_BY_ID)) { - if ( - $entity->has(self::ATTR_CREATED_BY_ID) && - $user->getId() === $entity->get(self::ATTR_CREATED_BY_ID) - ) { - return true; - } - } - - if ($entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)) { - if ($entity->hasLinkMultipleId(self::FIELD_ASSIGNED_USERS, $user->getId())) { - return true; - } - } - - return false; - } - - /** - * @return bool - */ - public function checkInTeam(User $user, Entity $entity) - { - $userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS); - - if (!$entity->hasRelation(self::FIELD_TEAMS) || !$entity->hasAttribute(self::ATTR_ASSIGNED_TEAMS_IDS)) { - return false; - } - - $entityTeamIdList = $entity->getLinkMultipleIdList(self::FIELD_TEAMS); - - if (empty($entityTeamIdList)) { - return false; - } - - foreach ($userTeamIdList as $id) { - if (in_array($id, $entityTeamIdList)) { - return true; - } - } - - return false; - } - - public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool - { - if ($user->isAdmin()) { - return true; - } - - if ($this->checkEntity($user, $entity, $data, Table::ACTION_DELETE)) { - return true; - } - - if ($data->getEdit() === Table::LEVEL_NO && $data->getCreate() === Table::LEVEL_NO) { - return false; - } - - if ( - !$this->config->get('aclAllowDeleteCreated') || - !$entity->has(self::ATTR_CREATED_BY_ID) || - $entity->get(self::ATTR_CREATED_BY_ID) !== $user->getId() - ) { - return false; - } - - $isDeletedAllowed = false; - - if (!$entity->has(self::ATTR_ASSIGNED_USER_ID)) { - $isDeletedAllowed = true; - } - else { - if (!$entity->get(self::ATTR_ASSIGNED_USER_ID)) { - $isDeletedAllowed = true; - } - else if ($entity->get(self::ATTR_ASSIGNED_USER_ID) === $entity->get(self::ATTR_CREATED_BY_ID)) { - $isDeletedAllowed = true; - } - } - - if (!$isDeletedAllowed) { - return false; - } - - $createdAt = $entity->get('createdAt'); - - if (!$createdAt) { - return true; - } - - $deleteThresholdPeriod = $this->config->get( - 'aclAllowDeleteCreatedThresholdPeriod', - $this->allowDeleteCreatedThresholdPeriod - ); - - if (DateTimeUtil::isAfterThreshold($createdAt, $deleteThresholdPeriod)) { - return false; - } - - return true; - } - - /** - * @deprecated Use `$this->config`. - */ - protected function getConfig(): Config - { - return $this->config; - } - - /** - * @deprecated Use `$this->entityManager`. - */ - protected function getEntityManager(): EntityManager - { - return $this->entityManager; - } - - /** - * @deprecated Use `$this->aclManager`. - */ - protected function getAclManager(): AclManager - { - return $this->aclManager; - } } diff --git a/application/Espo/Core/Acl/Base.php b/application/Espo/Core/Acl/Base.php index 1c2f97bc8c..e231e9a666 100644 --- a/application/Espo/Core/Acl/Base.php +++ b/application/Espo/Core/Acl/Base.php @@ -31,14 +31,20 @@ namespace Espo\Core\Acl; use Espo\Core\Interfaces\Injectable; +use Espo\ORM\Entity; + +use Espo\Entities\User; + use Espo\Core\{ ORM\EntityManager, AclManager, Utils\Config, }; -/** @deprecated */ -class Base extends Acl implements Injectable +/** + * @deprecated Use AccessChecker interfaces instead. + */ +class Base implements AccessChecker, Injectable { protected $dependencyList = []; @@ -46,19 +52,95 @@ class Base extends Acl implements Injectable protected $injections = []; + protected $entityManager; + + protected $aclManager; + + protected $config; + + protected $defaultChecker; + + protected $scopeChecker; + public function __construct( EntityManager $entityManager, AclManager $aclManager, Config $config, - ?string $scope = null + DefaultAccessChecker $defaultChecker, + ScopeChecker $scopeChecker ) { - $this->scope = $scope; - - parent::__construct($entityManager, $aclManager, $config, $scope); + $this->entityManager = $entityManager; + $this->aclManager = $aclManager; + $this->config = $config; + $this->defaultChecker = $defaultChecker; + $this->scopeChecker = $scopeChecker; $this->init(); } + public function check(User $user, ScopeData $data): bool + { + return $this->defaultChecker->check($user, $data); + } + + public function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwnChecker( + function () use ($user, $entity): bool { + return (bool) $this->checkIsOwner($user, $entity); + } + ) + ->setInTeamChecker( + function () use ($user, $entity): bool { + return (bool) $this->checkInTeam($user, $entity); + } + ) + ->build(); + + return $this->scopeChecker->check($data, $action, $checkerData); + } + + public function checkScope(User $user, ScopeData $data, ?string $action = null): bool + { + if (!$action) { + return $this->defaultChecker->check($user, $data); + } + + if ($action === Table::ACTION_CREATE) { + return $this->defaultChecker->checkCreate($user, $data); + } + + if ($action === Table::ACTION_READ) { + return $this->defaultChecker->checkRead($user, $data); + } + + if ($action === Table::ACTION_EDIT) { + return $this->defaultChecker->checkEdit($user, $data); + } + + if ($action === Table::ACTION_DELETE) { + return $this->defaultChecker->checkDelete($user, $data); + } + + if ($action === Table::ACTION_STREAM) { + return $this->defaultChecker->checkStream($user, $data); + } + + return false; + } + + public function checkIsOwner(User $user, Entity $entity) + { + return $this->aclManager->checkOwnershipOwn($user, $entity); + } + + public function checkInTeam(User $user, Entity $entity) + { + return $this->aclManager->checkOwnershipTeam($user, $entity); + } + public function inject($name, $object) { $this->injections[$name] = $object; @@ -89,4 +171,19 @@ class Base extends Acl implements Injectable { return array_merge($this->dependencyList, $this->dependencies); } + + protected function getConfig(): Config + { + return $this->config; + } + + protected function getEntityManager(): EntityManager + { + return $this->entityManager; + } + + protected function getAclManager(): AclManager + { + return $this->aclManager; + } } diff --git a/application/Espo/Core/Acl/DefaultAccessChecker.php b/application/Espo/Core/Acl/DefaultAccessChecker.php new file mode 100644 index 0000000000..1c6c79a90a --- /dev/null +++ b/application/Espo/Core/Acl/DefaultAccessChecker.php @@ -0,0 +1,211 @@ +aclManager = $aclManager; + $this->config = $config; + $this->scopeChecker = $scopeChecker; + } + + private function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwnChecker( + function () use ($user, $entity): bool { + return $this->aclManager->checkOwnershipOwn($user, $entity); + } + ) + ->setInTeamChecker( + function () use ($user, $entity): bool { + return $this->aclManager->checkOwnershipTeam($user, $entity); + } + ) + ->build(); + + return $this->scopeChecker->check($data, $action, $checkerData); + } + + private function checkScope(User $user, ScopeData $data, ?string $action = null): bool + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(true) + ->build(); + + return $this->scopeChecker->check($data, $action, $checkerData); + } + + public function check(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data); + } + + public function checkCreate(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_CREATE); + } + + public function checkRead(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_READ); + } + + public function checkEdit(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_EDIT); + } + + public function checkDelete(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_DELETE); + } + + public function checkStream(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_STREAM); + } + + public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_CREATE); + } + + public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_READ); + } + + public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_EDIT); + } + + public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_STREAM); + } + + public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool + { + if ($this->checkEntity($user, $entity, $data, Table::ACTION_DELETE)) { + return true; + } + + if ($data->getEdit() === Table::LEVEL_NO && $data->getCreate() === Table::LEVEL_NO) { + return false; + } + + if ( + !$this->config->get('aclAllowDeleteCreated') || + !$entity->has(self::ATTR_CREATED_BY_ID) || + $entity->get(self::ATTR_CREATED_BY_ID) !== $user->getId() + ) { + return false; + } + + $isDeletedAllowed = false; + + if (!$entity->has(self::ATTR_ASSIGNED_USER_ID)) { + $isDeletedAllowed = true; + } + else { + if (!$entity->get(self::ATTR_ASSIGNED_USER_ID)) { + $isDeletedAllowed = true; + } + else if ($entity->get(self::ATTR_ASSIGNED_USER_ID) === $entity->get(self::ATTR_CREATED_BY_ID)) { + $isDeletedAllowed = true; + } + } + + if (!$isDeletedAllowed) { + return false; + } + + $createdAt = $entity->get(self::ATTR_CREATED_AT); + + if (!$createdAt) { + return true; + } + + $deleteThresholdPeriod = $this->config->get( + 'aclAllowDeleteCreatedThresholdPeriod', + self::ALLOW_DELETE_OWN_CREATED_PERIOD + ); + + if (DateTimeUtil::isAfterThreshold($createdAt, $deleteThresholdPeriod)) { + return false; + } + + return true; + } +} diff --git a/application/Espo/Core/Acl/DefaultOwnershipChecker.php b/application/Espo/Core/Acl/DefaultOwnershipChecker.php new file mode 100644 index 0000000000..d9891d31f6 --- /dev/null +++ b/application/Espo/Core/Acl/DefaultOwnershipChecker.php @@ -0,0 +1,104 @@ +hasAttribute(self::ATTR_ASSIGNED_USER_ID)) { + if ( + $entity->has(self::ATTR_ASSIGNED_USER_ID) && + $user->getId() === $entity->get(self::ATTR_ASSIGNED_USER_ID) + ) { + return true; + } + } + else if ($entity->hasAttribute(self::ATTR_CREATED_BY_ID)) { + if ( + $entity->has(self::ATTR_CREATED_BY_ID) && + $user->getId() === $entity->get(self::ATTR_CREATED_BY_ID) + ) { + return true; + } + } + + if ($entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)) { + if ($entity->hasLinkMultipleId(self::FIELD_ASSIGNED_USERS, $user->getId())) { + return true; + } + } + + return false; + } + + public function checkTeam(User $user, Entity $entity): bool + { + $userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS); + + if ( + !$entity->hasRelation(self::FIELD_TEAMS) || + !$entity->hasAttribute(self::ATTR_ASSIGNED_TEAMS_IDS) + ) { + return false; + } + + $entityTeamIdList = $entity->getLinkMultipleIdList(self::FIELD_TEAMS); + + if (empty($entityTeamIdList)) { + return false; + } + + foreach ($userTeamIdList as $id) { + if (in_array($id, $entityTeamIdList)) { + return true; + } + } + + return false; + } +} diff --git a/application/Espo/Core/Acl/DefaultTable.php b/application/Espo/Core/Acl/DefaultTable.php new file mode 100644 index 0000000000..06428c80d9 --- /dev/null +++ b/application/Espo/Core/Acl/DefaultTable.php @@ -0,0 +1,939 @@ +entityManager = $entityManager; + + $this->data = (object) [ + 'table' => (object) [], + 'fieldTable' => (object) [], + 'fieldTableQuickAccess' => (object) [], + ]; + + if ($this->isStrictModeForced) { + $this->isStrictMode = true; + } + else { + $this->isStrictMode = $config->get('aclStrictMode', true); + } + + $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.'); + } + + $this->valuePermissionList = $this->metadata + ->get(['app', $this->type, 'valuePermissionList'], []); + + $this->valuePermissionHighestLevels = $this->metadata + ->get(['app', $this->type, 'valuePermissionHighestLevels'], []); + + $this->initCacheKey(); + + if ($config && $config->get('useCache') && $this->dataCache->has($this->cacheKey)) { + $this->data = $this->dataCache->get($this->cacheKey); + } + else { + $this->load(); + + if ($config && $config->get('useCache')) { + $this->buildCache(); + } + } + } + + 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)) { + return ScopeData::fromRaw(false); + } + + $data = $this->data->table->$scope; + + if (is_string($data)) { + return $this->getScopeData($data); + } + + return ScopeData::fromRaw($data); + } + + /** + * Get a permission level. + */ + public function getPermissionLevel(string $permission): string + { + return $this->data->$permission ?? self::LEVEL_NO; + } + + private function load(): void + { + $valuePermissionLists = (object) []; + + foreach ($this->valuePermissionList as $permission) { + $valuePermissionLists->$permission = []; + } + + $aclTableList = []; + $fieldTableList = []; + + if (!$this->user->isAdmin()) { + $roleList = $this->getRoleList(); + + foreach ($roleList as $role) { + $aclTableList[] = $role->get('data'); + $fieldTableList[] = $role->get('fieldData'); + + foreach ($this->valuePermissionList as $permission) { + $valuePermissionLists->{$permission}[] = $role->get($permission); + } + } + + $aclTable = $this->mergeTableList($aclTableList); + $fieldTable = $this->mergeFieldTableList($fieldTableList); + + $this->applyDefault($aclTable, $fieldTable); + $this->applyDisabled($aclTable, $fieldTable); + $this->applyMandatory($aclTable, $fieldTable); + $this->applyAdditional($aclTable, $fieldTable, $valuePermissionLists); + } + + if ($this->user->isAdmin()) { + $aclTable = (object) []; + $fieldTable = (object) []; + + $this->applyHighest($aclTable, $fieldTable); + } + + foreach ($aclTable as $scope => $data) { + if (is_string($data) && isset($aclTable->$data)) { + $aclTable->$scope = $aclTable->$data; + } + } + + $this->data->table = $aclTable; + $this->data->fieldTable = $fieldTable; + + $this->fillFieldTableQuickAccess(); + + if (!$this->user->isAdmin()) { + $permissionsDefaultsGroupName = 'permissionsDefaults'; + + if ($this->isStrictMode) { + $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]) + ); + + if ($this->metadata->get('app.'.$this->type.'.mandatory.' . $permission)) { + $this->data->$permission = $this->metadata + ->get('app.'.$this->type.'.mandatory.' . $permission); + } + } + } + + if ($this->user->isAdmin()) { + foreach ($this->valuePermissionList as $permission) { + if (isset($this->valuePermissionHighestLevels[$permission])) { + $this->data->$permission = $this->valuePermissionHighestLevels[$permission]; + + continue; + } + + $this->data->$permission = self::LEVEL_ALL; + } + } + } + + protected function getRoleList(): 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 $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; + } + + protected function applyHighest(StdClass &$table, StdClass &$fieldTable): void + { + foreach ($this->getScopeList() as $scope) { + if ($this->metadata->get(['scopes', $scope, $this->type]) === 'boolean') { + $table->$scope = true; + + continue; + } + + if (!$this->metadata->get(['scopes', $scope, 'entity'])) { + continue; + } + + $table->$scope = (object) []; + + $actionList = $this->metadata->get( + ['scopes', $scope, $this->type . 'ActionList'], + $this->actionList + ); + + $highest = $this->metadata->get( + ['scopes', $scope, $this->type . 'HighestLevel'], + self::LEVEL_ALL + ); + + foreach ($actionList as $action) { + $table->$scope->$action = $highest; + + if (in_array($action, $this->booleanActionList)) { + $table->$scope->$action = 'yes'; + } + } + } + } + + protected function applyDefault(&$table, &$fieldTable): void + { + if ($this->user->isAdmin()) { + return; + } + + $defaultsGroupName = 'default'; + + if ($this->isStrictMode) { + $defaultsGroupName = 'strictDefault'; + } + + $data = $this->metadata->get(['app', $this->type, $defaultsGroupName, 'scopeLevel'], []); + + foreach ($data as $scope => $item) { + if (isset($table->$scope)) { + continue; + } + + $value = $item; + + if (is_array($item)) { + $value = (object) $item; + } + + $table->$scope = $value; + } + + $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')) { + continue; + } + + $fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", [])); + + $defaultScopeFieldData = $this->metadata + ->get('app.'.$this->type.'.'.$defaultsGroupName.'.scopeFieldLevel.' . $scope, []); + + foreach (array_merge($defaultFieldData, $defaultScopeFieldData) as $field => $f) { + if (!in_array($field, $fieldList)) { + continue; + } + + if (!isset($fieldTable->$scope)) { + $fieldTable->$scope = (object) []; + } + + if (isset($fieldTable->$scope->$field)) { + continue; + } + + $fieldTable->$scope->$field = (object) []; + + foreach ($this->fieldActionList as $action) { + $level = self::LEVEL_NO; + + if ($f === true) { + $level = self::LEVEL_YES; + } + else { + if (is_array($f) && isset($f[$action])) { + $level = $f[$action]; + } + } + + $fieldTable->$scope->$field->$action = $level; + } + } + } + + foreach ($this->getScopeWithAclList() as $scope) { + if (!isset($table->$scope)) { + $aclType = $this->metadata->get('scopes.' . $scope . '.' . $this->type); + + if ($aclType === true) { + $aclType = $this->defaultAclType; + } + + if (!empty($aclType)) { + $paramDefaultsName = 'scopeLevelTypesDefaults'; + + if ($this->isStrictMode) { + $paramDefaultsName = 'scopeLevelTypesStrictDefaults'; + } + + $defaultValue = $this->metadata + ->get( + ['app', $this->type, $paramDefaultsName, $aclType], + $this->metadata->get(['app', $this->type, $paramDefaultsName, 'record']) + ); + + if (is_array($defaultValue)) { + $defaultValue = (object) $defaultValue; + } + + $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); + } + } + } + } + } + } + } + } + + protected function applyMandatory(&$table, &$fieldTable): void + { + if ($this->user->isAdmin()) { + return; + } + + $data = $this->metadata->get('app.'.$this->type.'.mandatory.scopeLevel', []); + + foreach ($data as $scope => $item) { + $value = $item; + + if (is_array($item)) { + $value = (object) $item; + } + + $table->$scope = $value; + } + + $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')) { + continue; + } + + $fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", [])); + + $mandatoryScopeFieldData = $this->metadata + ->get('app.'.$this->type.'.mandatory.scopeFieldLevel.' . $scope, []); + + foreach (array_merge($mandatoryFieldData, $mandatoryScopeFieldData) as $field => $f) { + if (!in_array($field, $fieldList)) { + continue; + } + + if (!isset($fieldTable->$scope)) { + $fieldTable->$scope = (object) []; + } + + $fieldTable->$scope->$field = (object) []; + + foreach ($this->fieldActionList as $action) { + $level = self::LEVEL_NO; + + if ($f === true) { + $level = self::LEVEL_YES; + } + else { + if (is_array($f) && isset($f[$action])) { + $level = $f[$action]; + } + } + + $fieldTable->$scope->$field->$action = $level; + } + } + } + } + + protected function applyDisabled(&$table, &$fieldTable): void + { + if ($this->user->isAdmin()) { + return; + } + + foreach ($this->getScopeList() as $scope) { + if ($this->metadata->get('scopes.' . $scope . '.disabled')) { + $table->$scope = false; + + unset($fieldTable->$scope); + } + } + } + + protected function applyAdditional(&$table, &$fieldTable, &$valuePermissionLists): void + { + if ($this->user->isPortal()) { + foreach ($this->getScopeList() as $scope) { + $table->$scope = false; + + unset($fieldTable->$scope); + } + + foreach ($this->valuePermissionList as $permission) { + $valuePermissionLists->{$permission}[] = self::LEVEL_NO; + } + } + } + + private function mergeValueList(array $list, string $defaultValue): string + { + $result = null; + + foreach ($list as $level) { + if ($level === self::LEVEL_NOT_SET) { + continue; + } + + if (is_null($result)) { + $result = $level; + + continue; + } + + if ( + array_search($result, $this->levelList) > + array_search($level, $this->levelList) + ) { + $result = $level; + } + } + + if (is_null($result)) { + $result = $defaultValue; + } + + return $result; + } + + protected function getScopeWithAclList(): array + { + $scopeList = []; + + $scopes = $this->metadata->get('scopes'); + + foreach ($scopes as $scope => $d) { + if (empty($d['acl'])) { + continue; + } + + $scopeList[] = $scope; + } + + return $scopeList; + } + + protected function getScopeList(): array + { + $scopeList = []; + + $scopes = $this->metadata->get('scopes'); + + foreach ($scopes as $scope => $item) { + $scopeList[] = $scope; + } + + return $scopeList; + } + + private function mergeTableList(array $tableList): StdClass + { + $data = (object) []; + + $scopeList = $this->getScopeWithAclList(); + + foreach ($tableList as $table) { + foreach ($scopeList as $scope) { + if (!isset($table->$scope)) { + continue; + } + + $this->mergeTableListItem($data, $scope, $table->$scope); + } + } + + return $data; + } + + private function mergeTableListItem(StdClass $data, string $scope, $row): void + { + if ($row === false || $row === null) { + if (!isset($data->$scope)) { + $data->$scope = false; + } + + return; + } + + if ($row === true) { + $data->$scope = true; + + return; + } + + if (!isset($data->$scope)) { + $data->$scope = (object) []; + } + + if ($data->$scope === false) { + $data->$scope = (object) []; + } + + if (!is_object($row)) { + return; + } + + $actionList = $this->metadata + ->get(['scopes', $scope, $this->type . 'ActionList'], $this->actionList); + + foreach ($actionList as $i => $action) { + if (isset($row->$action)) { + $level = $row->$action; + + if (!isset($data->$scope->$action)) { + $data->$scope->$action = $level; + } + else if ( + array_search($data->$scope->$action, $this->levelList) > + array_search($level, $this->levelList) + ) { + $data->$scope->$action = $level; + } + + continue; + } + + if ($i === 0) { + continue; + } + + // @todo Remove everything below. + $previousAction = $this->actionList[$i - 1]; + + if (in_array($action, $this->booleanActionList)) { + $data->$scope->$action = self::LEVEL_YES; + } + else if ($action === self::ACTION_STREAM && isset($data->$scope->$previousAction)) { + $data->$scope->$action = $data->$scope->$previousAction; + } + } + } + + private function mergeFieldTableList(array $tableList): StdClass + { + $data = (object) []; + + $scopeList = $this->getScopeWithAclList(); + + foreach ($tableList as $table) { + foreach ($scopeList as $scope) { + if (!isset($table->$scope)) { + continue; + } + + if (!isset($data->$scope)) { + $data->$scope = (object) []; + } + + if (!is_object($table->$scope)) { + continue; + } + + $fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", [])); + + foreach (get_object_vars($table->$scope) as $field => $row) { + if (!is_object($row)) { + continue; + } + + if (!in_array($field, $fieldList)) { + continue; + } + + if (!isset($data->$scope->$field)) { + $data->$scope->$field = (object) []; + } + + foreach ($this->fieldActionList as $i => $action) { + if (!isset($row->$action)) { + continue; + } + + $level = $row->$action; + + if (!isset($data->$scope->$field->$action)) { + $data->$scope->$field->$action = $level; + } + else { + if ( + array_search( + $data->$scope->$field->$action, + $this->fieldLevelList + ) > array_search($level, $this->fieldLevelList) + ) { + $data->$scope->$field->$action = $level; + } + } + } + } + } + } + + return $data; + } + + private function buildCache(): void + { + $this->dataCache->store($this->cacheKey, $this->data); + } +} diff --git a/application/Espo/Core/Acl/GlobalRestricton.php b/application/Espo/Core/Acl/GlobalRestricton.php index bf480e6593..892c0dd008 100644 --- a/application/Espo/Core/Acl/GlobalRestricton.php +++ b/application/Espo/Core/Acl/GlobalRestricton.php @@ -34,12 +34,14 @@ use Espo\Core\{ Utils\DataCache, Utils\FieldUtil, Utils\Log, + Utils\Config, }; use StdClass; /** - * Lists of restricted fields can be obtained from here. Restricted fields are specified in metadata > entityAcl. + * Lists of restricted fields can be obtained from here. Restricted fields + * are specified in metadata > entityAcl. */ class GlobalRestricton { @@ -82,7 +84,11 @@ class GlobalRestricton private $log; public function __construct( - Metadata $metadata, DataCache $dataCache, FieldUtil $fieldUtil, Log $log, bool $useCache = true + Metadata $metadata, + DataCache $dataCache, + FieldUtil $fieldUtil, + Log $log, + Config $config ) { $this->metadata = $metadata; $this->dataCache = $dataCache; @@ -91,17 +97,17 @@ class GlobalRestricton $isFromCache = false; - if ($useCache) { - if ($this->dataCache->has($this->cacheKey)) { - $this->data = $this->dataCache->get($this->cacheKey); + $useCache = $config->get('useCache'); - $isFromCache = true; + if ($useCache && $this->dataCache->has($this->cacheKey)) { + $this->data = $this->dataCache->get($this->cacheKey); - if (!$this->data instanceof StdClass) { - $this->log->error("ACL GlobalRestricton: Bad data fetched from cache."); + $isFromCache = true; - $this->data = null; - } + if (!$this->data instanceof StdClass) { + $this->log->error("ACL GlobalRestricton: Bad data fetched from cache."); + + $this->data = null; } } @@ -109,19 +115,17 @@ class GlobalRestricton $this->buildData(); } - if ($useCache) { - if (!$isFromCache) { - $this->storeCacheFile(); - } + if ($useCache && !$isFromCache) { + $this->storeCacheFile(); } } - protected function storeCacheFile() + protected function storeCacheFile(): void { $this->dataCache->store($this->cacheKey, $this->data, true); } - protected function buildData() + protected function buildData(): void { $scopeList = array_keys($this->metadata->get(['entityDefs'], [])); @@ -160,6 +164,7 @@ class GlobalRestricton $scopeData->fields->$type = $resultFieldList; $scopeData->attributes->$type = $resultAttributeList; } + foreach ($this->linkTypeList as $type) { $resultLinkList = []; diff --git a/application/Espo/Core/Acl/OwnerUserFieldProvider.php b/application/Espo/Core/Acl/OwnerUserFieldProvider.php index 767ee98224..a6cbb390e6 100644 --- a/application/Espo/Core/Acl/OwnerUserFieldProvider.php +++ b/application/Espo/Core/Acl/OwnerUserFieldProvider.php @@ -57,7 +57,7 @@ class OwnerUserFieldProvider */ public function get(string $entityType) : ?string { - $value = $this->metadata->get(['entityAcl', $entityType, 'readOwnerUserField']); + $value = $this->metadata->get(['aclDefs', $entityType, 'readOwnerUserField']); if ($value) { return $value; diff --git a/application/Espo/Core/AclPortal/PortalScopeAcl.php b/application/Espo/Core/Acl/OwnershipChecker.php similarity index 93% rename from application/Espo/Core/AclPortal/PortalScopeAcl.php rename to application/Espo/Core/Acl/OwnershipChecker.php index fa8b02199f..9f050ccf8b 100644 --- a/application/Espo/Core/AclPortal/PortalScopeAcl.php +++ b/application/Espo/Core/Acl/OwnershipChecker.php @@ -27,11 +27,9 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\AclPortal; +namespace Espo\Core\Acl; -use Espo\Core\Acl\ScopeAcl; - -interface PortalScopeAcl extends ScopeAcl +interface OwnershipChecker { } diff --git a/application/Espo/Core/Acl/OwnershipCheckerFactory.php b/application/Espo/Core/Acl/OwnershipCheckerFactory.php new file mode 100644 index 0000000000..9b3c410feb --- /dev/null +++ b/application/Espo/Core/Acl/OwnershipCheckerFactory.php @@ -0,0 +1,102 @@ +metadata = $metadata; + $this->injectableFactory = $injectableFactory; + } + + /** + * Create an ownership checker. + * + * @throws NotImplemented + */ + public function create(string $scope, AclManager $aclManager): OwnershipChecker + { + $className = $this->getClassName($scope); + + $bindingContainer = $this->createBindingContainer($aclManager); + + return $this->injectableFactory->createWithBinding($className, $bindingContainer); + } + + private function getClassName(string $scope): string + { + $className = $this->metadata->get(['aclDefs', $scope, 'ownershipCheckerClassName']); + + if ($className) { + return $className; + } + + if (!$this->metadata->get(['scopes', $scope])) { + throw new NotImplemented(); + } + + return $this->defaultClassName; + } + + private function createBindingContainer(AclManager $aclManager): BindingContainer + { + $bindingData = new BindingData(); + + $binder = new Binder($bindingData); + + $binder->bindCallback( + AclManager::class, + function () use ($aclManager): AclManager { + return $aclManager; + } + ); + + return new BindingContainer($bindingData); + } +} diff --git a/application/Espo/Core/Acl/EntityAcl.php b/application/Espo/Core/Acl/OwnershipOwnChecker.php similarity index 89% rename from application/Espo/Core/Acl/EntityAcl.php rename to application/Espo/Core/Acl/OwnershipOwnChecker.php index 27f95ecdc3..143c43b4d5 100644 --- a/application/Espo/Core/Acl/EntityAcl.php +++ b/application/Espo/Core/Acl/OwnershipOwnChecker.php @@ -33,10 +33,10 @@ use Espo\ORM\Entity; use Espo\Entities\User; -interface EntityAcl +interface OwnershipOwnChecker extends OwnershipChecker { /** - * Check access to an entity. + * Check whether a user is an owner of an entity. */ - public function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool; + public function checkOwn(User $user, Entity $entity): bool; } diff --git a/application/Espo/Core/Acl/OwnershipTeamChecker.php b/application/Espo/Core/Acl/OwnershipTeamChecker.php new file mode 100644 index 0000000000..9ffc1b0674 --- /dev/null +++ b/application/Espo/Core/Acl/OwnershipTeamChecker.php @@ -0,0 +1,42 @@ +isFalse()) { + return false; + } + + if ($data->isTrue()) { + return true; + } + + if ($action === null) { + if ($data->hasNotNo()) { + return true; + } + + return false; + } + + $level = $data->get($action); + + if ($level === Table::LEVEL_ALL || $level === Table::LEVEL_YES) { + return true; + } + + if ($level === Table::LEVEL_NO) { + return false; + } + + if (!$checkerData) { + return false; + } + + if ($level === Table::LEVEL_OWN || $level === Table::LEVEL_TEAM) { + if ($checkerData->isOwn()) { + return true; + } + } + + if ($level === Table::LEVEL_TEAM) { + if ($checkerData->inTeam()) { + return true; + } + } + + return false; + } +} diff --git a/application/Espo/Core/Acl/ScopeCheckerData.php b/application/Espo/Core/Acl/ScopeCheckerData.php new file mode 100644 index 0000000000..e2aac9d043 --- /dev/null +++ b/application/Espo/Core/Acl/ScopeCheckerData.php @@ -0,0 +1,61 @@ +isOwnChecker = $isOwnChecker; + $this->inTeamChecker = $inTeamChecker; + } + + public function isOwn(): bool + { + return ($this->isOwnChecker)(); + } + + public function inTeam(): bool + { + return ($this->inTeamChecker)(); + } + + public static function createBuilder(): ScopeCheckerDataBuilder + { + return new ScopeCheckerDataBuilder(); + } +} diff --git a/application/Espo/Core/Acl/ScopeCheckerDataBuilder.php b/application/Espo/Core/Acl/ScopeCheckerDataBuilder.php new file mode 100644 index 0000000000..460ecd2dd9 --- /dev/null +++ b/application/Espo/Core/Acl/ScopeCheckerDataBuilder.php @@ -0,0 +1,104 @@ +isOwnChecker = function (): bool { + return false; + }; + + $this->inTeamChecker = function (): bool { + return false; + }; + } + + public function setIsOwn(bool $value): self + { + if ($value) { + $this->isOwnChecker = function (): bool { + return true; + }; + + return $this; + } + + $this->isOwnChecker = function (): bool { + return false; + }; + + return $this; + } + + public function setInTeam(bool $value): self + { + if ($value) { + $this->inTeamChecker = function (): bool { + return true; + }; + + return $this; + } + + $this->inTeamChecker = function (): bool { + return false; + }; + + return $this; + } + + public function setIsOwnChecker(callable $checker): self + { + $this->isOwnChecker = $checker; + + return $this; + } + + public function setInTeamChecker(callable $checker): self + { + $this->inTeamChecker = $checker; + + return $this; + } + + public function build(): ScopeCheckerData + { + return new ScopeCheckerData($this->isOwnChecker, $this->inTeamChecker); + } +} diff --git a/application/Espo/Core/Acl/ScopeData.php b/application/Espo/Core/Acl/ScopeData.php index 04c21f08a4..b79ce90ff8 100644 --- a/application/Espo/Core/Acl/ScopeData.php +++ b/application/Espo/Core/Acl/ScopeData.php @@ -53,20 +53,6 @@ class ScopeData throw new RuntimeException("Accessing ScopeData properties is not allowed."); } - /** - * Get a raw value. - * - * @return StdClass|bool - */ - public function getRaw() - { - if (!$this->isBoolean()) { - return clone $this->raw; - } - - return $this->raw; - } - /** * Is of boolean type. */ @@ -175,6 +161,12 @@ class ScopeData $obj->isBoolean = false; $obj->actionData = get_object_vars($raw); + + foreach ($obj->actionData as $item) { + if (!is_string($item)) { + throw new RuntimeException("Bad raw scope data."); + } + } } else if (is_bool($raw)) { $obj->isBoolean = true; diff --git a/application/Espo/Core/Acl/Table.php b/application/Espo/Core/Acl/Table.php index 4ddbc5c21f..6a197da534 100644 --- a/application/Espo/Core/Acl/Table.php +++ b/application/Espo/Core/Acl/Table.php @@ -29,27 +29,12 @@ namespace Espo\Core\Acl; -use Espo\Core\Exceptions\Error; - -use Espo\Entities\User; - -use Espo\Core\{ - ORM\EntityManager, - Utils\Config, - Utils\Metadata, - Utils\FieldUtil, - Utils\DataCache, - Utils\ObjectUtil, -}; - use StdClass; -use Traversable; /** - * A table is generated for each user. Multiple roles are merged into a single table. - * This table is used for access checking. + * Contains access levels for a user. */ -class Table +interface Table { public const LEVEL_YES = 'yes'; @@ -71,871 +56,40 @@ class Table public const ACTION_CREATE = 'create'; - protected $type = 'acl'; - - protected $defaultAclType = 'recordAllTeamOwnNo'; - - private $data = null; - - protected $cacheKey; - - protected $actionList = ['read', 'stream', 'edit', 'delete', 'create']; - - protected $booleanActionList = ['create']; - - protected $levelList = ['yes', 'all', 'team', 'own', 'no']; - - protected $fieldActionList = ['read', 'edit']; - - protected $fieldLevelList = ['yes', 'no']; - - protected $valuePermissionHighestLevels = []; - - protected $valuePermissionList = []; - - protected $forbiddenAttributesCache = []; - - protected $forbiddenFieldsCache = []; - - protected $isStrictModeForced = false; - - protected $isStrictMode = false; - - protected $entityManager; - - protected $user; - - protected $config; - - protected $metadata; - - protected $fieldUtil; - - protected $dataCache; - - public function __construct( - EntityManager $entityManager, - User $user, - Config $config, - Metadata $metadata, - FieldUtil $fieldUtil, - DataCache $dataCache - ) { - $this->entityManager = $entityManager; - - $this->data = (object) [ - 'table' => (object) [], - 'fieldTable' => (object) [], - 'fieldTableQuickAccess' => (object) [], - ]; - - if ($this->isStrictModeForced) { - $this->isStrictMode = true; - } - else { - $this->isStrictMode = $config->get('aclStrictMode', true); - } - - $this->user = $user; - $this->metadata = $metadata; - $this->fieldUtil = $fieldUtil; - $this->dataCache = $dataCache; - - if (!$this->user->isFetched()) { - throw new Error('User must be fetched before ACL check.'); - } - - $this->valuePermissionList = $this->metadata - ->get(['app', $this->type, 'valuePermissionList'], []); - - $this->valuePermissionHighestLevels = $this->metadata - ->get(['app', $this->type, 'valuePermissionHighestLevels'], []); - - $this->initCacheKey(); - - if ($config && $config->get('useCache') && $this->dataCache->has($this->cacheKey)) { - $this->data = $this->dataCache->get($this->cacheKey); - } - else { - $this->load(); - - if ($config && $config->get('useCache')) { - $this->buildCache(); - } - } - } - - protected function initCacheKey(): void - { - $this->cacheKey = 'acl/' . $this->user->id; - } - /** * Get a full map. */ - public function getMap(): StdClass - { - return ObjectUtil::clone($this->data); - } + public function getMap(): StdClass; /** * Get scope data. */ - public function getScopeData(string $scope): ScopeData - { - if (!isset($this->data->table->$scope)) { - return ScopeData::fromRaw(false); - } - - $data = $this->data->table->$scope; - - if (is_string($data)) { - return $this->getScopeData($data); - } - - return ScopeData::fromRaw($data); - } + public function getScopeData(string $scope): ScopeData; /** - * Get a permission. + * Get a permission level. */ - public function get(string $permission): ?string - { - if ($permission === 'table') { - return null; - } - - if (isset($this->data->$permission)) { - return $this->data->$permission; - } - - return self::LEVEL_NO; - } - - private function load() - { - $valuePermissionLists = (object) []; - - foreach ($this->valuePermissionList as $permission) { - $valuePermissionLists->$permission = []; - } - - $aclTableList = []; - $fieldTableList = []; - - if (!$this->user->isAdmin()) { - $roleList = $this->getRoleList(); - - foreach ($roleList as $role) { - $aclTableList[] = $role->get('data'); - $fieldTableList[] = $role->get('fieldData'); - - foreach ($this->valuePermissionList as $permission) { - $valuePermissionLists->{$permission}[] = $role->get($permission); - } - } - - $aclTable = $this->mergeTableList($aclTableList); - $fieldTable = $this->mergeFieldTableList($fieldTableList); - - $this->applyDefault($aclTable, $fieldTable); - $this->applyDisabled($aclTable, $fieldTable); - $this->applyMandatory($aclTable, $fieldTable); - $this->applyAdditional($aclTable, $fieldTable, $valuePermissionLists); - } - - if ($this->user->isAdmin()) { - $aclTable = (object) []; - $fieldTable = (object) []; - - $this->applyHighest($aclTable, $fieldTable); - } - - foreach ($aclTable as $scope => $data) { - if (is_string($data) && isset($aclTable->$data)) { - $aclTable->$scope = $aclTable->$data; - } - } - - $this->data->table = $aclTable; - $this->data->fieldTable = $fieldTable; - - $this->fillFieldTableQuickAccess(); - - if (!$this->user->isAdmin()) { - $permissionsDefaultsGroupName = 'permissionsDefaults'; - - if ($this->isStrictMode) { - $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]) - ); - - if ($this->metadata->get('app.'.$this->type.'.mandatory.' . $permission)) { - $this->data->$permission = $this->metadata - ->get('app.'.$this->type.'.mandatory.' . $permission); - } - } - } - - if ($this->user->isAdmin()) { - foreach ($this->valuePermissionList as $permission) { - if (isset($this->valuePermissionHighestLevels[$permission])) { - $this->data->$permission = $this->valuePermissionHighestLevels[$permission]; - - continue; - } - - $this->data->$permission = self::LEVEL_ALL; - } - } - } - - protected function getRoleList() - { - $roleList = []; - - $userRoleList = $this->entityManager - ->getRepository('User') - ->getRelation($this->user, 'roles') - ->find(); - - if (!$userRoleList instanceof Traversable) { - throw new Error(); - } - - foreach ($userRoleList as $role) { - $roleList[] = $role; - } - - $teamList = $this->entityManager - ->getRepository('User') - ->getRelation($this->user, 'teams') - ->find(); - - if (! $teamList instanceof Traversable) { - throw new Error(); - } - - 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 { - - $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 { - - $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; - } - - protected function applyHighest(StdClass &$table, StdClass &$fieldTable): void - { - foreach ($this->getScopeList() as $scope) { - if ($this->metadata->get(['scopes', $scope, $this->type]) === 'boolean') { - $table->$scope = true; - - continue; - } - - if (!$this->metadata->get(['scopes', $scope, 'entity'])) { - continue; - } - - $table->$scope = (object) []; - - $actionList = $this->metadata->get( - ['scopes', $scope, $this->type . 'ActionList'], - $this->actionList - ); - - $highest = $this->metadata->get( - ['scopes', $scope, $this->type . 'HighestLevel'], - self::LEVEL_ALL - ); - - foreach ($actionList as $action) { - $table->$scope->$action = $highest; - - if (in_array($action, $this->booleanActionList)) { - $table->$scope->$action = 'yes'; - } - } - } - } - - protected function applyDefault(&$table, &$fieldTable): void - { - if ($this->user->isAdmin()) { - return; - } - - $defaultsGroupName = 'default'; - - if ($this->isStrictMode) { - $defaultsGroupName = 'strictDefault'; - } - - $data = $this->metadata->get(['app', $this->type, $defaultsGroupName, 'scopeLevel'], []); - - foreach ($data as $scope => $item) { - if (isset($table->$scope)) { - continue; - } - - $value = $item; - - if (is_array($item)) { - $value = (object) $item; - } - - $table->$scope = $value; - } - - $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')) { - continue; - } - - $fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", [])); - - $defaultScopeFieldData = $this->metadata - ->get('app.'.$this->type.'.'.$defaultsGroupName.'.scopeFieldLevel.' . $scope, []); - - foreach (array_merge($defaultFieldData, $defaultScopeFieldData) as $field => $f) { - if (!in_array($field, $fieldList)) { - continue; - } - - if (!isset($fieldTable->$scope)) { - $fieldTable->$scope = (object) []; - } - - if (isset($fieldTable->$scope->$field)) { - continue; - } - - $fieldTable->$scope->$field = (object) []; - - foreach ($this->fieldActionList as $action) { - $level = self::LEVEL_NO; - - if ($f === true) { - $level = self::LEVEL_YES; - } - else { - if (is_array($f) && isset($f[$action])) { - $level = $f[$action]; - } - } - - $fieldTable->$scope->$field->$action = $level; - } - } - } - - foreach ($this->getScopeWithAclList() as $scope) { - if (!isset($table->$scope)) { - $aclType = $this->metadata->get('scopes.' . $scope . '.' . $this->type); - - if ($aclType === true) { - $aclType = $this->defaultAclType; - } - - if (!empty($aclType)) { - $paramDefaultsName = 'scopeLevelTypesDefaults'; - - if ($this->isStrictMode) { - $paramDefaultsName = 'scopeLevelTypesStrictDefaults'; - } - - $defaultValue = $this->metadata - ->get( - ['app', $this->type, $paramDefaultsName, $aclType], - $this->metadata->get(['app', $this->type, $paramDefaultsName, 'record']) - ); - - if (is_array($defaultValue)) { - $defaultValue = (object) $defaultValue; - } - - $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); - } - } - } - } - } - } - } - } - - protected function applyMandatory(&$table, &$fieldTable): void - { - if ($this->user->isAdmin()) { - return; - } - - $data = $this->metadata->get('app.'.$this->type.'.mandatory.scopeLevel', []); - - foreach ($data as $scope => $item) { - $value = $item; - - if (is_array($item)) { - $value = (object) $item; - } - - $table->$scope = $value; - } - - $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')) { - continue; - } - - $fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", [])); - - $mandatoryScopeFieldData = $this->metadata - ->get('app.'.$this->type.'.mandatory.scopeFieldLevel.' . $scope, []); - - foreach (array_merge($mandatoryFieldData, $mandatoryScopeFieldData) as $field => $f) { - if (!in_array($field, $fieldList)) { - continue; - } - - if (!isset($fieldTable->$scope)) { - $fieldTable->$scope = (object) []; - } - - $fieldTable->$scope->$field = (object) []; - - foreach ($this->fieldActionList as $action) { - $level = self::LEVEL_NO; - - if ($f === true) { - $level = self::LEVEL_YES; - } - else { - if (is_array($f) && isset($f[$action])) { - $level = $f[$action]; - } - } - - $fieldTable->$scope->$field->$action = $level; - } - } - } - } - - protected function applyDisabled(&$table, &$fieldTable): void - { - if ($this->user->isAdmin()) { - return; - } - - foreach ($this->getScopeList() as $scope) { - if ($this->metadata->get('scopes.' . $scope . '.disabled')) { - $table->$scope = false; - - unset($fieldTable->$scope); - } - } - } - - protected function applyAdditional(&$table, &$fieldTable, &$valuePermissionLists): void - { - if ($this->user->isPortal()) { - foreach ($this->getScopeList() as $scope) { - $table->$scope = false; - - unset($fieldTable->$scope); - } - - foreach ($this->valuePermissionList as $permission) { - $valuePermissionLists->{$permission}[] = self::LEVEL_NO; - } - } - } - - private function mergeValueList(array $list, string $defaultValue): string - { - $result = null; - - foreach ($list as $level) { - if ($level !== 'not-set') { - if (is_null($result)) { - $result = $level; - - continue; - } - - if (array_search($result, $this->levelList) > array_search($level, $this->levelList)) { - $result = $level; - } - } - } - - if (is_null($result)) { - $result = $defaultValue; - } - - return $result; - } - - protected function getScopeWithAclList(): array - { - $scopeList = []; - - $scopes = $this->metadata->get('scopes'); - - foreach ($scopes as $scope => $d) { - if (empty($d['acl'])) { - continue; - } - - $scopeList[] = $scope; - } - - return $scopeList; - } - - protected function getScopeList(): array - { - $scopeList = []; - - $scopes = $this->metadata->get('scopes'); - - foreach ($scopes as $scope => $d) { - $scopeList[] = $scope; - } - - return $scopeList; - } - - private function mergeTableList(array $tableList): StdClass - { - $data = (object) []; - - $scopeList = $this->getScopeWithAclList(); - - foreach ($tableList as $table) { - foreach ($scopeList as $scope) { - if (!isset($table->$scope)) { - continue; - } - - $this->mergeTableListItem($data, $scope, $table->$scope); - } - } - - return $data; - } - - private function mergeTableListItem(StdClass $data, string $scope, $row): void - { - if ($row === false || $row === null) { - if (!isset($data->$scope)) { - $data->$scope = false; - } - - return; - } - - if ($row === true) { - $data->$scope = true; - - return; - } - - if (!isset($data->$scope)) { - $data->$scope = (object) []; - } - - if ($data->$scope === false) { - $data->$scope = (object) []; - } - - if (!is_object($row)) { - return; - } - - $actionList = $this->metadata - ->get(['scopes', $scope, $this->type . 'ActionList'], $this->actionList); - - foreach ($actionList as $i => $action) { - if (isset($row->$action)) { - $level = $row->$action; - - if (!isset($data->$scope->$action)) { - $data->$scope->$action = $level; - } - else if ( - array_search($data->$scope->$action, $this->levelList) > - array_search($level, $this->levelList) - ) { - $data->$scope->$action = $level; - } - - continue; - } - - if ($i === 0) { - continue; - } - - // @todo Remove everything below. - $previousAction = $this->actionList[$i - 1]; - - if (in_array($action, $this->booleanActionList)) { - $data->$scope->$action = self::LEVEL_YES; - } - else if ($action === self::ACTION_STREAM && isset($data->$scope->$previousAction)) { - $data->$scope->$action = $data->$scope->$previousAction; - } - } - } - - private function mergeFieldTableList(array $tableList): StdClass - { - $data = (object) []; - - $scopeList = $this->getScopeWithAclList(); - - foreach ($tableList as $table) { - foreach ($scopeList as $scope) { - if (!isset($table->$scope)) { - continue; - } - - if (!isset($data->$scope)) { - $data->$scope = (object) []; - } - - if (!is_object($table->$scope)) { - continue; - } - - $fieldList = array_keys($this->metadata->get("entityDefs.{$scope}.fields", [])); - - foreach (get_object_vars($table->$scope) as $field => $row) { - if (!is_object($row)) { - continue; - } - - if (!in_array($field, $fieldList)) { - continue; - } - - if (!isset($data->$scope->$field)) { - $data->$scope->$field = (object) []; - } - - foreach ($this->fieldActionList as $i => $action) { - if (!isset($row->$action)) { - continue; - } - - $level = $row->$action; - - if (!isset($data->$scope->$field->$action)) { - $data->$scope->$field->$action = $level; - } - else { - if ( - array_search( - $data->$scope->$field->$action, - $this->fieldLevelList - ) > array_search($level, $this->fieldLevelList) - ) { - $data->$scope->$field->$action = $level; - } - } - } - } - } - } - - return $data; - } - - private function buildCache(): void - { - $this->dataCache->store($this->cacheKey, $this->data); - } + 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 + */ + 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 + */ + public function getScopeForbiddenFieldList(string $scope, string $action, string $thresholdLevel): array; } diff --git a/application/Espo/Core/Acl/GlobalRestrictonFactory.php b/application/Espo/Core/Acl/TableFactory.php similarity index 80% rename from application/Espo/Core/Acl/GlobalRestrictonFactory.php rename to application/Espo/Core/Acl/TableFactory.php index dcf05b5a7d..af4c8515cf 100644 --- a/application/Espo/Core/Acl/GlobalRestrictonFactory.php +++ b/application/Espo/Core/Acl/TableFactory.php @@ -29,27 +29,30 @@ namespace Espo\Core\Acl; +use Espo\Entities\User; + use Espo\Core\{ - Utils\Config, InjectableFactory, }; -class GlobalRestrictonFactory +class TableFactory { - private $config; - private $injectableFactory; - public function __construct(Config $config, InjectableFactory $injectableFactory) + public function __construct(InjectableFactory $injectableFactory) { - $this->config = $config; $this->injectableFactory = $injectableFactory; } - public function create(): GlobalRestricton + /** + * Create a table. + * + * @todo Use binding. + */ + public function create(User $user): Table { - return $this->injectableFactory->createWith(GlobalRestricton::class, [ - 'useCache' => $this->config->get('useCache'), + return $this->injectableFactory->createWith(DefaultTable::class, [ + 'user' => $user, ]); } } diff --git a/application/Espo/Core/Acl/Traits/DefaultAccessCheckerDependency.php b/application/Espo/Core/Acl/Traits/DefaultAccessCheckerDependency.php new file mode 100644 index 0000000000..91b7b1c8b8 --- /dev/null +++ b/application/Espo/Core/Acl/Traits/DefaultAccessCheckerDependency.php @@ -0,0 +1,102 @@ +defaultAccessChecker->check($user, $data); + } + + public function checkCreate(User $user, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkCreate($user, $data); + } + + public function checkRead(User $user, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkRead($user, $data); + } + + public function checkEdit(User $user, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkEdit($user, $data); + } + + public function checkDelete(User $user, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkDelete($user, $data); + } + + public function checkStream(User $user, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkStream($user, $data); + } + + public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkEntityCreate($user, $entity, $data); + } + + public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkEntityRead($user, $entity, $data); + } + + public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkEntityEdit($user, $entity, $data); + } + + public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkEntityDelete($user, $entity, $data); + } + + public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool + { + return $this->defaultAccessChecker->checkEntityStream($user, $entity, $data); + } +} diff --git a/application/Espo/Core/AclManager.php b/application/Espo/Core/AclManager.php index 2f145d9321..66442021af 100644 --- a/application/Espo/Core/AclManager.php +++ b/application/Espo/Core/AclManager.php @@ -35,98 +35,114 @@ use Espo\Entities\User; use Espo\Core\{ ORM\EntityManager, - Acl\AclFactory, - Acl\GlobalRestrictonFactory, + Acl, Acl\GlobalRestricton, Acl\OwnerUserFieldProvider, - Acl as UserAclWrapper, - Acl\Table as Table, - Acl\ScopeAcl, - Acl\EntityAcl, - Acl\EntityCreateAcl, - Acl\EntityReadAcl, - Acl\EntityEditAcl, - Acl\EntityDeleteAcl, - Acl\EntityStreamAcl, - Acl\LevelProvider, + Acl\TableFactory, + Acl\Table, + Acl\OwnershipCheckerFactory, + Acl\OwnershipChecker, + Acl\OwnershipOwnChecker, + Acl\OwnershipTeamChecker, + Acl\AccessCheckerFactory, + Acl\AccessChecker, + Acl\AccessCreateChecker, + Acl\AccessReadChecker, + Acl\AccessEditChecker, + Acl\AccessDeleteChecker, + Acl\AccessStreamChecker, + Acl\AccessEntityCreateChecker, + Acl\AccessEntityReadChecker, + Acl\AccessEntityEditChecker, + Acl\AccessEntityDeleteChecker, + Acl\AccessEntityStreamChecker, Acl\Exceptions\NotImplemented, }; use StdClass; -use RuntimeException; +use InvalidArgumentException; /** - * Used to check access for a specific user. + * A central access point for access checking. */ class AclManager { - protected $implementationHashMap = []; + private $accessCheckerHashMap = []; - private $tableHashMap = []; + private $ownershipCheckerHashMap = []; - protected $tableClassName = Table::class; + protected $tableHashMap = []; - protected $userAclClassName = UserAclWrapper::class; + protected $userAclClassName = Acl::class; protected const PERMISSION_ASSIGNMENT = 'assignment'; - private $actionInterfaceMap = [ - Table::ACTION_CREATE => EntityCreateAcl::class, - Table::ACTION_READ => EntityReadAcl::class, - Table::ACTION_EDIT => EntityEditAcl::class, - Table::ACTION_DELETE => EntityDeleteAcl::class, - Table::ACTION_STREAM => EntityStreamAcl::class, + private $entityActionInterfaceMap = [ + Table::ACTION_CREATE => AccessEntityCreateChecker::class, + Table::ACTION_READ => AccessEntityReadChecker::class, + Table::ACTION_EDIT => AccessEntityEditChecker::class, + Table::ACTION_DELETE => AccessEntityDeleteChecker::class, + Table::ACTION_STREAM => AccessEntityStreamChecker::class, ]; - protected $injectableFactory; + private $actionInterfaceMap = [ + Table::ACTION_CREATE => AccessCreateChecker::class, + Table::ACTION_READ => AccessReadChecker::class, + Table::ACTION_EDIT => AccessEditChecker::class, + Table::ACTION_DELETE => AccessDeleteChecker::class, + Table::ACTION_STREAM => AccessStreamChecker::class, + ]; - protected $entityManager; + protected $accessCheckerFactory; - protected $aclFactory; + protected $ownershipCheckerFactory; + + protected $tableFactory; protected $globalRestricton; protected $ownerUserFieldProvider; - public function __construct( - InjectableFactory $injectableFactory, - EntityManager $entityManager, - AclFactory $aclFactory, - GlobalRestrictonFactory $globalRestrictonFactory, - OwnerUserFieldProvider $ownerUserFieldProvider - ) { - $this->injectableFactory = $injectableFactory; - $this->entityManager = $entityManager; - $this->aclFactory = $aclFactory; - $this->ownerUserFieldProvider = $ownerUserFieldProvider; + protected $entityManager; - $this->globalRestricton = $globalRestrictonFactory->create(); + public function __construct( + AccessCheckerFactory $accessCheckerFactory, + OwnershipCheckerFactory $ownershipCheckerFactory, + TableFactory $tableFactory, + GlobalRestricton $globalRestricton, + OwnerUserFieldProvider $ownerUserFieldProvider, + EntityManager $entityManager + ) { + $this->accessCheckerFactory = $accessCheckerFactory; + $this->ownershipCheckerFactory = $ownershipCheckerFactory; + $this->tableFactory = $tableFactory; + $this->globalRestricton = $globalRestricton; + $this->ownerUserFieldProvider = $ownerUserFieldProvider; + $this->entityManager = $entityManager; } /** - * Get an ACL implementation for a scope. - * - * @return ScopeAcl|EntityAcl|LevelProvider| - * EntityCreateAcl|EntityReadAcl|EntityEditAcl|EntityDeleteAcl|EntityStreamAcl + * Get an access checker for a specific scope. */ - public function getImplementation(string $scope): object + protected function getAccessChecker(string $scope): AccessChecker { - if (!array_key_exists($scope, $this->implementationHashMap)) { - $this->implementationHashMap[$scope] = $this->aclFactory->create($scope); + if (!array_key_exists($scope, $this->accessCheckerHashMap)) { + $this->accessCheckerHashMap[$scope] = $this->accessCheckerFactory->create($scope, $this); } - return $this->implementationHashMap[$scope]; + return $this->accessCheckerHashMap[$scope]; } - protected function getEntityImplementation(string $scope): EntityAcl + /** + * Get an ownership checker for a specific scope. + */ + protected function getOwnershipChecker(string $scope): OwnershipChecker { - $impl = $this->getImplementation($scope); - - if (!$impl instanceof EntityAcl) { - throw new RuntimeException("Acl must implement EntityAcl interface."); + if (!array_key_exists($scope, $this->ownershipCheckerHashMap)) { + $this->ownershipCheckerHashMap[$scope] = $this->ownershipCheckerFactory->create($scope, $this); } - return $impl; + return $this->ownershipCheckerHashMap[$scope]; } protected function getTable(User $user): Table @@ -138,14 +154,15 @@ class AclManager } if (!array_key_exists($key, $this->tableHashMap)) { - $this->tableHashMap[$key] = $this->injectableFactory->createWith($this->tableClassName, [ - 'user' => $user, - ]); + $this->tableHashMap[$key] = $this->tableFactory->create($user); } return $this->tableHashMap[$key]; } + /** + * Get a full access data map. + */ public function getMap(User $user): StdClass { return $this->getTable($user)->getMap(); @@ -153,30 +170,26 @@ class AclManager /** * Get an access level for a specific scope and action. + * + * @todo Use LevelProvider? */ public function getLevel(User $user, string $scope, string $action): string { $data = $this->getTable($user)->getScopeData($scope); - $impl = $this->getImplementation($scope); - - if ($impl instanceof LevelProvider) { - return $impl->getLevel($user, $data, $action); - } - return $data->get($action); } /** * Get a permission. E.g. 'assignment' permission. */ - public function get(User $user, string $permission): ?string + public function getPermissionLevel(User $user, string $permission): string { if (substr($permission, -10) !== 'Permission') { $permission .= 'Permission'; } - return $this->getTable($user)->get($permission); + return $this->getTable($user)->getPermissionLevel($permission); } /** @@ -212,9 +225,12 @@ class AclManager } /** - * Check a scope or entity. If $action is omitted, it will check whether a scope level is set to 'enabled'. + * Check a scope or entity. If $action is omitted, it will check whether + * a scope level is set to 'enabled'. * * @param string|Entity $subject An entity type or entity. + * + * @throws NotImplemented */ public function check(User $user, $subject, ?string $action = null): bool { @@ -230,13 +246,13 @@ class AclManager return $this->checkEntity($user, $entity, $action); } - return false; + throw new InvalidArgumentException(); } /** * Check access to a specific entity. * - * @throws NotImplemented If not implemented by Acl class. + * @throws NotImplemented */ public function checkEntity(User $user, Entity $entity, string $action = Table::ACTION_READ): bool { @@ -244,7 +260,7 @@ class AclManager $data = $this->getTable($user)->getScopeData($scope); - $impl = $this->getImplementation($scope); + $checker = $this->getAccessChecker($scope); if (!$action) { $action = Table::ACTION_READ; @@ -252,28 +268,29 @@ class AclManager $methodName = 'checkEntity' . ucfirst($action); - $interface = $this->actionInterfaceMap[$action] ?? null; + $interface = $this->entityActionInterfaceMap[$action] ?? null; - if ($interface && $impl instanceof $interface) { - return $impl->$methodName($user, $entity, $data); + if ($interface && $checker instanceof $interface) { + return $checker->$methodName($user, $entity, $data); } - if (!$impl instanceof EntityAcl) { - throw new NotImplemented("Acl for '{$scope}' does not implement EntityAcl interface."); - } - - if (method_exists($impl, $methodName)) { + if (method_exists($checker, $methodName)) { // For backward compatibility. - return $impl->$methodName($user, $entity, $data); + return $checker->$methodName($user, $entity, $data); } - return $impl->checkEntity($user, $entity, $data, $action); + if (method_exists($checker, 'checkEntity')) { + // For backward compatibility. + return $checker->checkEntity($user, $entity, $data, $action); + } + + throw new NotImplemented("No entity access checker for '{$scope}' action '{$action}'."); } /** * Check 'read' access to a specific entity. * - * @throws NotImplemented If not implemented by Acl class. + * @throws NotImplemented. */ public function checkEntityRead(User $user, Entity $entity): bool { @@ -283,7 +300,7 @@ class AclManager /** * Check 'create' access to a specific entity. * - * @throws NotImplemented If not implemented by Acl class. + * @throws NotImplemented */ public function checkEntityCreate(User $user, Entity $entity): bool { @@ -293,7 +310,7 @@ class AclManager /** * Check 'edit' access to a specific entity. * - * @throws NotImplemented If not implemented by Acl class. + * @throws NotImplemented */ public function checkEntityEdit(User $user, Entity $entity): bool { @@ -303,7 +320,7 @@ class AclManager /** * Check 'delete' access to a specific entity. * - * @throws NotImplemented If not implemented by Acl class. + * @throws NotImplemented */ public function checkEntityDelete(User $user, Entity $entity): bool { @@ -313,7 +330,7 @@ class AclManager /** * Check 'stream' access to a specific entity. * - * @throws NotImplemented If not implemented by Acl class. + * @throws NotImplemented */ public function checkEntityStream(User $user, Entity $entity): bool { @@ -321,81 +338,64 @@ class AclManager } /** - * Whether a user is owned of an entity (record). Usually 'assignedUser' field is used for checking. + * Check whether a user is an owner of an entity. */ - public function checkIsOwner(User $user, Entity $entity): bool + public function checkOwnershipOwn(User $user, Entity $entity): bool { - return (bool) $this->getEntityImplementation($entity->getEntityType())->checkIsOwner($user, $entity); + $checker = $this->getOwnershipChecker($entity->getEntityType()); + + if (!$checker instanceof OwnershipOwnChecker) { + return false; + } + + return $checker->checkOwn($user, $entity); } /** - * Whether a user team list overlaps with teams set in an entity. + * Check whether an entity belongs to a user team. */ - public function checkInTeam(User $user, Entity $entity): bool + public function checkOwnershipTeam(User $user, Entity $entity): bool { - return (bool) $this->getEntityImplementation($entity->getEntityType())->checkInTeam($user, $entity); + $checker = $this->getOwnershipChecker($entity->getEntityType()); + + if (!$checker instanceof OwnershipTeamChecker) { + return false; + } + + return $checker->checkTeam($user, $entity); } /** * Check access to scope. If $action is omitted, it will check whether a scope level is set to 'enabled'. * - * @throws NotImplemented If not implemented by Acl class. + * @throws NotImplemented If not implemented by an access checker class. */ public function checkScope(User $user, string $scope, ?string $action = null): bool { $data = $this->getTable($user)->getScopeData($scope); - $impl = $this->getImplementation($scope); + $checker = $this->getAccessChecker($scope); - if (!$impl instanceof ScopeAcl) { - throw new NotImplemented("Acl for '{$scope}' does not implement ScopeAcl interface."); + if (!$action) { + return $checker->check($user, $data); } - return $impl->checkScope($user, $data, $action); - } + $methodName = 'check' . ucfirst($action); - /** - * @deprecated Use checkUserPermission instead. - */ - public function checkUser(User $user, string $permission, User $target): bool - { - if ($this->get($user, $permission) === Table::LEVEL_ALL) { - return true; + $interface = $this->actionInterfaceMap[$action] ?? null; + + if ($interface && $checker instanceof $interface) { + return $checker->$methodName($user, $data); } - if ($this->get($user, $permission) === Table::LEVEL_NO) { - if ($target->id === $user->id) { - return true; - } + // For backward compatibility. + $methodName = 'checkScope'; - return false; + if (!method_exists($checker, $methodName)) { + throw new NotImplemented("No access checker for '{$scope}' action '{$action}'."); } - if ($this->get($user, $permission) === Table::LEVEL_TEAM) { - if ($target->id === $user->id) { - return true; - } - - $targetTeamIdList = $target->getTeamIdList(); - - $inTeam = false; - - foreach ($user->getTeamIdList() as $id) { - if (in_array($id, $targetTeamIdList)) { - $inTeam = true; - - break; - } - } - - if ($inTeam) { - return true; - } - - return false; - } - - return false; + return $checker->$methodName($user, $data, $action); } protected function getGlobalRestrictionTypeList(User $user, string $action = Table::ACTION_READ): array @@ -426,6 +426,7 @@ class AclManager /** * Get attributes forbidden for a user. * + * @param $thresholdLevel Should not be used. Stands for possible future enhancements. * @return array */ public function getScopeForbiddenAttributeList( @@ -435,30 +436,25 @@ class AclManager string $thresholdLevel = Table::LEVEL_NO ): array { - $list = []; + $list = array_merge( + $this->getTable($user)->getScopeForbiddenAttributeList( + $scope, + $action, + $thresholdLevel + ), + $this->getScopeRestrictedAttributeList( + $scope, + $this->getGlobalRestrictionTypeList($user, $action) + ) + ); - if (!$user->isAdmin()) { - $list = $this->getTable($user)->getScopeForbiddenAttributeList($scope, $action, $thresholdLevel); - } - - if ($thresholdLevel === Table::LEVEL_NO) { - $list = array_merge( - $list, - $this->getScopeRestrictedAttributeList( - $scope, - $this->getGlobalRestrictionTypeList($user, $action) - ) - ); - - $list = array_values($list); - } - - return $list; + return array_unique($list); } /** * Get fields forbidden for a user. * + * @param $thresholdLevel Should not be used. Stands for possible future enhancements. * @return array */ public function getScopeForbiddenFieldList( @@ -468,30 +464,25 @@ class AclManager string $thresholdLevel = Table::LEVEL_NO ): array { - $list = []; + $list = array_merge( + $this->getTable($user)->getScopeForbiddenFieldList( + $scope, + $action, + $thresholdLevel + ), + $this->getScopeRestrictedFieldList( + $scope, + $this->getGlobalRestrictionTypeList($user, $action) + ) + ); - if (!$user->isAdmin()) { - $list = $this->getTable($user)->getScopeForbiddenFieldList($scope, $action, $thresholdLevel); - } - - if ($thresholdLevel === Table::LEVEL_NO) { - $list = array_merge( - $list, - $this->getScopeRestrictedFieldList( - $scope, - $this->getGlobalRestrictionTypeList($user, $action) - ) - ); - - $list = array_values($list); - } - - return $list; + return array_unique($list); } /** * Get links forbidden for a user. * + * @param $thresholdLevel Should not be used. Stands for possible future enhancements. * @return array */ public function getScopeForbiddenLinkList( @@ -501,21 +492,10 @@ class AclManager string $thresholdLevel = Table::LEVEL_NO ): array { - $list = []; - - if ($thresholdLevel === Table::LEVEL_NO) { - $list = array_merge( - $list, - $this->getScopeRestrictedLinkList( - $scope, - $this->getGlobalRestrictionTypeList($user, $action) - ) - ); - - $list = array_values($list); - } - - return $list; + return $this->getScopeRestrictedLinkList( + $scope, + $this->getGlobalRestrictionTypeList($user, $action) + ); } /** @@ -574,7 +554,7 @@ class AclManager /** * Create a wrapper for a specific user. */ - public function createUserAcl(User $user): UserAclWrapper + public function createUserAcl(User $user): Acl { $className = $this->userAclClassName; @@ -597,10 +577,13 @@ class AclManager $list = []; foreach ($typeList as $type) { - $list = array_merge($list, $this->globalRestricton->getScopeRestrictedFieldList($scope, $type)); + $list = array_merge( + $list, + $this->globalRestricton->getScopeRestrictedFieldList($scope, $type) + ); } - return array_values($list); + return array_unique($list); } return $this->globalRestricton->getScopeRestrictedFieldList($scope, $type); @@ -620,10 +603,13 @@ class AclManager $list = []; foreach ($typeList as $type) { - $list = array_merge($list, $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type)); + $list = array_merge( + $list, + $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type) + ); } - return array_values($list); + return array_unique($list); } return $this->globalRestricton->getScopeRestrictedAttributeList($scope, $type); @@ -643,10 +629,13 @@ class AclManager $list = []; foreach ($typeList as $type) { - $list = array_merge($list, $this->globalRestricton->getScopeRestrictedLinkList($scope, $type)); + $list = array_merge( + $list, + $this->globalRestricton->getScopeRestrictedLinkList($scope, $type) + ); } - return array_values($list); + return array_unique($list); } return $this->globalRestricton->getScopeRestrictedLinkList($scope, $type); @@ -660,4 +649,80 @@ class AclManager { return $this->ownerUserFieldProvider->get($entityType); } + + /** + * @deprecated User `checkOwnershipOwn`. + */ + public function checkIsOwner(User $user, Entity $entity): bool + { + return $this->checkOwnershipOwn($user, $entity); + } + + /** + * @deprecated User `checkOwnershipTeam`. + */ + public function checkInTeam(User $user, Entity $entity): bool + { + return $this->checkOwnershipTeam($user, $entity); + } + + /** + * @deprecated + */ + public function getImplementation(string $scope): object + { + return $this->getAccessChecker($scope); + } + + /** + * @deprecated Use `getPermissionLevel` instead. + */ + public function get(User $user, string $permission): string + { + return $this->getPermissionLevel($user, $permission); + } + + /** + * @deprecated Use `checkUserPermission` instead. + */ + public function checkUser(User $user, string $permission, User $target): bool + { + if ($this->getPermissionLevel($user, $permission) === Table::LEVEL_ALL) { + return true; + } + + if ($this->getPermissionLevel($user, $permission) === Table::LEVEL_NO) { + if ($target->getId() === $user->getId()) { + return true; + } + + return false; + } + + if ($this->get($user, $permission) === Table::LEVEL_TEAM) { + if ($target->getId() === $user->getId()) { + return true; + } + + $targetTeamIdList = $target->getTeamIdList(); + + $inTeam = false; + + foreach ($user->getTeamIdList() as $id) { + if (in_array($id, $targetTeamIdList)) { + $inTeam = true; + + break; + } + } + + if ($inTeam) { + return true; + } + + return false; + } + + return false; + } } diff --git a/application/Espo/Core/AclPortal/Acl.php b/application/Espo/Core/AclPortal/Acl.php index 335372c6e9..9e6123d32d 100644 --- a/application/Espo/Core/AclPortal/Acl.php +++ b/application/Espo/Core/AclPortal/Acl.php @@ -29,9 +29,10 @@ namespace Espo\Core\AclPortal; -use Espo\Core\Acl\Acl as BaseAcl; - -class Acl extends BaseAcl implements PortalScopeAcl +/** + * @deprecated Use AccessChecker interfaces instead. + */ +class Acl extends Base { - use Portal; + } diff --git a/application/Espo/Core/AclPortal/Base.php b/application/Espo/Core/AclPortal/Base.php index a04cbdfa8c..ecbf76bace 100644 --- a/application/Espo/Core/AclPortal/Base.php +++ b/application/Espo/Core/AclPortal/Base.php @@ -29,9 +29,177 @@ namespace Espo\Core\AclPortal; +use Espo\Core\Interfaces\Injectable; -/** @deprecated */ -class Base extends \Espo\Core\Acl\Base implements PortalScopeAcl +use Espo\ORM\Entity; + +use Espo\Entities\User; + +use Espo\Core\{ + ORM\EntityManager, + Portal\AclManager as PortalAclManager, + Utils\Config, + Acl\AccessChecker, + Acl\ScopeData, + Portal\Acl\Table, + Portal\Acl\ScopeChecker, + Portal\Acl\ScopeCheckerData, + Portal\Acl\DefaultAccessChecker, +}; + +/** + * @deprecated Use AccessChecker interfaces instead. + */ +class Base implements AccessChecker, Injectable { - use Portal; + protected $dependencyList = []; + + protected $dependencies = []; + + protected $injections = []; + + protected $entityManager; + + protected $aclManager; + + protected $config; + + protected $scopeChecker; + + protected $defaultChecker; + + public function __construct( + EntityManager $entityManager, + PortalAclManager $aclManager, + Config $config, + ScopeChecker $scopeChecker, + DefaultAccessChecker $defaultChecker + ) { + $this->entityManager = $entityManager; + $this->aclManager = $aclManager; + $this->config = $config; + $this->scopeChecker = $scopeChecker; + $this->defaultChecker = $defaultChecker; + + $this->init(); + } + + public function check(User $user, ScopeData $data): bool + { + return $this->defaultChecker->check($user, $data); + } + + public function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwnChecker( + function () use ($user, $entity): bool { + return (bool) $this->checkIsOwner($user, $entity); + } + ) + ->setInAccountChecker( + function () use ($user, $entity): bool { + return (bool) $this->checkInAccount($user, $entity); + } + ) + ->setInContactChecker( + function () use ($user, $entity): bool { + return (bool) $this->checkIsOwnContact($user, $entity); + } + ) + ->build(); + + return $this->scopeChecker->check($data, $action, $checkerData); + } + + public function checkScope(User $user, ScopeData $data, ?string $action = null): bool + { + if (!$action) { + return $this->defaultChecker->check($user, $data); + } + + if ($action === Table::ACTION_CREATE) { + return $this->defaultChecker->checkCreate($user, $data); + } + + if ($action === Table::ACTION_READ) { + return $this->defaultChecker->checkRead($user, $data); + } + + if ($action === Table::ACTION_EDIT) { + return $this->defaultChecker->checkEdit($user, $data); + } + + if ($action === Table::ACTION_DELETE) { + return $this->defaultChecker->checkDelete($user, $data); + } + + if ($action === Table::ACTION_STREAM) { + return $this->defaultChecker->checkStream($user, $data); + } + + return false; + } + + public function checkIsOwner(User $user, Entity $entity) + { + return $this->aclManager->checkOwnershipOwn($user, $entity); + } + + public function checkInAccount(User $user, Entity $entity) + { + return $this->aclManager->checkOwnershipAccount($user, $entity); + } + + public function checkIsOwnContact(User $user, Entity $entity) + { + return $this->aclManager->checkOwnershipContact($user, $entity); + } + + protected function getConfig(): Config + { + return $this->config; + } + + protected function getEntityManager(): EntityManager + { + return $this->entityManager; + } + + protected function getAclManager(): AclManager + { + return $this->aclManager; + } + + public function inject($name, $object) + { + $this->injections[$name] = $object; + } + + protected function init() + { + } + + protected function getInjection($name) + { + return $this->injections[$name] ?? $this->$name ?? null; + } + + protected function addDependencyList(array $list) + { + foreach ($list as $item) { + $this->addDependency($item); + } + } + + protected function addDependency($name) + { + $this->dependencyList[] = $name; + } + + public function getDependencyList() + { + return array_merge($this->dependencyList, $this->dependencies); + } } diff --git a/application/Espo/Core/AclPortal/Portal.php b/application/Espo/Core/AclPortal/Portal.php deleted file mode 100644 index 6f29522923..0000000000 --- a/application/Espo/Core/AclPortal/Portal.php +++ /dev/null @@ -1,222 +0,0 @@ -isFalse()) { - return false; - } - - if ($data->isTrue()) { - return true; - } - - $isOwner = null; - - if (isset($entityAccessData['isOwner'])) { - $isOwner = $entityAccessData['isOwner']; - } - - $inAccount = null; - - if (isset($entityAccessData['inAccount'])) { - $inAccount = $entityAccessData['inAccount']; - } - - $isOwnContact = null; - - if (isset($entityAccessData['isOwnContact'])) { - $isOwnContact = $entityAccessData['isOwnContact']; - } - - if (is_null($action)) { - return true; - } - - $value = $data->get($action); - - if ($value === Table::LEVEL_ALL || $value === Table::LEVEL_YES) { - return true; - } - - if (!$value || $value === Table::LEVEL_NO) { - return false; - } - - if (is_null($isOwner)) { - if ($entity) { - $isOwner = $this->checkIsOwner($user, $entity); - } - else { - return true; - } - } - - if ($isOwner) { - if ($value === Table::LEVEL_OWN || $value === Table::LEVEL_ACCOUNT || $value === Table::LEVEL_CONTACT) { - return true; - } - } - - if ($value === Table::LEVEL_ACCOUNT) { - if (is_null($inAccount) && $entity) { - $inAccount = $this->checkInAccount($user, $entity); - } - - if ($inAccount) { - return true; - } - - if (is_null($isOwnContact) && $entity) { - $isOwnContact = $this->checkIsOwnContact($user, $entity); - } - - if ($isOwnContact) { - return true; - } - } - - if ($value === Table::LEVEL_CONTACT) { - if (is_null($isOwnContact) && $entity) { - $isOwnContact = $this->checkIsOwnContact($user, $entity); - } - - if ($isOwnContact) { - return true; - } - } - - return false; - } - - /** - * @return bool - */ - public function checkIsOwner(User $user, Entity $entity) - { - if ($entity->hasAttribute('createdById')) { - if ($entity->has('createdById') && $user->id === $entity->get('createdById')) { - return true; - } - } - - return false; - } - - /** - * @return bool - */ - public function checkInAccount(User $user, Entity $entity) - { - $accountIdList = $user->getLinkMultipleIdList('accounts'); - - if (!count($accountIdList)) { - return false; - } - - if ($entity->hasAttribute('accountId') && $entity->getRelationParam('account', 'entity') === 'Account') { - if (in_array($entity->get('accountId'), $accountIdList)) { - return true; - } - } - - if ($entity->hasRelation('accounts') && $entity->getRelationParam('accounts', 'entity') === 'Account') { - $repository = $this->getEntityManager()->getRepository($entity->getEntityType()); - - foreach ($accountIdList as $accountId) { - if ($repository->isRelated($entity, 'accounts', $accountId)) { - return true; - } - } - } - - if ($entity->hasAttribute('parentId') && $entity->hasRelation('parent')) { - if ( - $entity->get('parentType') === 'Account' && - in_array($entity->get('parentId'), $accountIdList) - ) { - return true; - } - } - - return false; - } - - /** - * @return bool - */ - public function checkIsOwnContact(User $user, Entity $entity) - { - $contactId = $user->get('contactId'); - - if (!$contactId) { - return false; - } - - if ($entity->hasAttribute('contactId') && $entity->getRelationParam('contact', 'entity') === 'Contact') { - if ($entity->get('contactId') === $contactId) { - return true; - } - } - - if ($entity->hasRelation('contacts') && $entity->getRelationParam('contacts', 'entity') === 'Contact') { - $repository = $this->getEntityManager()->getRepository($entity->getEntityType()); - - if ($repository->isRelated($entity, 'contacts', $contactId)) { - return true; - } - } - - if ($entity->hasAttribute('parentId') && $entity->hasRelation('parent')) { - if ($entity->get('parentType') === 'Contact' && $entity->get('parentId') === $contactId) { - return true; - } - } - - return false; - } -} diff --git a/application/Espo/Core/Portal/Acl.php b/application/Espo/Core/Portal/Acl.php index 3c1f6f6c1c..05ce8f52c7 100644 --- a/application/Espo/Core/Portal/Acl.php +++ b/application/Espo/Core/Portal/Acl.php @@ -31,27 +31,62 @@ namespace Espo\Core\Portal; use Espo\ORM\Entity; +use Espo\Entities\User; + use Espo\Core\Acl as BaseAcl; class Acl extends BaseAcl { + public function __construct(AclManager $aclManager, User $user) + { + parent::__construct($aclManager, $user); + } + + /** + * Whether 'read' access is set to 'account' for a specific scope. + */ public function checkReadOnlyAccount(string $scope): bool { return $this->aclManager->checkReadOnlyAccount($this->user, $scope); } + /** + * Whether 'read' access is set to 'contact' for a specific scope. + */ public function checkReadOnlyContact(string $scope): bool { return $this->aclManager->checkReadOnlyContact($this->user, $scope); } - public function checkInAccount(Entity $entity): bool + /** + * Check whether an entity belongs to a user account. + */ + public function checkOwnershipAccount(Entity $entity): bool { - return $this->aclManager->checkInAccount($this->user, $entity); + return $this->aclManager->checkOwnershipAccount($this->user, $entity); } + /** + * Check whether an entity belongs to a user contact. + */ + public function checkOwnershipContact(Entity $entity): bool + { + return $this->aclManager->checkOwnershipContact($this->user, $entity); + } + + /** + * @deprecate + */ + public function checkInAccount(Entity $entity): bool + { + return $this->checkOwnershipAccount($entity); + } + + /** + * @deprecate + */ public function checkIsOwnContact(Entity $entity): bool { - return $this->aclManager->checkIsOwnContact($this->user, $entity); + return $this->checkOwnershipContact($entity); } } diff --git a/application/Espo/Core/Portal/Acl/AccessCheckerFactory.php b/application/Espo/Core/Portal/Acl/AccessCheckerFactory.php new file mode 100644 index 0000000000..6528d0c58c --- /dev/null +++ b/application/Espo/Core/Portal/Acl/AccessCheckerFactory.php @@ -0,0 +1,115 @@ +classFinder = $classFinder; + $this->metadata = $metadata; + $this->injectableFactory = $injectableFactory; + } + + /** + * Create an access checker. + * + * @throws NotImplemented + */ + public function create(string $scope, PortalAclManager $aclManager): AccessChecker + { + $className = $this->getClassName($scope); + + $bindingContainer = $this->createBindingContainer($aclManager); + + return $this->injectableFactory->createWithBinding($className, $bindingContainer); + } + + private function getClassName(string $scope): string + { + $className1 = $this->metadata->get(['aclDefs', $scope, 'portalAccessCheckerClassName']); + + if ($className1) { + return $className1; + } + + if (!$this->metadata->get(['scopes', $scope])) { + throw new NotImplemented(); + } + + // For backward compatibility. + $className2 = $this->classFinder->find('AclPortal', $scope); + + if ($className2) { + return $className2; + } + + return $this->defaultClassName; + } + + private function createBindingContainer(PortalAclManager $aclManager): BindingContainer + { + $bindingData = new BindingData(); + + $binder = new Binder($bindingData); + + $binder->bindCallback( + PortalAclManager::class, + function () use ($aclManager): PortalAclManager { + return $aclManager; + } + ); + + return new BindingContainer($bindingData); + } +} diff --git a/application/Espo/Core/Portal/Acl/DefaultAccessChecker.php b/application/Espo/Core/Portal/Acl/DefaultAccessChecker.php new file mode 100644 index 0000000000..cff492ec4b --- /dev/null +++ b/application/Espo/Core/Portal/Acl/DefaultAccessChecker.php @@ -0,0 +1,164 @@ +aclManager = $aclManager; + $this->config = $config; + $this->scopeChecker = $scopeChecker; + } + + private function checkEntity(User $user, Entity $entity, ScopeData $data, string $action): bool + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwnChecker( + function () use ($user, $entity): bool { + return $this->aclManager->checkOwnershipOwn($user, $entity); + } + ) + ->setInAccountChecker( + function () use ($user, $entity): bool { + return $this->aclManager->checkOwnershipAccount($user, $entity); + } + ) + ->setInContactChecker( + function () use ($user, $entity): bool { + return $this->aclManager->checkOwnershipContact($user, $entity); + } + ) + ->build(); + + return $this->scopeChecker->check($data, $action, $checkerData); + } + + private function checkScope(User $user, ScopeData $data, ?string $action = null): bool + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInAccount(true) + ->setInContact(true) + ->build(); + + return $this->scopeChecker->check($data, $action, $checkerData); + } + + public function check(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data); + } + + public function checkCreate(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_CREATE); + } + + public function checkRead(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_READ); + } + + public function checkEdit(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_EDIT); + } + + public function checkDelete(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_DELETE); + } + + public function checkStream(User $user, ScopeData $data): bool + { + return $this->checkScope($user, $data, Table::ACTION_STREAM); + } + + public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_CREATE); + } + + public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_READ); + } + + public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_EDIT); + } + + public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_STREAM); + } + + public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool + { + return $this->checkEntity($user, $entity, $data, Table::ACTION_DELETE); + } +} diff --git a/application/Espo/Core/Portal/Acl/DefaultOwnershipChecker.php b/application/Espo/Core/Portal/Acl/DefaultOwnershipChecker.php new file mode 100644 index 0000000000..97c51ce6a3 --- /dev/null +++ b/application/Espo/Core/Portal/Acl/DefaultOwnershipChecker.php @@ -0,0 +1,178 @@ +entityManager = $entityManager; + } + + public function checkOwn(User $user, Entity $entity): bool + { + if ($entity->hasAttribute(self::ATTR_CREATED_BY_ID)) { + if ( + $entity->has(self::ATTR_CREATED_BY_ID) && + $user->getId() === $entity->get(self::ATTR_CREATED_BY_ID) + ) { + return true; + } + } + + return false; + } + + public function checkAccount(User $user, Entity $entity): bool + { + $accountIdList = $user->getLinkMultipleIdList(self::FIELD_ACCOUNTS); + + if (!count($accountIdList)) { + return false; + } + + if ( + $entity->hasAttribute(self::ATTR_ACCOUNT_ID) && + $entity->getRelationParam(self::FIELD_ACCOUNT, 'entity') === self::ENTITY_ACCOUNT + ) { + if (in_array($entity->get(self::ATTR_ACCOUNT_ID), $accountIdList)) { + return true; + } + } + + if ( + $entity->hasRelation(self::FIELD_ACCOUNTS) && + $entity->getRelationParam(self::FIELD_ACCOUNTS, 'entity') === self::ENTITY_ACCOUNT + ) { + $repository = $this->entityManager->getRepository($entity->getEntityType()); + + foreach ($accountIdList as $accountId) { + if ($repository->isRelated($entity, self::FIELD_ACCOUNTS, $accountId)) { + return true; + } + } + } + + if ($entity->hasAttribute(self::ATTR_PARENT_ID) && $entity->hasRelation(self::FIELD_PARENT)) { + if ( + $entity->get(self::ATTR_PARENT_TYPE) === self::ENTITY_ACCOUNT && + in_array($entity->get(self::ATTR_PARENT_ID), $accountIdList) + ) { + return true; + } + } + + return false; + } + + public function checkContact(User $user, Entity $entity): bool + { + $contactId = $user->get(self::ATTR_CONTACT_ID); + + if (!$contactId) { + return false; + } + + if ( + $entity->hasAttribute(self::ATTR_CONTACT_ID) && + $entity->getRelationParam(self::FIELD_CONTACT, 'entity') === self::ENTITY_CONTACT + ) { + if ($entity->get(self::ATTR_CONTACT_ID) === $contactId) { + return true; + } + } + + if ( + $entity->hasRelation(self::FIELD_CONTACTS) && + $entity->getRelationParam(self::FIELD_CONTACTS, 'entity') === self::ENTITY_CONTACT + ) { + $repository = $this->entityManager->getRepository($entity->getEntityType()); + + if ($repository->isRelated($entity, self::FIELD_CONTACTS, $contactId)) { + return true; + } + } + + if ($entity->hasAttribute(self::ATTR_PARENT_ID) && $entity->hasRelation(self::FIELD_PARENT)) { + if ( + $entity->get(self::ATTR_PARENT_TYPE) === self::ENTITY_CONTACT && + $entity->get(self::ATTR_PARENT_ID) === $contactId + ) { + return true; + } + } + + return false; + } +} diff --git a/application/Espo/Core/Portal/Acl/OwnershipAccountChecker.php b/application/Espo/Core/Portal/Acl/OwnershipAccountChecker.php new file mode 100644 index 0000000000..8dbc56edc5 --- /dev/null +++ b/application/Espo/Core/Portal/Acl/OwnershipAccountChecker.php @@ -0,0 +1,44 @@ +metadata = $metadata; + $this->injectableFactory = $injectableFactory; + } + + /** + * Create an ownership checker. + * + * @throws NotImplemented + */ + public function create(string $scope, PortalAclManager $aclManager): OwnershipChecker + { + $className = $this->getClassName($scope); + + $bindingContainer = $this->createBindingContainer($aclManager); + + return $this->injectableFactory->createWithBinding($className, $bindingContainer); + } + + private function getClassName(string $scope): string + { + $className = $this->metadata->get(['aclDefs', $scope, 'portalOwnershipCheckerClassName']); + + if ($className) { + return $className; + } + + if (!$this->metadata->get(['scopes', $scope])) { + throw new NotImplemented(); + } + + return $this->defaultClassName; + } + + private function createBindingContainer(PortalAclManager $aclManager): BindingContainer + { + $bindingData = new BindingData(); + + $binder = new Binder($bindingData); + + $binder->bindCallback( + PortalAclManager::class, + function () use ($aclManager): PortalAclManager { + return $aclManager; + } + ); + + return new BindingContainer($bindingData); + } +} diff --git a/application/Espo/Core/Portal/Acl/OwnershipContactChecker.php b/application/Espo/Core/Portal/Acl/OwnershipContactChecker.php new file mode 100644 index 0000000000..70a2d54d7f --- /dev/null +++ b/application/Espo/Core/Portal/Acl/OwnershipContactChecker.php @@ -0,0 +1,44 @@ +isFalse()) { + return false; + } + + if ($data->isTrue()) { + return true; + } + + if ($action === null) { + if ($data->hasNotNo()) { + return true; + } + + return false; + } + + $level = $data->get($action); + + if ($level === Table::LEVEL_ALL || $level === Table::LEVEL_YES) { + return true; + } + + if ($level === Table::LEVEL_NO) { + return false; + } + + if (!$checkerData) { + return false; + } + + if ($level === Table::LEVEL_OWN || $level === Table::LEVEL_ACCOUNT || $level === Table::LEVEL_CONTACT) { + if ($checkerData->isOwn()) { + return true; + } + } + + if ($level === Table::LEVEL_ACCOUNT || $level === Table::LEVEL_CONTACT) { + if ($checkerData->inContact()) { + return true; + } + } + + if ($level === Table::LEVEL_ACCOUNT) { + if ($checkerData->inAccount()) { + return true; + } + } + + return false; + } +} diff --git a/application/Espo/Core/AclPortal/AclFactory.php b/application/Espo/Core/Portal/Acl/ScopeCheckerData.php similarity index 62% rename from application/Espo/Core/AclPortal/AclFactory.php rename to application/Espo/Core/Portal/Acl/ScopeCheckerData.php index 12a8833215..15401de074 100644 --- a/application/Espo/Core/AclPortal/AclFactory.php +++ b/application/Espo/Core/Portal/Acl/ScopeCheckerData.php @@ -27,37 +27,43 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\AclPortal; +namespace Espo\Core\Portal\Acl; -use Espo\Core\{ - Utils\ClassFinder, - InjectableFactory, -}; - -class AclFactory +/** + * Scope checker data. + */ +class ScopeCheckerData { - protected $baseClassName = Acl::class; + private $isOwnChecker; - private $classFinder; + private $inAccountChecker; - private $injectableFactory; + private $inContactChecker; - public function __construct(ClassFinder $classFinder, InjectableFactory $injectableFactory) + public function __construct(callable $isOwnChecker, callable $inAccountChecker, callable $inContactChecker) { - $this->classFinder = $classFinder; - $this->injectableFactory = $injectableFactory; + $this->isOwnChecker = $isOwnChecker; + $this->inAccountChecker = $inAccountChecker; + $this->inContactChecker = $inContactChecker; } - public function create(string $scope) : PortalScopeAcl + public function isOwn(): bool { - $className = $this->classFinder->find('AclPortal', $scope); + return ($this->isOwnChecker)(); + } - if (!$className) { - $className = $this->baseClassName; - } + public function inAccount(): bool + { + return ($this->inAccountChecker)(); + } - return $this->injectableFactory->createWith($className, [ - 'scope' => $scope, // todo remove ? - ]); + public function inContact(): bool + { + return ($this->inContactChecker)(); + } + + public static function createBuilder(): ScopeCheckerDataBuilder + { + return new ScopeCheckerDataBuilder(); } } diff --git a/application/Espo/Core/Portal/Acl/ScopeCheckerDataBuilder.php b/application/Espo/Core/Portal/Acl/ScopeCheckerDataBuilder.php new file mode 100644 index 0000000000..48a6136f9a --- /dev/null +++ b/application/Espo/Core/Portal/Acl/ScopeCheckerDataBuilder.php @@ -0,0 +1,134 @@ +isOwnChecker = function (): bool { + return false; + }; + + $this->inAccountChecker = function (): bool { + return false; + }; + + $this->inContactChecker = function (): bool { + return false; + }; + } + + public function setIsOwn(bool $value): self + { + if ($value) { + $this->isOwnChecker = function (): bool { + return true; + }; + + return $this; + } + + $this->isOwnChecker = function (): bool { + return false; + }; + + return $this; + } + + public function setInAccount(bool $value): self + { + if ($value) { + $this->inAccountChecker = function (): bool { + return true; + }; + + return $this; + } + + $this->inAccountChecker = function (): bool { + return false; + }; + + return $this; + } + + public function setInContact(bool $value): self + { + if ($value) { + $this->inContactChecker = function (): bool { + return true; + }; + + return $this; + } + + $this->inContactChecker = function (): bool { + return false; + }; + + return $this; + } + + public function setIsOwnChecker(callable $checker): self + { + $this->isOwnChecker = $checker; + + return $this; + } + + public function setInAccountChecker(callable $checker): self + { + $this->inAccountChecker = $checker; + + return $this; + } + + public function setInContactChecker(callable $checker): self + { + $this->inContactChecker = $checker; + + return $this; + } + + public function build(): ScopeCheckerData + { + return new ScopeCheckerData($this->isOwnChecker, $this->inAccountChecker, $this->inContactChecker); + } +} diff --git a/application/Espo/Core/AclPortal/Table.php b/application/Espo/Core/Portal/Acl/Table.php similarity index 78% rename from application/Espo/Core/AclPortal/Table.php rename to application/Espo/Core/Portal/Acl/Table.php index 3456af70a1..03d50f1e41 100644 --- a/application/Espo/Core/AclPortal/Table.php +++ b/application/Espo/Core/Portal/Acl/Table.php @@ -27,9 +27,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\AclPortal; - -use Espo\Core\Exceptions\Error; +namespace Espo\Core\Portal\Acl; use Espo\Entities\{ User, @@ -37,7 +35,7 @@ use Espo\Entities\{ }; use Espo\Core\{ - Acl\Table as BaseTable, + Acl\DefaultTable as BaseTable, ORM\EntityManager, Utils\Config, Utils\Metadata, @@ -45,7 +43,7 @@ use Espo\Core\{ Utils\DataCache, }; -use Traversable; +use RuntimeException; class Table extends BaseTable { @@ -59,7 +57,14 @@ class Table extends BaseTable protected $defaultAclType = 'recordAllOwnNo'; - protected $levelList = ['yes', 'all', 'account', 'contact', 'own', 'no']; + protected $levelList = [ + self::LEVEL_YES, + self::LEVEL_ALL, + self::LEVEL_ACCOUNT. + self::LEVEL_CONTACT, + self::LEVEL_OWN, + self::LEVEL_NO, + ]; protected $isStrictModeForced = true; @@ -73,7 +78,7 @@ class Table extends BaseTable DataCache $dataCache ) { if (empty($portal)) { - throw new Error("No portal was passed to AclPortal\\Table constructor."); + throw new RuntimeException("No portal was passed to AclPortal\\Table constructor."); } $this->portal = $portal; @@ -81,12 +86,12 @@ class Table extends BaseTable parent::__construct($entityManager, $user, $config, $metadata, $fieldUtil, $dataCache); } - protected function initCacheKey() : void + protected function initCacheKey(): void { - $this->cacheKey = 'aclPortal/' . $this->portal->id.'/' . $this->user->id; + $this->cacheKey = 'aclPortal/' . $this->portal->getId() . '/' . $this->user->getId(); } - protected function getRoleList() : array + protected function getRoleList(): array { $roleList = []; @@ -95,10 +100,6 @@ class Table extends BaseTable ->getRelation($this->user, 'portalRoles') ->find(); - if (! $userRoleList instanceof Traversable) { - throw new Error(); - } - foreach ($userRoleList as $role) { $roleList[] = $role; } @@ -108,10 +109,6 @@ class Table extends BaseTable ->getRelation($this->portal, 'portalRoles') ->find(); - if (! $portalRoleList instanceof Traversable) { - throw new Error(); - } - foreach ($portalRoleList as $role) { $roleList[] = $role; } @@ -119,18 +116,18 @@ class Table extends BaseTable return $roleList; } - protected function getScopeWithAclList() : array + protected function getScopeWithAclList(): array { $scopeList = []; $scopes = $this->metadata->get('scopes'); - foreach ($scopes as $scope => $d) { - if (empty($d['acl'])) { + foreach ($scopes as $scope => $item) { + if (empty($item['acl'])) { continue; } - if (empty($d['aclPortal'])) { + if (empty($item['aclPortal'])) { continue; } @@ -140,7 +137,7 @@ class Table extends BaseTable return $scopeList; } - protected function applyDefault(&$table, &$fieldTable) : void + protected function applyDefault(&$table, &$fieldTable): void { parent::applyDefault($table, $fieldTable); @@ -151,7 +148,7 @@ class Table extends BaseTable } } - protected function applyDisabled(&$table, &$fieldTable) : void + protected function applyDisabled(&$table, &$fieldTable): void { foreach ($this->getScopeList() as $scope) { $d = $this->metadata->get('scopes.' . $scope); @@ -164,7 +161,7 @@ class Table extends BaseTable } } - protected function applyAdditional(&$table, &$fieldTable, &$valuePermissionLists) : void + protected function applyAdditional(&$table, &$fieldTable, &$valuePermissionLists): void { } } diff --git a/application/Espo/Core/Acl/AclFactory.php b/application/Espo/Core/Portal/Acl/TableFactory.php similarity index 72% rename from application/Espo/Core/Acl/AclFactory.php rename to application/Espo/Core/Portal/Acl/TableFactory.php index 0846f363a3..14f1611711 100644 --- a/application/Espo/Core/Acl/AclFactory.php +++ b/application/Espo/Core/Portal/Acl/TableFactory.php @@ -27,37 +27,34 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\Acl; +namespace Espo\Core\Portal\Acl; + +use Espo\Entities\{ + User, + Portal, +}; use Espo\Core\{ - Utils\ClassFinder, InjectableFactory, }; -class AclFactory +class TableFactory { - protected $baseClassName = Acl::class; - - private $classFinder; - private $injectableFactory; - public function __construct(ClassFinder $classFinder, InjectableFactory $injectableFactory) + public function __construct(InjectableFactory $injectableFactory) { - $this->classFinder = $classFinder; $this->injectableFactory = $injectableFactory; } - public function create(string $scope): ScopeAcl + /** + * Create a table. + */ + public function create(User $user, Portal $portal): Table { - $className = $this->classFinder->find('Acl', $scope); - - if (!$className) { - $className = $this->baseClassName; - } - - return $this->injectableFactory->createWith($className, [ - 'scope' => $scope, // todo remove ? + return $this->injectableFactory->createWith(Table::class, [ + 'user' => $user, + 'portal' => $portal, ]); } } diff --git a/application/Espo/Core/Portal/AclManager.php b/application/Espo/Core/Portal/AclManager.php index 7cbcc46d92..9089431a74 100644 --- a/application/Espo/Core/Portal/AclManager.php +++ b/application/Espo/Core/Portal/AclManager.php @@ -29,7 +29,10 @@ namespace Espo\Core\Portal; -use Espo\ORM\Entity; +use Espo\ORM\{ + Entity, + EntityManager, +}; use Espo\Entities\{ User, @@ -37,49 +40,46 @@ use Espo\Entities\{ }; use Espo\Core\{ - AclPortal\Table as Table, - AclPortal\Acl as BasePortalAcl, - AclPortal\AclFactory as PortalAclFactory, - Acl\ScopeAcl, - Acl\GlobalRestrictonFactory, + Portal\Acl\Table as Table, + Portal\Acl\AccessCheckerFactory, + Portal\Acl\OwnershipCheckerFactory, + Portal\Acl\OwnershipAccountChecker, + Portal\Acl\OwnershipContactChecker, + Portal\Acl\TableFactory, + Portal\Acl, + Acl\GlobalRestricton, Acl\OwnerUserFieldProvider, Acl\Table as TableBase, - Portal\Acl as UserAclWrapper, - AclManager as BaseAclManager, - InjectableFactory, - ORM\EntityManager, + AclManager as InternalAclManager, }; use StdClass; use RuntimeException; -class AclManager extends BaseAclManager +class AclManager extends InternalAclManager { - protected $tableClassName = Table::class; + protected $userAclClassName = Acl::class; - protected $userAclClassName = UserAclWrapper::class; - - protected $baseImplementationClassName = BasePortalAcl::class; - - private $mainManager = null; + private $internalAclManager = null; private $portal = null; public function __construct( - InjectableFactory $injectableFactory, - EntityManager $entityManager, - PortalAclFactory $aclFactory, - GlobalRestrictonFactory $globalRestrictonFactory, + AccessCheckerFactory $accessCheckerFactory, + OwnershipCheckerFactory $ownershipCheckerFactory, + TableFactory $tableFactory, + GlobalRestricton $globalRestricton, OwnerUserFieldProvider $ownerUserFieldProvider, - BaseAclManager $mainManager + EntityManager $entityManager, + InternalAclManager $internalAclManager ) { - $this->injectableFactory = $injectableFactory; - $this->entityManager = $entityManager; - $this->aclFactory = $aclFactory; + $this->accessCheckerFactory = $accessCheckerFactory; + $this->ownershipCheckerFactory = $ownershipCheckerFactory; + $this->tableFactory = $tableFactory; + $this->globalRestricton = $globalRestricton; $this->ownerUserFieldProvider = $ownerUserFieldProvider; - $this->mainManager = $mainManager; - - $this->globalRestricton = $globalRestrictonFactory->create(); + $this->entityManager = $entityManager; + $this->internalAclManager = $internalAclManager; } public function setPortal(Portal $portal): void @@ -98,46 +98,23 @@ class AclManager extends BaseAclManager protected function getTable(User $user): TableBase { - $key = $user->id; + $key = $user->getId(); - if (empty($key)) { + if (!$key) { $key = spl_object_hash($user); } - if (empty($this->tableHashMap[$key])) { - $this->tableHashMap[$key] = $this->injectableFactory->createWith($this->tableClassName, [ - 'user' => $user, - 'portal' => $this->getPortal(), - ]); + if (!array_key_exists($key, $this->tableHashMap)) { + $this->tableHashMap[$key] = $this->tableFactory->create($user, $this->getPortal()); } return $this->tableHashMap[$key]; } - public function checkReadOnlyAccount(User $user, string $scope): bool - { - return $this->getLevel($user, $scope, Table::ACTION_READ) === Table::LEVEL_ACCOUNT; - } - - public function checkReadOnlyContact(User $user, string $scope): bool - { - return $this->getLevel($user, $scope, Table::ACTION_READ)=== Table::LEVEL_CONTACT; - } - - public function checkInAccount(User $user, Entity $entity): bool - { - return (bool) $this->getEntityImplementation($entity->getEntityType())->checkInAccount($user, $entity); - } - - public function checkIsOwnContact(User $user, Entity $entity): bool - { - return (bool) $this->getEntityImplementation($entity->getEntityType())->checkIsOwnContact($user, $entity); - } - public function getMap(User $user): StdClass { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->getMap($user); + return $this->internalAclManager->getMap($user); } return parent::getMap($user); @@ -146,25 +123,25 @@ class AclManager extends BaseAclManager public function getLevel(User $user, string $scope, string $action): string { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->getLevel($user, $scope, $action); + return $this->internalAclManager->getLevel($user, $scope, $action); } return parent::getLevel($user, $scope, $action); } - public function get(User $user, string $permission): ?string + public function getPermissionLevel(User $user, string $permission): string { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->get($user, $permission); + return $this->internalAclManager->getPermissionLevel($user, $permission); } - return parent::get($user, $permission); + return parent::getPermissionLevel($user, $permission); } public function checkReadOnlyTeam(User $user, string $scope): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkReadOnlyTeam($user, $scope); + return $this->internalAclManager->checkReadOnlyTeam($user, $scope); } return false; @@ -173,7 +150,7 @@ class AclManager extends BaseAclManager public function checkReadNo(User $user, string $scope): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkReadNo($user, $scope); + return $this->internalAclManager->checkReadNo($user, $scope); } return parent::checkReadNo($user, $scope); @@ -182,7 +159,7 @@ class AclManager extends BaseAclManager public function checkReadOnlyOwn(User $user, string $scope): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkReadOnlyOwn($user, $scope); + return $this->internalAclManager->checkReadOnlyOwn($user, $scope); } return parent::checkReadOnlyOwn($user, $scope); @@ -191,16 +168,32 @@ class AclManager extends BaseAclManager public function checkReadAll(User $user, string $scope): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkReadAll($user, $scope); + return $this->internalAclManager->checkReadAll($user, $scope); } return parent::checkReadAll($user, $scope); } + /** + * Whether 'read' access is set to 'account' for a specific scope. + */ + public function checkReadOnlyAccount(User $user, string $scope): bool + { + return $this->getLevel($user, $scope, Table::ACTION_READ) === Table::LEVEL_ACCOUNT; + } + + /** + * Whether 'read' access is set to 'contact' for a specific scope. + */ + public function checkReadOnlyContact(User $user, string $scope): bool + { + return $this->getLevel($user, $scope, Table::ACTION_READ)=== Table::LEVEL_CONTACT; + } + public function check(User $user, $subject, ?string $action = null): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->check($user, $subject, $action); + return $this->internalAclManager->check($user, $subject, $action); } return parent::check($user, $subject, $action); @@ -209,34 +202,67 @@ class AclManager extends BaseAclManager public function checkEntity(User $user, Entity $entity, string $action = Table::ACTION_READ): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkEntity($user, $entity, $action); + return $this->internalAclManager->checkEntity($user, $entity, $action); } return parent::checkEntity($user, $entity, $action); } - public function checkIsOwner(User $user, Entity $entity): bool + public function checkUserPermission(User $user, $target, string $permissionType = 'user'): bool { - if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkIsOwner($user, $entity); - } - - return parent::checkIsOwner($user, $entity); + return $this->internalAclManager->checkUserPermission($user, $target, $permissionType); } - public function checkInTeam(User $user, Entity $entity): bool + public function checkOwnershipOwn(User $user, Entity $entity): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkInTeam($user, $entity); + return $this->internalAclManager->checkOwnershipOwn($user, $entity); } - return parent::checkInTeam($user, $entity); + return parent::checkOwnershipOwn($user, $entity); + } + + public function checkOwnershipTeam(User $user, Entity $entity): bool + { + if ($this->checkUserIsNotPortal($user)) { + return $this->internalAclManager->checkOwnershipTeam($user, $entity); + } + + return parent::checkOwnershipOwn($user, $entity); + } + + /** + * Check whether an entity belongs to a user account. + */ + public function checkOwnershipAccount(User $user, Entity $entity): bool + { + $checker = $this->getOwnershipChecker($entity->getEntityType()); + + if (!$checker instanceof OwnershipAccountChecker) { + return false; + } + + return $checker->checkAccount($user, $entity); + } + + /** + * Check whether an entity belongs to a user account. + */ + public function checkOwnershipContact(User $user, Entity $entity): bool + { + $checker = $this->getOwnershipChecker($entity->getEntityType()); + + if (!$checker instanceof OwnershipContactChecker) { + return false; + } + + return $checker->checkContact($user, $entity); } public function checkScope(User $user, string $scope, ?string $action = null): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkScope($user, $scope, $action); + return $this->internalAclManager->checkScope($user, $scope, $action); } return parent::checkScope($user, $scope, $action); @@ -245,7 +271,7 @@ class AclManager extends BaseAclManager public function checkUser(User $user, string $permission, User $entity): bool { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->checkUser($user, $permission, $entity); + return $this->internalAclManager->checkUser($user, $permission, $entity); } return parent::checkUser($user, $permission, $entity); @@ -259,7 +285,7 @@ class AclManager extends BaseAclManager ): array { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel); + return $this->internalAclManager->getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel); } return parent::getScopeForbiddenAttributeList($user, $scope, $action, $thresholdLevel); @@ -273,7 +299,7 @@ class AclManager extends BaseAclManager ): array { if ($this->checkUserIsNotPortal($user)) { - return $this->mainManager->getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel); + return $this->internalAclManager->getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel); } return parent::getScopeForbiddenFieldList($user, $scope, $action, $thresholdLevel); @@ -283,4 +309,44 @@ class AclManager extends BaseAclManager { return !$user->isPortal(); } + + /** + * @deprecated Use `getPermissionLevel` instead. + */ + public function get(User $user, string $permission): string + { + return $this->getPermissionLevel($user, $permission); + } + + /** + * @deprecated + */ + public function checkIsOwner(User $user, Entity $entity): bool + { + return $this->checkOwnershipOwn($user, $entity); + } + + /** + * @deprecated + */ + public function checkInTeam(User $user, Entity $entity): bool + { + return $this->checkOwnershipTeam($user, $entity); + } + + /** + * @deprecated + */ + public function checkInAccount(User $user, Entity $entity): bool + { + return $this->checkOwnershipAccount($user, $entity); + } + + /** + * @deprecated + */ + public function checkIsOwnContact(User $user, Entity $entity): bool + { + return $this->checkOwnershipContact($user, $entity); + } } diff --git a/application/Espo/Core/Portal/Loaders/AclManager.php b/application/Espo/Core/Portal/Loaders/AclManager.php index 7601b796bc..ad9769de04 100644 --- a/application/Espo/Core/Portal/Loaders/AclManager.php +++ b/application/Espo/Core/Portal/Loaders/AclManager.php @@ -48,10 +48,10 @@ class AclManager implements Loader $this->internalAclManager = $internalAclManager; } - public function load() : PortalAclManager + public function load(): PortalAclManager { return $this->injectableFactory->createWith(PortalAclManager::class,[ - 'mainManager' => $this->internalAclManager, + 'internalAclManager' => $this->internalAclManager, ]); } } diff --git a/application/Espo/Modules/Crm/Acl/Meeting.php b/application/Espo/Modules/Crm/Acl/Meeting.php index 85e344534e..2731232179 100644 --- a/application/Espo/Modules/Crm/Acl/Meeting.php +++ b/application/Espo/Modules/Crm/Acl/Meeting.php @@ -37,10 +37,9 @@ use Espo\Core\{ Acl\Acl, Acl\ScopeData, Acl\Table, - Acl\EntityReadAcl, }; -class Meeting extends Acl implements EntityReadAcl +class Meeting extends Acl { public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool { diff --git a/application/Espo/Modules/Crm/AclPortal/KnowledgeBaseArticle.php b/application/Espo/Modules/Crm/AclPortal/KnowledgeBaseArticle.php index 84b79efff9..ff24ae18e2 100644 --- a/application/Espo/Modules/Crm/AclPortal/KnowledgeBaseArticle.php +++ b/application/Espo/Modules/Crm/AclPortal/KnowledgeBaseArticle.php @@ -36,11 +36,10 @@ use Espo\ORM\Entity; use Espo\Core\{ Acl\ScopeData, Acl\Table, - Acl\EntityReadAcl, AclPortal\Acl as Acl, }; -class KnowledgeBaseArticle extends Acl implements EntityReadAcl +class KnowledgeBaseArticle extends Acl { public function checkEntityRead(EntityUser $user, Entity $entity, ScopeData $data): bool { diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityAcl/Call.json b/application/Espo/Modules/Crm/Resources/metadata/aclDefs/Call.json similarity index 100% rename from application/Espo/Modules/Crm/Resources/metadata/entityAcl/Call.json rename to application/Espo/Modules/Crm/Resources/metadata/aclDefs/Call.json diff --git a/application/Espo/Modules/Crm/Resources/metadata/entityAcl/Meeting.json b/application/Espo/Modules/Crm/Resources/metadata/aclDefs/Meeting.json similarity index 100% rename from application/Espo/Modules/Crm/Resources/metadata/entityAcl/Meeting.json rename to application/Espo/Modules/Crm/Resources/metadata/aclDefs/Meeting.json diff --git a/application/Espo/Resources/metadata/aclDefs/Attachment.json b/application/Espo/Resources/metadata/aclDefs/Attachment.json new file mode 100644 index 0000000000..9fc5e9fd31 --- /dev/null +++ b/application/Espo/Resources/metadata/aclDefs/Attachment.json @@ -0,0 +1,4 @@ +{ + "accessCheckerClassName": "Espo\\Classes\\Acl\\Attachment\\AccessChecker", + "ownershipCheckerClassName": "Espo\\Classes\\Acl\\Attachment\\OwnershipChecker" +} \ No newline at end of file diff --git a/application/Espo/Resources/metadata/aclDefs/Email.json b/application/Espo/Resources/metadata/aclDefs/Email.json new file mode 100644 index 0000000000..6ae915feee --- /dev/null +++ b/application/Espo/Resources/metadata/aclDefs/Email.json @@ -0,0 +1,3 @@ +{ + "readOwnerUserField": "users" +} \ No newline at end of file diff --git a/application/Espo/Resources/metadata/app/metadata.json b/application/Espo/Resources/metadata/app/metadata.json index 83204ac194..9329c5a34c 100644 --- a/application/Espo/Resources/metadata/app/metadata.json +++ b/application/Espo/Resources/metadata/app/metadata.json @@ -22,6 +22,7 @@ ["app", "actions", "__ANY__", "implementationClassName"], ["selectDefs"], ["recordDefs"], + ["aclDefs"], ["authenticationMethods", "__ANY__", "implementationClassName"] ] } diff --git a/application/Espo/Resources/metadata/entityAcl/Email.json b/application/Espo/Resources/metadata/entityAcl/Email.json index bd2cda920a..8d816e7df0 100644 --- a/application/Espo/Resources/metadata/entityAcl/Email.json +++ b/application/Espo/Resources/metadata/entityAcl/Email.json @@ -8,6 +8,5 @@ "users": { "onlyAdmin": true } - }, - "readOwnerUserField": "users" + } } diff --git a/tests/unit/Espo/Core/Acl/ScopeCheckerDataTest.php b/tests/unit/Espo/Core/Acl/ScopeCheckerDataTest.php new file mode 100644 index 0000000000..6f6b116522 --- /dev/null +++ b/tests/unit/Espo/Core/Acl/ScopeCheckerDataTest.php @@ -0,0 +1,99 @@ +build(); + + $this->assertEquals(false, $checkerData->isOwn()); + $this->assertEquals(false, $checkerData->inTeam()); + } + + public function testCheckerData1() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(true) + ->build(); + + $this->assertEquals(true, $checkerData->isOwn()); + $this->assertEquals(true, $checkerData->inTeam()); + } + + public function testCheckerData2() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeamChecker( + function (): bool { + return true; + } + ) + ->build(); + + $this->assertEquals(false, $checkerData->isOwn()); + $this->assertEquals(true, $checkerData->inTeam()); + } + + public function testCheckerData3() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwnChecker( + function (): bool { + return true; + } + ) + ->setInTeamChecker( + function (): bool { + return false; + } + ) + ->build(); + + $this->assertEquals(true, $checkerData->isOwn()); + $this->assertEquals(false, $checkerData->inTeam()); + } +} diff --git a/tests/unit/Espo/Core/Acl/ScopeCheckerTest.php b/tests/unit/Espo/Core/Acl/ScopeCheckerTest.php new file mode 100644 index 0000000000..6a4924715c --- /dev/null +++ b/tests/unit/Espo/Core/Acl/ScopeCheckerTest.php @@ -0,0 +1,435 @@ +scopeChecker = new ScopeChecker(); + } + + public function testCheckerNoData1() + { + $data = ScopeData::fromRaw(false); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(false, $result); + } + + public function testCheckerNoData2() + { + $data = ScopeData::fromRaw(true); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(true, $result); + } + + public function testCheckerNoData3() + { + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_NO, + ], + ); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(false, $result); + } + + public function testCheckerNoData4() + { + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ALL, + ], + ); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(true, $result); + } + + public function testCheckerNoData5() + { + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_TEAM, + ], + ); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(true, $result); + } + + public function testCheckerActionNoData1() + { + $data = ScopeData::fromRaw(false); + + $result = $this->scopeChecker->check($data, Table::ACTION_CREATE); + + $this->assertEquals(false, $result); + } + + public function testCheckerActionNoData2() + { + $data = ScopeData::fromRaw(true); + + $result = $this->scopeChecker->check($data, Table::ACTION_CREATE); + + $this->assertEquals(true, $result); + } + + public function testCheckerData1() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ALL, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData2() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_TEAM, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData3() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData4() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_NO, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData5() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData6() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_TEAM, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData7() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_TEAM, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData8() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_CREATE => Table::LEVEL_YES, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_CREATE, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData9() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData10() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData11() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_TEAM, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData12() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData13() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw(false); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerDataNoAction1() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(true) + ->build(); + + $data = ScopeData::fromRaw(false); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerDataNoAction2() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInTeam(false) + ->build(); + + $data = ScopeData::fromRaw(true); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerDataNoAction3() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_TEAM, + ], + ); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerDataNoAction4() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInTeam(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_CREATE => Table::LEVEL_NO, + Table::ACTION_READ => Table::LEVEL_NO, + ], + ); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(false, $result); + } +} diff --git a/tests/unit/Espo/Core/Acl/ScopeDataTest.php b/tests/unit/Espo/Core/Acl/ScopeDataTest.php index 850a3ad2c6..b1930790bd 100644 --- a/tests/unit/Espo/Core/Acl/ScopeDataTest.php +++ b/tests/unit/Espo/Core/Acl/ScopeDataTest.php @@ -39,7 +39,6 @@ use RuntimeException; class ScopeDataTest extends \PHPUnit\Framework\TestCase { - protected function setUp() : void { } @@ -52,8 +51,6 @@ class ScopeDataTest extends \PHPUnit\Framework\TestCase $this->assertTrue($data->isTrue()); $this->assertFalse($data->isFalse()); - $this->assertTrue($data->getRaw()); - $this->assertEquals(Table::LEVEL_NO, $data->getDelete()); } @@ -65,8 +62,6 @@ class ScopeDataTest extends \PHPUnit\Framework\TestCase $this->assertTrue($data->isFalse()); $this->assertFalse($data->isTrue()); - $this->assertFalse($data->getRaw()); - $this->assertEquals(Table::LEVEL_NO, $data->getDelete()); } @@ -103,10 +98,6 @@ class ScopeDataTest extends \PHPUnit\Framework\TestCase $this->assertEquals(Table::LEVEL_TEAM, $data->getEdit()); $this->assertEquals(Table::LEVEL_NO, $data->getDelete()); - $this->assertEquals($raw, $data->getRaw()); - - $this->assertNotSame($raw, $data->getRaw()); - $this->assertTrue($data->hasNotNo()); } @@ -145,4 +136,13 @@ class ScopeDataTest extends \PHPUnit\Framework\TestCase $data->read; } + + public function testBadKey() + { + $this->expectException(RuntimeException::class); + + ScopeData::fromRaw((object) [ + Table::ACTION_CREATE => false, + ]); + } } diff --git a/tests/unit/Espo/Core/AclPortal/ScopeCheckerDataTest.php b/tests/unit/Espo/Core/AclPortal/ScopeCheckerDataTest.php new file mode 100644 index 0000000000..ce68b16910 --- /dev/null +++ b/tests/unit/Espo/Core/AclPortal/ScopeCheckerDataTest.php @@ -0,0 +1,108 @@ +build(); + + $this->assertEquals(false, $checkerData->isOwn()); + $this->assertEquals(false, $checkerData->inAccount()); + $this->assertEquals(false, $checkerData->inContact()); + } + + public function testCheckerData1() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInAccount(true) + ->setInContact(true) + ->build(); + + $this->assertEquals(true, $checkerData->isOwn()); + $this->assertEquals(true, $checkerData->inAccount()); + $this->assertEquals(true, $checkerData->inContact()); + } + + public function testCheckerData2() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(false) + ->setInAccountChecker( + function (): bool { + return true; + } + ) + ->build(); + + $this->assertEquals(false, $checkerData->isOwn()); + $this->assertEquals(true, $checkerData->inAccount()); + } + + public function testCheckerData3() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwnChecker( + function (): bool { + return false; + } + ) + ->setInAccountChecker( + function (): bool { + return false; + } + ) + ->setInContactChecker( + function (): bool { + return true; + } + ) + ->build(); + + $this->assertEquals(false, $checkerData->isOwn()); + $this->assertEquals(false, $checkerData->inAccount()); + $this->assertEquals(true, $checkerData->inContact()); + } +} diff --git a/tests/unit/Espo/Core/AclPortal/ScopeCheckerTest.php b/tests/unit/Espo/Core/AclPortal/ScopeCheckerTest.php new file mode 100644 index 0000000000..617eba1076 --- /dev/null +++ b/tests/unit/Espo/Core/AclPortal/ScopeCheckerTest.php @@ -0,0 +1,529 @@ +scopeChecker = new ScopeChecker(); + } + + public function testCheckerNoData1() + { + $data = ScopeData::fromRaw(false); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(false, $result); + } + + public function testCheckerNoData2() + { + $data = ScopeData::fromRaw(true); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(true, $result); + } + + public function testCheckerNoData3() + { + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_NO, + ], + ); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(false, $result); + } + + public function testCheckerNoData4() + { + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ALL, + ], + ); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(true, $result); + } + + public function testCheckerNoData5() + { + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ACCOUNT, + ], + ); + + $result = $this->scopeChecker->check($data); + + $this->assertEquals(true, $result); + } + + public function testCheckerActionNoData1() + { + $data = ScopeData::fromRaw(false); + + $result = $this->scopeChecker->check($data, Table::ACTION_CREATE); + + $this->assertEquals(false, $result); + } + + public function testCheckerActionNoData2() + { + $data = ScopeData::fromRaw(true); + + $result = $this->scopeChecker->check($data, Table::ACTION_CREATE); + + $this->assertEquals(true, $result); + } + + public function testCheckerData1() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ALL, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData2a() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ACCOUNT, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData2b() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_CONTACT, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData3() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData4() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_NO, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData5a() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setInAccount(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData5b() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData6a() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setInAccount(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ACCOUNT, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData6b() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ACCOUNT, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData7a() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ACCOUNT, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData7b() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ACCOUNT, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + + public function testCheckerData7c() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_CONTACT, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData8() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_CREATE => Table::LEVEL_YES, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_CREATE, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData9() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData10() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_OWN, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData11() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_CONTACT, + ], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerData12() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInAccount(true) + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [], + ); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerData13() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->build(); + + $data = ScopeData::fromRaw(false); + + $result = $this->scopeChecker->check($data, Table::ACTION_READ, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerDataNoAction1() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInAccount(true) + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw(false); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(false, $result); + } + + public function testCheckerDataNoAction2() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->build(); + + $data = ScopeData::fromRaw(true); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerDataNoAction3() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInAccount(true) + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_ACCOUNT, + ], + ); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerDataNoAction4() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInAccount(false) + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_READ => Table::LEVEL_CONTACT, + ], + ); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(true, $result); + } + + public function testCheckerDataNoAction5() + { + $checkerData = ScopeCheckerData + ::createBuilder() + ->setIsOwn(true) + ->setInContact(true) + ->build(); + + $data = ScopeData::fromRaw( + (object) [ + Table::ACTION_CREATE => Table::LEVEL_NO, + Table::ACTION_READ => Table::LEVEL_NO, + ], + ); + + $result = $this->scopeChecker->check($data, null, $checkerData); + + $this->assertEquals(false, $result); + } +}