Skip to content

Commit e37e689

Browse files
committed
Add PHP and YAML adapters for ACL and Allow config
Introduces AbstractAclAdapter and AbstractAllowAdapter holding the section-processing logic shared across all adapters. INI adapters now extend the base classes; new PhpAclAdapter, PhpAllowAdapter, YamlAclAdapter and YamlAllowAdapter only implement the file parsing step. YAML adapters require symfony/yaml, declared as a suggest dependency and used for tests via require-dev.
1 parent 4ae2d03 commit e37e689

19 files changed

Lines changed: 751 additions & 131 deletions

composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@
3535
"cakephp/debug_kit": "^5.0.1",
3636
"composer/semver": "^3.0",
3737
"fig-r/psr2r-sniffer": "dev-master",
38-
"phpunit/phpunit": "^11.5 || ^12.1 || ^13.0"
38+
"phpunit/phpunit": "^11.5 || ^12.1 || ^13.0",
39+
"symfony/yaml": "^6.4 || ^7.0"
40+
},
41+
"suggest": {
42+
"symfony/yaml": "Required when using the YAML ACL/Allow adapters (^6.4 || ^7.0)."
3943
},
4044
"minimum-stability": "stable",
4145
"prefer-stable": true,

docs/AuthenticationAdapter.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
### Authentication Adapters
22
For adapters to define allow/deny (public/protected) per controller action.
33

4+
#### Built-in adapters
5+
6+
| Adapter | Class | Default file | Notes |
7+
|---------|-------|--------------|-------|
8+
| INI | `TinyAuth\Auth\AllowAdapter\IniAllowAdapter` | `auth_allow.ini` | Default. Zero dependencies. |
9+
| PHP | `TinyAuth\Auth\AllowAdapter\PhpAllowAdapter` | `auth_allow.php` | Returns a plain `return [...]` array. Zero dependencies. |
10+
| YAML | `TinyAuth\Auth\AllowAdapter\YamlAllowAdapter` | `auth_allow.yml` | Requires `symfony/yaml` (`composer require symfony/yaml`). |
11+
12+
Switch the adapter via the `allowAdapter` configuration key, e.g.:
13+
14+
```php
15+
'TinyAuth' => [
16+
'allowAdapter' => \TinyAuth\Auth\AllowAdapter\PhpAllowAdapter::class,
17+
'allowFile' => 'auth_allow.php',
18+
],
19+
```
20+
21+
The PHP/YAML files use the same section/value shape as the INI variant — top-level keys are `Plugin.Prefix/Controller` identifiers and values are comma-separated action lists.
22+
23+
#### Custom adapters
24+
425
Implement the AllowAdapterInterface and make sure your `getAllow()` method returns an array like so:
526
```php
627
// normal controller

docs/AuthorizationAdapter.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
### Authorization Adapters
22
For RBAC ACL adapters.
33

4+
#### Built-in adapters
5+
6+
| Adapter | Class | Default file | Notes |
7+
|---------|-------|--------------|-------|
8+
| INI | `TinyAuth\Auth\AclAdapter\IniAclAdapter` | `auth_acl.ini` | Default. Zero dependencies. |
9+
| PHP | `TinyAuth\Auth\AclAdapter\PhpAclAdapter` | `auth_acl.php` | Returns a plain `return [...]` array. Zero dependencies. |
10+
| YAML | `TinyAuth\Auth\AclAdapter\YamlAclAdapter` | `auth_acl.yml` | Requires `symfony/yaml` (`composer require symfony/yaml`). |
11+
12+
Switch the adapter via the `aclAdapter` configuration key, e.g.:
13+
14+
```php
15+
'TinyAuth' => [
16+
'aclAdapter' => \TinyAuth\Auth\AclAdapter\YamlAclAdapter::class,
17+
'aclFile' => 'auth_acl.yml',
18+
],
19+
```
20+
21+
The PHP/YAML files use the same section/key/value shape as the INI variant — top-level keys are `Plugin.Prefix/Controller` identifiers and each section maps action names (or comma-separated action lists) to comma-separated role lists.
22+
23+
#### Custom adapters
24+
425
Implement the AclAdapterInterface and make sure your `getAcl()` method returns an array like so:
526
```php
627
// normal controller
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace TinyAuth\Auth\AclAdapter;
4+
5+
use Cake\Core\Configure;
6+
use TinyAuth\Utility\Utility;
7+
8+
abstract class AbstractAclAdapter implements AclAdapterInterface {
9+
10+
/**
11+
* Loads the raw section => data array from the underlying config source.
12+
*
13+
* The returned shape matches `parse_ini_file($path, true)` — top-level keys are
14+
* section identifiers (e.g. `Tags`, `Plugin.Admin/Tags`) and each value is a
15+
* `actions => roles` map.
16+
*
17+
* @param array<string, mixed> $config Current TinyAuth configuration values.
18+
* @return array<string, array<string, string>>
19+
*/
20+
abstract protected function parseConfig(array $config): array;
21+
22+
/**
23+
* @param array $availableRoles A list of available user roles.
24+
* @param array<string, mixed> $config Current TinyAuth configuration values.
25+
* @return array
26+
*/
27+
public function getAcl(array $availableRoles, array $config): array {
28+
$sections = $this->parseConfig($config);
29+
30+
$acl = [];
31+
foreach ($sections as $key => $array) {
32+
$acl[$key] = Utility::deconstructIniKey($key);
33+
if (Configure::read('debug')) {
34+
$acl[$key]['map'] = $array;
35+
}
36+
$acl[$key]['deny'] = [];
37+
$acl[$key]['allow'] = [];
38+
39+
foreach ($array as $actions => $roles) {
40+
$roles = explode(',', $roles);
41+
$actions = explode(',', $actions);
42+
43+
$deniedRoles = [];
44+
foreach ($roles as $roleId => $role) {
45+
$role = trim($role);
46+
if (!$role) {
47+
continue;
48+
}
49+
$denied = mb_substr($role, 0, 1) === '!';
50+
if ($denied) {
51+
$role = mb_substr($role, 1);
52+
if (!array_key_exists($role, $availableRoles)) {
53+
unset($roles[$roleId]);
54+
55+
continue;
56+
}
57+
58+
unset($roles[$roleId]);
59+
$deniedRoles[] = $role;
60+
61+
continue;
62+
}
63+
64+
if (!array_key_exists($role, $availableRoles) && $role !== '*') {
65+
unset($roles[$roleId]);
66+
67+
continue;
68+
}
69+
if ($role === '*') {
70+
unset($roles[$roleId]);
71+
$roles = array_merge($roles, array_keys($availableRoles));
72+
}
73+
}
74+
75+
foreach ($actions as $action) {
76+
$action = trim($action);
77+
if (!$action) {
78+
continue;
79+
}
80+
81+
foreach ($roles as $role) {
82+
$role = trim($role);
83+
if (!$role) {
84+
continue;
85+
}
86+
$roleName = strtolower($role);
87+
88+
$newRole = $availableRoles[$roleName];
89+
$acl[$key]['allow'][$action][$roleName] = $newRole;
90+
}
91+
foreach ($deniedRoles as $role) {
92+
$role = trim($role);
93+
if (!$role) {
94+
continue;
95+
}
96+
$roleName = strtolower($role);
97+
98+
$newRole = $availableRoles[$roleName];
99+
$acl[$key]['deny'][$action][$roleName] = $newRole;
100+
}
101+
}
102+
}
103+
}
104+
105+
return $acl;
106+
}
107+
108+
}

src/Auth/AclAdapter/IniAclAdapter.php

Lines changed: 5 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -2,99 +2,16 @@
22

33
namespace TinyAuth\Auth\AclAdapter;
44

5-
use Cake\Core\Configure;
65
use TinyAuth\Utility\Utility;
76

8-
class IniAclAdapter implements AclAdapterInterface {
7+
class IniAclAdapter extends AbstractAclAdapter {
98

109
/**
11-
* {@inheritDoc}
12-
*
13-
* @return array
10+
* @param array<string, mixed> $config Current TinyAuth configuration values.
11+
* @return array<string, array<string, string>>
1412
*/
15-
public function getAcl(array $availableRoles, array $config): array {
16-
$iniArray = Utility::parseFiles($config['filePath'], $config['file']);
17-
18-
$acl = [];
19-
foreach ($iniArray as $key => $array) {
20-
$acl[$key] = Utility::deconstructIniKey($key);
21-
if (Configure::read('debug')) {
22-
$acl[$key]['map'] = $array;
23-
}
24-
$acl[$key]['deny'] = [];
25-
$acl[$key]['allow'] = [];
26-
27-
foreach ($array as $actions => $roles) {
28-
// Get all roles used in the current INI section
29-
$roles = explode(',', $roles);
30-
$actions = explode(',', $actions);
31-
32-
$deniedRoles = [];
33-
foreach ($roles as $roleId => $role) {
34-
$role = trim($role);
35-
if (!$role) {
36-
continue;
37-
}
38-
$denied = mb_substr($role, 0, 1) === '!';
39-
if ($denied) {
40-
$role = mb_substr($role, 1);
41-
if (!array_key_exists($role, $availableRoles)) {
42-
unset($roles[$roleId]);
43-
44-
continue;
45-
}
46-
47-
unset($roles[$roleId]);
48-
$deniedRoles[] = $role;
49-
50-
continue;
51-
}
52-
53-
// Prevent undefined roles appearing in the iniMap
54-
if (!array_key_exists($role, $availableRoles) && $role !== '*') {
55-
unset($roles[$roleId]);
56-
57-
continue;
58-
}
59-
if ($role === '*') {
60-
unset($roles[$roleId]);
61-
$roles = array_merge($roles, array_keys($availableRoles));
62-
}
63-
}
64-
65-
foreach ($actions as $action) {
66-
$action = trim($action);
67-
if (!$action) {
68-
continue;
69-
}
70-
71-
foreach ($roles as $role) {
72-
$role = trim($role);
73-
if (!$role) {
74-
continue;
75-
}
76-
$roleName = strtolower($role);
77-
78-
// Lookup role id by name in roles array
79-
$newRole = $availableRoles[$roleName];
80-
$acl[$key]['allow'][$action][$roleName] = $newRole;
81-
}
82-
foreach ($deniedRoles as $role) {
83-
$role = trim($role);
84-
if (!$role) {
85-
continue;
86-
}
87-
$roleName = strtolower($role);
88-
89-
// Lookup role id by name in roles array
90-
$newRole = $availableRoles[$roleName];
91-
$acl[$key]['deny'][$action][$roleName] = $newRole;
92-
}
93-
}
94-
}
95-
}
96-
97-
return $acl;
13+
protected function parseConfig(array $config): array {
14+
return Utility::parseFiles($config['filePath'], $config['file']);
9815
}
9916

10017
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace TinyAuth\Auth\AclAdapter;
4+
5+
use Cake\Core\Exception\CakeException;
6+
7+
class PhpAclAdapter extends AbstractAclAdapter {
8+
9+
/**
10+
* @param array<string, mixed> $config Current TinyAuth configuration values.
11+
* @throws \Cake\Core\Exception\CakeException
12+
* @return array<string, array<string, string>>
13+
*/
14+
protected function parseConfig(array $config): array {
15+
$paths = $config['filePath'] ?? null;
16+
if ($paths === null) {
17+
$paths = ROOT . DS . 'config' . DS;
18+
}
19+
20+
$list = [];
21+
foreach ((array)$paths as $path) {
22+
$file = $path . $config['file'];
23+
if (!file_exists($file)) {
24+
throw new CakeException(sprintf('Missing TinyAuth config file (%s)', $file));
25+
}
26+
27+
$data = include $file;
28+
if (!is_array($data)) {
29+
throw new CakeException(sprintf('Invalid TinyAuth config file (%s)', $file));
30+
}
31+
32+
$list += $data;
33+
}
34+
35+
return $list;
36+
}
37+
38+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace TinyAuth\Auth\AclAdapter;
4+
5+
use Cake\Core\Exception\CakeException;
6+
use Symfony\Component\Yaml\Yaml;
7+
8+
class YamlAclAdapter extends AbstractAclAdapter {
9+
10+
/**
11+
* @param array<string, mixed> $config Current TinyAuth configuration values.
12+
* @throws \Cake\Core\Exception\CakeException
13+
* @return array<string, array<string, string>>
14+
*/
15+
protected function parseConfig(array $config): array {
16+
if (!class_exists(Yaml::class)) {
17+
throw new CakeException(
18+
'YamlAclAdapter requires symfony/yaml. Install via: composer require symfony/yaml',
19+
);
20+
}
21+
22+
$paths = $config['filePath'] ?? null;
23+
if ($paths === null) {
24+
$paths = ROOT . DS . 'config' . DS;
25+
}
26+
27+
$list = [];
28+
foreach ((array)$paths as $path) {
29+
$file = $path . $config['file'];
30+
if (!file_exists($file)) {
31+
throw new CakeException(sprintf('Missing TinyAuth config file (%s)', $file));
32+
}
33+
34+
$data = Yaml::parseFile($file);
35+
if (!is_array($data)) {
36+
throw new CakeException(sprintf('Invalid TinyAuth config file (%s)', $file));
37+
}
38+
39+
$list += $data;
40+
}
41+
42+
return $list;
43+
}
44+
45+
}

0 commit comments

Comments
 (0)