stream update note access control

This commit is contained in:
Yuri Kuznetsov
2021-06-29 17:27:12 +03:00
parent 0ab3da1512
commit 25281d0c6f
6 changed files with 345 additions and 84 deletions
@@ -0,0 +1,95 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Utils\Acl;
use Espo\Entities\User;
use Espo\ORM\EntityManager;
use Espo\Core\AclManager;
use Espo\Core\Portal\AclManagerContainer as PortalAclManagerContainer;
use Espo\Core\Exceptions\Error;
class UserAclManagerProvider
{
private $entityManager;
private $aclManager;
private $portalAclManagerContainer;
private $user;
private $map = [];
public function __construct(
EntityManager $entityManager,
AclManager $aclManager,
PortalAclManagerContainer $portalAclManagerContainer,
User $user
) {
$this->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;
}
}
+13
View File
@@ -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');
+71 -47
View File
@@ -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;
}
}
+28 -21
View File
@@ -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);
}
}
@@ -0,0 +1,109 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Services\Stream;
use Espo\Entities\Note;
use Espo\Entities\User;
use Espo\Core\Utils\Acl\UserAclManagerProvider;
class NoteAccessControl
{
private $userAclManagerProvider;
public function __construct(UserAclManagerProvider $userAclManagerProvider)
{
$this->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);
}
}
}
@@ -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');