diff --git a/application/Espo/Classes/Acl/Email/AssignmentChecker.php b/application/Espo/Classes/Acl/Email/AssignmentChecker.php new file mode 100644 index 0000000000..fbe58e4642 --- /dev/null +++ b/application/Espo/Classes/Acl/Email/AssignmentChecker.php @@ -0,0 +1,51 @@ +classFinder = $classFinder; + $this->metadata = $metadata; + $this->injectableFactory = $injectableFactory; + } + + /** + * Create an access checker. + * + * @throws NotImplemented + */ + public function create(string $scope): AssignmentChecker + { + $className = $this->getClassName($scope); + + return $this->injectableFactory->create($className); + } + + private function getClassName(string $scope): string + { + $className = $this->metadata->get(['aclDefs', $scope, 'assignmentCheckerClassName']); + + if ($className) { + return $className; + } + + if (!$this->metadata->get(['scopes', $scope])) { + throw new NotImplemented(); + } + + return $this->defaultClassName; + } +} diff --git a/application/Espo/Core/Acl/AssignmentChecker/AssignmentCheckerManager.php b/application/Espo/Core/Acl/AssignmentChecker/AssignmentCheckerManager.php new file mode 100644 index 0000000000..85fb73b36a --- /dev/null +++ b/application/Espo/Core/Acl/AssignmentChecker/AssignmentCheckerManager.php @@ -0,0 +1,78 @@ +metadata = $metadata; + $this->factory = $factory; + } + + public function check(User $user, Entity $entity): bool + { + $entityType = $entity->getEntityType(); + + $checker = $this->getChecker($entityType); + + return $checker->check($user, $entity); + } + + private function getChecker(string $entityType): AssignmentChecker + { + + if (!array_key_exists($entityType, $this->checkerCache)) { + $this->loadChecker($entityType); + } + + return $this->checkerCache[$entityType]; + } + + private function loadChecker(string $entityType): void + { + $this->checkerCache[$entityType] = $this->factory->create($entityType); + } +} diff --git a/application/Espo/Core/Acl/AssignmentChecker/DefaultAssignmentChecker.php b/application/Espo/Core/Acl/AssignmentChecker/DefaultAssignmentChecker.php new file mode 100644 index 0000000000..c162b49c99 --- /dev/null +++ b/application/Espo/Core/Acl/AssignmentChecker/DefaultAssignmentChecker.php @@ -0,0 +1,353 @@ +aclManager = $aclManager; + $this->entityManager = $entityManager; + $this->ormDefs = $ormDefs; + } + + public function check(User $user, Entity $entity): bool + { + if (!$this->isPermittedAssignedUser($user, $entity)) { + return false; + } + + if (!$this->isPermittedTeams($user, $entity)) { + return false; + } + + if ( $this->hasAssignedUsersField($entity->getEntityType())) { + if (!$this->isPermittedAssignedUsers($user, $entity)) { + return false; + } + } + + return true; + } + + private function hasAssignedUsersField(string $entityType): bool + { + $entityDefs = $this->ormDefs->getEntity($entityType); + + return + $entityDefs->hasField(self::FIELD_ASSIGNED_USERS) && + $entityDefs->getField(self::FIELD_ASSIGNED_USERS)->getType() === 'linkMultiple' && + $entityDefs->hasRelation(self::FIELD_ASSIGNED_USERS) && + $entityDefs->getRelation(self::FIELD_ASSIGNED_USERS)->getForeignEntityType() === 'User'; + } + + protected function isPermittedAssignedUser(User $user, Entity $entity): bool + { + if (!$entity->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) { + return true; + } + + $assignedUserId = $entity->get(self::ATTR_ASSIGNED_USER_ID); + + if ($user->isPortal()) { + if (!$entity->isAttributeChanged(self::ATTR_ASSIGNED_USER_ID) && !$assignedUserId) { + return true; + } + + return false; + } + + $assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission'); + + if ( + $assignmentPermission === Table::LEVEL_YES || + !in_array($assignmentPermission, [Table::LEVEL_TEAM, Table::LEVEL_NO]) + ) { + return true; + } + + $toProcess = false; + + if (!$entity->isNew()) { + if ($entity->isAttributeChanged(self::ATTR_ASSIGNED_USER_ID)) { + $toProcess = true; + } + } + else { + $toProcess = true; + } + + if (!$toProcess) { + return true; + } + + if (empty($assignedUserId)) { + if ($assignmentPermission === Table::LEVEL_NO && !$user->isApi()) { + return false; + } + + return true; + } + + if ($assignmentPermission === Table::LEVEL_NO) { + if ($user->id !== $assignedUserId) { + return false; + } + } + else if ($assignmentPermission === Table::LEVEL_TEAM) { + $teamIdList = $user->get(self::ATTR_TEAMS_IDS); + + if ( + !$this->entityManager + ->getRepository('User') + ->checkBelongsToAnyOfTeams($assignedUserId, $teamIdList) + ) { + return false; + } + } + + return true; + } + + protected function isPermittedTeams(User $user, Entity $entity): bool + { + $assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission'); + + if (!in_array($assignmentPermission, [Table::LEVEL_TEAM, Table::LEVEL_NO])) { + return true; + } + + if (!$entity->hasLinkMultipleField(self::FIELD_TEAMS)) { + return true; + } + + $teamIdList = $entity->getLinkMultipleIdList(self::FIELD_TEAMS); + + if (empty($teamIdList)) { + return $this->isPermittedTeamsEmpty($user, $entity); + } + + $newIdList = []; + + if (!$entity->isNew()) { + $existingIdList = []; + + $teamCollection = $this->entityManager + ->getRDBRepository($entity->getEntityType()) + ->getRelation($entity, self::FIELD_TEAMS) + ->select('id') + ->find(); + + foreach ($teamCollection as $team) { + $existingIdList[] = $team->getId(); + } + + foreach ($teamIdList as $id) { + if (!in_array($id, $existingIdList)) { + $newIdList[] = $id; + } + } + } + else { + $newIdList = $teamIdList; + } + + if (empty($newIdList)) { + return true; + } + + $userTeamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS); + + foreach ($newIdList as $id) { + if (!in_array($id, $userTeamIdList)) { + return false; + } + } + + return true; + } + + private function isPermittedTeamsEmpty(User $user, Entity $entity): bool + { + $assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission'); + + if ($assignmentPermission !== Table::LEVEL_TEAM) { + return true; + } + + if ($entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)) { + $assignedUserIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS); + + if (empty($assignedUserIdList)) { + return false; + } + } + else if ($entity->hasAttribute(self::ATTR_ASSIGNED_USER_ID)) { + if (!$entity->get(self::ATTR_ASSIGNED_USER_ID)) { + return false; + } + } + + return true; + } + + protected function isPermittedAssignedUsers(User $user, Entity $entity): bool + { + if (!$entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)) { + return true; + } + + if ($user->isPortal()) { + if (count($entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS)) === 0) { + return true; + } + } + + $assignmentPermission = $this->aclManager->getPermissionLevel($user, 'assignmentPermission'); + + if ( + $assignmentPermission === Table::LEVEL_YES || + !in_array($assignmentPermission, [Table::LEVEL_TEAM, Table::LEVEL_NO]) + ) { + return true; + } + + $toProcess = false; + + if (!$entity->isNew()) { + $userIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS); + + if ($entity->isAttributeChanged(self::ATTR_ASSIGNED_USERS_IDS)) { + $toProcess = true; + } + } + else { + $toProcess = true; + } + + $userIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS); + + if (!$toProcess) { + return true; + } + + if (empty($userIdList)) { + if ($assignmentPermission === Table::LEVEL_NO && !$user->isApi()) { + return false; + } + + return true; + } + + if ($assignmentPermission === Table::LEVEL_NO) { + return $this->isPermittedAssignedUsersLevelNo($user, $entity); + } + + if ($assignmentPermission === Table::LEVEL_TEAM) { + return $this->isPermittedAssignedUsersLevelTeam($user, $entity); + } + + return true; + } + + private function isPermittedAssignedUsersLevelNo(User $user, Entity $entity): bool + { + $userIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS); + + $fetchedAssignedUserIdList = $entity->getFetched(self::ATTR_ASSIGNED_USERS_IDS); + + foreach ($userIdList as $userId) { + if (!$entity->isNew() && in_array($userId, $fetchedAssignedUserIdList)) { + continue; + } + + if ($user->getId() !== $userId) { + return false; + } + } + + return true; + } + + private function isPermittedAssignedUsersLevelTeam(User $user, Entity $entity): bool + { + $userIdList = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS); + + $fetchedAssignedUserIdList = $entity->getFetched(self::ATTR_ASSIGNED_USERS_IDS); + + $teamIdList = $user->getLinkMultipleIdList(self::FIELD_TEAMS); + + foreach ($userIdList as $userId) { + if (!$entity->isNew() && in_array($userId, $fetchedAssignedUserIdList)) { + continue; + } + + if ( + !$this->entityManager + ->getRepository('User') + ->checkBelongsToAnyOfTeams($userId, $teamIdList) + ) { + return false; + } + } + + return true; + } +} diff --git a/application/Espo/Core/Di/AssignmentCheckerManagerAware.php b/application/Espo/Core/Di/AssignmentCheckerManagerAware.php new file mode 100644 index 0000000000..bae031c54c --- /dev/null +++ b/application/Espo/Core/Di/AssignmentCheckerManagerAware.php @@ -0,0 +1,37 @@ +assignmentCheckerManager = $assignmentCheckerManager; + } +} diff --git a/application/Espo/Core/Record/Service.php b/application/Espo/Core/Record/Service.php index 82d80c7703..07cd15afad 100644 --- a/application/Espo/Core/Record/Service.php +++ b/application/Espo/Core/Record/Service.php @@ -84,7 +84,8 @@ class Service implements Crud, Di\FieldUtilAware, Di\FieldValidationManagerAware, Di\RecordServiceContainerAware, - Di\SelectBuilderFactoryAware + Di\SelectBuilderFactoryAware, + Di\AssignmentCheckerManagerAware { use Di\ConfigSetter; use Di\ServiceFactorySetter; @@ -97,6 +98,7 @@ class Service implements Crud, use Di\FieldValidationManagerSetter; use Di\RecordServiceContainerSetter; use Di\SelectBuilderFactorySetter; + use Di\AssignmentCheckerManagerSetter; protected $getEntityBeforeUpdate = false; @@ -364,253 +366,21 @@ class Service implements Crud, } /** - * @return void * @throws Forbidden */ - public function processAssignmentCheck(Entity $entity) + protected function processAssignmentCheck(Entity $entity): void { if (!$this->checkAssignment($entity)) { throw new Forbidden("Assignment failure: assigned user or team not allowed."); } } + /** + * Check whether assignment can be applied for an entity. + */ public function checkAssignment(Entity $entity): bool { - if (!$this->isPermittedAssignedUser($entity)) { - return false; - } - - if (!$this->isPermittedTeams($entity)) { - return false; - } - - if ($entity->hasLinkMultipleField('assignedUsers')) { - if (!$this->isPermittedAssignedUsers($entity)) { - return false; - } - } - - return true; - } - - public function isPermittedAssignedUsers(Entity $entity): bool - { - if (!$entity->hasLinkMultipleField('assignedUsers')) { - return true; - } - - if ($this->user->isPortal()) { - if (count($entity->getLinkMultipleIdList('assignedUsers')) === 0) { - return true; - } - } - - $assignmentPermission = $this->acl->get('assignmentPermission'); - - if ( - $assignmentPermission === true || - $assignmentPermission === AclTable::LEVEL_YES || - !in_array($assignmentPermission, [AclTable::LEVEL_TEAM, AclTable::LEVEL_NO]) - ) { - return true; - } - - $toProcess = false; - - if (!$entity->isNew()) { - $userIdList = $entity->getLinkMultipleIdList('assignedUsers'); - - if ($entity->isAttributeChanged('assignedUsersIds')) { - $toProcess = true; - } - } - else { - $toProcess = true; - } - - $userIdList = $entity->getLinkMultipleIdList('assignedUsers'); - - if ($toProcess) { - if (empty($userIdList)) { - if ($assignmentPermission == AclTable::LEVEL_NO && !$this->user->isApi()) { - return false; - } - - return true; - } - - $fetchedAssignedUserIdList = $entity->getFetched('assignedUsersIds'); - - if ($assignmentPermission === AclTable::LEVEL_NO) { - foreach ($userIdList as $userId) { - if (!$entity->isNew() && in_array($userId, $fetchedAssignedUserIdList)) { - continue; - } - - if ($this->user->id != $userId) { - return false; - } - } - } else if ($assignmentPermission === AclTable::LEVEL_TEAM) { - $teamIdList = $this->user->getLinkMultipleIdList('teams'); - - foreach ($userIdList as $userId) { - if (!$entity->isNew() && in_array($userId, $fetchedAssignedUserIdList)) { - continue; - } - - if ( - !$this->entityManager - ->getRepository('User') - ->checkBelongsToAnyOfTeams($userId, $teamIdList) - ) { - return false; - } - } - } - } - - return true; - } - - public function isPermittedAssignedUser(Entity $entity): bool - { - if (!$entity->hasAttribute('assignedUserId')) { - return true; - } - - $assignedUserId = $entity->get('assignedUserId'); - - if ($this->user->isPortal()) { - if (!$entity->isAttributeChanged('assignedUserId') && empty($assignedUserId)) { - return true; - } - } - - $assignmentPermission = $this->acl->get('assignmentPermission'); - - if ( - $assignmentPermission === true || - $assignmentPermission === AclTable::LEVEL_YES || - !in_array($assignmentPermission, [AclTable::LEVEL_TEAM, AclTable::LEVEL_NO]) - ) { - return true; - } - - $toProcess = false; - - if (!$entity->isNew()) { - if ($entity->isAttributeChanged('assignedUserId')) { - $toProcess = true; - } - } else { - $toProcess = true; - } - - if ($toProcess) { - if (empty($assignedUserId)) { - if ($assignmentPermission === AclTable::LEVEL_NO && !$this->user->isApi()) { - return false; - } - - return true; - } - - if ($assignmentPermission === AclTable::LEVEL_NO) { - if ($this->user->id !== $assignedUserId) { - return false; - } - } - else if ($assignmentPermission === AclTable::LEVEL_TEAM) { - $teamIdList = $this->user->get('teamsIds'); - - if ( - !$this->entityManager - ->getRepository('User') - ->checkBelongsToAnyOfTeams($assignedUserId, $teamIdList) - ) { - return false; - } - } - } - - return true; - } - - public function isPermittedTeams(Entity $entity): bool - { - $assignmentPermission = $this->acl->get('assignmentPermission'); - - if ( - empty($assignmentPermission) || - $assignmentPermission === true || - !in_array($assignmentPermission, [AclTable::LEVEL_TEAM, AclTable::LEVEL_NO]) - ) { - return true; - } - - if (!$entity->hasLinkMultipleField('teams')) { - return true; - } - - $teamIdList = $entity->getLinkMultipleIdList('teams'); - - if (empty($teamIdList)) { - if ($assignmentPermission === 'team') { - if ($entity->hasLinkMultipleField('assignedUsers')) { - $assignedUserIdList = $entity->getLinkMultipleIdList('assignedUsers'); - - if (empty($assignedUserIdList)) { - return false; - } - } - else if ($entity->hasAttribute('assignedUserId')) { - if (!$entity->get('assignedUserId')) { - return false; - } - } - } - - return true; - } - - $newIdList = []; - - if (!$entity->isNew()) { - $existingIdList = []; - - $teamCollection = $this->entityManager - ->getRepository($entity->getEntityType()) - ->getRelation($entity, 'teams') - ->select('id') - ->find(); - - foreach ($teamCollection as $team) { - $existingIdList[] = $team->id; - } - - foreach ($teamIdList as $id) { - if (!in_array($id, $existingIdList)) { - $newIdList[] = $id; - } - } - } else { - $newIdList = $teamIdList; - } - - if (empty($newIdList)) { - return true; - } - - $userTeamIdList = $this->user->getLinkMultipleIdList('teams'); - - foreach ($newIdList as $id) { - if (!in_array($id, $userTeamIdList)) { - return false; - } - } - - return true; + return $this->assignmentCheckerManager->check($this->user, $entity); } protected function filterInputAttribute($attribute, $value) diff --git a/application/Espo/Resources/metadata/aclDefs/Email.json b/application/Espo/Resources/metadata/aclDefs/Email.json index a9ef77831b..8fe35a3411 100644 --- a/application/Espo/Resources/metadata/aclDefs/Email.json +++ b/application/Espo/Resources/metadata/aclDefs/Email.json @@ -3,5 +3,6 @@ "ownershipCheckerClassName": "Espo\\Classes\\Acl\\Email\\OwnershipChecker", "portalAccessCheckerClassName": "Espo\\Classes\\AclPortal\\Email\\AccessChecker", "portalOwnershipCheckerClassName": "Espo\\Classes\\AclPortal\\Email\\OwnershipChecker", + "assignmentCheckerClassName": "Espo\\Classes\\Acl\\Email\\AssignmentChecker", "readOwnerUserField": "users" } diff --git a/application/Espo/Resources/metadata/app/containerServices.json b/application/Espo/Resources/metadata/app/containerServices.json index f6c2f13ef7..ce9ebf28c4 100644 --- a/application/Espo/Resources/metadata/app/containerServices.json +++ b/application/Espo/Resources/metadata/app/containerServices.json @@ -80,6 +80,9 @@ "fieldValidationManager": { "className": "Espo\\Core\\FieldValidation\\FieldValidationManager" }, + "assignmentCheckerManager": { + "className": "Espo\\Core\\Acl\\AssignmentChecker\\AssignmentCheckerManager" + }, "hasher": { "className": "Espo\\Core\\Utils\\Hasher" }, diff --git a/application/Espo/Services/Email.php b/application/Espo/Services/Email.php index d624be26e6..69fc29a857 100644 --- a/application/Espo/Services/Email.php +++ b/application/Espo/Services/Email.php @@ -963,14 +963,4 @@ class Email extends Record implements return $data; } - - public function isPermittedAssignedUser(Entity $entity): bool - { - return true; - } - - public function isPermittedAssignedUsers(Entity $entity): bool - { - return true; - } } diff --git a/application/Espo/Services/Note.php b/application/Espo/Services/Note.php index 4faa8940ac..9b3826758b 100644 --- a/application/Espo/Services/Note.php +++ b/application/Espo/Services/Note.php @@ -190,7 +190,7 @@ class Note extends Record $entity->set('post', $post); } - public function processAssignmentCheck(Entity $entity) + protected function processAssignmentCheck(Entity $entity): void { if (!$entity->isNew()) { return;