mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-19 10:30:59 -06:00
d5087ef3b9
bug-2 (Postgres) — replace_oidc_roles read existing rows under default READ COMMITTED with no row lock. Two concurrent OIDC callbacks for the same user_id (racing token refreshes with differing claim sets) could both observe the same baseline and produce a final role state matching neither caller's intent. Adds .with_for_update() to the SELECT so the existing rows for this user are locked for the duration of the transaction. The lock is per-user_id, not table-wide; unrelated user writes are unaffected. Empty result sets acquire no locks, so a brand-new user with no rows yet still allows two callers to proceed and merge via ON CONFLICT DO NOTHING — that's a permissive race that self-heals on the next reconciliation cycle, documented in code. perf-1 (SQLite) — replace_oidc_roles took the SQLite global write lock unconditionally via BEGIN IMMEDIATE before reading. Steady-state re-logins (claims unchanged, no INSERT/DELETE needed) paid the lock cost for nothing and serialised against unrelated writers. Replaces with a double-check pattern: phase 1 reads under the default deferred transaction (no write lock), computes the diff, and returns (set(), set()) on no-op. Phase 2, only when mutation is needed, commits the read txn, escalates to BEGIN IMMEDIATE, RE-READS, and re-computes the diff under the lock before writing. The returned (added, removed) reflects what was actually written, so caller logging in apply_role_mapping stays truthful even when concurrent writers shifted state between the two reads. The OR IGNORE on insert is now defense-in-depth (the lock makes it unnecessary) but kept as a safety net.