Skip to content

Commit 99dd603

Browse files
committed
Improve api RBAC to support className definition. Intial cached RBAC class.
1 parent e17dc5e commit 99dd603

File tree

3 files changed

+140
-1
lines changed

3 files changed

+140
-1
lines changed

src/Rbac/ApiRbac.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use CakeDC\Auth\Rbac\PermissionMatchResult;
2323
use CakeDC\Auth\Rbac\RbacInterface;
2424
use CakeDC\Auth\Rbac\Rules\Rule;
25+
use CakeDC\Auth\Rbac\Rules\RuleRegistry;
2526
use Psr\Http\Message\ServerRequestInterface;
2627
use Psr\Log\LogLevel;
2728

@@ -181,6 +182,9 @@ protected function _matchPermission(array $permission, $user, $role, ServerReque
181182

182183
if (is_callable($value)) {
183184
$return = (bool)call_user_func($value, $user, $role, $request);
185+
} elseif (is_array($value) && isset($value['className'])) {
186+
$ruleInstance = RuleRegistry::get($value['className'], $value['options'] ?? []);
187+
$return = (bool)$ruleInstance->allowed($user, $role, $request);
184188
} elseif ($value instanceof Rule) {
185189
$return = (bool)$value->allowed($user, $role, $request);
186190
} elseif ($key === 'bypassAuth' && $value === true) {

src/Rbac/CachedApiRbac.php

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* Copyright 2010 - 2025, Cake Development Corporation (https://www.cakedc.com)
6+
*
7+
* Licensed under The MIT License
8+
* Redistributions of files must retain the above copyright notice.
9+
*
10+
* @copyright Copyright 2010 - 2025, Cake Development Corporation (https://www.cakedc.com)
11+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
12+
*/
13+
14+
namespace CakeDC\Api\Rbac;
15+
16+
use Cake\Cache\Cache;
17+
use Cake\Utility\Hash;
18+
use CakeDC\Api\Rbac\ApiRbac;
19+
use Psr\Http\Message\ServerRequestInterface;
20+
use Psr\Log\LogLevel;
21+
22+
/**
23+
* Class CachedApiRbac
24+
*
25+
* Cached version of ApiRbac for improved performance.
26+
* Builds a permissions map based on service/action and caches it.
27+
*/
28+
class CachedApiRbac extends ApiRbac
29+
{
30+
/**
31+
* A map of rules
32+
*
33+
* @var array[] rules array
34+
*/
35+
protected $permissionsMap = [];
36+
37+
/**
38+
* CachedApiRbac constructor.
39+
*
40+
* @param array $config Class configuration
41+
*/
42+
public function __construct($config = [])
43+
{
44+
parent::__construct($config);
45+
$this->permissionsMap = Cache::remember('api_permissions_map', function () {
46+
return $this->buildPermissionsMap();
47+
}, '_cakedc_api_auth_');
48+
}
49+
50+
/**
51+
* Build permissions map for caching
52+
*
53+
* @return array
54+
*/
55+
public function buildPermissionsMap()
56+
{
57+
$asArray = function ($permission, $key, $default = null) {
58+
if ($default !== null && !array_key_exists($key, $permission)) {
59+
return [$default, '_'];
60+
}
61+
if (!array_key_exists($key, $permission) || $permission[$key] == false || $permission[$key] == null) {
62+
return ['_'];
63+
}
64+
$item = $permission[$key];
65+
if (is_string($item)) {
66+
return [$item];
67+
}
68+
return (array)$item;
69+
};
70+
71+
$map = [];
72+
foreach ($this->permissions as $permission) {
73+
if (isset($permission['role'])) {
74+
$role = $permission['role'];
75+
} else {
76+
$role = '*';
77+
}
78+
$roles = (array)$role;
79+
foreach ($roles as $role) {
80+
$services = $asArray($permission, 'service', '*');
81+
foreach ($services as $service) {
82+
$key = $service;
83+
$map[$role][$key][] = $permission;
84+
}
85+
}
86+
}
87+
88+
return $map;
89+
}
90+
91+
/**
92+
* Match against permissions using cached map
93+
*
94+
* @param array|\ArrayAccess $user current user array
95+
* @param \Psr\Http\Message\ServerRequestInterface $request request
96+
* @return bool true if there is a match in permissions
97+
*/
98+
public function checkPermissions($user, ServerRequestInterface $request)
99+
{
100+
$roleField = $this->getConfig('role_field');
101+
$defaultRole = $this->getConfig('default_role');
102+
$role = Hash::get($user, $roleField, $defaultRole);
103+
104+
$service = $request->getAttribute('service');
105+
if ($service === null) {
106+
return false;
107+
}
108+
109+
$serviceName = $service->getName();
110+
$keys = [$serviceName, '*'];
111+
112+
foreach ([$role, '*'] as $checkRole) {
113+
if (!array_key_exists($checkRole, $this->permissionsMap)) {
114+
continue;
115+
}
116+
foreach ($keys as $key) {
117+
if (!array_key_exists($key, $this->permissionsMap[$checkRole])) {
118+
continue;
119+
}
120+
$permissions = $this->permissionsMap[$checkRole][$key];
121+
foreach ($permissions as $permission) {
122+
$matchResult = $this->_matchPermission($permission, $user, $role, $request);
123+
if ($matchResult !== null) {
124+
if ($this->getConfig('log')) {
125+
$this->log($matchResult->getReason(), LogLevel::DEBUG);
126+
}
127+
128+
return $matchResult->isAllowed();
129+
}
130+
}
131+
}
132+
}
133+
134+
return false;
135+
}
136+
}

tests/TestCase/Integration/Service/Action/Auth/JwtLoginActionTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ public function tearDown(): void
6464
public function testSuccessLogin()
6565
{
6666
$this->sendRequest('/auth/jwt_login', 'POST', ['username' => 'user-1', 'password' => '12345']);
67-
print_r((string)$this->_response->getBody());
6867
$result = $this->getJsonResponse();
6968
$this->assertSuccess($result);
7069
$this->assertTrue(is_array($result['data']));

0 commit comments

Comments
 (0)