From 6d9798c5c8435a320f433855f643b79fa49d9e61 Mon Sep 17 00:00:00 2001 From: shuv1337 Date: Fri, 23 Oct 2026 01:08:59 -0700 Subject: [PATCH] e2e: refuse a member's connection that runs as the owner's account (#15) A member who can use the app saves an MCP connection whose runsAs names the owner's personal account. The product route refuses it with ConnectionAccessInvalid (reason account) and links nothing for the member, so a swapped saved profile cannot impersonate another principal. --- e2e/tests/mcp-connections.spec.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/e2e/tests/mcp-connections.spec.ts b/e2e/tests/mcp-connections.spec.ts index 23561f7497..45014529de 100644 --- a/e2e/tests/mcp-connections.spec.ts +++ b/e2e/tests/mcp-connections.spec.ts @@ -379,6 +379,26 @@ export default defineApp({ user: { accounts: { service } } }, async () => ({ too ok: true, value: { code: "AccountChoiceUnsupported", status: 400 }, }); + // A member who can use the app cannot save a connection that runs as the owner's own + // account: it is refused for the account and nothing is linked for the member. + const access = `${path}/access`; + const { revision } = yield* body( + Schema.Struct({ revision: Schema.String }), + yield* api.request(actors.owner, "GET", access), + ); + const shared = yield* api.request(actors.owner, "PATCH", access, { + revision, + audience: { kind: "everyone" }, + }); + expect(shared.status, JSON.stringify(shared.body)).toBe(200); + const borrowed = yield* api.request(actors.member, "POST", `${prefix}/mcp-connections`, { + id: randomUUID(), + name: `Connection ${randomUUID().slice(0, 6)}`, + apps: [{ app: app.id, runsAs: [{ kind: "account", id: work }], tools: { kind: "all" } }], + }); + expect(borrowed.status, JSON.stringify(borrowed.body)).toBe(400); + expect(borrowed.body).toMatchObject({ _tag: "ConnectionAccessInvalid", reason: "account" }); + expect(yield* linkedAccountIds(actors.member, path)).toEqual([]); // Another account linked in place of the connection's is outside the connection: the // call is refused and nothing is substituted. yield* linkAccount(actors.owner, path, "service", personal);