MemberJunction / MemberJunction/MJ

UserCache never refreshes on user/role writes — new users can't log in and role grants aren't enforced until the 180s timer (never on PostgreSQL)

Open
#4,247 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TSQL
Stars
29
Forks
6
Avg merge
2d 1h
Merged PRs (30d)
323

Description

## Problem

`UserCache` (packages/GenericDatabaseProvider/src/UserCache.ts) holds every user plus each user's role assignments, and it is the ONLY server-side source for both: the auth hot path resolves users by scanning `UserCache.Instance.Users` (packages/MJServer/src/auth/index.ts, `verifyUserRecord`), and each request's `UserInfo.UserRoles` — the roles all permission aggregation (including field-level security) runs against — comes straight from the cached snapshot.

But `UserCache` has **no event subscription of any kind**. Its refresh triggers are:

- **SQL Server**: a self-rescheduling timer armed at bootstrap (packages/SQLServerDataProvider/src/config.ts:30) — default 180s. A failed refresh silently kills the loop (the reschedule sits inside the success branch).
- **PostgreSQL**: `UserCache.Instance.Refresh(provider)` is called **once with no interval** (packages/MJServer/src/index.ts:354) — it never refreshes again after boot.
- On-demand fallbacks that are either config-gated off by default (`userHandling.updateCacheWhenNotFound`) or manual (`SyncRolesAndUsers` mutation).

## Consequences

1. An admin creates a user through MJ Explorer → that user **cannot authenticate** for up to 180s (SQL Server) or until restart (PostgreSQL) — `verifyUserRecord` finds nothing and either auto-provisions a *different* record or rejects.
2. An admin grants/revokes a role → **enforcement does not change** for the same window. For field-level security this means a tightened rule aimed at a role a user was just added to is not applied to that user's requests.
3. Removing a role has the same lag — a revoked grant keeps working.

Note the historical trap here: the (now removed) hardcoded permission-entity refresh lists included `MJ: User Roles`, but they refreshed **metadata**, and metadata does not contain user→role mappings (`MJ: Users`/`MJ: User Roles` are not MJ_Metadata dataset items; `AllMetadata` has no `AllUsers`/`AllUserRoles`). So that entry never actually fixed this — the lever was pointed at the wrong cache.

## Proposed fix

Two small, precise mechanisms (refresh the *user state*, not the metadata graph):

1. **Server**: `UserCache` subscribes to the MJGlobal event bus for save/delete of `MJ: Users` and `MJ: User Roles` and does a debounced (~500ms) `LoadUsers` reload — the same once-per-bus subscription pattern as `ProviderBase`'s static invalidation fan-out. This also fixes the PostgreSQL never-refreshes hole and removes the dependence on the fragile timer.
2. **Client**: on a `remote-invalidate` for those two entities, `GraphQLDataProvider` refetches `GetCurrentUser` (one cheap query) instead of doing anything to metadata — so an open browser's own role list converges without a reload.

Also worth fixing while in there: the timer's reschedule should survive a failed refresh (move it out of the `if (users)` block or wrap in try/finally).

## Context

Found while root-causing the FLS over-the-wire stale-metadata leak (branch `JF_Entity_Field_Security`); full evidence trail in that branch's `plans/fls-it79-wire-leak.md` ("Coverage note" section). The metadata half of the propagation story is solved there by dataset-membership-driven refresh in `ProviderBase`; this issue is the remaining user/role half.

Contributor guide

Open the contributing guide

Research direction

Start with packages/GenericDatabaseProvider/src/UserCache.ts and compare its refresh behavior with packages/SQLServerDataProvider/src/config.ts and ProviderBase's static invalidation fan-out. Trace verifyUserRecord in packages/MJServer/src/auth/index.ts, the PostgreSQL call in packages/MJServer/src/index.ts, and GraphQLDataProvider's remote-invalidate handling. Done means user and role writes propagate to server and client user state, including PostgreSQL, while failed timer refreshes do not stop future attempts.

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, sql, typescript
Domain
authentication, authorization, backend, frontend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.