From 9463c9ea558cb9656e97e77018b9dff2def420a6 Mon Sep 17 00:00:00 2001 From: Yurii Date: Sat, 21 Feb 2026 17:28:47 +0200 Subject: [PATCH] forbid disabled link --- application/Espo/Core/Acl.php | 15 ++++++++++++++ .../Espo/Core/Acl/GlobalRestriction.php | 7 +++++++ application/Espo/Core/AclManager.php | 15 ++++++++++++++ .../Espo/Core/Record/Access/LinkCheck.php | 4 ++++ schema/metadata/entityDefs.json | 4 ++++ tests/integration/Espo/Core/Acl/AclTest.php | 20 +++++++++++++++++++ 6 files changed, 65 insertions(+) diff --git a/application/Espo/Core/Acl.php b/application/Espo/Core/Acl.php index 07c6dd48d9..e8985c629b 100644 --- a/application/Espo/Core/Acl.php +++ b/application/Espo/Core/Acl.php @@ -276,6 +276,21 @@ class Acl return $this->aclManager->checkField($this->user, $scope, $field, $action); } + /** + * Check access to a link. + * + * @param string $scope A scope (entity type). + * @param string $link A link to check. + * @param Table::ACTION_READ|Table::ACTION_EDIT $action An action. + * @noinspection PhpDocSignatureInspection + * + * @since 9.4.0 + */ + public function checkLink(string $scope, string $link, string $action = Table::ACTION_READ): bool + { + return $this->aclManager->checkLink($this->user, $scope, $link, $action); + } + /** * Get links forbidden for a user. * diff --git a/application/Espo/Core/Acl/GlobalRestriction.php b/application/Espo/Core/Acl/GlobalRestriction.php index 9c5c2ac7f1..4a6cc54244 100644 --- a/application/Espo/Core/Acl/GlobalRestriction.php +++ b/application/Espo/Core/Acl/GlobalRestriction.php @@ -187,6 +187,13 @@ class GlobalRestriction $value = $this->metadata->get(['entityDefs', $scope, 'links', $link, $type]); } + if ( + $type === self::TYPE_FORBIDDEN && + $this->metadata->get("entityDefs.$scope.links.$link.disabled") + ) { + $value = true; + } + if (!$value) { continue; } diff --git a/application/Espo/Core/AclManager.php b/application/Espo/Core/AclManager.php index ae6f90693c..a3c741a8bb 100644 --- a/application/Espo/Core/AclManager.php +++ b/application/Espo/Core/AclManager.php @@ -579,6 +579,21 @@ class AclManager return !in_array($field, $this->getScopeForbiddenFieldList($user, $scope, $action)); } + /** + * Check access to a link. + * + * @param string $scope A scope (entity type). + * @param string $link A link to check. + * @param Table::ACTION_READ|Table::ACTION_EDIT $action An action. + * @noinspection PhpDocSignatureInspection + * + * @since 9.4.0 + */ + public function checkLink(User $user, string $scope, string $link, string $action = Table::ACTION_READ): bool + { + return !in_array($link, $this->getScopeForbiddenLinkList($user, $scope, $action)); + } + /** * Whether a user has access to another user over a specific permission. * diff --git a/application/Espo/Core/Record/Access/LinkCheck.php b/application/Espo/Core/Record/Access/LinkCheck.php index a881a44f5a..0e0901bf74 100644 --- a/application/Espo/Core/Record/Access/LinkCheck.php +++ b/application/Espo/Core/Record/Access/LinkCheck.php @@ -291,6 +291,10 @@ class LinkCheck { $entityType = $entity->getEntityType(); + if (!$this->acl->checkLink($entityType, $link)) { + throw new ForbiddenSilent("Link $link is forbidden."); + } + /** @var AclTable::ACTION_*|null $action */ $action = $this->getParam($entityType, $link, 'linkRequiredAccess'); diff --git a/schema/metadata/entityDefs.json b/schema/metadata/entityDefs.json index 132863863d..bef33f87d6 100644 --- a/schema/metadata/entityDefs.json +++ b/schema/metadata/entityDefs.json @@ -1494,6 +1494,10 @@ "type": "boolean", "description": "The link will be hidden from the user on the UI but not disabled." }, + "disabled": { + "type": "boolean", + "description": "Disables the link." + }, "apiSpecDisabled": { "type": "boolean", "description": "Do not print the link in the API specification. As of v9.3." diff --git a/tests/integration/Espo/Core/Acl/AclTest.php b/tests/integration/Espo/Core/Acl/AclTest.php index a56852c88b..281e87ecef 100644 --- a/tests/integration/Espo/Core/Acl/AclTest.php +++ b/tests/integration/Espo/Core/Acl/AclTest.php @@ -156,4 +156,24 @@ class AclTest extends BaseTestCase $this->assertFalse($acl->checkField(Account::ENTITY_TYPE, 'assignedUser')); $this->assertTrue($acl->checkField(Account::ENTITY_TYPE, 'name')); } + + public function testDisabledLink(): void + { + $metadata = $this->getMetadata(); + + $metadata->set('entityDefs', 'Account', [ + 'links' => [ + 'opportunities' => [ + 'disabled' => true, + ] + ] + ]); + $metadata->save(); + + $this->reCreateApplication(); + + $acl = $this->getContainer()->getByClass(Acl::class); + + $this->assertFalse($acl->checkLink('Account', 'opportunities')); + } }