diff --git a/application/Espo/Classes/RecordHooks/Role/BeforeSaveValidate.php b/application/Espo/Classes/RecordHooks/Role/BeforeSaveValidate.php new file mode 100644 index 0000000000..d1111f17e0 --- /dev/null +++ b/application/Espo/Classes/RecordHooks/Role/BeforeSaveValidate.php @@ -0,0 +1,224 @@ +. + * + * 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 Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Classes\RecordHooks\Role; + +use Espo\Core\Acl\Table; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Portal\Acl\Table as TablePortal; +use Espo\Core\Record\Hook\SaveHook; +use Espo\Core\Utils\Metadata; +use Espo\Entities\PortalRole; +use Espo\Entities\Role; +use Espo\ORM\Entity; +use stdClass; + +/** + * @noinspection PhpUnused + * @implements SaveHook + */ +class BeforeSaveValidate implements SaveHook +{ + /** @var string[] */ + private array $levelList = [ + Table::LEVEL_YES, + Table::LEVEL_ALL, + Table::LEVEL_TEAM, + Table::LEVEL_OWN, + Table::LEVEL_NO, + ]; + + /** @var string[] */ + private array $portalLevelList = [ + Table::LEVEL_YES, + Table::LEVEL_ALL, + TablePortal::LEVEL_ACCOUNT, + TablePortal::LEVEL_CONTACT, + Table::LEVEL_OWN, + Table::LEVEL_NO, + ]; + + public function __construct( + private Metadata $metadata + ) {} + + public function process(Entity $entity): void + { + $this->validateData($entity); + $this->validateFieldData($entity); + } + + /** + * @throws BadRequest + */ + private function validateData(Role|PortalRole $entity): void + { + if (!$entity->has('data')) { + return; + } + + /** @var array $data */ + $data = get_object_vars($entity->get('data')); + + foreach ($data as $scope => $item) { + if (!is_bool($item) && !$item instanceof stdClass) { + throw new BadRequest("Bad data. Should be bool or object."); + } + + $this->validateDataItem($scope, $entity, $item); + } + } + + /** + * @throws BadRequest + */ + private function validateDataItem(string $scope, Role|PortalRole $entity, bool|stdClass $item): void + { + $key = $entity instanceof PortalRole ? + 'aclPortal' : 'acl'; + + $type = $this->metadata->get("scopes.$scope.$key"); + + if ($type === 'boolean') { + if (!is_bool($item)) { + throw new BadRequest("Bad data. Value for *$scope* should be be bool."); + } + + return; + } + + if ($type === null) { + throw new BadRequest("Bad data. Scope *$scope* is not allowed."); + } + + if ($item === false) { + return; + } + + if (is_bool($item)) { + throw new BadRequest("Bad data. Value for *$scope* should be be false or object."); + } + + $actions = [ + Table::ACTION_CREATE, + Table::ACTION_READ, + Table::ACTION_EDIT, + Table::ACTION_DELETE, + Table::ACTION_STREAM, + ]; + + $levels = $entity instanceof PortalRole ? + $this->portalLevelList : $this->levelList; + + foreach ($actions as $action) { + if (!property_exists($item, $action)) { + continue; + } + + $level = $item->$action; + + if (!in_array($level, $levels)) { + throw new BadRequest("Level `$level` is not allowed for action *$action* for *$scope*."); + } + } + } + + /** + * @throws BadRequest + */ + private function validateFieldData(Role|PortalRole $entity): void + { + /** @var array $data */ + $data = get_object_vars($entity->get('fieldData')); + + foreach ($data as $scope => $item) { + if (!$item instanceof stdClass) { + throw new BadRequest("Bad field-level data. Should be object."); + } + + $this->validateFieldDataItem($scope, $entity, $item); + } + } + + /** + * @throws BadRequest + */ + private function validateFieldDataItem(string $scope, PortalRole|Role $entity, stdClass $item): void + { + $disabledKey = $entity instanceof PortalRole ? 'aclPortalFieldLevelDisabled' : 'aclFieldLevelDisabled'; + $key = $entity instanceof PortalRole ? 'aclPortal' : 'acl'; + + if ( + !$this->metadata->get("scopes.$scope.entity") || + !$this->metadata->get("scopes.$scope.$key") || + $this->metadata->get("scopes.$scope.$disabledKey") + ) { + throw new BadRequest("Bad field-level data. Scope *$scope* is not allowed."); + } + + /** @var array $data */ + $data = get_object_vars($item); + + foreach ($data as $field => $fieldItem) { + if (!$fieldItem instanceof stdClass) { + throw new BadRequest("Data for field *$field*, scope *$scope* should be object."); + } + + $this->validateFieldDataItemItem($scope, $field, $fieldItem); + } + } + + /** + * @throws BadRequest + */ + private function validateFieldDataItemItem(string $scope, string $field, stdClass $item): void + { + $actions = [ + Table::ACTION_READ, + Table::ACTION_EDIT, + ]; + + $levels = [ + Table::LEVEL_YES, + Table::LEVEL_NO, + ]; + + foreach ($actions as $action) { + if (!property_exists($item, $action)) { + continue; + } + + $level = $item->$action; + + if (!in_array($level, $levels)) { + throw new BadRequest("Level `$level` is not allowed for *$scope*, field *$field*."); + } + } + } +} diff --git a/application/Espo/Entities/PortalRole.php b/application/Espo/Entities/PortalRole.php index eb668a20de..593fc7873d 100644 --- a/application/Espo/Entities/PortalRole.php +++ b/application/Espo/Entities/PortalRole.php @@ -29,7 +29,9 @@ namespace Espo\Entities; -class PortalRole extends \Espo\Core\ORM\Entity +use Espo\Core\ORM\Entity; + +class PortalRole extends Entity { public const ENTITY_TYPE = 'PortalRole'; } diff --git a/application/Espo/Entities/Role.php b/application/Espo/Entities/Role.php index 85d791b54e..3059fd5ff6 100644 --- a/application/Espo/Entities/Role.php +++ b/application/Espo/Entities/Role.php @@ -29,7 +29,9 @@ namespace Espo\Entities; -class Role extends \Espo\Core\ORM\Entity +use Espo\Core\ORM\Entity; + +class Role extends Entity { public const ENTITY_TYPE = 'Role'; } diff --git a/application/Espo/Resources/metadata/recordDefs/PortalRole.json b/application/Espo/Resources/metadata/recordDefs/PortalRole.json index a04d7655cd..63e882bd26 100644 --- a/application/Espo/Resources/metadata/recordDefs/PortalRole.json +++ b/application/Espo/Resources/metadata/recordDefs/PortalRole.json @@ -3,5 +3,11 @@ "delete": { "allowed": true } - } + }, + "beforeCreateHookClassNameList": [ + "Espo\\Classes\\RecordHooks\\Role\\BeforeSaveValidate" + ], + "beforeUpdateHookClassNameList": [ + "Espo\\Classes\\RecordHooks\\Role\\BeforeSaveValidate" + ] } diff --git a/application/Espo/Resources/metadata/recordDefs/Role.json b/application/Espo/Resources/metadata/recordDefs/Role.json index a04d7655cd..63e882bd26 100644 --- a/application/Espo/Resources/metadata/recordDefs/Role.json +++ b/application/Espo/Resources/metadata/recordDefs/Role.json @@ -3,5 +3,11 @@ "delete": { "allowed": true } - } + }, + "beforeCreateHookClassNameList": [ + "Espo\\Classes\\RecordHooks\\Role\\BeforeSaveValidate" + ], + "beforeUpdateHookClassNameList": [ + "Espo\\Classes\\RecordHooks\\Role\\BeforeSaveValidate" + ] }