From aa53bc89b4865bea718f23ef5abbffa8af507e04 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 2 Mar 2024 13:30:41 +0200 Subject: [PATCH] output filters --- .../Espo/Classes/Record/User/OutputFilter.php | 74 +++++++++++++++++++ .../Espo/Core/Record/Output/Filter.php | 45 +++++++++++ .../Core/Record/Output/FilterProvider.php | 74 +++++++++++++++++++ application/Espo/Core/Record/Service.php | 27 +++++++ .../Espo/Core/Utils/Metadata/Builder.php | 1 + .../Resources/metadata/recordDefs/User.json | 3 + application/Espo/Services/User.php | 30 -------- schema/metadata/recordDefs.json | 7 ++ 8 files changed, 231 insertions(+), 30 deletions(-) create mode 100644 application/Espo/Classes/Record/User/OutputFilter.php create mode 100644 application/Espo/Core/Record/Output/Filter.php create mode 100644 application/Espo/Core/Record/Output/FilterProvider.php diff --git a/application/Espo/Classes/Record/User/OutputFilter.php b/application/Espo/Classes/Record/User/OutputFilter.php new file mode 100644 index 0000000000..74f6ad319a --- /dev/null +++ b/application/Espo/Classes/Record/User/OutputFilter.php @@ -0,0 +1,74 @@ +. + * + * 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\Record\User; + +use Espo\Core\Authentication\Logins\Hmac; +use Espo\Core\Record\Output\Filter; +use Espo\Core\Utils\ApiKey; +use Espo\Entities\User; +use Espo\ORM\Entity; + +/** + * @implements Filter + */ +class OutputFilter implements Filter +{ + public function __construct( + private User $user, + private ApiKey $apiKey + ) {} + + public function filter(Entity $entity): void + { + $entity->clear('sendAccessInfo'); + + $this->filterApiUser($entity); + } + + private function filterApiUser(User $entity): void + { + if (!$entity->isApi()) { + return; + } + + if ($this->user->isAdmin()) { + if ($entity->getAuthMethod() === Hmac::NAME) { + $secretKey = $this->apiKey->getSecretKeyForUserId($entity->getId()); + + $entity->set('secretKey', $secretKey); + } + + return; + } + + $entity->clear('apiKey'); + $entity->clear('secretKey'); + } +} diff --git a/application/Espo/Core/Record/Output/Filter.php b/application/Espo/Core/Record/Output/Filter.php new file mode 100644 index 0000000000..bdf1f53a0a --- /dev/null +++ b/application/Espo/Core/Record/Output/Filter.php @@ -0,0 +1,45 @@ +. + * + * 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\Core\Record\Output; + +use Espo\ORM\Entity; + +/** + * Filters entity attribute values for output. + * + * @template TEntity of Entity + */ +interface Filter +{ + /** + * @param TEntity $entity + */ + public function filter(Entity $entity): void; +} diff --git a/application/Espo/Core/Record/Output/FilterProvider.php b/application/Espo/Core/Record/Output/FilterProvider.php new file mode 100644 index 0000000000..2bbd0ab1a1 --- /dev/null +++ b/application/Espo/Core/Record/Output/FilterProvider.php @@ -0,0 +1,74 @@ +. + * + * 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\Core\Record\Output; + +use Espo\Core\Acl; +use Espo\Core\Binding\BindingContainerBuilder; +use Espo\Core\InjectableFactory; +use Espo\Core\Utils\Metadata; +use Espo\Entities\User; +use Espo\ORM\Entity; + +class FilterProvider +{ + public function __construct( + private InjectableFactory $injectableFactory, + private Metadata $metadata, + private Acl $acl, + private User $user + ) {} + + /** + * @return Filter[] + */ + public function get(string $entityType): array + { + $classNameList = $this->getClassNameList($entityType); + + $binding = BindingContainerBuilder::create() + ->bindInstance(User::class, $this->user) + ->bindInstance(Acl::class, $this->acl) + ->build(); + + return array_map( + fn ($className) => $this->injectableFactory->createWithBinding($className, $binding), + $classNameList + ); + } + + /** + * @return class-string>[] + */ + private function getClassNameList(string $entityType): array + { + /** @var class-string>[] */ + return $this->metadata->get("recordDefs.$entityType.outputFilterClassNameList") ?? []; + } +} diff --git a/application/Espo/Core/Record/Service.php b/application/Espo/Core/Record/Service.php index bb82e4fed0..7cdf249e80 100644 --- a/application/Espo/Core/Record/Service.php +++ b/application/Espo/Core/Record/Service.php @@ -272,6 +272,8 @@ class Service implements Crud, private ?array $createFilterList = null; /** @var ?Filter[] */ private ?array $updateFilterList = null; + /** @var ?Output\Filter[] */ + private ?array $outputFilterList = null; protected const MAX_SELECT_TEXT_ATTRIBUTE_LENGTH = 10000; @@ -397,6 +399,7 @@ class Service implements Crud, throw new ForbiddenSilent("No 'read' access."); } + /** @noinspection PhpDeprecationInspection */ $this->prepareEntityForOutput($entity); return $entity; @@ -750,6 +753,7 @@ class Service implements Crud, } foreach ($duplicates as $e) { + /** @noinspection PhpDeprecationInspection */ $this->prepareEntityForOutput($e); } @@ -831,6 +835,7 @@ class Service implements Crud, $this->loadAdditionalFields($entity); + /** @noinspection PhpDeprecationInspection */ $this->prepareEntityForOutput($entity); $this->processActionHistoryRecord(Action::CREATE, $entity); @@ -915,6 +920,7 @@ class Service implements Crud, $this->loadAdditionalFields($entity); } + /** @noinspection PhpDeprecationInspection */ $this->prepareEntityForOutput($entity); $this->processActionHistoryRecord(Action::UPDATE, $entity); @@ -1008,6 +1014,7 @@ class Service implements Crud, /** @noinspection PhpDeprecationInspection */ $this->loadListAdditionalFields($entity, $preparedSearchParams); + /** @noinspection PhpDeprecationInspection */ $this->prepareEntityForOutput($entity); } @@ -1180,6 +1187,7 @@ class Service implements Crud, /** @noinspection PhpDeprecationInspection */ $this->loadListAdditionalFields($itemEntity, $preparedSearchParams); + /** @noinspection PhpDeprecationInspection */ $recordService->prepareEntityForOutput($itemEntity); } @@ -1667,6 +1675,8 @@ class Service implements Crud, /** * Prepare an entity for output. Clears not allowed attributes. * + * Do not extend. Prefer metadata recordDefs > outputFilterClassNameList. + * * @param TEntity $entity * @return void * @todo Add void return type in v9.0. @@ -1720,6 +1730,23 @@ class Service implements Crud, foreach ($forbiddenAttributeList as $attribute) { $entity->clear($attribute); } + + foreach ($this->getOutputFilterList() as $filter) { + $filter->filter($entity); + } + } + + /** + * @return Output\Filter[] + */ + private function getOutputFilterList(): array + { + if ($this->outputFilterList === null) { + $this->outputFilterList = + $this->injectableFactory->create(Output\FilterProvider::class)->get($this->entityType); + } + + return $this->outputFilterList; } private function createEntityDuplicator(): EntityDuplicator diff --git a/application/Espo/Core/Utils/Metadata/Builder.php b/application/Espo/Core/Utils/Metadata/Builder.php index 7977f90eed..0c0508d141 100644 --- a/application/Espo/Core/Utils/Metadata/Builder.php +++ b/application/Espo/Core/Utils/Metadata/Builder.php @@ -59,6 +59,7 @@ class Builder ['recordDefs', self::ANY_KEY, 'selectApplierClassNameList'], ['recordDefs', self::ANY_KEY, 'createInputFilterClassNameList'], ['recordDefs', self::ANY_KEY, 'updateInputFilterClassNameList'], + ['recordDefs', self::ANY_KEY, 'outputFilterClassNameList'], ['recordDefs', self::ANY_KEY, 'beforeReadHookClassNameList'], ['recordDefs', self::ANY_KEY, 'beforeCreateHookClassNameList'], ['recordDefs', self::ANY_KEY, 'beforeUpdateHookClassNameList'], diff --git a/application/Espo/Resources/metadata/recordDefs/User.json b/application/Espo/Resources/metadata/recordDefs/User.json index 3d204a6158..4141dc43ae 100644 --- a/application/Espo/Resources/metadata/recordDefs/User.json +++ b/application/Espo/Resources/metadata/recordDefs/User.json @@ -12,6 +12,9 @@ "userName", "type" ], + "outputFilterClassNameList": [ + "Espo\\Classes\\Record\\User\\OutputFilter" + ], "readLoaderClassNameList": [ "Espo\\Classes\\FieldProcessing\\User\\LastAccessLoader" ], diff --git a/application/Espo/Services/User.php b/application/Espo/Services/User.php index 45c720fd05..d9433d79d0 100644 --- a/application/Espo/Services/User.php +++ b/application/Espo/Services/User.php @@ -29,7 +29,6 @@ namespace Espo\Services; -use Espo\Core\Authentication\Logins\Hmac; use Espo\Core\Exceptions\Conflict; use Espo\Core\Exceptions\NotFound; use Espo\Core\Mail\Exceptions\SendingError; @@ -40,7 +39,6 @@ use Espo\Core\Exceptions\Forbidden; use Espo\Core\Record\CreateParams; use Espo\Core\Record\DeleteParams; use Espo\Core\Record\UpdateParams; -use Espo\Core\Utils\ApiKey as ApiKeyUtil; use Espo\Core\Utils\PasswordHash; use Espo\ORM\Entity; use Espo\ORM\Query\SelectBuilder; @@ -216,34 +214,6 @@ class User extends Record ->sendPassword($user, $password); } - public function prepareEntityForOutput(Entity $entity) - { - assert($entity instanceof UserEntity); - - parent::prepareEntityForOutput($entity); - - $entity->clear('sendAccessInfo'); - - if ($entity->isApi()) { - if ($this->user->isAdmin()) { - if ($entity->getAuthMethod() === Hmac::NAME) { - $secretKey = $this->getSecretKeyForUserId($entity->getId()); - $entity->set('secretKey', $secretKey); - } - } else { - $entity->clear('apiKey'); - $entity->clear('secretKey'); - } - } - } - - protected function getSecretKeyForUserId(string $id): ?string - { - $apiKeyUtil = $this->injectableFactory->create(ApiKeyUtil::class); - - return $apiKeyUtil->getSecretKeyForUserId($id); - } - /** * @throws Conflict */ diff --git a/schema/metadata/recordDefs.json b/schema/metadata/recordDefs.json index 01971c828b..10d5db30fa 100644 --- a/schema/metadata/recordDefs.json +++ b/schema/metadata/recordDefs.json @@ -180,6 +180,13 @@ "type": "string" } }, + "outputFilterClassNameList": { + "description": "Output filters. Should implement Espo\\Core\\Record\\Output\\Filter. As of v8.2.", + "type": "array", + "items": { + "type": "string" + } + }, "beforeReadHookClassNameList": { "description": "Before-read hooks. Should implement the Espo\\Core\\Record\\Hook\\ReadHook interface.", "type": "array",