Gate page reads through a single PageReadAuthorizer#1072
Merged
Conversation
JeroenDeDauw
marked this pull request as ready for review
July 17, 2026 14:41
Member
Author
|
LGTM |
The read gate was one decision -- "may this Authority read this page?" -- written five times: AuthorityBasedSubjectAuthorizer, CachingSchemaLookup, CachingMappingLookup, WikiPageLayoutLookup and DatabaseSchemaNameLookup each called authority->authorizeRead( 'read', $title ), and four followed it with the same five-line denial log. Three classes grew a LoggerInterface only to host that block, four test classes each had their own copy of a testGateUsesBindingAuthorizeRead, and three of those carried the comment "Mirrors AuthorityBasedSubjectAuthorizerTest::testDeniedReadIsLogged". That copying had already drifted once: both caching lookups used probablyCan, an advisory hint verb, for a binding gate, and both had to be fixed by hand. SubjectReadAuthorizer could not absorb the other four because of its name: it holds no Subject semantics -- it is newTitle() plus authorizeRead -- and two of its callers gate an RDF export and a revision's page, not a Subject. Schemas, Layouts and Mappings live on pages and need the same check, but a thing called SubjectReadAuthorizer cannot plausibly gate a Schema page. Rename it to PageReadAuthorizer, move it off AuthorityBasedSubjectAuthorizer into AuthorityBasedPageReadAuthorizer, and give it the two entry points its callers need: by PageId for the Application queries, which resolve a Subject to a page through the graph, and by Title for the Persistence lookups, which already resolved one for their own content fetch. SubjectContentRepository sets the precedent for keying one page two ways. The verb and the denial log are now pinned once, in the authorizer's own test. Denials from the Schema name filter are now logged like every other denial, where before that one call site deliberately stayed silent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GetSubjectQuery and GetPageSubjectsQuery each had a private pageIsReadable with the same signature and the opposite meaning: one treats an unresolved page as readable, the other as denied. Both needed a docblock to say so, and one of those docblocks existed mainly to point at the other. GetPageSubjectsQuery's helper had a single call site and, by its own docblock, guarded a case that cannot happen today. Inline it: the fail-closed check is three tokens, and a named helper plus eight lines of prose is not warranted by "if a SubjectLookup that bypasses the graph is ever wired in". That leaves one such check, in GetSubjectQuery, where null does not mean "readable" but "no page to gate". Name it pageIsReadableOrUnresolved, which is what it decides, and the docblock only has to explain why unresolved is allowed there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JeroenDeDauw
force-pushed
the
pr-1058-page-read-authorizer
branch
from
July 17, 2026 15:54
e48d216 to
06e1c09
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follows-up to #1058
The design half of an AI review of #1058. #1058 is correct — this is about the shape it leaves behind, so it is entirely advisory and behaviour-preserving. Not a bug fix, and independent of #1071 (which carries that review's two blocking fixes; the two branches touch
DatabaseSchemaNameLookupTestand will want a trivial conflict resolution whichever lands second).The problem
The read gate is one decision — may this Authority read this page? — written five times. All five call
authority->authorizeRead( 'read', $title ); four follow it with the same five-line denial log:AuthorityBasedSubjectAuthorizerCachingSchemaLookupCachingMappingLookupWikiPageLayoutLookupDatabaseSchemaNameLookupThree of those classes grew a
LoggerInterfaceused only for that block. Four test classes each hold their owntestGateUsesBindingAuthorizeRead, and three carry the comment// Mirrors AuthorityBasedSubjectAuthorizerTest::testDeniedReadIsLogged.This is not hypothetical drift: it already happened once. Both caching lookups used
probablyCan— an advisory hint verb — for a binding gate, and #1058 has to fix both by hand. The PR repairs the drift and re-creates the conditions for it, at five sites instead of two.Why the name was the blocker
SubjectReadAuthorizercontains no Subject semantics — it isnewTitle()plusauthorizeRead— and two of its five callers gate an RDF export (ExportPageRdfApi) and a revision's page (GetSubjectApi), not a Subject. Schemas, Layouts and Mappings all live on pages and need the identical check, but a thing calledSubjectReadAuthorizercannot plausibly gate aSchemapage — so four sites hand-rolled it instead.The interface's existence is fine (3 Application callers, mirroring
SubjectWriteAuthorizer). The name and the host class were the defects.What this does
SubjectReadAuthorizer→PageReadAuthorizer, moved offAuthorityBasedSubjectAuthorizerinto its ownAuthorityBasedPageReadAuthorizer, which owns the denial logging.authorizeReadByPageIdfor the Application queries (which resolve a Subject to a page via the graph) andauthorizeReadByPageTitlefor the Persistence lookups (which already resolved aTitlefor their own content fetch, so keying by id would cost a pointless second page lookup).SubjectContentRepositorysets the precedent for keying one page two ways.AuthorityBasedSubjectAuthorizergoes back to two interfaces and drops its logger; the three lookups drop theirs; two drop theirAuthority.GetPageSubjectsQuery::pageIsReadableis inlined (single call site; its own docblock says the case it guards cannot happen today), and the survivor inGetSubjectQuerybecomespageIsReadableOrUnresolved— there null does not mean "readable" but "no page to gate". Two same-named helpers with opposite meanings, defended by cross-referencing docblocks, are gone.Net: 5 gate implementations → 1, 4 log copies → 1, 4
LoggerInterfacedependencies → 0, 5 verb tests → 1.One intentional behaviour change
DatabaseSchemaNameLookupnow logs denials like every other site; it previously stayed silent on the grounds that enumeration would log proportionally to list size. That reasoning was already inconsistent — theexpand=relationsloops in both queries log per referenced subject via the authorizer — and the log exists so a denial shaped as absence stays diagnosable, which applies just as much to "why is my Schema missing from the list?". Easy to revert if you disagree; it is the only non-cosmetic delta.Verification
Behaviour-preserving, checked rather than assumed.
make csclean; full suite green (1690 tests). Re-ran the live denial matrix on a dev wiki under agetUserPermissionsErrorsExpensiveACL (the hookprobablyCanskips, so this also proves the RIGOR_FULL gate survived):page/{id}/subjects→ empty map,subject/{id}→{"subject":null},rdf→ 404,validate→ 404,schema/{name}→{"schema":null},schema-names→[],expand=relations→ restricted target omitted, readable control page unaffected. All byte-identical to #1058 before the refactor.