drupal-mcp-connector 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/drupal-graphql-introspect.md +2 -2
- package/.claude/commands/drupal-graphql.md +4 -2
- package/CHANGELOG.md +34 -0
- package/README.md +41 -16
- package/package.json +8 -4
- package/src/lib/security.js +46 -6
- package/src/tools/graphql.js +17 -5
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type."
|
|
2
|
+
description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type. Requires security.allowGraphql (off outside the development preset by default)."
|
|
3
3
|
argument-hint: "[site] [typeName]"
|
|
4
4
|
allowed-tools: mcp__drupal__drupal_graphql_introspect
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
Call the `mcp__drupal__drupal_graphql_introspect` MCP tool.
|
|
8
8
|
|
|
9
|
-
Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type.
|
|
9
|
+
Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type. Requires security.allowGraphql (off outside the development preset by default).
|
|
10
10
|
|
|
11
11
|
Parse the request in `$ARGUMENTS` into this tool's parameters:
|
|
12
12
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Execute a GraphQL query against a Drupal site. Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which exposes a read-only schema
|
|
2
|
+
description: "Execute a GraphQL query against a Drupal site. Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which exposes a read-only schema. GraphQL is off unless security.allowGraphql is true (development preset only by default) because raw results bypass entity allowlists and field redaction. Mutations also require allowGraphqlMutations. Use drupal_graphql_introspect first to discover available types and fields. Example query: query GetArticle($id: String!) { nodeById(id: $id) { title ... on NodeArticle { body { value } } } } Example mutation (only if your GraphQL Compose schema enables mutations): mutation CreateArticle($title: String!, $body: String!) { createNodeArticle(data: { title: $title, body: { value: $body, format: \"full_html\" } }) { entity { title uuid } errors { message } } }"
|
|
3
3
|
argument-hint: "<query> [site] [variables] [operationName]"
|
|
4
4
|
allowed-tools: mcp__drupal__drupal_graphql
|
|
5
5
|
---
|
|
@@ -8,7 +8,9 @@ Call the `mcp__drupal__drupal_graphql` MCP tool.
|
|
|
8
8
|
|
|
9
9
|
Execute a GraphQL query against a Drupal site.
|
|
10
10
|
Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which
|
|
11
|
-
exposes a read-only schema
|
|
11
|
+
exposes a read-only schema. GraphQL is off unless security.allowGraphql is true
|
|
12
|
+
(development preset only by default) because raw results bypass entity allowlists
|
|
13
|
+
and field redaction. Mutations also require allowGraphqlMutations.
|
|
12
14
|
Use drupal_graphql_introspect first to discover available types and fields.
|
|
13
15
|
|
|
14
16
|
Example query:
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.2.0] - 2026-07-30
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- **Default security preset is `production-strict` (#140).** Omitting `security`
|
|
14
|
+
or passing `{}` no longer opens the site under the `development` preset.
|
|
15
|
+
Local and integration work must set `"preset": "development"` (or another
|
|
16
|
+
explicit preset) deliberately. Migration: add
|
|
17
|
+
`"security": { "preset": "development" }` to any site config that relied on
|
|
18
|
+
the old open default.
|
|
19
|
+
- **GraphQL tools fail closed (#142).** `drupal_graphql` and
|
|
20
|
+
`drupal_graphql_introspect` require `security.allowGraphql` (true only on the
|
|
21
|
+
`development` preset). Mutations still need `allowGraphqlMutations` as well.
|
|
22
|
+
When GraphQL is opted in, results remain raw (no entity allowlist/redaction
|
|
23
|
+
on that path); prefer JSON:API entity tools for policy-bound reads.
|
|
24
|
+
- **Dependency advisories cleared (#128).** Bump
|
|
25
|
+
`@modelcontextprotocol/sdk` to `^1.30.0` and pin overrides for
|
|
26
|
+
`@hono/node-server` `^2.0.5` and `postcss` `^8.5.18` so `npm audit` reports
|
|
27
|
+
zero vulnerabilities.
|
|
28
|
+
|
|
29
|
+
### Documentation
|
|
30
|
+
- README, security guide, threat model, architecture, tools reference, and
|
|
31
|
+
SECURITY.md updated for the secure default, GraphQL gate, and residual-risk
|
|
32
|
+
table.
|
|
33
|
+
|
|
34
|
+
## [2.1.1] - 2026-07-31
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
- **npm package page / README.** Security model table lists all six presets and
|
|
38
|
+
the 2.1 connector-side gates (entity policy on specialized tools, publish/draft
|
|
39
|
+
defaults, upload roots, fail-closed HTTPS). Quick start leads with `npm install`
|
|
40
|
+
for package consumers; clone path remains for development.
|
|
41
|
+
- Normalize `package.json` `repository.url` to the form npm expects
|
|
42
|
+
(`git+https://…`).
|
|
43
|
+
|
|
10
44
|
## [2.1.0] - 2026-07-31
|
|
11
45
|
|
|
12
46
|
### Security
|
package/README.md
CHANGED
|
@@ -125,7 +125,11 @@ Other MCP agents don't need this step — they get the same coverage from the pe
|
|
|
125
125
|
prompts above.
|
|
126
126
|
|
|
127
127
|
### Security Model
|
|
128
|
-
|
|
128
|
+
|
|
129
|
+
Defense-in-depth presets, enforced connector-side and complemented by Drupal-side
|
|
130
|
+
governance (MCP Sentinel). **Default when `security` is omitted:**
|
|
131
|
+
`production-strict` (read-only, sensitive types denied). Local development must
|
|
132
|
+
set `"preset": "development"` explicitly.
|
|
129
133
|
|
|
130
134
|
```json
|
|
131
135
|
"security": { "preset": "auditor" }
|
|
@@ -133,12 +137,22 @@ Defense-in-depth with four one-line presets, enforced connector-side and complem
|
|
|
133
137
|
|
|
134
138
|
| Preset | What it does |
|
|
135
139
|
|--------|-------------|
|
|
136
|
-
| `development` | Everything allowed — local development only |
|
|
137
|
-
| `content-editor` | Create/edit
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
140
|
+
| `development` | Everything allowed, including GraphQL — local development only |
|
|
141
|
+
| `content-editor` | Create/edit content + structural entities; no deletes; no publishing; secrets/governance/account types denied |
|
|
142
|
+
| `config-editor` | content-editor + site-building config read + governed config write (developer tier) |
|
|
143
|
+
| `auditor` | Read-only; secrets/governance/account types denied; user PII redacted when user is allowed |
|
|
144
|
+
| `production-strict` | Read-only; same sensitive denylist as auditor; broad PII field redaction (**default**) |
|
|
145
|
+
| `write-plane` | Create/update content set for agents; no deletes; no GraphQL; publish off by default |
|
|
146
|
+
|
|
147
|
+
Additional connector-side gates (2.1+ / 2.2+):
|
|
148
|
+
|
|
149
|
+
- **Entity allowlists** apply to specialized tools (`drupal_*_node`, media, taxonomy), not only `drupal_entity_*`.
|
|
150
|
+
- **Publish gate:** `status: true` and `moderation_state: "published"` require `allowPublish`. Media create defaults **unpublished**. Published moderated node updates without a moderation state default to **draft** (forward revision).
|
|
151
|
+
- **Uploads** only from `MCP_UPLOAD_ROOT` (or the process cwd); sensitive paths (`.env*`, `.ssh`, connector `config.json`) are refused.
|
|
152
|
+
- **HTTPS:** non-loopback binds require `MCP_AUTH_TOKEN` (or `MCP_ALLOW_UNAUTHENTICATED=1` behind a trusted proxy); non-loopback TLS defaults to 120 req/min rate limiting.
|
|
153
|
+
- **GraphQL is off by default.** `drupal_graphql` / introspect require `security.allowGraphql` (true only on the `development` preset). Raw GraphQL results still bypass entity allowlists and field redaction — prefer JSON:API entity tools when connector policy must hold. Mutations also need `allowGraphqlMutations`.
|
|
140
154
|
|
|
141
|
-
|
|
155
|
+
Full detail: **[docs/security.md](docs/security.md)** and **[docs/security-hardening.md](docs/security-hardening.md)**.
|
|
142
156
|
|
|
143
157
|
---
|
|
144
158
|
|
|
@@ -154,30 +168,38 @@ Presets layer with entity allow/deny lists, per-bundle operation rules, and fiel
|
|
|
154
168
|
|
|
155
169
|
## Quick Start
|
|
156
170
|
|
|
171
|
+
### From npm (recommended for operators)
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
npm install -g drupal-mcp-connector
|
|
175
|
+
# or: npx -y drupal-mcp-connector
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Point your MCP client at the installed binary (path from `which drupal-mcp-connector`
|
|
179
|
+
or `node_modules/.bin/drupal-mcp-connector`). For multi-site or non-env config,
|
|
180
|
+
clone the repo (or copy `config/config.example.json` beside a small launcher) and
|
|
181
|
+
pass a config path — see **[docs/getting-started.md](docs/getting-started.md)**.
|
|
182
|
+
|
|
183
|
+
### From a git clone (development)
|
|
184
|
+
|
|
157
185
|
```bash
|
|
158
|
-
# 1. Clone and install
|
|
159
186
|
git clone https://github.com/Wilkes-Liberty/drupal-mcp-connector
|
|
160
187
|
cd drupal-mcp-connector
|
|
161
188
|
npm install
|
|
162
|
-
|
|
163
|
-
# 2. Configure
|
|
164
189
|
cp config/config.example.json config/config.json
|
|
165
|
-
# Edit config/config.json —
|
|
166
|
-
|
|
167
|
-
# 3. Run (stdio transport)
|
|
190
|
+
# Edit config/config.json — baseUrl, api backend, auth, security preset
|
|
168
191
|
node src/index.js
|
|
169
192
|
```
|
|
170
193
|
|
|
171
194
|
### Register with an MCP client
|
|
172
195
|
|
|
173
|
-
Most desktop and CLI MCP clients launch the connector over **stdio
|
|
196
|
+
Most desktop and CLI MCP clients launch the connector over **stdio**:
|
|
174
197
|
|
|
175
198
|
```json
|
|
176
199
|
{
|
|
177
200
|
"mcpServers": {
|
|
178
201
|
"drupal": {
|
|
179
|
-
"command": "
|
|
180
|
-
"args": ["/absolute/path/to/drupal-mcp-connector/src/index.js"],
|
|
202
|
+
"command": "drupal-mcp-connector",
|
|
181
203
|
"env": {
|
|
182
204
|
"DRUPAL_BASE_URL": "https://mysite.com",
|
|
183
205
|
"DRUPAL_API_TOKEN": "your-token-here"
|
|
@@ -187,7 +209,10 @@ Most desktop and CLI MCP clients launch the connector over **stdio**. Add an ent
|
|
|
187
209
|
}
|
|
188
210
|
```
|
|
189
211
|
|
|
190
|
-
|
|
212
|
+
Or with an absolute path to `src/index.js` / the installed package. For multi-client
|
|
213
|
+
or remote use, run the HTTPS transport and register the endpoint instead — see
|
|
214
|
+
**[docs/getting-started.md](docs/getting-started.md)** and
|
|
215
|
+
**[docs/mcp-clients.md](docs/mcp-clients.md)**.
|
|
191
216
|
|
|
192
217
|
---
|
|
193
218
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "A secure, multi-site Model Context Protocol (MCP) connector for Drupal — dual-protocol JSON:API and GraphQL.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"homepage": "https://github.com/Wilkes-Liberty/drupal-mcp-connector",
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/Wilkes-Liberty/drupal-mcp-connector.git"
|
|
34
|
+
"url": "git+https://github.com/Wilkes-Liberty/drupal-mcp-connector.git"
|
|
35
35
|
},
|
|
36
36
|
"bugs": {
|
|
37
37
|
"url": "https://github.com/Wilkes-Liberty/drupal-mcp-connector/issues"
|
|
@@ -58,16 +58,20 @@
|
|
|
58
58
|
"syntax-check": "for f in src/lib/*.js src/tools/*.js src/index.js; do node --input-type=module --check < $f && echo \"$f ✓\"; done"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
62
62
|
"graphql": "^17.0.0",
|
|
63
63
|
"node-fetch": "^3.3.2",
|
|
64
64
|
"ssh2": "^1.16.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"eslint": "^10.4.1",
|
|
68
|
-
"eslint-plugin-security": "^4.0.0",
|
|
69
68
|
"eslint-plugin-n": "^18.1.0",
|
|
69
|
+
"eslint-plugin-security": "^4.0.0",
|
|
70
70
|
"globals": "^17.6.0",
|
|
71
71
|
"vitest": "^4.1.8"
|
|
72
|
+
},
|
|
73
|
+
"overrides": {
|
|
74
|
+
"@hono/node-server": "^2.0.5",
|
|
75
|
+
"postcss": "^8.5.18"
|
|
72
76
|
}
|
|
73
77
|
}
|
package/src/lib/security.js
CHANGED
|
@@ -12,13 +12,14 @@ import { parse } from "graphql";
|
|
|
12
12
|
*
|
|
13
13
|
* ─── Quick presets ────────────────────────────────────────────────────────
|
|
14
14
|
*
|
|
15
|
-
* "preset": "development" Everything allowed.
|
|
15
|
+
* "preset": "development" Everything allowed. Opt-in only — set explicitly.
|
|
16
16
|
* "preset": "content-editor" Create/edit content (nodes, media, terms, paragraphs, blocks,
|
|
17
17
|
* menu links, redirects, aliases, files). No deletes. Config read-only.
|
|
18
18
|
* "preset": "config-editor" content-editor + site-building config READ + governed config
|
|
19
19
|
* read/write (Developer tier). Model changes go via the config bridge.
|
|
20
|
-
* "preset": "auditor" Read-only.
|
|
21
|
-
* "preset": "production-strict" Read-only.
|
|
20
|
+
* "preset": "auditor" Read-only. Sensitive entity types denied. User fields redacted.
|
|
21
|
+
* "preset": "production-strict" Read-only. Sensitive types denied. Redacts PII. **Default** when
|
|
22
|
+
* security is omitted or has no preset (#140).
|
|
22
23
|
* "preset": "write-plane" Governed writes (no delete/mutations) on the content set
|
|
23
24
|
* (node, term, media + structural content entities).
|
|
24
25
|
*
|
|
@@ -31,7 +32,8 @@ import { parse } from "graphql";
|
|
|
31
32
|
*
|
|
32
33
|
* readOnly true → reject all create/update/delete/graphql-mutation calls
|
|
33
34
|
* allowDestructive false → reject all delete operations
|
|
34
|
-
* allowPublish false → reject a write carrying status:true
|
|
35
|
+
* allowPublish false → reject a write carrying status:true / moderation_state:published
|
|
36
|
+
* allowGraphql false → reject drupal_graphql + introspect (raw GraphQL is policy-free, #142)
|
|
35
37
|
* allowGraphqlMutations false → reject drupal_graphql when mutation is detected
|
|
36
38
|
* allowConfigRead false → reject drupal_config_get / drupal_config_list
|
|
37
39
|
* allowConfigWrite false → reject drupal_config_set
|
|
@@ -118,6 +120,7 @@ const PRESETS = {
|
|
|
118
120
|
readOnly: false,
|
|
119
121
|
allowDestructive: true,
|
|
120
122
|
allowPublish: true, // mirrors allowDestructive: everything allowed
|
|
123
|
+
allowGraphql: true, // raw GraphQL allowed only in open mode (#142)
|
|
121
124
|
allowGraphqlMutations: true,
|
|
122
125
|
allowConfigRead: true,
|
|
123
126
|
allowConfigWrite: true,
|
|
@@ -130,6 +133,7 @@ const PRESETS = {
|
|
|
130
133
|
"content-editor": {
|
|
131
134
|
readOnly: false,
|
|
132
135
|
allowDestructive: false, // no deletes
|
|
136
|
+
allowGraphql: false, // freeform GraphQL bypasses entity denylists (#142)
|
|
133
137
|
allowGraphqlMutations: false,
|
|
134
138
|
allowConfigRead: true, // config read-only
|
|
135
139
|
allowConfigWrite: false,
|
|
@@ -151,6 +155,7 @@ const PRESETS = {
|
|
|
151
155
|
// The Drupal-side governance layer remains authoritative; this is defence in depth.
|
|
152
156
|
readOnly: false,
|
|
153
157
|
allowDestructive: false, // no deletes
|
|
158
|
+
allowGraphql: false,
|
|
154
159
|
allowGraphqlMutations: false,
|
|
155
160
|
allowConfigRead: true,
|
|
156
161
|
allowConfigWrite: true, // governed config writes via drupal_config_set
|
|
@@ -171,6 +176,7 @@ const PRESETS = {
|
|
|
171
176
|
auditor: {
|
|
172
177
|
readOnly: true,
|
|
173
178
|
allowDestructive: false,
|
|
179
|
+
allowGraphql: false,
|
|
174
180
|
allowGraphqlMutations: false,
|
|
175
181
|
allowConfigRead: true, // read-only inspection of config
|
|
176
182
|
allowConfigWrite: false,
|
|
@@ -189,6 +195,7 @@ const PRESETS = {
|
|
|
189
195
|
"production-strict": {
|
|
190
196
|
readOnly: true,
|
|
191
197
|
allowDestructive: false,
|
|
198
|
+
allowGraphql: false,
|
|
192
199
|
allowGraphqlMutations: false,
|
|
193
200
|
allowConfigRead: false, // nothing implicit; opt in per site
|
|
194
201
|
allowConfigWrite: false,
|
|
@@ -203,6 +210,7 @@ const PRESETS = {
|
|
|
203
210
|
// Drupal-side governance layer remains authoritative; this is defence in depth.
|
|
204
211
|
readOnly: false,
|
|
205
212
|
allowDestructive: false, // no deletes
|
|
213
|
+
allowGraphql: false, // JSON:API write plane; GraphQL is policy-free (#142)
|
|
206
214
|
allowGraphqlMutations: false, // writes go through the JSON:API plane
|
|
207
215
|
allowConfigRead: true, // config read-only
|
|
208
216
|
allowConfigWrite: false,
|
|
@@ -225,15 +233,22 @@ const PRESETS = {
|
|
|
225
233
|
* @param {object} site Site config (reads site.security).
|
|
226
234
|
* @returns {object} Effective security config used by the assert/redact helpers.
|
|
227
235
|
*/
|
|
236
|
+
/** Default when `security.preset` is omitted — least privilege, not open mode (#140). */
|
|
237
|
+
export const DEFAULT_SECURITY_PRESET = "production-strict";
|
|
238
|
+
|
|
228
239
|
export function resolveSecurityConfig(site) {
|
|
229
240
|
const raw = site.security ?? {};
|
|
230
|
-
|
|
241
|
+
// Explicit preset wins; bare {} / missing security → production-strict (#140).
|
|
242
|
+
// Open mode requires `preset: "development"` (or another named preset).
|
|
243
|
+
const presetName = raw.preset ?? DEFAULT_SECURITY_PRESET;
|
|
244
|
+
const preset = PRESETS[presetName] ?? PRESETS[DEFAULT_SECURITY_PRESET];
|
|
231
245
|
|
|
232
246
|
// Merge: explicit keys in site.security override the preset
|
|
233
247
|
return {
|
|
234
248
|
readOnly: raw.readOnly ?? preset.readOnly,
|
|
235
249
|
allowDestructive: raw.allowDestructive ?? preset.allowDestructive,
|
|
236
250
|
allowPublish: raw.allowPublish ?? preset.allowPublish ?? false,
|
|
251
|
+
allowGraphql: raw.allowGraphql ?? preset.allowGraphql ?? false,
|
|
237
252
|
allowGraphqlMutations: raw.allowGraphqlMutations ?? preset.allowGraphqlMutations,
|
|
238
253
|
allowConfigRead: raw.allowConfigRead ?? preset.allowConfigRead ?? false,
|
|
239
254
|
allowConfigWrite: raw.allowConfigWrite ?? preset.allowConfigWrite ?? false,
|
|
@@ -450,7 +465,30 @@ function graphqlHasMutation(query) {
|
|
|
450
465
|
* @returns {void} No-op for read-only (query) documents.
|
|
451
466
|
* @throws {SecurityError} if the document is a mutation and writes/mutations are disabled.
|
|
452
467
|
*/
|
|
468
|
+
/**
|
|
469
|
+
* Gate GraphQL queries and schema introspection. Freeform GraphQL returns raw
|
|
470
|
+
* data that does not pass through entity allowlists or field redaction (#142),
|
|
471
|
+
* so it is off by default outside the development preset. Opt in with
|
|
472
|
+
* `security.allowGraphql: true`.
|
|
473
|
+
*
|
|
474
|
+
* @param {object} secConfig Resolved security config.
|
|
475
|
+
* @param {string} [operationLabel] Label for the error message.
|
|
476
|
+
* @returns {void}
|
|
477
|
+
* @throws {SecurityError} if GraphQL is disabled for this site.
|
|
478
|
+
*/
|
|
479
|
+
export function assertGraphqlAllowed(secConfig, operationLabel = "graphql") {
|
|
480
|
+
if (secConfig.allowGraphql) return;
|
|
481
|
+
throw new SecurityError(
|
|
482
|
+
"GraphQL tools are disabled for this site (allowGraphql: false). " +
|
|
483
|
+
"Raw GraphQL responses bypass connector entity allowlists and field redaction. " +
|
|
484
|
+
`Blocked: ${operationLabel}. ` +
|
|
485
|
+
"Set security.allowGraphql = true to enable (prefer development preset or an explicit opt-in)."
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
453
489
|
export function assertGraphqlMutationAllowed(secConfig, query) {
|
|
490
|
+
// Queries still require allowGraphql (#142); mutations require both flags.
|
|
491
|
+
assertGraphqlAllowed(secConfig, "graphql mutation");
|
|
454
492
|
const isMutation = graphqlHasMutation(query);
|
|
455
493
|
if (!isMutation) return;
|
|
456
494
|
|
|
@@ -681,9 +719,11 @@ export function getSecuritySummary(site) {
|
|
|
681
719
|
const cfg = resolveSecurityConfig(site);
|
|
682
720
|
return {
|
|
683
721
|
site: site._name,
|
|
684
|
-
preset: site.security?.preset ??
|
|
722
|
+
preset: site.security?.preset ?? `${DEFAULT_SECURITY_PRESET} (default)`,
|
|
685
723
|
readOnly: cfg.readOnly,
|
|
686
724
|
allowDestructive: cfg.allowDestructive,
|
|
725
|
+
allowPublish: cfg.allowPublish,
|
|
726
|
+
allowGraphql: cfg.allowGraphql,
|
|
687
727
|
allowGraphqlMutations: cfg.allowGraphqlMutations,
|
|
688
728
|
allowConfigRead: cfg.allowConfigRead,
|
|
689
729
|
allowConfigWrite: cfg.allowConfigWrite,
|
package/src/tools/graphql.js
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Raw GraphQL execution and schema introspection against a Drupal site. Require
|
|
5
5
|
* the GraphQL Compose module (drupal.org/project/graphql_compose), which
|
|
6
|
-
* exposes a read-only schema (no mutations)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* exposes a read-only schema (no mutations).
|
|
7
|
+
*
|
|
8
|
+
* Policy: responses are raw GraphQL data — they do **not** pass connector entity
|
|
9
|
+
* allowlists or field redaction (#142). Tools are gated by `allowGraphql`
|
|
10
|
+
* (false outside the development preset unless opted in). Mutations also require
|
|
11
|
+
* `allowGraphqlMutations` (enforced in the handler and by index.js middleware).
|
|
9
12
|
*
|
|
10
13
|
* Per-site config: set "graphqlEndpoint" to override "/graphql".
|
|
11
14
|
* Auth reuses the same credentials as the JSON:API tools.
|
|
@@ -13,6 +16,9 @@
|
|
|
13
16
|
|
|
14
17
|
import { getSiteConfig } from "../lib/config.js";
|
|
15
18
|
import { drupalGraphqlFetch } from "../lib/drupal-fetch.js";
|
|
19
|
+
import {
|
|
20
|
+
resolveSecurityConfig, assertGraphqlAllowed, assertGraphqlMutationAllowed,
|
|
21
|
+
} from "../lib/security.js";
|
|
16
22
|
|
|
17
23
|
// ---------------------------------------------------------------------------
|
|
18
24
|
// Implementations
|
|
@@ -29,6 +35,9 @@ import { drupalGraphqlFetch } from "../lib/drupal-fetch.js";
|
|
|
29
35
|
*/
|
|
30
36
|
async function runGraphql({ site: siteName, query, variables = {}, operationName }) {
|
|
31
37
|
const site = getSiteConfig(siteName);
|
|
38
|
+
const sec = resolveSecurityConfig(site);
|
|
39
|
+
assertGraphqlAllowed(sec, "drupal_graphql");
|
|
40
|
+
assertGraphqlMutationAllowed(sec, query);
|
|
32
41
|
const json = await drupalGraphqlFetch(site, { query, variables, operationName });
|
|
33
42
|
|
|
34
43
|
if (json.errors?.length) {
|
|
@@ -54,6 +63,7 @@ async function runGraphql({ site: siteName, query, variables = {}, operationName
|
|
|
54
63
|
*/
|
|
55
64
|
async function introspectGraphql({ site: siteName, typeName }) {
|
|
56
65
|
const site = getSiteConfig(siteName);
|
|
66
|
+
assertGraphqlAllowed(resolveSecurityConfig(site), "drupal_graphql_introspect");
|
|
57
67
|
|
|
58
68
|
// If a specific type is requested, get detailed field info for it.
|
|
59
69
|
if (typeName) {
|
|
@@ -128,7 +138,9 @@ export const definitions = [
|
|
|
128
138
|
name: "drupal_graphql",
|
|
129
139
|
description: `Execute a GraphQL query against a Drupal site.
|
|
130
140
|
Requires the GraphQL Compose module (drupal.org/project/graphql_compose), which
|
|
131
|
-
exposes a read-only schema
|
|
141
|
+
exposes a read-only schema. GraphQL is off unless security.allowGraphql is true
|
|
142
|
+
(development preset only by default) because raw results bypass entity allowlists
|
|
143
|
+
and field redaction. Mutations also require allowGraphqlMutations.
|
|
132
144
|
Use drupal_graphql_introspect first to discover available types and fields.
|
|
133
145
|
|
|
134
146
|
Example query:
|
|
@@ -158,7 +170,7 @@ Example mutation (only if your GraphQL Compose schema enables mutations):
|
|
|
158
170
|
},
|
|
159
171
|
{
|
|
160
172
|
name: "drupal_graphql_introspect",
|
|
161
|
-
description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type.",
|
|
173
|
+
description: "Introspect the Drupal GraphQL schema. Omit typeName for a full schema overview; provide typeName to get detailed fields and args for a specific type. Requires security.allowGraphql (off outside the development preset by default).",
|
|
162
174
|
inputSchema: {
|
|
163
175
|
type: "object",
|
|
164
176
|
properties: {
|