Skip to content

Commit 041bf1d

Browse files
committed
Document undocumented features: impersonation, custom adapters, middleware/policy, CLI commands
Audit of src/ vs docs/ surfaced public-facing functionality the original prose never described: - New authentication/impersonation.md — PrimaryKeySessionAuthenticator with the impersonate() flow, full config table, start/detect/stop pattern - New guide/custom-adapters.md — interfaces, return shapes for both Allow and ACL adapters, registration via TinyAuth.allowAdapter / aclAdapter, skeleton DB-driven example - New authorization/middleware.md — RequestAuthorizationMiddleware config table, RequestPolicy reference, both unauthorized handlers (ForbiddenRedirect, ForbiddenCakeRedirect) with full options - New reference/cli.md — tiny_auth add and tiny_auth sync with all arguments, options, interactive mode, and a typical project workflow Sidebar updated to surface the four new pages.
1 parent 730ae56 commit 041bf1d

5 files changed

Lines changed: 467 additions & 0 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function unifiedSidebar() {
1010
{ text: '5-min Quick Start', link: '/guide/quick-start' },
1111
{ text: 'Installation', link: '/guide/install' },
1212
{ text: 'Configuration', link: '/guide/configuration' },
13+
{ text: 'Custom Adapters', link: '/guide/custom-adapters' },
1314
{ text: 'Troubleshooting', link: '/guide/troubleshooting' },
1415
{ text: 'Upgrade Guide', link: '/guide/upgrade' },
1516
],
@@ -19,6 +20,7 @@ function unifiedSidebar() {
1920
collapsed: false,
2021
items: [
2122
{ text: 'Setup & INI', link: '/authentication/' },
23+
{ text: 'Impersonation', link: '/authentication/impersonation' },
2224
{ text: 'Custom Adapter', link: '/authentication/adapter' },
2325
],
2426
},
@@ -27,6 +29,7 @@ function unifiedSidebar() {
2729
collapsed: false,
2830
items: [
2931
{ text: 'Setup & INI', link: '/authorization/' },
32+
{ text: 'Middleware & Policy', link: '/authorization/middleware' },
3033
{ text: 'Custom Adapter', link: '/authorization/adapter' },
3134
{ text: 'Multi-Role', link: '/authorization/multi-role' },
3235
],
@@ -39,6 +42,13 @@ function unifiedSidebar() {
3942
{ text: 'AuthPanel (DebugKit)', link: '/auth-panel' },
4043
],
4144
},
45+
{
46+
text: 'Reference',
47+
collapsed: true,
48+
items: [
49+
{ text: 'CLI Commands', link: '/reference/cli' },
50+
],
51+
},
4252
]
4353
}
4454

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Impersonation
2+
3+
TinyAuth ships a `PrimaryKeySessionAuthenticator` that enables the classic "admin logs in as another user" pattern. The original (impersonator) identity is stored in a separate session key so you can return to it at any time.
4+
5+
## Setup
6+
7+
Replace the default Cake `SessionAuthenticator` with TinyAuth's variant in your `AuthenticationServiceProvider`:
8+
9+
```php
10+
use TinyAuth\Authenticator\PrimaryKeySessionAuthenticator;
11+
12+
$service->loadAuthenticator(PrimaryKeySessionAuthenticator::class, [
13+
'sessionKey' => 'Auth',
14+
'identifierKey' => 'key', // identifier lookup key
15+
'idField' => 'id', // user PK column
16+
'impersonateSessionKey' => 'AuthImpersonator', // where the original user's id is stashed
17+
'cache' => false, // optional in-process SessionCache
18+
]);
19+
```
20+
21+
The authenticator only persists the user's primary key in the session — the rest of the identity is reloaded on each request via the configured identifier. That's what makes safe role/permission swaps possible.
22+
23+
### Config keys
24+
25+
| Key | Default | What it does |
26+
| --- | --- | --- |
27+
| `sessionKey` | from parent | Session key holding the active user's primary key. |
28+
| `identifierKey` | `key` | The identifier field used by the loaded `IdentifierInterface` to look up the user. |
29+
| `idField` | `id` | The column on the user record containing the primary key. |
30+
| `impersonateSessionKey` | (none — set this) | Session key that stores the ORIGINAL impersonator's id during an active impersonation. |
31+
| `cache` | `false` | If `true`, results are stored in `TinyAuth\Utility\SessionCache` to avoid re-loading the user every request. Cleared on logout. |
32+
33+
## Start impersonating
34+
35+
In your controller (typically an admin action), call `impersonate()`:
36+
37+
```php
38+
use TinyAuth\Authenticator\PrimaryKeySessionAuthenticator;
39+
40+
public function impersonate($targetUserId) {
41+
$service = $this->Authentication->getAuthenticationService();
42+
$authenticator = $service->authenticators()->get('PrimaryKeySession');
43+
44+
$impersonator = $this->Authentication->getIdentity()->getOriginalData();
45+
$impersonated = $this->fetchTable('Users')->get($targetUserId);
46+
47+
$result = $authenticator->impersonate(
48+
$this->getRequest(),
49+
$this->getResponse(),
50+
$impersonator,
51+
$impersonated,
52+
);
53+
54+
$this->setRequest($result['request']);
55+
56+
return $this->redirect(['controller' => 'Pages', 'action' => 'home']);
57+
}
58+
```
59+
60+
After this, `$this->Authentication->getIdentity()` returns the **impersonated** user, and the original impersonator's id is in `$session->read('AuthImpersonator')`.
61+
62+
## Detect an active impersonation
63+
64+
```php
65+
$impersonatorId = $this->getRequest()->getSession()->read('AuthImpersonator');
66+
if ($impersonatorId) {
67+
// Show "Stop impersonating" banner
68+
}
69+
```
70+
71+
## Stop impersonating
72+
73+
Reverse the swap by writing the impersonator's id back into the active session key and clearing the impersonate marker:
74+
75+
```php
76+
public function stopImpersonating() {
77+
$session = $this->getRequest()->getSession();
78+
$impersonatorId = $session->read('AuthImpersonator');
79+
80+
if (!$impersonatorId) {
81+
$this->Flash->error(__('Not currently impersonating.'));
82+
83+
return $this->redirect($this->referer());
84+
}
85+
86+
$session->write('Auth', $impersonatorId);
87+
$session->delete('AuthImpersonator');
88+
89+
return $this->redirect(['controller' => 'Pages', 'action' => 'home']);
90+
}
91+
```
92+
93+
## Constraints
94+
95+
- Calling `impersonate()` while already impersonating throws `Cake\Http\Exception\UnauthorizedException` — only one level deep.
96+
- `cache => true` is in-process only (`SessionCache` lives for the duration of the request). It's a micro-optimization, not a persistent cache.
97+
- The authenticator only stores the primary key, not the full user data. If your `IdentifierInterface` is slow, expect the cost on every request unless `cache` is enabled.
98+
99+
## Authorization gate
100+
101+
Don't forget to gate the `impersonate` and `stopImpersonating` actions in your `auth_acl.ini`:
102+
103+
```ini
104+
[Admin/Users]
105+
impersonate = admin
106+
stopImpersonating = *
107+
```

docs/authorization/middleware.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Middleware & Policy
2+
3+
The component-based authorization documented in [Authorization Setup](/authorization/) covers most apps. For middleware-based authorization (or routes that bypass controllers entirely), TinyAuth ships its own middleware and policy.
4+
5+
## RequestAuthorizationMiddleware
6+
7+
A drop-in replacement for `Authorization\Middleware\RequestAuthorizationMiddleware` that applies the TinyAuth allow / ACL rules instead of policies. Works with any controller / route (no per-controller wiring needed).
8+
9+
> **Order matters:** add this AFTER `AuthorizationMiddleware`, `AuthenticationMiddleware`, and `RoutingMiddleware`.
10+
11+
### Wiring
12+
13+
```php
14+
use TinyAuth\Middleware\RequestAuthorizationMiddleware;
15+
16+
// in Application::middleware()
17+
$middlewareQueue
18+
->add(new \Authentication\Middleware\AuthenticationMiddleware($this))
19+
->add(new \Authorization\Middleware\AuthorizationMiddleware($this))
20+
->add(new RequestAuthorizationMiddleware([
21+
'identityAttribute' => 'identity',
22+
'method' => 'access',
23+
'unauthorizedHandler' => 'TinyAuth.ForbiddenCakeRedirect',
24+
]));
25+
```
26+
27+
### Config keys
28+
29+
| Key | Default | What it does |
30+
| --- | --- | --- |
31+
| `identityAttribute` | inherits parent | Request attribute name that holds the authenticated identity. |
32+
| `method` | inherits parent | Policy method invoked to check access (defaults to the parent middleware's). |
33+
| `unauthorizedHandler` | inherits parent | Handler used when authorization fails. Use TinyAuth's two flavors below. |
34+
35+
The middleware picks up TinyAuth's full Configure block (`Configure::read('TinyAuth')`) automatically — no need to repeat keys here.
36+
37+
## RequestPolicy
38+
39+
Used internally by `RequestAuthorizationMiddleware` to evaluate the allow / ACL rules against the current request. You generally don't instantiate it directly, but if you wire `Authorization\Middleware\RequestAuthorizationMiddleware` yourself (instead of TinyAuth's subclass), point its `RequestAuthorizationMiddleware` at TinyAuth's policy:
40+
41+
```php
42+
use Authorization\Policy\MapResolver;
43+
use Cake\Http\ServerRequest;
44+
use TinyAuth\Policy\RequestPolicy;
45+
46+
$resolver = new MapResolver();
47+
$resolver->map(ServerRequest::class, RequestPolicy::class);
48+
```
49+
50+
`RequestPolicy::canAccess(?IdentityInterface $identity, ServerRequest $request): bool` returns `true` if the current user (or guest) may access the route.
51+
52+
## Unauthorized handlers
53+
54+
When authorization fails, the middleware throws `Authorization\Exception\ForbiddenException`. TinyAuth ships two handlers for converting that into a redirect.
55+
56+
### `TinyAuth.ForbiddenRedirect`
57+
58+
Redirects to a fixed URL string. Useful for landing on a generic "/" or a public error page.
59+
60+
| Option | Default | Notes |
61+
| --- | --- | --- |
62+
| `exceptions` | `[ForbiddenException::class]` | Exception classes this handler responds to. |
63+
| `url` | `/` | Target URL (string). |
64+
| `queryParam` | `redirect` | Query parameter that captures the original URL (so you can come back after login). |
65+
| `statusCode` | `302` | Redirect status. |
66+
| `unauthorizedMessage` | localized "You are not authorized to access that location." | Flash message. Set to `false` to suppress. |
67+
68+
```php
69+
'unauthorizedHandler' => [
70+
'className' => 'TinyAuth.ForbiddenRedirect',
71+
'url' => '/',
72+
'queryParam' => 'redirect',
73+
'unauthorizedMessage' => __('Please log in.'),
74+
],
75+
```
76+
77+
### `TinyAuth.ForbiddenCakeRedirect`
78+
79+
Redirects to a CakePHP URL array — typically the login action.
80+
81+
| Option | Default | Notes |
82+
| --- | --- | --- |
83+
| `exceptions` | `[ForbiddenException::class]` | |
84+
| `url` | `['controller' => 'Users', 'action' => 'login']` | Cake URL array. |
85+
| `queryParam` | `redirect` | |
86+
| `statusCode` | `302` | |
87+
| `unauthorizedMessage` | same default | |
88+
89+
```php
90+
'unauthorizedHandler' => [
91+
'className' => 'TinyAuth.ForbiddenCakeRedirect',
92+
'url' => ['plugin' => null, 'prefix' => false, 'controller' => 'Users', 'action' => 'login'],
93+
],
94+
```
95+
96+
### Non-HTML requests
97+
98+
Both handlers re-throw the original `ForbiddenException` if the request has a non-HTML extension (`_ext` other than `'html'`). API consumers get a proper 403 instead of a 302 to a login page.
99+
100+
## When to use middleware vs component
101+
102+
| Use the **component** | Use the **middleware** |
103+
| --- | --- |
104+
| Standard MVC apps where every request hits a controller | Routes that bypass controllers (custom routing, JSON-RPC, etc.) |
105+
| You want fine-grained per-controller config | You want a single global rule application |
106+
| Easier to debug (per-controller `beforeFilter`) | Cleaner stack — auth runs before routing dispatches |
107+
108+
The two approaches are not mutually exclusive but typically you pick one. The component is the simpler choice; the middleware is the right choice when you need authorization to run regardless of the controller.

docs/guide/custom-adapters.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Custom Adapters
2+
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.
4+
5+
## When you'd want a custom adapter
6+
7+
- **Live editing** — admins editing rules from a UI rather than redeploying a file. The [TinyAuthBackend plugin](https://github.com/dereuromark/cakephp-tinyauth-backend) already provides this.
8+
- **Multi-tenant rules** — different rule sets per host / customer / region.
9+
- **Remote source** — central auth service, central feature flags, etc.
10+
11+
If your rules are stable and per-environment, the INI files are usually fine. Don't overbuild this.
12+
13+
## Authentication: the AllowAdapterInterface
14+
15+
```php
16+
namespace TinyAuth\Auth\AllowAdapter;
17+
18+
interface AllowAdapterInterface {
19+
public function getAllow(array $config): array;
20+
}
21+
```
22+
23+
`getAllow()` returns the **public-action whitelist**, keyed by section. The reference implementation is [`IniAllowAdapter`](https://github.com/dereuromark/cakephp-tinyauth/blob/master/src/Auth/AllowAdapter/IniAllowAdapter.php).
24+
25+
Each entry should look like:
26+
27+
```php
28+
[
29+
'Articles' => [
30+
'controller' => 'Articles',
31+
'plugin' => null,
32+
'prefix' => null,
33+
'allow' => ['index', 'view'],
34+
'deny' => [],
35+
],
36+
'Admin/Users' => [
37+
'controller' => 'Users',
38+
'plugin' => null,
39+
'prefix' => 'Admin',
40+
'allow' => ['login'],
41+
'deny' => [],
42+
],
43+
];
44+
```
45+
46+
`controller`, `plugin`, `prefix` come from parsing the section key (e.g. `MyPlugin.Admin/Articles``plugin='MyPlugin'`, `prefix='Admin'`, `controller='Articles'`). The `Utility::deconstructIniKey()` helper does this for you.
47+
48+
`allow` is the list of public actions. `deny` is the list of explicitly-denied actions (using the `!action` syntax in INI).
49+
50+
## Authorization: the AclAdapterInterface
51+
52+
```php
53+
namespace TinyAuth\Auth\AclAdapter;
54+
55+
interface AclAdapterInterface {
56+
public function getAcl(array $availableRoles, array $config): array;
57+
}
58+
```
59+
60+
`getAcl()` returns the **role-permission map**, keyed by section. The reference implementation is [`IniAclAdapter`](https://github.com/dereuromark/cakephp-tinyauth/blob/master/src/Auth/AclAdapter/IniAclAdapter.php).
61+
62+
`$availableRoles` is `['admin' => 1, 'user' => 2, ...]` so you can look up role IDs by name.
63+
64+
Each entry should look like:
65+
66+
```php
67+
[
68+
'Articles' => [
69+
'controller' => 'Articles',
70+
'plugin' => null,
71+
'prefix' => null,
72+
'allow' => [
73+
'index' => ['admin' => 1, 'user' => 2],
74+
'edit' => ['admin' => 1],
75+
],
76+
'deny' => [
77+
'delete' => ['user' => 2],
78+
],
79+
],
80+
];
81+
```
82+
83+
`allow` is `[action => [roleName => roleId, ...]]` — a single action can have multiple roles. `deny` follows the same shape and is checked first; an action denied for a role overrides any allow.
84+
85+
## Registering your adapter
86+
87+
Set the relevant config key in `app.php`:
88+
89+
```php
90+
'TinyAuth' => [
91+
// For authentication (allow whitelist):
92+
'allowAdapter' => \App\Auth\DatabaseAllowAdapter::class,
93+
94+
// For authorization (ACL):
95+
'aclAdapter' => \App\Auth\DatabaseAclAdapter::class,
96+
],
97+
```
98+
99+
That's it — TinyAuth instantiates the adapter and calls `getAllow()` / `getAcl()` once per request (cached, see [Configuration](/guide/configuration#cache-busting)).
100+
101+
## Skeleton example
102+
103+
```php
104+
namespace App\Auth;
105+
106+
use TinyAuth\Auth\AclAdapter\AclAdapterInterface;
107+
108+
class DatabaseAclAdapter implements AclAdapterInterface {
109+
110+
public function getAcl(array $availableRoles, array $config): array {
111+
$acl = [];
112+
113+
$rows = $this->fetchTable('AuthRules')->find()->toArray();
114+
115+
foreach ($rows as $row) {
116+
$key = $row->section; // e.g. "Admin/Articles"
117+
if (!isset($acl[$key])) {
118+
$acl[$key] = $this->_parseSectionKey($key);
119+
$acl[$key]['allow'] = [];
120+
$acl[$key]['deny'] = [];
121+
}
122+
123+
$bucket = $row->is_deny ? 'deny' : 'allow';
124+
$roleId = $availableRoles[$row->role] ?? null;
125+
if ($roleId === null) {
126+
continue;
127+
}
128+
$acl[$key][$bucket][$row->action][$row->role] = $roleId;
129+
}
130+
131+
return $acl;
132+
}
133+
134+
protected function _parseSectionKey(string $key): array {
135+
return \TinyAuth\Utility\Utility::deconstructIniKey($key);
136+
}
137+
}
138+
```
139+
140+
## See also
141+
142+
- [TinyAuthBackend](https://github.com/dereuromark/cakephp-tinyauth-backend) — a ready-made admin GUI for editing both stores.
143+
- [Configuration: Custom adapters](/guide/configuration#custom-adapters) — the config key reference.

0 commit comments

Comments
 (0)