Skip to content

Commit 05f06c1

Browse files
committed
ACP2E-4114: Production ACL Permission Check caused Performance Degradation – populateAcl Method is the bottleneck
1 parent 692c869 commit 05f06c1

File tree

3 files changed

+169
-7
lines changed

3 files changed

+169
-7
lines changed

app/code/Magento/Authorization/Test/Unit/Model/Acl/AclRetrieverTest.php

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Magento\Authorization\Model\UserContextInterface;
1919
use Magento\Framework\Acl;
2020
use Magento\Framework\Acl\Builder;
21+
use Magento\Framework\Acl\Role\CurrentRoleContext;
2122
use Magento\Framework\Exception\AuthorizationException;
2223
use PHPUnit\Framework\MockObject\MockObject;
2324
use PHPUnit\Framework\TestCase;
@@ -172,14 +173,78 @@ protected function createAclRetriever()
172173
/**
173174
* @var Builder|MockObject $aclBuilderMock
174175
*/
175-
$aclBuilderMock = $this->createPartialMock(Builder::class, ['getAcl']);
176+
$aclBuilderMock = $this->createPartialMock(Builder::class, ['getAcl', 'resetRuntimeAcl']);
176177
$aclBuilderMock->expects($this->any())->method('getAcl')->willReturn($aclMock);
177178

178179
return new AclRetriever(
179180
$aclBuilderMock,
180181
$roleCollectionFactoryMock,
181182
$rulesCollectionFactoryMock,
182-
$this->getMockForAbstractClass(LoggerInterface::class)
183+
$this->getMockForAbstractClass(LoggerInterface::class),
184+
new CurrentRoleContext()
183185
);
184186
}
187+
188+
public function testResetAclWhenRoleChanges(): void
189+
{
190+
// Set up rules collection
191+
$rulesMock = $this->getMockBuilder(Rules::class)
192+
->addMethods(['getResourceId'])
193+
->onlyMethods(['__wakeup'])
194+
->disableOriginalConstructor()
195+
->getMock();
196+
$rulesMock->method('getResourceId')->willReturn('Magento_Backend::dashboard');
197+
198+
$rulesCollectionMock = $this->createPartialMock(
199+
RulesCollection::class,
200+
['getByRoles', 'load', 'getItems']
201+
);
202+
$rulesCollectionMock->method('getByRoles')->willReturnSelf();
203+
$rulesCollectionMock->method('load')->willReturnSelf();
204+
$rulesCollectionMock->method('getItems')->willReturn([$rulesMock]);
205+
206+
$rulesCollectionFactoryMock = $this->createPartialMock(
207+
RulesCollectionFactory::class,
208+
['create']
209+
);
210+
$rulesCollectionFactoryMock->method('create')->willReturn($rulesCollectionMock);
211+
212+
// ACL and builder mocks
213+
$aclMock = $this->createPartialMock(Acl::class, ['hasResource', 'isAllowed']);
214+
$aclMock->method('hasResource')->willReturn(true);
215+
$aclMock->method('isAllowed')->willReturn(true);
216+
217+
$aclBuilderMock = $this->createPartialMock(Builder::class, ['getAcl', 'resetRuntimeAcl']);
218+
$aclBuilderMock->expects($this->any())->method('getAcl')->willReturn($aclMock);
219+
$aclBuilderMock->expects($this->exactly(2))->method('resetRuntimeAcl');
220+
221+
$roleCollectionFactoryMock = $this->createPartialMock(
222+
RoleCollectionFactory::class,
223+
['create']
224+
);
225+
$roleCollectionMock = $this->createPartialMock(
226+
RoleCollection::class,
227+
['setUserFilter', 'getFirstItem']
228+
);
229+
$roleCollectionMock->method('setUserFilter')->willReturnSelf();
230+
$roleMock = $this->createPartialMock(Role::class, ['getId', '__wakeup']);
231+
$roleMock->method('getId')->willReturn(2);
232+
$roleCollectionMock->method('getFirstItem')->willReturn($roleMock);
233+
$roleCollectionFactoryMock->method('create')->willReturn($roleCollectionMock);
234+
235+
$currentRoleContext = new CurrentRoleContext();
236+
237+
$retriever = new AclRetriever(
238+
$aclBuilderMock,
239+
$roleCollectionFactoryMock,
240+
$rulesCollectionFactoryMock,
241+
$this->getMockForAbstractClass(LoggerInterface::class),
242+
$currentRoleContext
243+
);
244+
245+
// First call sets roleId to 2
246+
$retriever->getAllowedResourcesByRole(2);
247+
// Second call with different role should trigger resetRuntimeAcl()
248+
$retriever->getAllowedResourcesByRole(3);
249+
}
185250
}

app/code/Magento/Authorization/Test/Unit/Model/Acl/Loader/RuleTest.php

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Magento\Framework\Acl;
1212
use Magento\Framework\Acl\Data\CacheInterface;
1313
use Magento\Framework\Acl\RootResource;
14+
use Magento\Framework\Acl\Role\CurrentRoleContext;
1415
use Magento\Framework\App\ResourceConnection;
1516
use Magento\Framework\Serialize\Serializer\Json;
1617
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
@@ -54,8 +55,8 @@ protected function setUp(): void
5455
{
5556
$this->rootResource = new RootResource('Magento_Backend::all');
5657
$this->resourceMock = $this->getMockBuilder(ResourceConnection::class)
58+
->onlyMethods(['getConnection', 'getTableName'])
5759
->addMethods(['getTable'])
58-
->onlyMethods(['getConnection'])
5960
->disableOriginalConstructor()
6061
->getMock();
6162
$this->aclDataCacheMock = $this->getMockForAbstractClass(CacheInterface::class);
@@ -147,4 +148,81 @@ public function testPopulateAclFromCache(): void
147148

148149
$this->model->populateAcl($aclMock);
149150
}
151+
152+
/**
153+
* Ensure that when a role context is present, rules are loaded from the role-specific cache key
154+
* and applied accordingly.
155+
*/
156+
public function testPopulateAclForSpecificRoleFromCache(): void
157+
{
158+
$roleId = 10;
159+
$rules = [
160+
['role_id' => $roleId, 'resource_id' => 'Magento_Backend::all', 'permission' => 'allow'],
161+
['role_id' => $roleId, 'resource_id' => 'Magento_Backend::admin', 'permission' => 'allow'],
162+
];
163+
164+
$roleContext = $this->createMock(CurrentRoleContext::class);
165+
$roleContext->method('getRoleId')->willReturn($roleId);
166+
167+
// Expect the role-specific cache key to be read
168+
$this->aclDataCacheMock->expects($this->once())
169+
->method('load')
170+
->with(Rule::ACL_RULE_CACHE_KEY . '_' . $roleId)
171+
->willReturn(json_encode($rules));
172+
173+
// ACL expectations: allow for root, then for specific resource
174+
$aclMock = $this->createMock(Acl::class);
175+
$aclMock->method('hasResource')->willReturn(true);
176+
$calls = [];
177+
$aclMock->method('allow')
178+
->willReturnCallback(function ($role, $resource, $privilege) use (&$calls) {
179+
$calls[] = [$role, $resource, $privilege];
180+
return null;
181+
});
182+
183+
$connectionMock = $this->getMockBuilder(\Magento\Framework\DB\Adapter\AdapterInterface::class)
184+
->disableOriginalConstructor()
185+
->getMock();
186+
$connectionMock->method('fetchRow')->willReturn([]); // Return empty array for any DB fetchRow() call
187+
188+
$selectMock = $this->getMockBuilder('stdClass')
189+
->addMethods(['from', 'where', 'limit'])
190+
->getMock();
191+
$selectMock->method('from')->willReturnSelf();
192+
$selectMock->method('where')->willReturnSelf();
193+
$selectMock->method('limit')->willReturnSelf();
194+
$connectionMock->method('select')->willReturn($selectMock);
195+
$this->resourceMock->method('getConnection')->willReturn($connectionMock);
196+
$this->resourceMock->method('getTableName')->willReturn('authorization_role'); // Return dummy table name
197+
198+
$objectManager = new ObjectManager($this);
199+
$model = $objectManager->getObject(
200+
Rule::class,
201+
[
202+
'rootResource' => $this->rootResource,
203+
'resource' => $this->resourceMock,
204+
'aclDataCache' => $this->aclDataCacheMock,
205+
'serializer' => $this->serializerMock,
206+
'roleContext' => $roleContext,
207+
]
208+
);
209+
210+
$model->populateAcl($aclMock);
211+
212+
$foundRootResourceAllow = false;
213+
$foundAdminResourceAllow = false;
214+
foreach ($calls as $call) {
215+
[$role, $resource, $privilege] = $call;
216+
if ($privilege === null && (int)$role === $roleId) {
217+
if ($resource === 'Magento_Backend::all') {
218+
$foundRootResourceAllow = true;
219+
}
220+
if ($resource === 'Magento_Backend::admin') {
221+
$foundAdminResourceAllow = true;
222+
}
223+
}
224+
}
225+
$this->assertTrue($foundRootResourceAllow, 'Expected allow() call for Magento_Backend::all with given role');
226+
$this->assertTrue($foundAdminResourceAllow, 'Expected allow() call for Magento_Backend::admin with given role');
227+
}
150228
}

lib/internal/Magento/Framework/Authorization/Test/Unit/Policy/AclTest.php

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2012 Adobe
4+
* All Rights Reserved.
55
*/
66
declare(strict_types=1);
77

88
namespace Magento\Framework\Authorization\Test\Unit\Policy;
99

1010
use Laminas\Permissions\Acl\Exception\InvalidArgumentException;
1111
use Magento\Framework\Acl\Builder;
12+
use Magento\Framework\Acl\Role\CurrentRoleContext;
1213
use Magento\Framework\Authorization\Policy\Acl;
1314
use PHPUnit\Framework\MockObject\MockObject;
1415
use PHPUnit\Framework\TestCase;
@@ -39,7 +40,7 @@ protected function setUp(): void
3940
$this->_aclMock = $this->createMock(FrameworkAcl::class);
4041
$this->_aclBuilderMock = $this->createMock(Builder::class);
4142
$this->_aclBuilderMock->expects($this->any())->method('getAcl')->willReturn($this->_aclMock);
42-
$this->_model = new Acl($this->_aclBuilderMock);
43+
$this->_model = new Acl($this->_aclBuilderMock, $this->createMock(CurrentRoleContext::class));
4344
}
4445

4546
/**
@@ -61,7 +62,7 @@ public function testIsAllowedReturnsTrueIfResourceIsAllowedToRole(): void
6162
public function testIsAllowedReturnsFalseIfRoleDoesntExist(): void
6263
{
6364
$this->_aclMock->expects($this->once())
64-
->method('isAllowed')
65+
->method('isAllowed')
6566
->with('some_role', 'some_resource')
6667
->willThrowException(new InvalidArgumentException());
6768

@@ -89,4 +90,22 @@ function ($arg1, $arg2) {
8990
$this->_aclMock->expects($this->once())->method('hasResource')->with('some_resource')->willReturn(false);
9091
$this->assertTrue($this->_model->isAllowed('some_role', 'some_resource'));
9192
}
93+
94+
public function testSetsAndResetsRoleContext(): void
95+
{
96+
$roleId = 123;
97+
$roleContext = $this->getMockBuilder(CurrentRoleContext::class)
98+
->onlyMethods(['setRoleId', '_resetState'])
99+
->getMock();
100+
$model = new Acl($this->_aclBuilderMock, $roleContext);
101+
102+
$roleContext->expects($this->once())->method('setRoleId')->with($roleId);
103+
$this->_aclMock->expects($this->once())
104+
->method('isAllowed')
105+
->with($roleId, 'some_resource')
106+
->willReturn(true);
107+
$roleContext->expects($this->once())->method('_resetState');
108+
109+
$this->assertTrue($model->isAllowed($roleId, 'some_resource'));
110+
}
92111
}

0 commit comments

Comments
 (0)