Wiki source code of Access Doors
Last modified by Vincent Massol on 2026/09/04 20:04
Show last authors
| author | version | line-number | content |
|---|---|---|---|
| 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 {{code}}application-ai-llm-mcp-api{{/code}} module (package {{code}}org.xwiki.contrib.llm.mcp{{/code}}), so a tool contributed by any module can inject them: {{code}}MCPDocumentAccess{{/code}}, {{code}}MCPWikiReach{{/code}} and {{code}}MCPDocumentSearch{{/code}}. Two are internal to the server module (package {{code}}org.xwiki.contrib.llm.mcp.internal.access{{/code}}): {{code}}MCPSpaceFilter{{/code}}, which the public doors apply on the caller's behalf, and {{code}}MCPRowQuery{{/code}}. | ||
| 4 | |||
| 5 | A tool must never resolve a {{code}}DocumentReference{{/code}} 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 | {{/code}} | ||
| 18 | |||
| 19 | Applies in order: reach gate → {{code}}ContextualAuthorizationManager.hasAccess{{/code}} → space filter. Throws {{code}}MCPAccessDeniedException{{/code}} with an agent-facing refusal message. | ||
| 20 | |||
| 21 | {{plantuml}} | ||
| 22 | @startuml | ||
| 23 | !theme bluegray | ||
| 24 | start | ||
| 25 | :a tool calls resolveAndAuthorize\nwith the reference it was given; | ||
| 26 | :resolve it against the current context; | ||
| 27 | if (the target is in another wiki?) then (yes) | ||
| 28 | if (this endpoint may reach that wiki?) then (no) | ||
| 29 | :refused: cross-wiki reach\nis not enabled for this endpoint; | ||
| 30 | stop | ||
| 31 | else (yes) | ||
| 32 | endif | ||
| 33 | else (no) | ||
| 34 | endif | ||
| 35 | if (the user has the required right\non the document?) then (no) | ||
| 36 | :refused: you do not have\npermission to view or edit it; | ||
| 37 | stop | ||
| 38 | else (yes) | ||
| 39 | endif | ||
| 40 | if (the space filter allows it?) then (no) | ||
| 41 | :refused: outside the content\nthis endpoint exposes; | ||
| 42 | stop | ||
| 43 | else (yes) | ||
| 44 | endif | ||
| 45 | :the tool receives the DocumentReference; | ||
| 46 | stop | ||
| 47 | @enduml | ||
| 48 | {{/plantuml}} | ||
| 49 | |||
| 50 | == MCPSpaceFilter == | ||
| 51 | |||
| 52 | 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. | ||
| 53 | |||
| 54 | {{code language="java"}} | ||
| 55 | @Role | ||
| 56 | public interface MCPSpaceFilter { | ||
| 57 | boolean isAllowed(DocumentReference reference); | ||
| 58 | List<String> filterQueries(); | ||
| 59 | } | ||
| 60 | {{/code}} | ||
| 61 | |||
| 62 | |=Method |=Returns | ||
| 63 | |{{code}}isAllowed(reference){{/code}} |Whether that one document passes the configured filter mode and entries | ||
| 64 | |{{code}}filterQueries(){{/code}} |The Solr filter query clauses that scope search results to the same set | ||
| 65 | |||
| 66 | A space entry covers the space and its whole subtree; a document entry matches exactly one document. Both fail //closed// on read errors: {{code}}isAllowed{{/code}} denies, and {{code}}filterQueries{{/code}} returns a match-nothing {{code}}-*:*{{/code}} clause. | ||
| 67 | |||
| 68 | == MCPWikiReach == | ||
| 69 | |||
| 70 | Decides how far an endpoint reaches beyond its own wiki. | ||
| 71 | |||
| 72 | {{code language="java"}} | ||
| 73 | @Role | ||
| 74 | public interface MCPWikiReach { | ||
| 75 | boolean isReachEnabled(); | ||
| 76 | boolean canReachWiki(String wikiId); | ||
| 77 | List<String> resolveSearchWikis(String wikiParam); | ||
| 78 | } | ||
| 79 | {{/code}} | ||
| 80 | |||
| 81 | |=Method |=Returns | ||
| 82 | |{{code}}isReachEnabled(){{/code}} |Whether this endpoint may cross wikis at all | ||
| 83 | |{{code}}canReachWiki(wikiId){{/code}} |Whether that one target wiki is reachable | ||
| 84 | |{{code}}resolveSearchWikis(wikiParam){{/code}} |The wikis a search covers, from the tool's {{code}}wiki{{/code}} parameter: blank → the current wiki, {{code}}"all"{{/code}} → the whole farm, an id → that single wiki, validated | ||
| 85 | |||
| 86 | Backed by the main-wiki-only {{code}}reachEnabledWikis{{/code}} list. A reach-enabled endpoint reaches every wiki in the farm regardless of the target's own enable gate, tool toggles, or space filter. | ||
| 87 | |||
| 88 | == MCPDocumentSearch == | ||
| 89 | |||
| 90 | Builds a Solr search query scoped to wikis, space filter, and rights. | ||
| 91 | |||
| 92 | {{code language="java"}} | ||
| 93 | @Role | ||
| 94 | public interface MCPDocumentSearch { | ||
| 95 | Query createQuery(String statement, List<String> additionalFilterQueries, | ||
| 96 | List<String> targetWikiIds); | ||
| 97 | } | ||
| 98 | {{/code}} | ||
| 99 | |||
| 100 | Uses {{code}}SecureQuery{{/code}} with {{code}}checkCurrentUser(true){{/code}}, wiki-scope clauses, and the space filter — the single query path for {{code}}query_documents{{/code}}. | ||
| 101 | |||
| 102 | == MCPAccessDeniedException == | ||
| 103 | |||
| 104 | {{code language="java"}} | ||
| 105 | public class MCPAccessDeniedException extends Exception { | ||
| 106 | public MCPAccessDeniedException(String message) { ... } | ||
| 107 | } | ||
| 108 | {{/code}} | ||
| 109 | |||
| 110 | Carries the agent-facing refusal message. Thrown by {{code}}MCPDocumentAccess.resolveAndAuthorize(){{/code}} and caught by the tool to produce an {{code}}isError{{/code}} result. | ||
| 111 | |||
| 112 | == MCPRowQuery == | ||
| 113 | |||
| 114 | 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. | ||
| 115 | |||
| 116 | * Every fetch limit is clamped to at most {{code}}MAX_FETCH_PER_QUERY{{/code}} (2000 rows), so a broad statement cannot materialize an unbounded row set. | ||
| 117 | * {{code}}resolveInto{{/code}} resolves a row explicitly into the target wiki, and {{code}}isAuthorized{{/code}} applies the space filter and then the view right, in that order. | ||
| 118 | * Hidden-page exclusion is explicit HQL rather than a query filter, so it holds regardless of the calling user's own profile preference. |