Repository navigation
Optimize user_can() with request-scoped principal revalidation #6
Description
Context
PR #1 changed Auth::user_can() to call revalidate() before every authorization decision, preventing stale-session privileges after roles are revoked or a user is suspended/deleted. This is secure, but repeatedly reloads the principal (including roles and eager relations/context) when applications call user_can() many times in one HTTP request.
Reference: #1 and commit 255bb3b.
Goal
Revalidate the session principal once per request, then reuse the validated principal for subsequent user_can() calls within that same request. Preserve security semantics across requests.
Proposed implementation
- Introduce a request-scoped principal-validation state in the Auth abstraction (or equivalent request-context mechanism).
- On the first
user_can()in a request, revalidate the principal using the existing backend refresh; subsequent checks can reuse that validated principal, while resource/delegation checks must still evaluate their actual authorization relationships. - Make
RequireRoleand directuser_can()callers share the same validation state, so middleware validation is not repeated unnecessarily. - Preserve the public
revalidate()API as an explicit forced refresh (it must never silently return cached state). - Invalidate the validation state on login, logout, session/principal replacement, and local role/status or identity mutations that could affect the current user.
- Bound the state to an actual request, not to the PHP session. Traditional PHP-FPM instantiates Auth per request, but long-running runtimes (RoadRunner/Swoole/workers) need an explicit/request-lifecycle reset or a reliably request-local context; do not assume a reused Auth object means a new HTTP request.
- Preserve
user_can()return conventions:falsefor unauthenticated boolean checks,[]for list checks, and exceptions for invalid signatures.
Acceptance criteria
- Multiple
user_can()calls in one request perform at most one principal reload, including whenRequireRolealready validated it. - A role removal/addition becomes visible on the first authorization check of the next request; suspension/deletion logs out and denies the user on that check.
- Same-request mutations that affect the current user's permissions invalidate cached validation; the next check reflects the changes.
- Explicit
revalidate()always reloads the principal, even after another successful validation. - Tests cover role revocation/grant, suspension/deletion, multiple checks, middleware handoff, and long-running-worker request-boundary resets.
- Add a query-count / instrumented-backend regression test proving only one principal refresh per request, without changing the behavior of relational permission lookups.
- Document the guarantee (fresh at request boundary, not necessarily on every check within a request), performance trade-off, and request-lifecycle integration for long-running PHP workers.
Security note
The cache must not persist beyond the request. Cross-request role revocation and account suspension are mandatory; revocation that happens concurrently during an already-running request is not guaranteed to be observed unless the caller explicitly forces revalidate().
Activity