|
15 | 15 |
|
16 | 16 | use Generator; |
17 | 17 | use Horde\SessionHandler\Exception\CapabilityException; |
18 | | -use Horde\SessionHandler\Exception\SessionException; |
19 | 18 |
|
20 | 19 | /** |
21 | 20 | * Cross-session inspection and administration service. |
22 | 21 | * |
23 | | - * Operates on stored sessions other than the one currently active in this |
24 | | - * PHP process. Callers include admin tools (list active sessions, force |
25 | | - * logout one), security flows (password change → invalidate all OTHER |
26 | | - * sessions for the user), and ops jobs (background cleanup). |
| 22 | + * Decorates {@see SessionHandler} with operations relevant to admin tools |
| 23 | + * and security flows: list, load, force-destroy, look up by user, sign-out |
| 24 | + * everywhere. Callers include `base/admin/sessions.php`, password-change |
| 25 | + * handlers ("invalidate other sessions for this user"), and ops jobs. |
27 | 26 | * |
28 | | - * This is deliberately separate from {@see SessionHandler}, which manages |
29 | | - * the lifecycle of the SINGLE active session in the current request. |
| 27 | + * Design rule: code that reaches for SessionAdministrator should never |
| 28 | + * also need to reach for {@see SessionHandler}. Every operation that an |
| 29 | + * admin caller might need is exposed here either as a pass-through to |
| 30 | + * SessionHandler (for the lifecycle ops that apply to any session by id) |
| 31 | + * or as a user-aware method composed over those primitives. SessionHandler |
| 32 | + * itself stays focused on the active-session lifecycle. |
| 33 | + * |
| 34 | + * Active-session-only operations on SessionHandler — `create()`, `save()`, |
| 35 | + * `regenerate()` and the SessionHandlerInterface plumbing — are |
| 36 | + * deliberately NOT proxied. They don't make sense for arbitrary stored |
| 37 | + * sessions; admin tools should not be able to mint new ones, mutate |
| 38 | + * others' state, or rotate IDs of foreign sessions. |
30 | 39 | * |
31 | 40 | * Capability requirements |
32 | 41 | * ----------------------- |
33 | | - * The injected backend must implement {@see SessionStorageBackend} (mandatory) |
34 | | - * for delete/load. Methods that enumerate sessions additionally require |
35 | | - * {@see IterableSessionBackend}; calling them against a non-iterable backend |
36 | | - * throws {@see CapabilityException}. |
| 42 | + * The capability checks belong to the underlying SessionHandler — we just |
| 43 | + * forward calls. listAll() throws {@see CapabilityException} when the |
| 44 | + * backend is not iterable; getMetadata() throws when the backend lacks |
| 45 | + * metadata support; expire() throws when the backend lacks administrative |
| 46 | + * expiration support. Mandatory ops (load, destroy) work on any backend. |
37 | 47 | * |
38 | | - * Methods that filter by user (findByUser, destroyAllForUser) inspect the |
39 | | - * payload of each enumerated session. The user identifier is read via the |
40 | | - * "horde/auth/userId" path used by HordeSession, matching the wire layout |
41 | | - * of {@see \Horde\Core\Session\HordeSession::getAuthenticatedUser()}. Other |
42 | | - * Session implementations can extract via the same path or override by |
43 | | - * subclassing. |
| 48 | + * User-aware methods |
| 49 | + * ------------------ |
| 50 | + * findByUser/destroyAllForUser inspect the payload of each enumerated |
| 51 | + * session via the protected {@see extractUserId()} hook. The default |
| 52 | + * implementation reads "horde/auth/userId" — the wire layout used by |
| 53 | + * {@see \Horde\Core\Session\HordeSession::getAuthenticatedUser()}. Other |
| 54 | + * Session implementations can override extractUserId() in a subclass. |
44 | 55 | * |
45 | 56 | * Performance note |
46 | 57 | * ---------------- |
|
52 | 63 | class SessionAdministrator |
53 | 64 | { |
54 | 65 | public function __construct( |
55 | | - protected readonly SessionStorageBackend $backend, |
56 | | - protected readonly SessionFactory $factory, |
57 | | - protected readonly SessionSerializer $serializer, |
| 66 | + protected readonly SessionHandler $handler, |
58 | 67 | ) {} |
59 | 68 |
|
60 | 69 | /** |
61 | | - * Enumerate all known session IDs in the backend. |
| 70 | + * Enumerate all known session IDs. |
62 | 71 | * |
63 | 72 | * @return Generator<SessionId> |
64 | 73 | * |
65 | 74 | * @throws CapabilityException If the backend cannot enumerate sessions. |
66 | | - * @throws SessionException On backend failure during enumeration. |
67 | 75 | */ |
68 | 76 | public function listAll(): Generator |
69 | 77 | { |
70 | | - if (!$this->backend instanceof IterableSessionBackend) { |
71 | | - throw new CapabilityException( |
72 | | - 'Backend ' . $this->backend::class . ' does not support session enumeration; ' |
73 | | - . 'implement IterableSessionBackend to enable listing.' |
74 | | - ); |
75 | | - } |
76 | | - |
77 | | - foreach ($this->backend->listSessions() as $id) { |
| 78 | + foreach ($this->handler->listSessions() as $id) { |
78 | 79 | yield $id; |
79 | 80 | } |
80 | 81 | } |
81 | 82 |
|
82 | 83 | /** |
83 | 84 | * Load a stored session by ID. |
84 | 85 | * |
85 | | - * Returns null if the session does not exist, has expired, or its |
86 | | - * payload is corrupt and cannot be deserialized. |
| 86 | + * Pass-through to {@see SessionHandler::load()}. Returns null if the |
| 87 | + * session does not exist, has expired, or its payload is corrupt. |
87 | 88 | */ |
88 | 89 | public function load(SessionId $id): ?Session |
89 | 90 | { |
90 | | - $payload = $this->backend->load($id); |
91 | | - if ($payload === null) { |
92 | | - return null; |
93 | | - } |
94 | | - |
95 | | - try { |
96 | | - $data = $this->serializer->deserialize($payload); |
97 | | - } catch (SessionException) { |
98 | | - return null; |
99 | | - } |
100 | | - |
101 | | - return $this->factory->restore($id, $data); |
| 91 | + return $this->handler->load($id); |
102 | 92 | } |
103 | 93 |
|
104 | 94 | /** |
105 | 95 | * Forcibly delete a session. |
106 | 96 | * |
107 | | - * Idempotent: removing a non-existent session is not an error. |
| 97 | + * Pass-through to {@see SessionHandler::destroySession()}. Idempotent. |
108 | 98 | */ |
109 | 99 | public function destroy(SessionId $id): void |
110 | 100 | { |
111 | | - $this->backend->delete($id); |
| 101 | + $this->handler->destroySession($id); |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * Mark a session expired without deleting it. |
| 106 | + * |
| 107 | + * Pass-through to {@see SessionHandler::expire()}. Useful for "soft |
| 108 | + * logout" flows where a record of the session should remain for audit |
| 109 | + * but the session is no longer valid for authentication. |
| 110 | + * |
| 111 | + * @throws CapabilityException If the backend does not support |
| 112 | + * administrative expiration. |
| 113 | + */ |
| 114 | + public function expire(SessionId $id): void |
| 115 | + { |
| 116 | + $this->handler->expire($id); |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Retrieve session metadata (creation time, last modification, expiry). |
| 121 | + * |
| 122 | + * Pass-through to {@see SessionHandler::getMetadata()}. Returns null |
| 123 | + * when the backend supports metadata but no record matches the id. |
| 124 | + * |
| 125 | + * @throws CapabilityException If the backend does not support metadata. |
| 126 | + */ |
| 127 | + public function getMetadata(SessionId $id): ?SessionMetadata |
| 128 | + { |
| 129 | + return $this->handler->getMetadata($id); |
112 | 130 | } |
113 | 131 |
|
114 | 132 | /** |
115 | 133 | * Locate every session whose authenticated user matches $userId. |
116 | 134 | * |
117 | | - * Iterates the backend and inspects each payload. Sessions that fail |
118 | | - * to deserialize are silently skipped — they're invalid and should be |
119 | | - * cleaned up by GC, not reported here. |
| 135 | + * Iterates and inspects each payload. Sessions that fail to load are |
| 136 | + * silently skipped — they're invalid, GC will clean them. |
120 | 137 | * |
121 | 138 | * @return list<SessionId> |
122 | 139 | * |
@@ -144,8 +161,8 @@ public function findByUser(string $userId): array |
144 | 161 | * Forcibly destroy all of $userId's sessions, optionally keeping one. |
145 | 162 | * |
146 | 163 | * Used by "sign out everywhere" flows and by password-change handlers |
147 | | - * that want to invalidate other live sessions while leaving the |
148 | | - * current one intact. |
| 164 | + * that invalidate other live sessions while leaving the caller's |
| 165 | + * current session intact. |
149 | 166 | * |
150 | 167 | * @param SessionId|null $exceptId Session to keep (typically the |
151 | 168 | * caller's current session ID). |
|
0 commit comments