Skip to content

Commit 759147e

Browse files
authored
Add PHP adapter for ACL and Allow config (#168)
* 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. * Note PHP and YAML built-ins in custom-adapters guide Master's new VitePress docs introduced a custom-adapters page that described tinyauth as having only INI-based stores. Update the intro to point at the now-three built-in formats so readers don't reach for a custom adapter when they just want a different file format. * Showcase lenient YAML quoting in test fixtures Strip defensive quotes from comma-separated role/action lists where YAML's block-scalar rules tolerate them, and drop the unnecessary quotes around comma-bearing keys like 'edit,view' and 'view, edit'. Add header comments explaining when quoting IS required (only when a value/key starts with a YAML reserved character: *, !, &, >, |, etc.) so future readers don't reach for quotes by reflex. Tests still pass — the parsed structure is unchanged, only the on-disk shape is more idiomatic. * Widen symfony/yaml constraint to allow v8 Composer can pick the highest version compatible with the host PHP: v6.4 (PHP 8.1+), v7.x (PHP 8.2+) or v8.x (PHP 8.4+). Tinyauth's PHP floor is 8.2, so v8 is reachable for users on PHP 8.4+ without breaking the prefer-lowest path on 8.2/8.3. * Drop YAML adapter, ship PHP-only alongside INI YAML treats `*` and `!` as reserved metasyntax (alias reference and tag), but those are exactly the characters TinyAuth's ACL grammar uses most: `*` for "all actions/roles" and `!action` / `!role` for negation. Every realistic YAML auth_acl.yml/auth_allow.yml ends up sprinkled with quoted keys, which is the opposite of "human-friendly" — and a fixture that needs a legend explaining what to quote is the wrong format for this domain. PHP arrays don't have this problem: `*` and `!index` are just strings, no escaping ceremony, IDE-friendly, no extra dependency. INI stays the zero-dependency default. The AbstractAclAdapter / AbstractAllowAdapter base classes from this PR are kept — INI and PHP both inherit from them, and a third-party YAML (or any other format) adapter is now ~30 lines for whoever wants it.
1 parent 863552d commit 759147e

13 files changed

Lines changed: 489 additions & 131 deletions

File tree

docs/authentication/adapter.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
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+
11+
Switch the adapter via the `allowAdapter` configuration key, e.g.:
12+
13+
```php
14+
'TinyAuth' => [
15+
'allowAdapter' => \TinyAuth\Auth\AllowAdapter\PhpAllowAdapter::class,
16+
'allowFile' => 'auth_allow.php',
17+
],
18+
```
19+
20+
The PHP file uses the same section/value shape as the INI variant — top-level keys are `Plugin.Prefix/Controller` identifiers and values are comma-separated action lists.
21+
22+
#### Custom adapters
23+
424
Implement the AllowAdapterInterface and make sure your `getAllow()` method returns an array like so:
525
```php
626
// normal controller

docs/authorization/adapter.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
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+
11+
Switch the adapter via the `aclAdapter` configuration key, e.g.:
12+
13+
```php
14+
'TinyAuth' => [
15+
'aclAdapter' => \TinyAuth\Auth\AclAdapter\PhpAclAdapter::class,
16+
'aclFile' => 'auth_acl.php',
17+
],
18+
```
19+
20+
The PHP file uses 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.
21+
22+
#### Custom adapters
23+
424
Implement the AclAdapterInterface and make sure your `getAcl()` method returns an array like so:
525
```php
626
// normal controller

docs/guide/custom-adapters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Custom Adapters
22

3-
TinyAuth's two INI-based stores (`auth_allow.ini` for public actions, `auth_acl.ini` for role permissions) are just the default backends. You can swap either one out for a database-driven, API-driven, or any other source by implementing a small interface.
3+
TinyAuth ships with two file-based backends (`Ini`, `Php` — see [Authorization Adapters](/authorization/adapter) and [Authentication Adapters](/authentication/adapter)) for the public-action whitelist (`auth_allow.*`) and the role permissions (`auth_acl.*`). If a different format is all you need, just switch the `aclAdapter` / `allowAdapter` config key. You only need a custom adapter when the data has to come from somewhere else entirely — a database, an API, etc.
44

55
## When you'd want a custom adapter
66

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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace TinyAuth\Auth\AllowAdapter;
4+
5+
use Cake\Core\Configure;
6+
use TinyAuth\Utility\Utility;
7+
8+
abstract class AbstractAllowAdapter implements AllowAdapterInterface {
9+
10+
/**
11+
* Loads the raw section => actions array from the underlying config source.
12+
*
13+
* The returned shape matches `parse_ini_file($path, false)` — top-level keys
14+
* are section identifiers (e.g. `Users`, `Admin/Users`) and each value is the
15+
* raw comma-separated action list.
16+
*
17+
* @param array<string, mixed> $config Current TinyAuth configuration values.
18+
* @return array<string, string>
19+
*/
20+
abstract protected function parseConfig(array $config): array;
21+
22+
/**
23+
* @param array<string, mixed> $config Current TinyAuth configuration values.
24+
* @return array
25+
*/
26+
public function getAllow(array $config): array {
27+
$sections = $this->parseConfig($config);
28+
29+
$auth = [];
30+
foreach ($sections as $key => $actions) {
31+
$auth[$key] = Utility::deconstructIniKey($key);
32+
33+
$actions = explode(',', $actions);
34+
foreach ($actions as $k => $action) {
35+
$action = trim($action);
36+
if ($action === '') {
37+
unset($actions[$k]);
38+
39+
continue;
40+
}
41+
$actions[$k] = $action;
42+
}
43+
44+
if (Configure::read('debug')) {
45+
$auth[$key]['map'] = $actions;
46+
}
47+
$auth[$key]['deny'] = [];
48+
$auth[$key]['allow'] = [];
49+
50+
foreach ($actions as $action) {
51+
$denied = mb_substr($action, 0, 1) === '!';
52+
if ($denied) {
53+
$auth[$key]['deny'][] = mb_substr($action, 1);
54+
55+
continue;
56+
}
57+
58+
$auth[$key]['allow'][] = $action;
59+
}
60+
}
61+
62+
return $auth;
63+
}
64+
65+
}

0 commit comments

Comments
 (0)