grand acl refactoring

This commit is contained in:
Yuri Kuznetsov
2021-04-10 16:25:40 +03:00
parent e6edc6ae03
commit f7c8b62f6c
79 changed files with 5220 additions and 1875 deletions
+1 -2
View File
@@ -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
{
+4 -1
View File
@@ -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;
}
+1 -2
View File
@@ -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
{
+3 -4
View File
@@ -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();
+4 -1
View File
@@ -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;
}
+1 -4
View File
@@ -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
{
+2 -5
View File
@@ -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;
}
}
+1 -4
View File
@@ -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)
{
+1 -2
View File
@@ -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
{
+1 -2
View File
@@ -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
{
+5 -2
View File
@@ -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;
+3 -4
View File
@@ -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;
}
}
+5 -2
View File
@@ -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;
@@ -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;
}
}
@@ -0,0 +1,52 @@
<?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\Classes\Acl\Attachment;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Core\{
Acl\OwnershipOwnChecker,
};
class OwnershipChecker implements OwnershipOwnChecker
{
private const ATTR_CREATED_BY_ID = 'createdById';
public function checkOwn(User $user, Entity $entity): bool
{
if ($user->getId() === $entity->get(self::ATTR_CREATED_BY_ID)) {
return true;
}
return false;
}
}
+56 -19
View File
@@ -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<string>
*/
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<string>
*/
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<string>
*/
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);
}
}
@@ -0,0 +1,40 @@
<?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\Acl;
use Espo\Entities\User;
interface AccessChecker
{
/**
* Check access to scope. Should return TRUE if at least one action is allowed.
*/
public function check(User $user, ScopeData $data): bool;
}
@@ -0,0 +1,114 @@
<?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\Acl;
use Espo\Core\{
Utils\ClassFinder,
Utils\Metadata,
InjectableFactory,
Acl\Exceptions\NotImplemented,
AclManager,
Binding\BindingContainer,
Binding\Binder,
Binding\BindingData,
};
class AccessCheckerFactory
{
private $defaultClassName = DefaultAccessChecker::class;
private $classFinder;
private $metadata;
private $injectableFactory;
public function __construct(
ClassFinder $classFinder,
Metadata $metadata,
InjectableFactory $injectableFactory
) {
$this->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);
}
}
@@ -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;
}
@@ -0,0 +1,40 @@
<?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\Acl;
use Espo\Entities\User;
interface AccessDeleteChecker extends AccessChecker
{
/**
* Check 'delete' access.
*/
public function checkDelete(User $user, ScopeData $data): bool;
}
@@ -31,10 +31,10 @@ namespace Espo\Core\Acl;
use Espo\Entities\User;
interface ScopeAcl
interface AccessEditChecker extends AccessChecker
{
/**
* Check access to a scope.
* Check 'edit' access.
*/
public function checkScope(User $user, ScopeData $data, ?string $action = null): bool;
public function checkEdit(User $user, ScopeData $data): bool;
}
@@ -0,0 +1,40 @@
<?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\Acl;
interface AccessEntityCREDChecker extends
AccessEntityCreateChecker,
AccessEntityReadChecker,
AccessEntityEditChecker,
AccessEntityDeleteChecker
{
}
@@ -0,0 +1,41 @@
<?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\Acl;
interface AccessEntityCREDSChecker extends
AccessEntityCreateChecker,
AccessEntityReadChecker,
AccessEntityEditChecker,
AccessEntityDeleteChecker,
AccessEntityStreamChecker
{
}
@@ -33,10 +33,10 @@ use Espo\ORM\Entity;
use Espo\Entities\User;
interface EntityCreateAcl
interface AccessEntityCreateChecker extends AccessCreateChecker
{
/**
* Check 'create' access.
* Check 'create' access for an entity.
*/
public function checkEntityCreate(User $user, Entity $entity, ScopeData $data): bool;
}
@@ -33,10 +33,10 @@ use Espo\ORM\Entity;
use Espo\Entities\User;
interface EntityDeleteAcl
interface AccessEntityDeleteChecker extends AccessDeleteChecker
{
/**
* Check 'delete' access.
* Check 'delete' access for an entity.
*/
public function checkEntityDelete(User $user, Entity $entity, ScopeData $data): bool;
}
@@ -33,10 +33,10 @@ use Espo\ORM\Entity;
use Espo\Entities\User;
interface EntityEditAcl
interface AccessEntityEditChecker extends AccessEditChecker
{
/**
* Check 'edit' access.
* Check 'edit' access for an entity.
*/
public function checkEntityEdit(User $user, Entity $entity, ScopeData $data): bool;
}
@@ -33,10 +33,10 @@ use Espo\ORM\Entity;
use Espo\Entities\User;
interface EntityReadAcl
interface AccessEntityReadChecker extends AccessReadChecker
{
/**
* Check 'read' access.
* Check 'read' access for entity.
*/
public function checkEntityRead(User $user, Entity $entity, ScopeData $data): bool;
}
@@ -33,10 +33,10 @@ use Espo\ORM\Entity;
use Espo\Entities\User;
interface EntityStreamAcl
interface AccessEntityStreamChecker extends AccessStreamChecker
{
/**
* Check 'stream' access.
* Check 'stream' access for an entity.
*/
public function checkEntityStream(User $user, Entity $entity, ScopeData $data): bool;
}
@@ -0,0 +1,40 @@
<?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\Acl;
use Espo\Entities\User;
interface AccessReadChecker extends AccessChecker
{
/**
* Check 'read' access.
*/
public function checkRead(User $user, ScopeData $data): bool;
}
@@ -0,0 +1,40 @@
<?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\Acl;
use Espo\Entities\User;
interface AccessStreamChecker extends AccessChecker
{
/**
* Check 'stream' access.
*/
public function checkStream(User $user, ScopeData $data): bool;
}
+2 -273
View File
@@ -29,281 +29,10 @@
namespace Espo\Core\Acl;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Core\{
Acl\Table,
ORM\EntityManager,
AclManager,
Utils\Config,
Utils\DateTime as DateTimeUtil,
};
/**
* An implementation for access checking for entities. Can be overridden in `Acl` namespace.
* @deprecated Use AccessChecker interfaces instead.
*/
class Acl implements ScopeAcl, EntityAcl, EntityDeleteAcl
class Acl extends Base
{
protected const ATTR_CREATED_BY_ID = 'createdById';
protected const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
protected const ATTR_ASSIGNED_USERS_IDS = 'assignedUsersIds';
protected const ATTR_ASSIGNED_TEAMS_IDS = 'teamsIds';
protected const FIELD_TEAMS = 'teams';
protected const FIELD_ASSIGNED_USERS = 'assignedUsers';
protected $scope;
protected $ownerUserIdAttribute = null;
protected $allowDeleteCreatedThresholdPeriod = '24 hours';
protected $checkIsOwnerentityManager;
protected $entityManager;
protected $aclManager;
protected $config;
public function __construct(
EntityManager $entityManager,
AclManager $aclManager,
Config $config,
string $scope = null
) {
$this->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;
}
}
+103 -6
View File
@@ -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;
}
}
@@ -0,0 +1,211 @@
<?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\Acl;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Core\{
Acl\Table,
AclManager,
Utils\Config,
Utils\DateTime as DateTimeUtil,
};
/**
* A default implementation for access checking.
*/
class DefaultAccessChecker implements
AccessEntityCreateChecker,
AccessEntityReadChecker,
AccessEntityEditChecker,
AccessEntityDeleteChecker,
AccessEntityStreamChecker
{
private const ATTR_CREATED_BY_ID = 'createdById';
private const ATTR_CREATED_AT = 'createdAt';
private const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
private const ALLOW_DELETE_OWN_CREATED_PERIOD = '24 hours';
private $aclManager;
private $config;
private $scopeChecker;
public function __construct(
AclManager $aclManager,
Config $config,
ScopeChecker $scopeChecker
) {
$this->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;
}
}
@@ -0,0 +1,104 @@
<?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\Acl;
use Espo\ORM\Entity;
use Espo\Entities\User;
/**
* A default implementation for ownership checking.
*/
class DefaultOwnershipChecker implements OwnershipOwnChecker, OwnershipTeamChecker
{
private const ATTR_CREATED_BY_ID = 'createdById';
private const ATTR_ASSIGNED_USER_ID = 'assignedUserId';
private const ATTR_ASSIGNED_TEAMS_IDS = 'teamsIds';
private const FIELD_TEAMS = 'teams';
private const FIELD_ASSIGNED_USERS = 'assignedUsers';
public function checkOwn(User $user, Entity $entity): bool
{
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;
}
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;
}
}
+939
View File
@@ -0,0 +1,939 @@
<?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\Acl;
use Espo\Entities\User;
use Espo\Core\{
ORM\EntityManager,
Utils\Config,
Utils\Metadata,
Utils\FieldUtil,
Utils\DataCache,
Utils\ObjectUtil,
};
use StdClass;
use RuntimeException;
/**
* A table is generated for a user. Multiple roles are merged into a single table.
* Stores access levels.
*/
class DefaultTable implements Table
{
protected const LEVEL_NOT_SET = 'not-set';
protected $type = 'acl';
protected $defaultAclType = 'recordAllTeamOwnNo';
private $data = null;
protected $cacheKey;
protected $actionList = [
self::ACTION_READ,
self::ACTION_STREAM,
self::ACTION_EDIT,
self::ACTION_DELETE,
self::ACTION_CREATE,
];
protected $booleanActionList = [
self::ACTION_CREATE,
];
protected $levelList = [
self::LEVEL_YES,
self::LEVEL_ALL,
self::LEVEL_TEAM,
self::LEVEL_OWN,
self::LEVEL_NO,
];
protected $fieldActionList = [
self::ACTION_READ,
self::ACTION_EDIT,
];
protected $fieldLevelList = [
self::LEVEL_YES,
self::LEVEL_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 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);
}
}
+21 -16
View File
@@ -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 = [];
@@ -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;
@@ -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
{
}
@@ -0,0 +1,102 @@
<?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\Acl;
use Espo\Core\{
Utils\Metadata,
InjectableFactory,
Acl\Exceptions\NotImplemented,
AclManager,
Binding\BindingContainer,
Binding\Binder,
Binding\BindingData,
};
class OwnershipCheckerFactory
{
private $defaultClassName = DefaultOwnershipChecker::class;
private $metadata;
private $injectableFactory;
public function __construct(
Metadata $metadata,
InjectableFactory $injectableFactory
) {
$this->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);
}
}
@@ -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;
}
@@ -0,0 +1,42 @@
<?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\Acl;
use Espo\ORM\Entity;
use Espo\Entities\User;
interface OwnershipTeamChecker extends OwnershipChecker
{
/**
* Check whether an entity belongs to a user team.
*/
public function checkTeam(User $user, Entity $entity): bool;
}
@@ -0,0 +1,87 @@
<?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\Acl;
/**
* Checks scope access.
*/
class ScopeChecker
{
public function __construct()
{
}
public function check(ScopeData $data, ?string $action = null, ?ScopeCheckerData $checkerData = null): bool
{
if ($data->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;
}
}
@@ -0,0 +1,61 @@
<?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\Acl;
/**
* Scope checker data.
*/
class ScopeCheckerData
{
private $isOwnChecker;
private $inTeamChecker;
public function __construct(callable $isOwnChecker, callable $inTeamChecker)
{
$this->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();
}
}
@@ -0,0 +1,104 @@
<?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\Acl;
/**
* Builds scope checker data.
*/
class ScopeCheckerDataBuilder
{
private $isOwnChecker;
private $inTeamChecker;
public function __construct()
{
$this->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);
}
}
+6 -14
View File
@@ -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;
+28 -874
View File
@@ -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<string>
*/
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<string>
*/
public function getScopeForbiddenFieldList(string $scope, string $action, string $thresholdLevel): array;
}
@@ -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,
]);
}
}
@@ -0,0 +1,102 @@
<?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\Acl\Traits;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Core\{
Acl\ScopeData,
Acl\DefaultAccessChecker,
};
trait DefaultAccessCheckerDependency
{
/**
* @var DefaultAccessChecker
*/
private $defaultAccessChecker;
public function check(User $user, ScopeData $data): bool
{
return $this->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);
}
}
+255 -190
View File
@@ -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<string>
*/
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<string>
*/
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<string>
*/
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;
}
}
+5 -4
View File
@@ -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;
}
+171 -3
View File
@@ -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);
}
}
-222
View File
@@ -1,222 +0,0 @@
<?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\AclPortal;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Core\{
Acl\ScopeData,
};
trait Portal
{
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'];
}
$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;
}
}
+38 -3
View File
@@ -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);
}
}
@@ -0,0 +1,115 @@
<?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\Portal\Acl;
use Espo\Core\{
Utils\ClassFinder,
Utils\Metadata,
InjectableFactory,
Acl\Exceptions\NotImplemented,
Acl\AccessChecker,
Portal\AclManager as PortalAclManager,
Binding\BindingContainer,
Binding\Binder,
Binding\BindingData,
};
class AccessCheckerFactory
{
private $defaultClassName = DefaultAccessChecker::class;
private $classFinder;
private $metadata;
private $injectableFactory;
public function __construct(
ClassFinder $classFinder,
Metadata $metadata,
InjectableFactory $injectableFactory
) {
$this->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);
}
}
@@ -0,0 +1,164 @@
<?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\Portal\Acl;
use Espo\Entities\User;
use Espo\ORM\Entity;
use Espo\Core\{
Utils\Config,
Portal\AclManager as PortalAclManager,
Acl\ScopeData,
Acl\AccessEntityCreateChecker,
Acl\AccessEntityReadChecker,
Acl\AccessEntityEditChecker,
Acl\AccessEntityDeleteChecker,
Acl\AccessEntityStreamChecker,
};
/**
* A default implementation for access checking for portal.
*/
class DefaultAccessChecker implements
AccessEntityCreateChecker,
AccessEntityReadChecker,
AccessEntityEditChecker,
AccessEntityDeleteChecker,
AccessEntityStreamChecker
{
private $aclManager;
private $config;
private $scopeChecker;
public function __construct(
PortalAclManager $aclManager,
Config $config,
ScopeChecker $scopeChecker
) {
$this->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);
}
}
@@ -0,0 +1,178 @@
<?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\Portal\Acl;
use Espo\ORM\{
Entity,
EntityManager,
};
use Espo\Entities\User;
use Espo\Core\{
Acl\OwnershipOwnChecker,
};
/**
* A default implementation for ownership checking for portal.
*/
class DefaultOwnershipChecker implements
OwnershipOwnChecker,
OwnershipAccountChecker,
OwnershipContactChecker
{
private const ENTITY_ACCOUNT = 'Account';
private const ENTITY_CONTACT = 'Contact';
private const ATTR_CREATED_BY_ID = 'createdById';
private const ATTR_ACCOUNT_ID = 'accountId';
private const ATTR_CONTACT_ID = 'contactId';
private const ATTR_PARENT_ID = 'parentId';
private const ATTR_PARENT_TYPE = 'parentType';
private const FIELD_CONTACT = 'contact';
private const FIELD_CONTACTS = 'contacts';
private const FIELD_ACCOUNT = 'account';
private const FIELD_ACCOUNTS = 'accounts';
private const FIELD_PARENT = 'parent';
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->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;
}
}
@@ -0,0 +1,44 @@
<?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\Portal\Acl;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Core\Acl\OwnershipChecker;
interface OwnershipAccountChecker extends OwnershipChecker
{
/**
* Check whether an entity belongs to a portal user account.
*/
public function checkAccount(User $user, Entity $entity): bool;
}
@@ -0,0 +1,103 @@
<?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\Portal\Acl;
use Espo\Core\{
Utils\Metadata,
InjectableFactory,
Acl\Exceptions\NotImplemented,
Acl\OwnershipChecker,
Portal\AclManager as PortalAclManager,
Binding\BindingContainer,
Binding\Binder,
Binding\BindingData,
};
class OwnershipCheckerFactory
{
private $defaultClassName = DefaultOwnershipChecker::class;
private $metadata;
private $injectableFactory;
public function __construct(
Metadata $metadata,
InjectableFactory $injectableFactory
) {
$this->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);
}
}
@@ -0,0 +1,44 @@
<?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\Portal\Acl;
use Espo\ORM\Entity;
use Espo\Entities\User;
use Espo\Core\Acl\OwnershipChecker;
interface OwnershipContactChecker extends OwnershipChecker
{
/**
* Check whether an entity belongs to a portal user contact.
*/
public function checkContact(User $user, Entity $entity): bool;
}
@@ -0,0 +1,98 @@
<?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\Portal\Acl;
use Espo\Core\{
Acl\ScopeData,
Portal\Acl\Table,
};
/**
* Checks scope access.
*/
class ScopeChecker
{
public function __construct()
{
}
public function check(ScopeData $data, ?string $action = null, ?ScopeCheckerData $checkerData = null): bool
{
if ($data->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;
}
}
@@ -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();
}
}
@@ -0,0 +1,134 @@
<?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\Portal\Acl;
/**
* Builds scope checker data.
*/
class ScopeCheckerDataBuilder
{
private $isOwnChecker;
private $inAccountChecker;
private $inContactChecker;
public function __construct()
{
$this->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);
}
}
@@ -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
{
}
}
@@ -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,
]);
}
}
+145 -79
View File
@@ -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);
}
}
@@ -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,
]);
}
}
+1 -2
View File
@@ -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
{
@@ -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
{
@@ -0,0 +1,4 @@
{
"accessCheckerClassName": "Espo\\Classes\\Acl\\Attachment\\AccessChecker",
"ownershipCheckerClassName": "Espo\\Classes\\Acl\\Attachment\\OwnershipChecker"
}
@@ -0,0 +1,3 @@
{
"readOwnerUserField": "users"
}
@@ -22,6 +22,7 @@
["app", "actions", "__ANY__", "implementationClassName"],
["selectDefs"],
["recordDefs"],
["aclDefs"],
["authenticationMethods", "__ANY__", "implementationClassName"]
]
}
@@ -8,6 +8,5 @@
"users": {
"onlyAdmin": true
}
},
"readOwnerUserField": "users"
}
}
@@ -0,0 +1,99 @@
<?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 tests\unit\Espo\Core\Acl;
use Espo\Core\{
Acl\ScopeCheckerData,
};
class ScopeCheckerDataTest extends \PHPUnit\Framework\TestCase
{
protected function setUp() : void
{
}
public function testCheckerData0()
{
$checkerData = ScopeCheckerData
::createBuilder()
->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());
}
}
@@ -0,0 +1,435 @@
<?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 tests\unit\Espo\Core\Acl;
use Espo\Core\{
Acl\ScopeChecker,
Acl\ScopeCheckerData,
Acl\ScopeData,
Acl\Table,
};
class ScopeCheckerTest extends \PHPUnit\Framework\TestCase
{
/**
* @var ScopeChecker
*/
private $scopeChecker;
protected function setUp() : void
{
$this->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);
}
}
+9 -9
View File
@@ -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,
]);
}
}
@@ -0,0 +1,108 @@
<?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 tests\unit\Espo\Core\AclPortal;
use Espo\Core\{
AclPortal\ScopeCheckerData,
};
class ScopeCheckerDataTest extends \PHPUnit\Framework\TestCase
{
protected function setUp() : void
{
}
public function testCheckerData0()
{
$checkerData = ScopeCheckerData
::createBuilder()
->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());
}
}
@@ -0,0 +1,529 @@
<?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 tests\unit\Espo\Core\AclPortal;
use Espo\Core\{
AclPortal\ScopeChecker,
AclPortal\ScopeCheckerData,
AclPortal\Table,
Acl\ScopeData,
};
class ScopeCheckerTest extends \PHPUnit\Framework\TestCase
{
/**
* @var ScopeChecker
*/
private $scopeChecker;
protected function setUp() : void
{
$this->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);
}
}