From dcafff499c101072593b23ac5f94d36a9046c830 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 1/5] fix(43963): honor normalized extension names in Service::_loadExtensions --- src/Service/Service.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Service/Service.php b/src/Service/Service.php index a4db08d..b3d8b39 100644 --- a/src/Service/Service.php +++ b/src/Service/Service.php @@ -921,8 +921,8 @@ protected function _loadExtensions(): void } $registry = $this->getExtensions(); $extensions = $registry->normalizeArray($this->extensions); - foreach ($extensions as $properties) { - $instance = $registry->load($properties['class'], $properties['config']); + foreach ($extensions as $name => $config) { + $instance = $registry->load($name, $config); $this->_eventManager->on($instance); } } From ace671904d350cf64a15edcab820828086dedaa7 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 2/5] fix(43963): load app extensions by name in Action::_loadExtensions --- src/Service/Action/Action.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service/Action/Action.php b/src/Service/Action/Action.php index 20db40d..7b4257d 100644 --- a/src/Service/Action/Action.php +++ b/src/Service/Action/Action.php @@ -394,7 +394,7 @@ protected function _loadExtensions(): void $extensions = $registry->normalizeArray($this->extensions); foreach ($extensions as $name => $properties) { - $instance = $registry->load($properties['className'], $properties); + $instance = $registry->load($name, $properties); $this->_eventManager->on($instance); if ($instance->attachable()) { [, $prop] = pluginSplit($name); From 95b95abe84fc5c53f3265bd1fb0c665b9b4dcdc8 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 3/5] fix(43963): cast role to string in ApiRbac::checkPermissions --- src/Rbac/ApiRbac.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Rbac/ApiRbac.php b/src/Rbac/ApiRbac.php index f86e963..0defa37 100644 --- a/src/Rbac/ApiRbac.php +++ b/src/Rbac/ApiRbac.php @@ -119,7 +119,10 @@ public function checkPermissions(array|ArrayAccess $user, ServerRequestInterface { $roleField = $this->getConfig('role_field'); $defaultRole = $this->getConfig('default_role'); - $role = Hash::get($user, $roleField, $defaultRole); + // The role is matched against the permission 'role' key and passed to the rules, + // whose signature (CakeDC\Auth\Rbac\Rules\Rule) types it as string. Apps commonly + // store the role as an integer group id, so cast it here to avoid a TypeError. + $role = (string)Hash::get($user, $roleField, $defaultRole); if ($this->permissions === null) { return false; From 3992a9ecbaf6f796e6dac656ebaa1a0281216cef Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 4/5] test(43963): cover Service/Action extension loading --- .../TestCase/Service/ExtensionLoadingTest.php | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/TestCase/Service/ExtensionLoadingTest.php diff --git a/tests/TestCase/Service/ExtensionLoadingTest.php b/tests/TestCase/Service/ExtensionLoadingTest.php new file mode 100644 index 0000000..95e3b19 --- /dev/null +++ b/tests/TestCase/Service/ExtensionLoadingTest.php @@ -0,0 +1,72 @@ + config` map (the + * old CakePHP 4 `class`/`config` shape is gone), so extensions must be loaded by + * their normalized name. A plugin-prefixed extension (e.g. `CakeDC/Api.Log`) only + * carries a `className` key, and an app extension without a plugin prefix carries + * neither `class` nor `className`. + */ +class ExtensionLoadingTest extends TestCase +{ + /** + * A service configured with extensions must load them without a TypeError. + * + * @return void + */ + public function testServiceLoadsConfiguredExtension(): void + { + $service = new FallbackService([ + 'request' => new ServerRequest(), + 'response' => new Response(), + 'Extension' => ['CakeDC/Api.Log'], + ]); + + $this->assertTrue($service->getExtensions()->has('Log')); + } + + /** + * An action configured with an app extension (no plugin prefix) must load it. + * + * @return void + */ + public function testActionLoadsAppExtensionWithoutPluginPrefix(): void + { + $request = new ServerRequest(); + $response = new Response(); + $service = new FallbackService([ + 'request' => $request, + 'response' => $response, + ]); + $action = new CrudIndexAction([ + 'service' => $service, + 'request' => $request, + 'response' => $response, + 'Extension' => ['Sort'], + ]); + + $this->assertNotNull($action->getExtensions()); + $this->assertTrue($action->getExtensions()->has('Sort')); + } +} From 46a17c81b11c757578c5e42add9f8cc51ac21f23 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 5/5] test(43963): cover integer role in ApiRbac --- tests/TestCase/Rbac/ApiRbacTest.php | 88 +++++++++++++++++++++++ tests/TestCase/Rbac/RoleCapturingRule.php | 45 ++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 tests/TestCase/Rbac/ApiRbacTest.php create mode 100644 tests/TestCase/Rbac/RoleCapturingRule.php diff --git a/tests/TestCase/Rbac/ApiRbacTest.php b/tests/TestCase/Rbac/ApiRbacTest.php new file mode 100644 index 0000000..0046c4e --- /dev/null +++ b/tests/TestCase/Rbac/ApiRbacTest.php @@ -0,0 +1,88 @@ +clear(); + parent::tearDown(); + } + + /** + * Build a service with a resolved action and attach it to the request. + * + * @param array $permissions RBAC permissions. + * @param array $user User data. + * @return array{rbac: \CakeDC\Api\Rbac\ApiRbac, request: \Cake\Http\ServerRequest, user: array} + */ + protected function buildRequest(array $permissions, array $user = ['role' => 1, 'id' => 1]): array + { + ServiceRegistry::getServiceLocator()->clear(); + $this->_initializeRequest([ + 'params' => ['service' => 'articles'], + ], 'GET'); + $service = ServiceRegistry::getServiceLocator()->get('articles', [ + 'version' => null, + 'service' => 'articles', + 'request' => $this->request, + 'response' => $this->response, + 'baseUrl' => '/articles', + ]); + $service->dispatchPrepareAction(); + + $this->request = $this->request->withAttribute('service', $service); + + $rbac = new ApiRbac([ + 'api_permissions' => $permissions, + 'role_field' => 'role', + 'default_role' => 'user', + ]); + + return [$rbac, $this->request, $user]; + } + + /** + * Apps commonly store the role as an integer group id. The role is passed to + * the rules (typed `string`), so an integer role must be cast to string and + * must not raise a TypeError. + */ + public function testIntegerRoleIsPassedAsStringToRules(): void + { + $rule = new RoleCapturingRule(); + [$rbac, $request, $user] = $this->buildRequest([ + ['role' => 1, 'service' => 'articles', 'action' => 'index', 'rule' => $rule], + ]); + + $this->assertTrue($rbac->checkPermissions($user, $request)); + $this->assertTrue($rule->called); + $this->assertSame('1', $rule->receivedRole); + } +} diff --git a/tests/TestCase/Rbac/RoleCapturingRule.php b/tests/TestCase/Rbac/RoleCapturingRule.php new file mode 100644 index 0000000..50e14b8 --- /dev/null +++ b/tests/TestCase/Rbac/RoleCapturingRule.php @@ -0,0 +1,45 @@ +called = true; + $this->receivedRole = $role; + + return true; + } +}