From 25281d0c6fc85d62ca68810c99380e8fa3a7b8a8 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Tue, 29 Jun 2021 17:27:12 +0300 Subject: [PATCH] stream update note access control --- .../Core/Utils/Acl/UserAclManagerProvider.php | 95 ++++++++++++++ application/Espo/Entities/Note.php | 13 ++ application/Espo/Services/Notification.php | 118 +++++++++++------- application/Espo/Services/Stream.php | 49 ++++---- .../Services/Stream/NoteAccessControl.php | 109 ++++++++++++++++ .../Tools/EmailNotification/Processor.php | 45 ++++--- 6 files changed, 345 insertions(+), 84 deletions(-) create mode 100644 application/Espo/Core/Utils/Acl/UserAclManagerProvider.php create mode 100644 application/Espo/Services/Stream/NoteAccessControl.php diff --git a/application/Espo/Core/Utils/Acl/UserAclManagerProvider.php b/application/Espo/Core/Utils/Acl/UserAclManagerProvider.php new file mode 100644 index 0000000000..f572ead9fd --- /dev/null +++ b/application/Espo/Core/Utils/Acl/UserAclManagerProvider.php @@ -0,0 +1,95 @@ +entityManager = $entityManager; + $this->aclManager = $aclManager; + $this->portalAclManagerContainer = $portalAclManagerContainer; + $this->user = $user; + } + + /** + * @throws Error + */ + public function get(User $user): AclManager + { + $key = $user->getId() ?? spl_object_hash($user); + + if (!isset($this->map[$key])) { + $this->map[$key] = $this->load($user); + } + + return $this->map[$key]; + } + + private function load(User $user): AclManager + { + $aclManager = $this->aclManager; + + if ($user->isPortal() && !$this->user->isPortal()) { + $portal = $this->entityManager + ->getRDBRepository(User::ENTITY_TYPE) + ->getRelation($user, 'portals') + ->findOne(); + + if (!$portal) { + throw new Error("No portal for portal user '" . $user->getId() . "'."); + } + + $aclManager = $this->portalAclManagerContainer->get($portal); + } + + return $aclManager; + } +} diff --git a/application/Espo/Entities/Note.php b/application/Espo/Entities/Note.php index 2d934c079e..8307de80c8 100644 --- a/application/Espo/Entities/Note.php +++ b/application/Espo/Entities/Note.php @@ -31,6 +31,8 @@ namespace Espo\Entities; use Espo\Core\ORM\Entity; +use stdClass; + class Note extends Entity { public const ENTITY_TYPE = 'Note'; @@ -47,6 +49,12 @@ class Note extends Entity public const TYPE_POST = 'Post'; + public const TYPE_UPDATE = 'Update'; + + public const TYPE_STATUS = 'Status'; + + public const TYPE_CREATE = 'Create'; + private $aclIsProcessed = false; public function isPost(): bool @@ -74,6 +82,11 @@ class Note extends Entity return $this->get('parentId'); } + public function getData(): stdClass + { + return $this->get('data') ?? (object) []; + } + public function isInternal(): bool { return (bool) $this->get('isInternal'); diff --git a/application/Espo/Services/Notification.php b/application/Espo/Services/Notification.php index c3a064e9b0..f22b0b3663 100644 --- a/application/Espo/Services/Notification.php +++ b/application/Espo/Services/Notification.php @@ -33,11 +33,14 @@ use Espo\Core\Utils\Util; use Espo\Core\{ Record\Collection as RecordCollection, + Exceptions\Error, }; use Espo\Entities\Note; use Espo\Entities\User; +use Espo\Services\Stream\NoteAccessControl; + use Espo\Core\Di; class Notification extends \Espo\Services\Record implements @@ -48,7 +51,9 @@ class Notification extends \Espo\Services\Record implements protected $actionHistoryDisabled = true; - public function notifyAboutMentionInPost(string $userId, string $noteId) + private $noteAccessControl = null; + + public function notifyAboutMentionInPost(string $userId, string $noteId): void { $notification = $this->entityManager->getEntity('Notification'); @@ -63,7 +68,7 @@ class Notification extends \Espo\Services\Record implements $this->entityManager->saveEntity($notification); } - public function notifyAboutNote(array $userIdList, Note $note) + public function notifyAboutNote(array $userIdList, Note $note): void { $data = ['noteId' => $note->id]; @@ -141,7 +146,7 @@ class Notification extends \Espo\Services\Record implements } } - public function checkUserNoteAccess(User $user, Note $note) : bool + public function checkUserNoteAccess(User $user, Note $note): bool { if ($user->isPortal()) { if ($note->get('relatedType')) { @@ -170,7 +175,7 @@ class Notification extends \Espo\Services\Record implements return true; } - public function getNotReadCount(string $userId) : int + public function getNotReadCount(string $userId): int { $whereClause = array( 'userId' => $userId, @@ -208,7 +213,7 @@ class Notification extends \Espo\Services\Record implements return true; } - public function getList(string $userId, array $params = []) : RecordCollection + public function getList(string $userId, array $params = []): RecordCollection { $queryBuilder = $this->entityManager ->getQueryBuilder() @@ -219,6 +224,12 @@ class Notification extends \Espo\Services\Record implements 'userId' => $userId, ]; + $user = $this->entityManager->getEntity(User::ENTITY_TYPE, $userId); + + if (!$user) { + throw new Error("User not found."); + } + if (!empty($params['after'])) { $whereClause['createdAt>'] = $params['after']; } @@ -275,54 +286,58 @@ class Notification extends \Espo\Services\Record implements case 'MentionInPost': $note = $this->entityManager->getEntity('Note', $data->noteId); - if ($note) { - if ($note->get('parentId') && $note->get('parentType')) { - $parent = $this->entityManager - ->getEntity($note->get('parentType'), $note->get('parentId')); - - if ($parent) { - $note->set('parentName', $parent->get('name')); - } - } - else { - if (!$note->get('isGlobal')) { - $targetType = $note->get('targetType'); - - if (!$targetType || $targetType === 'users') { - $note->loadLinkMultipleField('users'); - } - - if ($targetType !== 'users') { - if (!$targetType || $targetType === 'teams') { - $note->loadLinkMultipleField('teams'); - } else if ($targetType === 'portals') { - $note->loadLinkMultipleField('portals'); - } - } - } - } - - if ($note->get('relatedId') && $note->get('relatedType')) { - $related = $this->entityManager - ->getEntity($note->get('relatedType'), $note->get('relatedId')); - - if ($related) { - $note->set('relatedName', $related->get('name')); - } - } - - $note->loadLinkMultipleField('attachments'); - - $entity->set('noteData', $note->toArray()); - } - else { + if (!$note) { unset($collection[$k]); $count--; $this->entityManager->removeEntity($entity); + + break; } + $this->getNoteAccessControl()->apply($note, $user); + + if ($note->get('parentId') && $note->get('parentType')) { + $parent = $this->entityManager + ->getEntity($note->get('parentType'), $note->get('parentId')); + + if ($parent) { + $note->set('parentName', $parent->get('name')); + } + } + else { + if (!$note->get('isGlobal')) { + $targetType = $note->get('targetType'); + + if (!$targetType || $targetType === 'users') { + $note->loadLinkMultipleField('users'); + } + + if ($targetType !== 'users') { + if (!$targetType || $targetType === 'teams') { + $note->loadLinkMultipleField('teams'); + } + else if ($targetType === 'portals') { + $note->loadLinkMultipleField('portals'); + } + } + } + } + + if ($note->get('relatedId') && $note->get('relatedType')) { + $related = $this->entityManager + ->getEntity($note->get('relatedType'), $note->get('relatedId')); + + if ($related) { + $note->set('relatedName', $related->get('name')); + } + } + + $note->loadLinkMultipleField('attachments'); + + $entity->set('noteData', $note->toArray()); + break; } } @@ -344,7 +359,7 @@ class Notification extends \Espo\Services\Record implements return new RecordCollection($collection, $count); } - protected function getIgnoreScopeList() : array + protected function getIgnoreScopeList(): array { $ignoreScopeList = []; @@ -366,4 +381,13 @@ class Notification extends \Espo\Services\Record implements return $ignoreScopeList; } + + private function getNoteAccessControl(): NoteAccessControl + { + if (!$this->noteAccessControl) { + $this->noteAccessControl = $this->injectableFactory->create(NoteAccessControl::class); + } + + return $this->noteAccessControl; + } } diff --git a/application/Espo/Services/Stream.php b/application/Espo/Services/Stream.php index f1e414c2f7..64d1b248b8 100644 --- a/application/Espo/Services/Stream.php +++ b/application/Espo/Services/Stream.php @@ -31,6 +31,7 @@ namespace Espo\Services; use Espo\Core\Exceptions\Forbidden; use Espo\Core\Exceptions\NotFound; +use Espo\Core\Exceptions\Error; use Espo\ORM\{ Entity, @@ -42,6 +43,7 @@ use Espo\{ Entities\User, Entities\Note as NoteEntity, Services\Notification as NotificationService, + Services\Stream\NoteAccessControl, }; use Espo\Core\{ @@ -52,11 +54,11 @@ use Espo\Core\{ AclManager, Acl\Exceptions\NotImplemented as AclNotImplemented, ServiceFactory, - Portal\AclManagerContainer as PortalAclManagerContainer, Utils\FieldUtil, Record\Collection as RecordCollection, Select\SelectBuilderFactory, Select\SearchParams, + Utils\Acl\UserAclManagerProvider, }; use StdClass; @@ -101,12 +103,12 @@ class Stream protected $serviceFactory; - protected $portalAclManagerContainer; - protected $fieldUtil; protected $selectBuilderFactory; + private $noteAccessControl; + protected const NOTE_ACL_PERIOD = '1 hour'; public function __construct( @@ -117,9 +119,10 @@ class Stream Acl $acl, AclManager $aclManager, ServiceFactory $serviceFactory, - PortalAclManagerContainer $portalAclManagerContainer, FieldUtil $fieldUtil, - SelectBuilderFactory $selectBuilderFactory + SelectBuilderFactory $selectBuilderFactory, + UserAclManagerProvider $userAclManagerProvider, + NoteAccessControl $noteAccessControl ) { $this->entityManager = $entityManager; $this->config = $config; @@ -128,9 +131,10 @@ class Stream $this->acl = $acl; $this->aclManager = $aclManager; $this->serviceFactory = $serviceFactory; - $this->portalAclManagerContainer = $portalAclManagerContainer; $this->fieldUtil = $fieldUtil; $this->selectBuilderFactory = $selectBuilderFactory; + $this->userAclManagerProvider = $userAclManagerProvider; + $this->noteAccessControl = $noteAccessControl; } protected function getNotificationService(): NotificationService @@ -876,6 +880,8 @@ class Stream foreach ($collection as $e) { $this->loadNoteAdditionalFields($e); + + $this->applyAccessControlToNote($e, $user); } $total = -2; @@ -1215,6 +1221,8 @@ class Stream if ($e->get('relatedId') && $e->get('relatedType')) { $e->loadParentNameField('related'); } + + $this->applyAccessControlToNote($e); } $count = $this->entityManager @@ -1964,22 +1972,12 @@ class Stream protected function getUserAclManager(User $user): ?AclManager { - $aclManager = $this->aclManager; - - if ($user->isPortal() && !$this->user->isPortal()) { - $portal = $this->entityManager - ->getRepository('User') - ->getRelation($user, 'portals') - ->findOne(); - - if (!$portal) { - return null; - } - - $aclManager = $this->portalAclManagerContainer->get($portal); + try { + return $this->userAclManagerProvider->get($user); + } + catch (Error $e) { + return null; } - - return $aclManager; } protected function getNotAllEntityTypeList(User $user): array @@ -2317,4 +2315,13 @@ class Stream $this->entityManager->saveEntity($note, $noteOptions); } + + public function applyAccessControlToNote(NoteEntity $note, ?User $user = null): void + { + if (!$user) { + $user = $this->user; + } + + $this->noteAccessControl->apply($note, $user); + } } diff --git a/application/Espo/Services/Stream/NoteAccessControl.php b/application/Espo/Services/Stream/NoteAccessControl.php new file mode 100644 index 0000000000..c2c548eb79 --- /dev/null +++ b/application/Espo/Services/Stream/NoteAccessControl.php @@ -0,0 +1,109 @@ +userAclManagerProvider = $userAclManagerProvider; + } + + public function apply(Note $note, User $user): void + { + if ($note->getType() === Note::TYPE_UPDATE && $note->getParentType()) { + $data = $note->getData(); + + $fields = $data->fields ?? []; + + $data->attributes = $data->attributes ?? (object) []; + $data->attributes->was = $data->attributes->was ?? (object) []; + $data->attributes->became = $data->attributes->became ?? (object) []; + + $forbiddenFieldList = $this->userAclManagerProvider + ->get($user) + ->getScopeForbiddenFieldList($user, $note->getParentType()); + + $forbiddenAttributeList = $this->userAclManagerProvider + ->get($user) + ->getScopeForbiddenAttributeList($user, $note->getParentType()); + + $data->fields = array_values(array_diff($fields, $forbiddenFieldList)); + + foreach ($forbiddenAttributeList as $attribute) { + unset($data->attributes->was->$attribute); + unset($data->attributes->became->$attribute); + } + + $note->set('data', $data); + } + + if ($note->getType() === Note::TYPE_STATUS && $note->getParentType()) { + $forbiddenFieldList = $this->userAclManagerProvider + ->get($user) + ->getScopeForbiddenFieldList($user, $note->getParentType()); + + $data = $note->getData(); + + $field = $data->field ?? null; + + if (in_array($field, $forbiddenFieldList)) { + $data->value = null; + $data->style = null; + } + + $note->set('data', $data); + } + + if ($note->getType() === Note::TYPE_CREATE && $note->getParentType()) { + $forbiddenFieldList = $this->userAclManagerProvider + ->get($user) + ->getScopeForbiddenFieldList($user, $note->getParentType()); + + $data = $note->getData(); + + $field = $data->statusField ?? null; + + if (in_array($field, $forbiddenFieldList)) { + $data->statusValue = null; + $data->statusStyle = null; + } + + $note->set('data', $data); + } + } +} diff --git a/application/Espo/Tools/EmailNotification/Processor.php b/application/Espo/Tools/EmailNotification/Processor.php index 496696ef64..26e940b493 100644 --- a/application/Espo/Tools/EmailNotification/Processor.php +++ b/application/Espo/Tools/EmailNotification/Processor.php @@ -34,6 +34,7 @@ use Espo\{ ORM\EntityManager, ORM\Query\SelectBuilder as SelectBuilder, Entities\User as UserEntity, + Entities\Note as NoteEntity, }; use Espo\Core\{ @@ -50,6 +51,8 @@ use Espo\Core\{ Utils\Log, }; +use Espo\Services\Stream\NoteAccessControl; + use Michelf\Markdown; use Exception; @@ -58,29 +61,31 @@ use StdClass; class Processor { - const HOURS_THERSHOLD = 5; + protected const HOURS_THERSHOLD = 5; - const PROCESS_MAX_COUNT = 200; + protected const PROCESS_MAX_COUNT = 200; - protected $htmlizer; + private $htmlizer; - protected $entityManager; + private $entityManager; - protected $htmlizerFactory; + private $htmlizerFactory; - protected $emailSender; + private $emailSender; - protected $config; + private $config; - protected $selectBuilderFactory; + private $selectBuilderFactory; - protected $injectableFactory; + private $injectableFactory; - protected $templateFileManager; + private $templateFileManager; - protected $metadata; + private $metadata; - protected $log; + private $log; + + private $noteAccessControl; public function __construct( EntityManager $entityManager, @@ -92,7 +97,8 @@ class Processor TemplateFileManager $templateFileManager, Metadata $metadata, Language $language, - Log $log + Log $log, + NoteAccessControl $noteAccessControl ) { $this->entityManager = $entityManager; $this->htmlizerFactory = $htmlizerFactory; @@ -104,6 +110,7 @@ class Processor $this->metadata = $metadata; $this->language = $language; $this->log = $log; + $this->noteAccessControl = $noteAccessControl; } protected $emailNotificationEntityHandlerHash = []; @@ -437,7 +444,7 @@ class Processor return $this->emailNotificationEntityHandlerHash[$entityType]; } - protected function processNotificationNotePost($note, $user): void + protected function processNotificationNotePost(NoteEntity $note, UserEntity $user): void { $parentId = $note->get('parentId'); $parentType = $note->get('parentType'); @@ -607,8 +614,10 @@ class Processor return $this->config->getSiteUrl(); } - protected function processNotificationNoteStatus($note, $user): void + protected function processNotificationNoteStatus(NoteEntity $note, UserEntity $user): void { + $this->noteAccessControl->apply($note, $user); + $parentId = $note->get('parentId'); $parentType = $note->get('parentType'); @@ -646,6 +655,10 @@ class Processor return; } + if ($noteData->value === null) { + return; + } + $data['value'] = $noteData->value; $data['field'] = $noteData->field; $data['valueTranslated'] = $this->language->translateOption($data['value'], $data['field'], $parentType); @@ -688,7 +701,7 @@ class Processor } } - protected function processNotificationNoteEmailReceived($note, $user): void + protected function processNotificationNoteEmailReceived(NoteEntity $note, UserEntity $user): void { $parentId = $note->get('parentId'); $parentType = $note->get('parentType');