From 599646c397f4155e587c3e8f9560cbf928dbce13 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 25 Oct 2025 12:47:21 +0300 Subject: [PATCH] metadata aclDependency anyScopeList --- .../Tools/App/Metadata/AclDependencyItem.php | 19 +++++++++-- .../App/Metadata/AclDependencyProvider.php | 14 +++++--- .../Espo/Tools/App/MetadataService.php | 32 +++++++++++++------ schema/metadata/app/metadata.json | 8 ++--- .../Espo/Tools/App/MetadataTest.php | 18 ++++++++++- 5 files changed, 68 insertions(+), 23 deletions(-) diff --git a/application/Espo/Tools/App/Metadata/AclDependencyItem.php b/application/Espo/Tools/App/Metadata/AclDependencyItem.php index 3dc0ac360c..2e26c5d8e7 100644 --- a/application/Espo/Tools/App/Metadata/AclDependencyItem.php +++ b/application/Espo/Tools/App/Metadata/AclDependencyItem.php @@ -31,10 +31,14 @@ namespace Espo\Tools\App\Metadata; class AclDependencyItem { + /** + * @param ?string[] $anyScopeList + */ public function __construct( private string $target, - private string $scope, - private ?string $field + private ?string $scope, + private ?string $field, + private ?array $anyScopeList = null, ) {} /** @@ -45,7 +49,7 @@ class AclDependencyItem return $this->target; } - public function getScope(): string + public function getScope(): ?string { return $this->scope; } @@ -54,4 +58,13 @@ class AclDependencyItem { return $this->field; } + + /** + * @return ?string[] + * @since 9.2.5 + */ + public function getAnyScopeList(): ?array + { + return $this->anyScopeList; + } } diff --git a/application/Espo/Tools/App/Metadata/AclDependencyProvider.php b/application/Espo/Tools/App/Metadata/AclDependencyProvider.php index 2996b818bd..8791b788d4 100644 --- a/application/Espo/Tools/App/Metadata/AclDependencyProvider.php +++ b/application/Espo/Tools/App/Metadata/AclDependencyProvider.php @@ -95,15 +95,13 @@ class AclDependencyProvider $data = []; foreach (($this->metadata->get(['app', 'metadata', 'aclDependencies']) ?? []) as $target => $item) { + $anyScopeList = $item['anyScopeList'] ?? null; $scope = $item['scope'] ?? null; $field = $item['field'] ?? null; - if (!$scope) { - continue; - } - $data[] = [ 'target' => $target, + 'anyScopeList' => $anyScopeList, 'scope' => $scope, 'field' => $field, ]; @@ -196,8 +194,14 @@ class AclDependencyProvider $target = $rawItem['target'] ?? null; $scope = $rawItem['scope'] ?? null; $field = $rawItem['field'] ?? null; + $anyScopeList = $rawItem['anyScopeList'] ?? null; - $list[] = new AclDependencyItem($target, $scope, $field); + $list[] = new AclDependencyItem( + target: $target, + scope: $scope, + field: $field, + anyScopeList: $anyScopeList, + ); } return $list; diff --git a/application/Espo/Tools/App/MetadataService.php b/application/Espo/Tools/App/MetadataService.php index 256d53f50e..6d6401559d 100644 --- a/application/Espo/Tools/App/MetadataService.php +++ b/application/Espo/Tools/App/MetadataService.php @@ -178,20 +178,32 @@ class MetadataService foreach ($this->aclDependencyProvider->get() as $dependencyItem) { $aclScope = $dependencyItem->getScope(); $aclField = $dependencyItem->getField(); + $anyScopeList = $dependencyItem->getAnyScopeList(); - if (!$aclScope) { - continue; + if ($anyScopeList) { + $skip = true; + + foreach ($anyScopeList as $itemScope) { + if ($this->acl->tryCheck($itemScope)) { + $skip = false; + + break; + } + } + + if ($skip) { + continue; + } } - if (!$this->acl->tryCheck($aclScope)) { - continue; - } + if ($aclScope) { + if (!$this->acl->tryCheck($aclScope)) { + continue; + } - if ( - $aclField && - in_array($aclField, $this->acl->getScopeForbiddenFieldList($aclScope)) - ) { - continue; + if ($aclField && in_array($aclField, $this->acl->getScopeForbiddenFieldList($aclScope))) { + continue; + } } $targetArr = explode('.', $dependencyItem->getTarget()); diff --git a/schema/metadata/app/metadata.json b/schema/metadata/app/metadata.json index cd9099eca7..5e321f943e 100644 --- a/schema/metadata/app/metadata.json +++ b/schema/metadata/app/metadata.json @@ -44,21 +44,21 @@ }, "aclDependencies": { "type": "object", - "description": "Rules making a metadata sections available for a user when they don't have access to a scope.", + "description": "Rules making a metadata sections available for the user when they don't have access to the scope.", "additionalProperties": { "description": "A metadata path, items are separated by dots.", "properties": { "scope": { "type": "string", - "description": "If a user have access to the scope, they will have access to a metadata section defined by a key." + "description": "If the user has access to the scope, they will have access to the metadata section defined by the key." }, "field": { "type": "string", - "description": "If a user have access to the field (of a scope), they will have access to a metadata section defined by a key." + "description": "If the user has access to the field (of the scope), they will have access to the metadata section defined by the key." }, "anyScopeList": { "type": "array", - "description": "Not supported. TBD.", + "description": "If the user has access to any of the list scopes, they will have access to the metadata section defined by the key. As of v9.2.5.", "items": { "type": "string" } diff --git a/tests/integration/Espo/Tools/App/MetadataTest.php b/tests/integration/Espo/Tools/App/MetadataTest.php index 4dbaa1b998..da8a23261f 100644 --- a/tests/integration/Espo/Tools/App/MetadataTest.php +++ b/tests/integration/Espo/Tools/App/MetadataTest.php @@ -29,6 +29,7 @@ namespace tests\integration\Espo\Tools\App; +use Espo\Core\Utils\Metadata; use Espo\Tools\App\MetadataService; use tests\integration\Core\BaseTestCase; @@ -36,6 +37,20 @@ class MetadataTest extends BaseTestCase { public function testAclDependency(): void { + $metadata = $this->getContainer()->getByClass(Metadata::class); + + $metadata->set('app', 'metadata', [ + 'aclDependencies' => [ + 'entityDefs.Campaign' => [ + 'anyScopeList' => ['Opportunity'], + ], + ], + ]); + + $metadata->save(); + + $this->reCreateApplication(); + $this->createUser('tester', [ 'data' => [ 'Lead' => false, @@ -55,6 +70,7 @@ class MetadataTest extends BaseTestCase $data = $this->getInjectableFactory()->create(MetadataService::class)->getDataForFrontend(); $this->assertIsArray($data?->entityDefs?->Lead?->fields?->source?->options); - $this->assertNull($data->entityDefs->Lead->fields?->name ?? null); + $this->assertNull($data->entityDefs->Lead->fields->name ?? null); + $this->assertNotNull($data?->entityDefs->Campaign ?? null); } }