Skip to content

Optimize user_can() with request-scoped principal revalidation #6

Open

Description

@objectivebot

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 RequireRole and direct user_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: false for 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 when RequireRole already 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions