Skip to content

Commit f7697e3

Browse files
authored
Add cache layer (#156)
* Add session layer * Fix up redirect issues on ext based content * Fix up migration status * Add cache deleting. * Rearrange panel * Docs. * Docs
1 parent 311a32d commit f7697e3

File tree

10 files changed

+312
-82
lines changed

10 files changed

+312
-82
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This branch is for **CakePHP 5.0+**. For details see [version map](https://githu
1717
### Authentication
1818
What are public actions, which ones need login?
1919

20-
- Powerful wildcard (*) operator.
20+
- Powerful default configs to get you started right away.
2121
- [Quick Setup](https://github.com/dereuromark/cakephp-tinyauth/blob/master/docs/Authentication.md#quick-setups) for 5 minute integration.
2222

2323
### Authorization
@@ -30,7 +30,7 @@ Once you are logged in, what actions can you see with your role(s)?
3030
### Useful helpers
3131
- AuthUser Component and Helper for stateful and stateless "auth data" access.
3232
- Authentication Component and Helper for `isPublic()` check on current other other actions.
33-
- Auth DebugKit panel for detailed insights into current URL and auth status.
33+
- Auth DebugKit panel for detailed insights into current URL and auth status, identity content and more.
3434

3535
## What's the idea?
3636
Default CakePHP authentication and authorization depends on code changes in at least each controller, maybe more classes.
@@ -39,10 +39,11 @@ This plugin hooks in with a single line of change and manages all that using con
3939
It is also possible to manage the config files without the need to code.
4040
And it can with adapters also be moved completely to the DB and managed by CRUD backend.
4141

42-
Ask yourself: Do you need the overhead and complexity involved with the full blown (RBAC DB) ACL? See also my post [acl-access-control-lists-revised/](https://www.dereuromark.de/2015/01/06/acl-access-control-lists-revised/).
42+
Ask yourself: Do you need the overhead and complexity involved with a full blown (RBAC DB) ACL or very specific Policy approaches?
43+
See also my post [acl-access-control-lists-revised/](https://www.dereuromark.de/2015/01/06/acl-access-control-lists-revised/).
4344
If not, then this plugin could very well be your answer and a super quick solution to your auth problem :)
4445

45-
But even if you don't leverage the authentication or authorization, the available AuthUserComponent and AuthUserHelper
46+
But even if you don't leverage the full authentication or authorization potential, the available AuthUserComponent and AuthUserHelper
4647
can be very useful when dealing with role based decisions in your controller or view level. They also work stand-alone.
4748

4849

docs/AuthenticationPlugin.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,25 @@ $this->AuthUser->identity();
133133
```
134134

135135

136-
For all the rest follow the plugin's documentation.
136+
### Caching
137+
Especially when you use the PrimaryKeySession authenticator and always pulling the live data
138+
from DB, you might want to consider adding a short-lived cache in between.
139+
The authenticator supports this directly:
140+
141+
In this case you need to manually invalidate the session cache every time a user modifies some of their
142+
data that is part of the session (e.g. username, email, roles, birthday, ...).
143+
For that you can use the following after the change was successful:
144+
```php
145+
use TinyAuth\Utility\SessionCache;
146+
147+
SessionCache::delete($userId);
148+
```
149+
This will force the session to be pulled (the ID), and the cache refilled with up-to-date data.
150+
151+
152+
---
153+
154+
155+
For all the rest, follow the plugin's documentation.
137156

138157
Then you use the [Authentication documentation](Authentication.md) to fill your INI config file.

docs/AuthorizationPlugin.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,31 @@ Then you use the [Authorization documentation](Authorization.md) to set up roles
6363

6464
#### Tips
6565

66-
It is recommended to use a POST check for the login flash message, and silently redirect away otherwise.
66+
You can add loginUpdate() method to your UsersTable to update the user's data here accordingly:
6767

68+
```php
69+
/**
70+
* @param \Authentication\Authenticator\ResultInterface $result
71+
*
72+
* @return void
73+
*/
74+
public function loginUpdate(ResultInterface $result): void
75+
{
76+
/** @var \App\Model\Entity\User $user */
77+
$user = $result->getData();
78+
$this->updateAll(['last_login' => new DateTime()], ['id' => $user->id]);
79+
}
80+
```
81+
Then hook it in:
6882
```php
6983
// Inside your AccountController::login() method
7084
$result = $this->Authentication->getResult();
7185
// If the user is logged in send them away.
7286
if ($result && $result->isValid()) {
73-
if ($this->request->is('post')) {
74-
$this->Users->loginUpdate($result);
75-
$target = $this->Authentication->getLoginRedirect() ?? '/';
76-
$this->Flash->success(__('You are now logged in.'));
77-
78-
return $this->redirect($target);
79-
}
87+
$this->Users->loginUpdate($result);
88+
$target = $this->Authentication->getLoginRedirect() ?? '/';
89+
$this->Flash->success(__('You are now logged in.'));
8090

81-
return $this->redirect(['controller' => 'Account', 'action' => 'index']);
91+
return $this->redirect($target);
8292
}
8393
```

src/Auth/AclTrait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ protected function _checkUser(ArrayAccess|array $user, array $params): bool {
104104
$superAdminColumn = $this->getConfig('idColumn');
105105
}
106106
if (!isset($user[$superAdminColumn])) {
107-
throw new CakeException('Missing super Admin Column in user table');
107+
throw new CakeException('Missing super admin column `' . $superAdminColumn . '` in user table');
108108
}
109109
if ($user[$superAdminColumn] === $this->getConfig('superAdmin')) {
110110
return true;

src/Authenticator/PrimaryKeySessionAuthenticator.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Cake\Http\Exception\UnauthorizedException;
1212
use Psr\Http\Message\ResponseInterface;
1313
use Psr\Http\Message\ServerRequestInterface;
14+
use TinyAuth\Utility\SessionCache;
1415

1516
/**
1617
* Session Authenticator with only ID
@@ -25,6 +26,7 @@ public function __construct(IdentifierInterface $identifier, array $config = [])
2526
$config += [
2627
'identifierKey' => 'key',
2728
'idField' => 'id',
29+
'cache' => false, // `true` to activate caching layer
2830
];
2931

3032
parent::__construct($identifier, $config);
@@ -46,11 +48,29 @@ public function authenticate(ServerRequestInterface $request): ResultInterface {
4648
return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
4749
}
4850

51+
if (!is_scalar($userId)) {
52+
// Maybe during migration? Let's remove this old one then
53+
$session->delete($sessionKey);
54+
55+
return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
56+
}
57+
58+
if ($this->getConfig('cache')) {
59+
$user = SessionCache::read((string)$userId);
60+
if ($user) {
61+
return new Result($user, Result::SUCCESS);
62+
}
63+
}
64+
4965
$user = $this->_identifier->identify([$this->getConfig('identifierKey') => $userId]);
5066
if (!$user) {
5167
return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
5268
}
5369

70+
if ($this->getConfig('cache')) {
71+
SessionCache::write((string)$userId, $user);
72+
}
73+
5474
return new Result($user, Result::SUCCESS);
5575
}
5676

@@ -73,6 +93,21 @@ public function persistIdentity(ServerRequestInterface $request, ResponseInterfa
7393
];
7494
}
7595

96+
/**
97+
* @inheritDoc
98+
*/
99+
public function clearIdentity(ServerRequestInterface $request, ResponseInterface $response): array {
100+
if ($this->getConfig('cache')) {
101+
$sessionKey = $this->getConfig('sessionKey');
102+
/** @var \Cake\Http\Session $session */
103+
$session = $request->getAttribute('session');
104+
$userId = $session->read($sessionKey);
105+
SessionCache::drop($userId);
106+
}
107+
108+
return parent::clearIdentity($request, $response);
109+
}
110+
76111
/**
77112
* Impersonates a user
78113
*

src/Middleware/UnauthorizedHandler/ForbiddenCakeRedirectHandler.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ class ForbiddenCakeRedirectHandler extends CakeRedirectHandler {
4545
* @return \Psr\Http\Message\ResponseInterface
4646
*/
4747
public function handle(Exception $exception, ServerRequestInterface $request, array $options = []): ResponseInterface {
48+
$params = (array)$request->getAttribute('params');
49+
if (!empty($params['_ext']) && $params['_ext'] !== 'html') {
50+
throw $exception;
51+
}
52+
4853
$response = parent::handle($exception, $request, $options);
4954

5055
$message = $options['unauthorizedMessage'] ?? __('You are not authorized to access that location.');

src/Middleware/UnauthorizedHandler/ForbiddenRedirectHandler.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ class ForbiddenRedirectHandler extends RedirectHandler {
4242
* @return \Psr\Http\Message\ResponseInterface
4343
*/
4444
public function handle(Exception $exception, ServerRequestInterface $request, array $options = []): ResponseInterface {
45+
$params = (array)$request->getAttribute('params');
46+
if (!empty($params['_ext']) && $params['_ext'] !== 'html') {
47+
throw $exception;
48+
}
49+
4550
$response = parent::handle($exception, $request, $options);
4651

4752
$message = $options['unauthorizedMessage'] ?? __('You are not authorized to access that location.');

src/Panel/AuthPanel.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ public function shutdown(EventInterface $event): void {
104104
$data['config'] = $authUserComponent->getConfig();
105105
$data['access'] = $access;
106106

107+
$data['identity'] = $request->getAttribute('identity');
108+
109+
/** @var \Authentication\AuthenticationService|null $auth */
110+
$auth = $request->getAttribute('authentication');
111+
$data['authenticationProvider'] = $auth ? $auth->getAuthenticationProvider() : null;
112+
$data['identificationProvider'] = $auth ? $auth->getIdentificationProvider() : null;
113+
107114
$this->_data = $data;
108115
}
109116

@@ -173,7 +180,7 @@ protected function _injectRole(array $user, $role, $id) {
173180
*
174181
* @return array
175182
*/
176-
protected function _generateUser($role, $id) {
183+
protected function _generateUser($role, $id): array {
177184
$user = [
178185
'id' => 0,
179186
];

src/Utility/SessionCache.php

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace TinyAuth\Utility;
4+
5+
use ArrayAccess;
6+
use Cake\Cache\Cache;
7+
use Cake\Core\Configure;
8+
use Cake\Core\Exception\CakeException;
9+
use Cake\Core\StaticConfigTrait;
10+
11+
/**
12+
* TinyAuth cache wrapper around session cache engine(s).
13+
*/
14+
class SessionCache {
15+
16+
use StaticConfigTrait;
17+
18+
protected static array $_defaultConfig = [
19+
'cache' => 'default',
20+
'prefix' => 'auth_user_',
21+
];
22+
23+
/**
24+
* Clears all session user info based on prefix
25+
*
26+
* @return void
27+
*/
28+
public static function clear(): void {
29+
$config = static::prepareConfig();
30+
static::assertValidCacheSetup($config);
31+
32+
if (!empty($config['groups'])) {
33+
foreach ((array)$config['groups'] as $group) {
34+
Cache::clearGroup($group, $config['cache']);
35+
}
36+
37+
return;
38+
}
39+
40+
Cache::clear($config['cache']);
41+
}
42+
43+
/**
44+
* @param string|int $userId
45+
* @param \ArrayAccess|array $data
46+
*
47+
* @return void
48+
*/
49+
public static function write(int|string $userId, ArrayAccess|array $data): void {
50+
$config = static::prepareConfig();
51+
52+
Cache::write(static::key($userId), $data, $config['cache']);
53+
}
54+
55+
/**
56+
* @param string|int $userId
57+
*
58+
* @return \ArrayAccess|array|null
59+
*/
60+
public static function read(int|string $userId): ArrayAccess|array|null {
61+
$config = static::prepareConfig();
62+
63+
return Cache::read(static::key($userId), $config['cache']) ?: null;
64+
}
65+
66+
/**
67+
* @param string|int $userId
68+
*
69+
* @return bool
70+
*/
71+
public static function delete(int|string $userId): bool {
72+
$config = static::prepareConfig();
73+
74+
return Cache::delete(static::key($userId), $config['cache']);
75+
}
76+
77+
/**
78+
* @param string|int $userId
79+
* @return string
80+
*/
81+
public static function key(int|string $userId): string {
82+
$config = static::prepareConfig();
83+
84+
static::assertValidCacheSetup($config);
85+
86+
return $config['prefix'] . $userId;
87+
}
88+
89+
/**
90+
* @param array<string, mixed> $config
91+
* @throws \Cake\Core\Exception\CakeException
92+
* @return void
93+
*/
94+
protected static function assertValidCacheSetup(array $config): void {
95+
if (!in_array($config['cache'], Cache::configured(), true)) {
96+
throw new CakeException(sprintf('Invalid or not configured TinyAuth cache `%s`', $config['cache']));
97+
}
98+
}
99+
100+
/**
101+
* @return array<string, mixed>
102+
*/
103+
protected static function prepareConfig(): array {
104+
$defaultConfig = static::$_defaultConfig;
105+
106+
return (array)Configure::read('TinyAuth') + $defaultConfig;
107+
}
108+
109+
}

0 commit comments

Comments
 (0)