@anchrd/intel-api 0.17.0 → 0.19.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/README.md +33 -4
- package/dist/intel/intel.js +52 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -263,6 +263,21 @@ grants them.
|
|
|
263
263
|
|
|
264
264
|
A missing capability answers **`403`**, and the screen says a permission is missing.
|
|
265
265
|
|
|
266
|
+
⚠️ **`mcp.connect` is the one that gates a whole endpoint rather than an action.** Intel's own MCP
|
|
267
|
+
endpoint (`POST /mcp`, the one an MCP client or a portal connects to) checks it **on every request**
|
|
268
|
+
and answers `403` without it — before any tool runs and before anything is read. Two things follow
|
|
269
|
+
for an operator:
|
|
270
|
+
|
|
271
|
+
- **Somebody who should use Intel through an MCP client needs `mcp.connect` in Intel**, beside their
|
|
272
|
+
`nodes.*` capabilities. Without it the client authenticates successfully and every call is refused.
|
|
273
|
+
- **Revoking it takes effect on the next call**, with no new sign-in and nothing to press. If a
|
|
274
|
+
Cloudflare Access policy in front of the endpoint also compares the `mcp` claim, that layer is the
|
|
275
|
+
slower of the two — the claim is written once per sign-in and a running session carries it for up
|
|
276
|
+
to 24 hours, while this check reads the live grant rows every time.
|
|
277
|
+
|
|
278
|
+
The web UI is unaffected: it speaks the HTTP surface, which is gated by `nodes.*`, `flows.*` and
|
|
279
|
+
`tools.*` as before.
|
|
280
|
+
|
|
266
281
|
### Layer two: the grant on the node
|
|
267
282
|
|
|
268
283
|
What somebody may reach *in this tree*, granted in Intel on a node. Four verbs — `read`, `write`,
|
|
@@ -348,10 +363,24 @@ both, every silent sign-in is refused and the Tools area shows "No access to the
|
|
|
348
363
|
correct behaviour for somebody outside every policy, and a misleading one for a deployment that
|
|
349
364
|
simply never enabled the setting.
|
|
350
365
|
|
|
351
|
-
⚠️ **
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
366
|
+
⚠️ **Somebody who carries the Gate capability `mcp.connect` at no service at all is not sent into
|
|
367
|
+
that sign-in.** Gate answers that question service-wide, Intel asks it before the first redirect,
|
|
368
|
+
and the person stays here reading a sentence that says whom to ask — rather than landing on the
|
|
369
|
+
portal's own error page, outside Intel, with no way back but the browser's back button
|
|
370
|
+
(anchrd/intel#434). Grant `mcp.connect` alongside the Intel capabilities to everyone who should
|
|
371
|
+
reach Tools.
|
|
372
|
+
|
|
373
|
+
Two things that guard deliberately does not do. It **names no server**: the answer is one bit, so
|
|
374
|
+
somebody who may reach a different product's MCP server walks on and sees what the portal offers
|
|
375
|
+
them. And it **promises nothing** — this is the part worth knowing before you grant: the portal
|
|
376
|
+
decides on the `mcp` claim frozen into the ID token, and a running Cloudflare Access session carries
|
|
377
|
+
the old claim for up to 24 hours. ⚠️ **Inside that window the old dead end is still open.** Somebody
|
|
378
|
+
who was granted `mcp.connect` a minute ago is not refused here, walks on, and Access then decides on
|
|
379
|
+
the claim state that produced the error page in the first place. The guard closes the trip that is
|
|
380
|
+
certainly pointless; it does not close this one. The way out is the same as before — sign out of
|
|
381
|
+
Access and in again, so a new ID token carries the new claim. (Not measured against a running
|
|
382
|
+
installation; the claim lifetime is the documented one.) Against a Gate too old to answer the
|
|
383
|
+
question at all, nobody is refused here.
|
|
355
384
|
|
|
356
385
|
The Tools screen distinguishes four situations rather than calling all of them "no access": a
|
|
357
386
|
refusal that really came back, an expired connection, a sign-in that broke, and a sign-in that was
|
package/dist/intel/intel.js
CHANGED
|
@@ -50,7 +50,32 @@ export function createIntel(deps) {
|
|
|
50
50
|
if (!authorization) {
|
|
51
51
|
return context.redirect(`/auth/login?returnTo=${connectReturnTo}`);
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
// ⚠️ Two questions, and both are answered BEFORE the first redirect leaves Intel (#434).
|
|
54
|
+
// `tools:read` is Intel's own door. `mcpPortalRefused()` is Gate's answer to a question Intel
|
|
55
|
+
// cannot ask itself: does this person carry `mcp.connect` at ANY service? Measured against the
|
|
56
|
+
// running installation, somebody without it was sent to `mcp.anchrd.sh/servers-callback` and
|
|
57
|
+
// landed on a white page saying "No allowed servers available, check your Zero trust policies"
|
|
58
|
+
// — a sentence written for an administrator, on a foreign domain, with not one clickable
|
|
59
|
+
// element on it.
|
|
60
|
+
//
|
|
61
|
+
// ⚠️ The helper, never the raw `mcpConnectAnywhere` beside it. The field has THREE states, and
|
|
62
|
+
// `undefined` means "this Gate is older than the question" (anchrd/gate#277), not "nowhere".
|
|
63
|
+
// `if (!authorization.mcpConnectAnywhere)` passes `tsc --noEmit` and `biome check` alike and
|
|
64
|
+
// would turn every reader of such an installation away without a word — the difference is a
|
|
65
|
+
// value, not a type, so nothing but this line prevents it (anchrd/gate#288).
|
|
66
|
+
//
|
|
67
|
+
// ⚠️ And it is deliberately NOT `can("mcp", "connect")`, which is what this guard was first
|
|
68
|
+
// built as: Gate trims `rules` to the ASKING service (GATE-64, least privilege), so that asks
|
|
69
|
+
// `intel.mcp.connect`. The portal is a door onto every server — somebody carrying
|
|
70
|
+
// `dwh.mcp.connect` reaches it and sees the dwh tools, and that spelling would have locked
|
|
71
|
+
// them out. The bit knows no service names, on purpose; it is one bit and carries no more.
|
|
72
|
+
//
|
|
73
|
+
// ⚠️ It spares the certainly wasted trip and promises no successful one: the portal decides
|
|
74
|
+
// on the `mcp` claim frozen into the ID token, and a running Access session carries the old
|
|
75
|
+
// claim for up to 24 hours. Somebody granted a minute ago is NOT refused here, walks on, and
|
|
76
|
+
// meets Access deciding on the very claim state this ticket is about — inside that window the
|
|
77
|
+
// old dead end is still open, and this guard does not close it. It closes the certain one.
|
|
78
|
+
if (!authorization.can("tools", "read") || authorization.mcpPortalRefused()) {
|
|
54
79
|
return context.redirect("/tools?connectError=permission_required");
|
|
55
80
|
}
|
|
56
81
|
try {
|
|
@@ -91,6 +116,32 @@ export function createIntel(deps) {
|
|
|
91
116
|
context.header("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
|
|
92
117
|
return context.json({ jsonrpc: "2.0", id: null, error: { code: -32003, message: "authentication required" } }, 401);
|
|
93
118
|
}
|
|
119
|
+
// ⚠️ Intel's OWN portal door, asked per request — and it is deliberately the service-trimmed
|
|
120
|
+
// `can`, the exact spelling the guard on `/auth/connect` above must never use (anchrd/gate#293).
|
|
121
|
+
// The two questions are different and both are needed:
|
|
122
|
+
//
|
|
123
|
+
// /auth/connect "is anything in the SHARED portal for this person" mcpPortalRefused()
|
|
124
|
+
// /mcp "may they reach THIS server" can("mcp", "connect")
|
|
125
|
+
//
|
|
126
|
+
// Until now only Cloudflare Access asked the second one, comparing the `mcp` claim of the Access
|
|
127
|
+
// application against the service name. That claim is collected when the person signs in and a
|
|
128
|
+
// running Access session carries it for up to 24 hours, so a revoked grant kept working for a
|
|
129
|
+
// day while `rules` — the same rows, read live on every call — already said no. This line moves
|
|
130
|
+
// the decision to where the answer is: a revocation lands on the NEXT call, with no new sign-in,
|
|
131
|
+
// no session revocation and nobody pressing anything. It also lets the Access policy go coarse
|
|
132
|
+
// ("is a Gate user") without opening the server to everyone.
|
|
133
|
+
//
|
|
134
|
+
// ⚠️ NOT `permits(...)`: `intel.admin` is the bootstrap escape for Intel's own resource ACLs
|
|
135
|
+
// (ADR-0004 §6), and there is no bootstrap here — this grant is set in Gate, not in Intel, so no
|
|
136
|
+
// admin has to be able to hand it to themselves through this door. Widening it would let people
|
|
137
|
+
// in whom the Access policy turns away today, which is the opposite of moving a check.
|
|
138
|
+
//
|
|
139
|
+
// ⚠️ Fails CLOSED, and that is the whole reason a `403` and not a `401`: the caller IS
|
|
140
|
+
// authenticated and merely may not, so a `WWW-Authenticate` challenge would send an MCP client
|
|
141
|
+
// round the OAuth loop it just completed — the certain dead end of #434 in another costume.
|
|
142
|
+
if (!authorization.can("mcp", "connect")) {
|
|
143
|
+
return context.json({ jsonrpc: "2.0", id: null, error: { code: -32003, message: "permission required" } }, 403);
|
|
144
|
+
}
|
|
94
145
|
return await handleMcp(context.req.raw, {
|
|
95
146
|
authorization,
|
|
96
147
|
// ⚠️ The very token this call was authorized with, so a tool that has to speak to Gate does
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"typecheck": "tsc --noEmit"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@anchrd/gate-sdk": "^0.
|
|
45
|
+
"@anchrd/gate-sdk": "^0.15.0",
|
|
46
46
|
"@anchrd/intel-contract": "^0.15.0",
|
|
47
47
|
"@cfworker/json-schema": "^4.1.1",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.30.0",
|