From 732ed483a984cddd7072c5de44e151b0d20ae497 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 1/6] 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 899c02b55e34d1a2a09f61b9c16d70bbda89de5a Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 2/6] 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 cdcfa1da92d97322fb19095d5f592f4364eb3c5d Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 3/6] 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 d83fc861a78239681623d98cdc2c057455a9106a Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 4/6] 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 5390a2f5c0c62a9e36306c068aa39733883efd41 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:00:13 -0400 Subject: [PATCH 5/6] 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; + } +} From f08b148148ad8b3745eda7a87dcdff1c28339420 Mon Sep 17 00:00:00 2001 From: Alberto Rodriguez Date: Tue, 13 Oct 2026 18:29:51 -0400 Subject: [PATCH 6/6] fix: add policy to ignore advisories for league/flysystem --- composer.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/composer.json b/composer.json index 474f887..6fe1d99 100644 --- a/composer.json +++ b/composer.json @@ -69,6 +69,13 @@ }, "config": { "sort-packages": true, + "policy": { + "advisories": { + "ignore": [ + "league/flysystem" + ] + } + }, "allow-plugins": { "cweagans/composer-patches": true, "dealerdirect/phpcodesniffer-composer-installer": true