Skip to content

Stay inactive until update.php has created the tables - #14

Merged
JeroenDeDauw merged 10 commits into
masterfrom
feature/tolerate-missing-schema
Aug 19, 2026
Merged

Stay inactive until update.php has created the tables#14
JeroenDeDauw merged 10 commits into
masterfrom
feature/tolerate-missing-schema

Conversation

@JeroenDeDauw

@JeroenDeDauw JeroenDeDauw commented Aug 19, 2026

Copy link
Copy Markdown
Member

An extension can be loaded on a wiki before update.php has run there — a farm switching it on
across many wikis at once does exactly that. It would arrive on wikis before any of them
has run update.php. Every entry point that reads a table now checks whether the tables are there,
and behaves as if the extension were not loaded when they are not, rather than failing with a
database error:

  • the code login route offers no button, and abstains from a request submitted anyway;
  • no account is reported as a member;
  • the last-login record, which runs for every login on the wiki, is skipped;
  • the management API answers 503 with the stable error code schema_missing.

The exception is the reader group: core's own data, and the mark provisioning puts on an account
before its roster row. While the roster cannot be read, the mark carries the rules that must not
lapse. An account holding it is refused a password — one set in that window would be a permanent
way in that the allowlist never governs — is dropped from a password reset rather than mailed a
temporary one, and where the allowlist governs single sign-on is refused that login too: an identity
provider still points at a removed member's parked account, and the roster that would clear such an
account is exactly what cannot be read. Every other account is left alone. Staff sign in through the
identity provider as before; refusing everyone would shut a working provider out of the wiki.

The check costs at most one tableExists query per request: it runs only where an entry point would
touch a table, and the answer is kept for the rest of the process, which for a web request is the
request. A wiki without the tables warns once per such process on the MemberAccess channel,
telling the administrator to run update.php. A connection that cannot answer counts as having the
tables, so an unanswerable check cannot switch the feature off on a wiki that has them. What loading
the extension changes wiki-wide is untouched: those are settings, not tables.

AI-authored — Claude Code, Opus 5 (max), review round by Fable 5 (max); detailed spec from @JeroenDeDauw, no redirections; diff not yet human-reviewed; test-first throughout, each guard mutation-tested by disabling it and watching its tests fail, full composer preflight green locally and CI green.

Production notes

Commits through 49d9cd4 are the original build by Opus 5 (max). The seven commits after it are a
pr-review round by Fable 5 (max) with Opus subagents — six independent review passes, each blocking
finding adversarially verified before fixing. The review found that on a wiki whose tables go missing
after it has members (the drop-and-rerun procedure in AGENTS.md, or a partial restore), the original
head turned two fail-closed crashes into fail-opens: a temporary password mailed into a member's
mailbox, and single sign-on back into a removed member's parked account. The reader-group fallback in
ecc2162 closes both and is a semantics change against the original spec's "single sign-on is left
alone". The other review commits are ordering and probe-liveness pins and README/docblock accuracy.

@codecov-commenter

codecov-commenter commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.12903% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.62%. Comparing base (a6590ee) to head (34d3a45).

Files with missing lines Patch % Lines
src/MemberAccessExtension.php 67.64% 11 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master      #14      +/-   ##
============================================
+ Coverage     86.27%   86.62%   +0.35%     
- Complexity      523      551      +28     
============================================
  Files            61       62       +1     
  Lines          1807     1907     +100     
============================================
+ Hits           1559     1652      +93     
- Misses          248      255       +7     

☔ 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 and others added 3 commits August 19, 2026 21:58
An extension can be loaded on a wiki before update.php has run there — a farm switching it on
across many wikis at once does exactly that. Every entry point that reads a table now asks whether
the tables are there, and behaves as if the extension were not loaded when they are not, rather
than failing with a database error:

* the code login route offers no button and abstains from a request submitted anyway, and no
  account is reported as a member, so a password change is not refused either;
* single sign-on is left alone, neither refused nor provisioned: refusing every login would shut a
  working identity provider out of the wiki;
* the last-login record, which runs for every login on the wiki, is skipped;
* a password reset keeps the accounts it names;
* the management API answers 503 with the stable error code `schema_missing`.

The question costs at most one `tableExists` query per request: it is asked only where an entry
point would touch a table, and answered from memory after the first time. A wiki without the tables
is warned once on the `MemberAccess` log channel to run update.php. A connection that cannot answer
counts as having the tables, so nothing here can turn a working wiki off. What loading the extension
changes wiki-wide is untouched, being settings rather than tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every request builds the entry points, and most go on to do nothing with the tables, so probing
from the constructor would put a query on requests that never need one. Nothing held that in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The installation note read as though nothing could log in, while a single sign-on login is left
exactly as the wiki has it, and named no channel to look on for the warning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JeroenDeDauw
JeroenDeDauw force-pushed the feature/tolerate-missing-schema branch from 904fcb0 to 49d9cd4 Compare August 19, 2026 19:58
@JeroenDeDauw
JeroenDeDauw marked this pull request as ready for review August 19, 2026 21:14
JeroenDeDauw and others added 7 commits August 20, 2026 00:35
…e away

A wiki without the tables is not always a fresh one: dropping them and re-running update.php is
this repository's own schema procedure, and a partial restore gets there too. On such a wiki the
accounts the allowlist created are still there, and leaving every route alone took two rules off
them that exist for exactly those accounts: a member never holds a password, and a removed
member's parked account is never signed back into.

The reader group is what is left to go on. It is core's data rather than the extension's,
provisioning adds it before the roster row, a removal leaves it in place, and the user-list handler
already reads it on its own. While the roster cannot be read it therefore stands in: an account
carrying it is refused a password, dropped from a password reset, and refused a single sign-on
login where the allowlist governs that route. It is the wider of the two answers, since the group
can also be given by hand, and that is the way round to err while the roster cannot say which
accounts are members. A wiki that runs update.php has the roster answering again on the next
request.

The rest keeps the leave-alone rule: staff carry no reader group and sign in as before, a login
with no account yet has no groups either and is still admitted, the code route stays closed, and
no account is reported as a member. The refusal says in the log what is known — the group and the
missing tables — rather than that the roster forgot the account, which no one can tell here, and
the warning the wiki already gets on this channel no longer claims the extension does nothing.

`isMemberName` is now `isRefusedAPassword`, since without the tables it answers about the group
rather than the roster, and it is the same rule the password reset filters on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The probe reads whether a table exists, which `IConnectionProvider` does not promise: it hands back
an `IReadableDatabase`, and only the object behind it happens to be maintainable. A future core
that narrows the concrete class takes every guard in the extension off at once and quietly, since a
connection that cannot be asked deliberately counts as having the tables — the wiki keeps working,
and the day it has no tables it fails with database errors instead.

The test cannot be seen failing against this code, since what it pins is the environment rather
than a branch: it asserts of the real wiki connection what the stub in the test beside it is built
to lack.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This provider offers a request to the login form and to nothing else, yet it asked whether the
tables were there before looking at which action it was being asked about. An account creation
render therefore paid the one query the whole feature is meant to cost, on a form that touches no
table of ours.

A password change or credential removal render reaches the question by another way regardless:
core follows the same call with an `allowsAuthenticationDataChange` of every request it collected,
which this provider answers about the account named. Account creation and account linking are what
the order spares.

What every input gets back is unchanged, which is why nothing new pins it: what moved is a query on
paths with no observable output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The gate answers who may call before it says anything about the tables, so that a wiki waiting for
update.php turns callers away exactly as it will once it has them. The right was pinned; the token
was not, and it is the half a caller from another site meets.

Verified by moving the schema question above the token: the write is then told the wiki has no
tables, which is a 503 where a 403 belongs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The filter replaced an up-to-date roster read, and the provider beside it
reads the group the same way, with the reason written next to it: a group
given moments ago that read as absent would let a temporary password
through. The filter's read was the one left answering from a replica.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test database has the tables, so a fixture with a roster row and no
group is what tells the implementations apart: one that reached for the
roster would find the row, answer with it, and pass every bare-account
test here while being a database error on a real wiki. Both refusal
sites get one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The installation note calls the code route not offered while the
sections above hand out what a route brings — the anonymous
autocreation right, the captcha relaxation, the session duration —
using the same word for the setting that asks for them. Registration
runs before services exist, so those cannot follow the tables: the
setting is called turned on, what it brings is said to arrive before
update.php has run, and the two docblocks that promised otherwise now
say what holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JeroenDeDauw
JeroenDeDauw merged commit bbbaf09 into master Aug 19, 2026
6 checks passed
@JeroenDeDauw
JeroenDeDauw deleted the feature/tolerate-missing-schema branch August 19, 2026 23:00
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