@anchrd/intel-api 0.18.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 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`,
@@ -116,6 +116,32 @@ export function createIntel(deps) {
116
116
  context.header("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
117
117
  return context.json({ jsonrpc: "2.0", id: null, error: { code: -32003, message: "authentication required" } }, 401);
118
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
+ }
119
145
  return await handleMcp(context.req.raw, {
120
146
  authorization,
121
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.18.0",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {