Skip to content

Refuse single sign-on into an account the roster forgot - #13

Merged
JeroenDeDauw merged 3 commits into
masterfrom
fix/sso-into-forgotten-accounts
Aug 18, 2026
Merged

Refuse single sign-on into an account the roster forgot#13
JeroenDeDauw merged 3 commits into
masterfrom
fix/sso-into-forgotten-accounts

Conversation

@JeroenDeDauw

Copy link
Copy Markdown
Member

Follows-up to #12

A post-merge review of #12 found one blocking defect, fixed here, along with test hardening and two
corrections to that PR's description.

The defect

An identity provider records the account it authenticated by user id, and the rename does not touch
that record. A removed member's next single sign-on login therefore arrived at the parked account,
which — having no roster row — was read as staff and admitted outside the allowlist: removal flipped
the member from governed by the allowlist to exempt from it, and deleting their allowlist entry
changed nothing, since the exempt path never consults it. PluggableAuth then wrote the provider's
address back onto the parked account, confirmed, undoing the removal's stripping of it and
re-opening the password-reset route into an account that still carries the reader group. The account
shows in no roster, and deactivation and removal both answer not_a_member for it — the state
issue #3 exists to eliminate, recreated for single sign-on members.

The reader group is the mark of an account the allowlist created: provisioning adds it before the
roster row, and removal leaves it in place. An account carrying the mark without a roster row is
therefore one the roster forgot rather than staff, and is now refused. The same refusal closes the
single sign-on route into an account left behind by a failed provisioning, the third defect of
issue #3. A removed member's way back in is the code route, which reaches a fresh account; their
single sign-on logins stay refused, since the provider keeps pointing at the parked account. The
README scopes both claims now, and the user-list handler's check of the same mark is renamed to say
what it reads, since holding the reader group no longer implies being a member.

Test hardening

  • Forgetting a member never ran against a roster holding anyone else, so a delete that forgot the
    whole roster survived the suite.
  • The CSRF gate was never exercised over DELETE, and the refused-rename answer never reached the
    endpoint's error mapping in a test.
  • The rename log's performer and the failure log's address hashing were unasserted.

Corrections to #12's description

  • "everyone who may manage members also holds block" is wrong: the extension grants
    memberaccess-manage to bureaucrats, and default bureaucrats hold userrights, noratelimit
    and renameuser, not block. A bureaucrat who is no sysop can therefore remove a deactivated,
    still-admitted member, undoing a deactivation they could neither place nor lift — though such an
    account could also grant itself sysop. Whether removal should require the block right remains
    open, now on corrected facts.
  • "their address is free again and reaches a new account at the next login" holds for the code
    route only; single sign-on stays refused, as above.

Considered, omitted

  • Blocking the parked account as part of removal: core-enforced and rename-proof, and it would hold
    even where the allowlist does not apply to single sign-on — but it places a block on behalf of a
    performer the endpoint deliberately does not require the block right of, so it belongs with that
    open decision rather than in this fix.
  • Stripping the reader group instead: the group only revokes, so removing it would un-revoke editing
    on the parked account, and it is the one mark that tells a forgotten account from staff. For the
    same reason, taking the group off a parked account by hand turns it into an exempt staff account.
  • A group-sync setup that removes the reader group at login would bypass the refusal; such a sync
    already destroys the reader revocations wholesale, and none of our deployments configures one.

AI-authored — Claude Code, Fable 5 (max); post-merge pr-review pass of #12, delegated by @JeroenDeDauw, no mid-run steering; diff not yet human-reviewed; three independent review agents converged on the defect and a refute-tasked verifier confirmed it against PluggableAuth/OpenIDConnect source; refusal tests written first and seen failing; local preflight green (PHPCS, PHPStan, 668 tests); removal exercised end-to-end on a dev wiki.

JeroenDeDauw and others added 3 commits August 18, 2026 22:32
An identity provider records the account it authenticated by user id, so
a removed member's next single sign-on login arrives at the parked
account. With the roster row gone, that login was read as staff and
admitted outside the allowlist: the removal flipped the member from
governed to exempt, on an account no roster lists and no endpoint
reaches, and the provider then wrote the stripped address back onto it,
confirmed.

The reader group is the mark of an account the allowlist created:
provisioning adds it before the roster row, and a removal leaves it in
place. An account carrying the mark without a roster row is therefore
one the roster forgot, and is refused rather than exempted. The same
refusal closes the single sign-on route into an account left behind by
a failed provisioning.

The user-list handler's check of the same mark is renamed to say what
it reads, since holding the reader group no longer implies being a
member.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Forgetting a member never ran against a roster holding anyone else, so
a delete that forgot the whole roster survived the suite. The CSRF gate
was never exercised over DELETE, the refused-rename answer never
reached the endpoint's error mapping in a test, and the rename log's
performer and the failure log's address hashing were unasserted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The REST framework answers the rethrown failure with an error response,
after which the request's transaction round commits as usual: nothing
rolls the atomic section back but this cancel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.27%. Comparing base (d1d538a) to head (663b134).

Additional details and impacted files
@@             Coverage Diff              @@
##             master      #13      +/-   ##
============================================
+ Coverage     85.83%   86.27%   +0.44%     
- Complexity      519      523       +4     
============================================
  Files            61       61              
  Lines          1793     1807      +14     
============================================
+ Hits           1539     1559      +20     
+ Misses          254      248       -6     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JeroenDeDauw
JeroenDeDauw marked this pull request as ready for review August 18, 2026 20:41
@JeroenDeDauw
JeroenDeDauw merged commit a6590ee into master Aug 18, 2026
6 checks passed
@JeroenDeDauw
JeroenDeDauw deleted the fix/sso-into-forgotten-accounts branch August 18, 2026 20:41
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.

2 participants