Skip to content

feat: carry authz stamps on UserDetailsImpl, unify session/JWT/PAT refresh#24496

Closed
netroms wants to merge 2 commits into
feat/userdetails-soft-refresh-v2from
feat/userdetails-authz-stamp
Closed

feat: carry authz stamps on UserDetailsImpl, unify session/JWT/PAT refresh#24496
netroms wants to merge 2 commits into
feat/userdetails-soft-refresh-v2from
feat/userdetails-authz-stamp

Conversation

@netroms

@netroms netroms commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Stacked on #24493 (review that first). Implements the follow-up direction: the soft-refresh freshness stamp moves from HTTP session attributes onto the immutable UserDetailsImpl snapshot itself, so sessions, JWT, and PAT all converge on one three-way check.

Changes

  • AuthzService: currentEpoch()/effectiveGen() replaced by refreshIfStale(principal); result identity tells callers whether to persist.
  • DefaultAuthzService: cache stores stamped snapshots directly (CachedUserDetails wrapper removed). Gen stamps are validated by a second epoch read after the build; if the epoch moved mid-build the gen is stamped 0 (unknown), failing safe. JWT/PAT snapshots gain the gen fast path: unrelated bumps re-stamp instead of rebuilding.
  • SoftRefreshSecurityContextRepository: session attributes (DHIS2_AUTHZ_GEN/DHIS2_AUTHZ_EPOCH) and their parsing removed; the repository only extracts, swaps, and persists. AuthzConstants deleted.
  • UserDetailsImpl: serialVersionUID pinned to the pre-stamp shape, so sessions serialized by earlier releases deserialize with stamps 0 and take one soft refresh instead of a forced re-login. No release-boundary session drain is needed. Guarded by UserDetailsImplSerializationTest.

Verification

  • Unit: DefaultAuthzServiceTest 10/10, SoftRefreshSecurityContextRepositoryTest 9/9, UserDetailsImplSerializationTest 3/3.
  • H2 stage: UserControllerTest 52, MeControllerTest 28, ApiTokenAuthenticationTest 11, JwtBearerTokenTest 6.
  • Postgres: AuthzSoftRefreshHooksTest 4/4, JdbcAuthzVersionStoreTest 7/7.
  • Live smoke (fresh PostGIS + embedded WAR, full Flyway): grant AND revoke visible on the next request of the same session, JSESSIONID unchanged, epoch fast path 200, disable still 302.

netroms added 2 commits July 19, 2026 06:19
Replace role/user-driven mass SessionRegistry.expireNow with a JDBC-backed
two-level invalidation scheme:

- authz_version table (Flyway V2_44_17) holds per-user ('user', <uid>) and
  per-role ('role', <uid>) generation stamps plus a single global epoch
  row; every bump advances the epoch in the same transaction as the entity
  change, so readers can never observe a stamp without its data. The store
  SQL is PostgreSQL-only like production; H2 test contexts run against an
  in-memory store wired in H2TestConfig.
- SoftRefreshSecurityContextRepository decorates the HttpSession delegate
  of the standard security context repository pair: when the epoch moved
  and the principal's effective generation differs, it rebuilds the
  immutable UserDetails and persists the refreshed context. Decorating
  the repository makes the refresh session-only by construction (JWT,
  PAT, and stateless authentications never load through it), skips
  impersonated sessions, and defers the check until a request actually
  accesses its authentication. No logout, no cookie change.
- AuthzService memoizes snapshots per username keyed by a pre-read epoch
  (stamp-before-build invariant) with striped single-flight rebuilds;
  JWT and PAT auth paths share the same epoch-validated cache.
- Role authority/restriction edits and role deletion bump one role key,
  O(1) regardless of member count. User role/org-unit changes and group
  membership deltas (import hook owning side + service paths) bump only
  affected users.
- Password change, disable, lock, and expiry keep hard session
  invalidation.
…fresh

Moves the soft-refresh freshness stamp (checked epoch + effective gen)
from HTTP session attributes onto the immutable UserDetailsImpl snapshot
itself. All three auth paths now converge on one three-way check in
DefaultAuthzService: epoch fast path, gen re-stamp path, rebuild path.

- AuthzService: currentEpoch()/effectiveGen() replaced by
  refreshIfStale(principal); identity of the result tells callers
  whether to persist.
- DefaultAuthzService: cache stores stamped snapshots directly
  (CachedUserDetails wrapper removed). Gen stamps are validated by a
  second epoch read after the build; if the epoch moved mid-build the
  gen is stamped 0 (unknown), failing safe. JWT/PAT snapshots gain the
  gen fast path: unrelated bumps re-stamp instead of rebuilding.
- SoftRefreshSecurityContextRepository: session attributes and long
  parsing removed; the repository only extracts, swaps, and persists.
  AuthzConstants deleted.
- UserDetailsImpl: serialVersionUID pinned to the pre-stamp shape, so
  sessions serialized by earlier releases deserialize with stamps 0 and
  take one soft refresh instead of a forced re-login. No release
  boundary session drain is needed. Guarded by
  UserDetailsImplSerializationTest.
@netroms

netroms commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Folded into #24493 per single-PR policy — identical tree, squashed into the one signed commit.

@netroms
netroms force-pushed the feat/userdetails-soft-refresh-v2 branch from a1636ab to cf053d4 Compare July 19, 2026 00:23
@netroms netroms closed this Jul 19, 2026
@netroms
netroms deleted the feat/userdetails-authz-stamp branch July 19, 2026 00:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant