Wiki source code of Access Doors

Last modified by gabrielc on 2026/10/21 11:18

Show last authors
1 The access doors centralize the MCP server's authorization and scoping decisions so that no tool re-implements them.
2
3 Three are public component roles of the ##application-ai-llm-mcp-api## module (package ##org.xwiki.contrib.llm.mcp##, each annotated ##@Role## and ##@Unstable##), so a tool contributed by any module can inject them: ##MCPDocumentAccess##, ##MCPWikiReach## and ##MCPDocumentSearch##. Two are internal to the server module (package ##org.xwiki.contrib.llm.mcp.internal.access##): ##MCPSpaceFilter##, which the public doors apply on the caller's behalf, and ##MCPRowQuery##.
4
5 A tool must never resolve a ##DocumentReference## itself: doing so escapes the space policy and can reach another wiki unchecked.
6
7 == MCPDocumentAccess ==
8
9 Single sanctioned way to resolve a reference and enforce authorization.
10
11 {{code language="java"}}
12 @Role
13 public interface MCPDocumentAccess {
14 DocumentReference resolveAndAuthorize(String reference, Right right)
15 throws MCPAccessDeniedException;
16
17 DocumentReference resolveAndAuthorize(String reference, Right right, WikiReference wikiContext)
18 throws MCPAccessDeniedException;
19 }
20 {{/code}}
21
22 Applies in order: reach gate, then ##ContextualAuthorizationManager.hasAccess##, then space filter. Throws ##MCPAccessDeniedException## with an agent-facing refusal message. The three-argument overload resolves an unqualified reference into the given wiki; a reference carrying an explicit wiki prefix that differs from ##wikiContext## is refused, so a tool's ##wiki## parameter and a prefixed reference cannot contradict each other.
23
24 {{plantuml}}
25 @startuml
26 !theme bluegray
27 start
28 :a tool calls resolveAndAuthorize\nwith the reference it was given;
29 :resolve it against the current context;
30 if (the target is in another wiki?) then (yes)
31 if (this endpoint may reach that wiki?) then (no)
32 :refused: cross-wiki reach\nis not enabled for this endpoint;
33 stop
34 else (yes)
35 endif
36 else (no)
37 endif
38 if (the user has the required right\non the document?) then (no)
39 :refused: you do not have\npermission to view or edit it;
40 stop
41 else (yes)
42 endif
43 if (the space filter allows it?) then (no)
44 :refused: outside the content\nthis endpoint exposes;
45 stop
46 else (yes)
47 endif
48 :the tool receives the DocumentReference;
49 stop
50 @enduml
51 {{/plantuml}}
52
53 == MCPSpaceFilter ==
54
55 Applies the MCP space whitelist or blacklist. Internal to the server module: the public doors above already apply it, so a tool in another module does not call it directly.
56
57 {{code language="java"}}
58 @Role
59 public interface MCPSpaceFilter {
60 boolean isAllowed(DocumentReference reference);
61 List<String> filterQueries();
62 void invalidate(String wikiId);
63 void invalidateAll();
64 }
65 {{/code}}
66
67 |=Method |=Returns |=What it does
68 |##isAllowed(reference)## |##boolean## |Whether that one document passes the configured filter mode and entries
69 |##filterQueries()## |##List<String>## |The Solr filter query clauses that scope search results to the same set
70 |##invalidate(wikiId)## |##void## |Drops the parsed filter cached for one source wiki
71 |##invalidateAll()## |##void## |Drops every wiki's parsed filter
72
73 A space entry covers the space and its whole subtree; a document entry matches exactly one document. An entry is resolved with the current resolver: ##Sandbox## lands in the source wiki, while a wiki-qualified ##second:Sandbox## lands in the wiki it names and therefore matches nothing in the source wiki. The parsed filter is cached per source wiki, and the configuration listener drops that cache when the wiki's configuration page is saved or deleted. Both checks fail //closed// on read errors: ##isAllowed## denies, and ##filterQueries## returns a match-nothing ##-*:*## clause.
74
75 == MCPWikiReach ==
76
77 Decides how far an endpoint reaches beyond its own wiki.
78
79 {{code language="java"}}
80 @Role
81 public interface MCPWikiReach {
82 boolean isReachEnabled();
83 boolean canReachWiki(String wikiId);
84 List<String> resolveSearchWikis(String wikiParam) throws MCPAccessDeniedException;
85 String resolveSingleWiki(String wikiParam) throws MCPAccessDeniedException;
86 }
87 {{/code}}
88
89 |=Method |=Returns |=What it does
90 |##isReachEnabled()## |##boolean## |Whether this endpoint may cross wikis at all
91 |##canReachWiki(wikiId)## |##boolean## |Whether that one target wiki is reachable: always true for the current wiki, otherwise whether cross-wiki reach is enabled
92 |##resolveSearchWikis(wikiParam)## |##List<String>## |The wikis a search covers: blank or the current wiki gives the current wiki, ##"all"## gives ##null## meaning the whole farm, an id gives that single wiki
93 |##resolveSingleWiki(wikiParam)## |##String## |The one wiki a single-wiki call operates on; there is no ##"all"## value here
94
95 Backed by the main-wiki-only ##reachEnabledWikis## list. A reach-enabled endpoint reaches every wiki in the farm regardless of the target's own enable gate, tool toggles, or space filter.
96
97 === Default posture ===
98
99 Until the main wiki's reach dashboard is saved for the first time, only the main wiki has reach and every other wiki fails closed. That first save materializes the list, after which it is authoritative and may exclude the main wiki too. If the main wiki's configuration page is untrusted, reach is off for every wiki, the main wiki included.
100
101 == MCPDocumentSearch ==
102
103 Builds a Solr search query scoped to wikis, space filter, and rights.
104
105 {{code language="java"}}
106 @Role
107 public interface MCPDocumentSearch {
108 Query createQuery(String statement, List<String> additionalFilterQueries,
109 List<String> targetWikiIds) throws QueryException;
110 }
111 {{/code}}
112
113 Uses ##SecureQuery## with ##checkCurrentUser(true)##, wiki-scope clauses, and the space filter. It is the single query path for ##query_documents##. A ##null## wiki list means the whole farm; an empty list is a defensive no-match.
114
115 == MCPAccessDeniedException ==
116
117 Checked exception (##extends Exception##) whose message is the agent-facing refusal. Thrown by ##MCPDocumentAccess.resolveAndAuthorize()## and caught by the tool to produce an ##isError## result.
118
119 == MCPRowQuery ==
120
121 Internal door to the authorized HQL row pipeline of the navigation tools: bounded row fetching against one target wiki, plus per-row resolution and authorization of the fetched rows.
122
123 {{code language="java"}}
124 @Role
125 public interface MCPRowQuery {
126 int MAX_FETCH_PER_QUERY = 2000;
127
128 List<Object[]> rows(String statement, String wiki, String bindName, Object bindValue, int limit)
129 throws QueryException;
130
131 List<Object[]> rows(String statement, String wiki, Map<String, Object> bindValues, int limit)
132 throws QueryException;
133
134 List<Object[]> hierarchyRows(String baseStatement, boolean showHidden, boolean hasSpaceAlias,
135 String wiki, String bindName, Object bindValue, int limit) throws QueryException;
136
137 DocumentReference resolveInto(String fullName, WikiReference wiki);
138 boolean isAuthorized(DocumentReference reference);
139 DocumentReference authorizedDocument(String fullName, WikiReference wiki);
140
141 record Contains(String text) { }
142 }
143 {{/code}}
144
145 |=Member |=Returns |=What it does
146 |##rows(statement, wiki, bindName, bindValue, limit)## |##List<Object[]>## |Runs one HQL statement against one wiki with an optional named bind
147 |##rows(statement, wiki, bindValues, limit)## |##List<Object[]>## |The same with any number of named binds; a ##Contains## value is escaped by the platform
148 |##hierarchyRows(base, showHidden, hasSpaceAlias, wiki, bindName, bindValue, limit)## |##List<Object[]>## |Composes the hidden-page clauses and a deterministic ##order by doc.fullName##
149 |##resolveInto(fullName, wiki)## |##DocumentReference## |Resolves a row explicitly into the wiki it was fetched from
150 |##isAuthorized(reference)## |##boolean## |Applies the space filter, then the view right, in that order
151 |##authorizedDocument(fullName, wiki)## |##DocumentReference## |The resolved reference when authorized, otherwise ##null##
152 |##Contains(text)## |bind value |Marks a raw substring bind, escaped at execution time
153
154 Every fetch limit is clamped into ##1..MAX_FETCH_PER_QUERY##, so a broad statement cannot materialize an unbounded row set. Hidden-page exclusion is explicit HQL rather than a query filter, so it holds regardless of the calling user's own profile preference. A single-column statement yields the scalar itself in each row element at runtime, despite the declared ##List<Object[]>## type.

Get Connected