diff --git a/src/Security/Handler/RoleSecurityHandler.php b/src/Security/Handler/RoleSecurityHandler.php index 9640870966..e986a87f76 100644 --- a/src/Security/Handler/RoleSecurityHandler.php +++ b/src/Security/Handler/RoleSecurityHandler.php @@ -71,17 +71,8 @@ public function isGranted(AdminInterface $admin, $attributes, ?object $object = $attributes = [$attributes]; } - // NEXT_MAJOR: Change the foreach to a single check. - $useAll = false; - foreach ($attributes as $pos => $attribute) { - // If the attribute is not already a ROLE_ we generate the related role. - if (\is_string($attribute) && !str_starts_with($attribute, 'ROLE_')) { - $attributes[$pos] = sprintf($this->getBaseRole($admin), $attribute); - // All the admin related role are available when you have the `_ALL` role. - $useAll = true; - } - } - + $useAll = $this->hasOnlyAdminRoles($attributes); + $attributes = $this->mapAttributes($attributes, $admin); $allRole = sprintf($this->getBaseRole($admin), 'ALL'); try { @@ -125,4 +116,51 @@ private function isAnyGranted(array $attributes, ?object $subject = null): bool return false; } + + /** + * @param array $attributes + */ + private function hasOnlyAdminRoles(array $attributes): bool + { + // NEXT_MAJOR: Change the foreach to a single check. + foreach ($attributes as $attribute) { + // If the attribute is not already a ROLE_ we generate the related role. + if (\is_string($attribute) && !str_starts_with($attribute, 'ROLE_')) { + return true; + } + } + + return false; + } + + /** + * @param array $attributes + * @param AdminInterface $admin + * + * @return array + */ + private function mapAttributes(array $attributes, AdminInterface $admin): array + { + $mappedAttributes = []; + + foreach ($attributes as $attribute) { + if (!\is_string($attribute) || str_starts_with($attribute, 'ROLE_')) { + $mappedAttributes[] = $attribute; + + continue; + } + + $baseRole = $this->getBaseRole($admin); + + $mappedAttributes[] = sprintf($baseRole, $attribute); + + foreach ($admin->getSecurityInformation() as $role => $permissions) { + if (\in_array($attribute, $permissions, true)) { + $mappedAttributes[] = sprintf($baseRole, $role); + } + } + } + + return array_unique($mappedAttributes); + } } diff --git a/tests/Security/Handler/RoleSecurityHandlerTest.php b/tests/Security/Handler/RoleSecurityHandlerTest.php index 76a0b6e92a..aa0ba79028 100644 --- a/tests/Security/Handler/RoleSecurityHandlerTest.php +++ b/tests/Security/Handler/RoleSecurityHandlerTest.php @@ -67,6 +67,54 @@ public function provideGetBaseRoleCases(): iterable yield ['ROLE_FOO_BAR_%s', 'FOO.BAR']; } + /** + * @dataProvider provideIsGrantedWithSecurityInformationCases + * + * @param array $informationMapping + * @param string[] $userRoles + */ + public function testIsGrantedWithSecurityInformation(array $informationMapping, array $userRoles, bool $expected): void + { + $handler = new RoleSecurityHandler($this->authorizationChecker, 'ROLE_SUPER_ADMIN'); + + $subject = new \stdClass(); + + $this->admin + ->method('getCode') + ->willReturn('test'); + $this->admin->expects(static::once()) + ->method('getSecurityInformation') + ->willReturn($informationMapping); + + $this->authorizationChecker + ->method('isGranted') + ->willReturnCallback(static function (mixed $attribute, mixed $subject = null) use ($userRoles): bool { + if ($attribute instanceof Expression) { + $attribute = (string) $attribute; + } + + if (\in_array($attribute, $userRoles, true)) { + return $subject instanceof \stdClass; + } + + return false; + }); + + static::assertSame($expected, $handler->isGranted($this->admin, 'EDIT', $subject)); + } + + /** + * @phpstan-return iterable, string[], bool}> + */ + public function provideIsGrantedWithSecurityInformationCases(): iterable + { + yield 'default mapping' => [[], ['ROLE_TEST_EDIT'], true]; + yield 'with single mapping' => [['VIEW' => ['EDIT', 'SHOW']], ['ROLE_TEST_VIEW'], true]; + yield 'with multiple mappings' => [['WRITE' => ['EDIT', 'SHOW'], 'MANAGE' => ['EDIT', 'SHOW', 'DELETE']], ['ROLE_TEST_MANAGE'], true]; + yield 'with all mapping' => [['ADMIN' => ['ALL']], ['ROLE_TEST_ALL'], true]; + yield 'with missing permission' => [['SHOW' => ['VIEW', 'SHOW']], ['ROLE_TEST_VIEW'], false]; + } + /** * NEXT_MAJOR: Remove the group legacy and only keep string $superAdminRoles and string|Expression $operation in dataProvider. *