Access Control
Date: 2026-07-22
Status: Draft
Context
NeoWiki data leaves the system through the REST API (lookups, caller-supplied Cypher/SPARQL queries, RDF export), through parse-time accessors (parser functions and Lua), and through projection into graph and SPARQL stores and RDF dumps. Earlier ADRs settled individual pieces: Neo4j is reachable only through the backend (ADR 13), SPARQL stores may be exposed directly (ADR 19), and graph nodes carry per-wiki identity (ADR 22). This ADR records the overall access-control model those pieces belong to, and lists the decisions that are still open.
Constraints the model rests on:
- In MediaWiki, whether a user may read a page is a per-title decision made by permission hooks: private-wiki mode and ACL extensions (page- or namespace-scoped) plug into
Authority. The hook evaluation is the permission; there is no static attribute that could be copied elsewhere and stay correct. - The stores NeoWiki projects into cannot enforce per-user reads: Neo4j's fine-grained access control is Enterprise-only, and QLever has a single server-wide access token. Whatever is in a store is readable by anyone who can query that store.
- MediaWiki's parser cache is shared across users. Parse output that varies by user either leaks into the shared cache or fragments it.
- In a wiki farm (BlueSpice Galaxy), several wikis share one graph, and access is controlled per wiki.
Decision
- MediaWiki is the sole permission authority. Every access decision runs in PHP against the caller's
Authority; no enforcement is delegated to a store. - The page is the unit of access control. Every NeoWiki entity is governed by the page that stores it (Subjects, Schemas, Layouts, Mappings): reading requires the page's
readpermission, checked at full rigor (authorizeRead, so ACL-extension hooks run); writing requiresedit. NeoWiki defines no ACL model of its own. - Per-subject ACLs are a non-goal. There is no access control finer than the page: no recorded use case needs it, deployments separate sensitive data by page or wiki, and sub-page ACLs would complicate every read surface. Revisit only with a concrete use case.
- A denied read is indistinguishable from absent data. Read surfaces gated on the page's
readpermission answer with anull, an empty list, or a404— never a403— and must not reveal existence through side channels such as counts (#1062). Write denials answer403, and must equally not reveal whether an unreadable page exists (#1061). rest-api.md documents this per endpoint. - Page-attributable results are filtered per row. Read surfaces whose results are traceable to an owning page resolve each row's page and drop rows the caller may not read. Because this costs one permission check per row, such surfaces must bound their result sizes.
- Graph projections carry scoping keys, not ACL state.
wiki_idandnamespaceIdlet query authors scope queries (ADR 22, graph-model). User groups and page restrictions are never projected, so nothing in a store goes stale when permissions change. - Raw query surfaces have whole-store read semantics. A raw query surface executes a caller-supplied Cypher or SPARQL query; its result rows are not attributable to pages and are not trimmed. The REST query endpoints are gated by the wiki-level
neowiki-queryright; granting that right gives read access to everything the wiki projects into the store. Exposing a store directly (which ADR 19 allows for SPARQL) is a different surface: see the projection decision below. - Raw queries will support server-side filter injection. A deployment can register scoping predicates (such as restricting to the current wiki) that core applies to every caller-supplied query, so scoping is enforced rather than left to each caller.
- Projections and dumps are generated without permission checks. We may add such support later, enabling a public projection (a public store with a public query endpoint, holding no restricted content) alongside a private one that includes restricted content.
Open decisions
- Parse-time read semantics. Today the parse path is inconsistent (#1059): Schema lookups are gated per user but their output is parser-cached user-agnostically; subject accessors (
{{#neowiki_value}}and thenwdata accessors) check only revision-deletion visibility, not pageread;{{#cypher_raw}},{{#sparql_raw}},nw.queryandnw.sparqlQuerycheck nothing.{{#view}}is the leak-free pattern: a placeholder rendered at parse time, data fetched per user over REST. TODO: decide the parse-path rule and what it means for each surface. - Cross-wiki subject display. Rendering a subject from another wiki goes through REST, not Cypher, so query-side scoping does not cover it. TODO: decide the check and the degradation behavior when the schema or subject is not accessible. Relates to ADR 23.
- Default grant of
neowiki-query. The right is granted to*by default. TODO: decide whether the default changes, and how deployments with restricted content are expected to configure it.
Out of scope
- Authentication (single sign-on, federated identity): a MediaWiki/deployment concern.
- Rights and licensing metadata and provenance recording (ECHOLOT T3.4): data features, not access control.
Consequences
- Every new surface that exposes NeoWiki data must be classified: page-attributable (per-row gate), raw query (whole-store semantics), projection/dump (no permission checks), or parse-time (pending above). There is no unclassified option.
- Restricting a page does not remove its data from stores; it changes what the backend returns.
- The filter-injection extension point must be designed and implemented for farms like BlueSpice Galaxy.
- Dumps and projections contain restricted content (unless it is omitted via a non-permission mechanism such as the Mappings), so they must not be exposed to readers who may not access that content.
Alternatives Considered
- Enforce inside the store (per-user store accounts, store-level ACLs): Enterprise-only in Neo4j, unavailable in QLever, and unable to express hook-based MediaWiki permissions. Rejected, consistent with ADR 13.
- Project ACL state into the graph for pre-query trimming (user groups, restriction markers): re-implements an open set of permission hooks as data and goes stale, because permission changes produce no revision to sync on.
Related
- ADR 13: Restrict Neo4j Access, ADR 19: Graph Database Architecture, ADR 22: Multi-wiki Graph Node Identity, ADR 23: Subject Sources
- rest-api.md Permissions, query-api.md Permissions, graph-model
- Issues: #1046 (per-page read enforcement), #350 (slot-level access)