fix: read-only scoped API keys cannot access MCP (#11875)

This commit is contained in:
Tom Moor
2026-03-26 00:15:28 -04:00
committed by GitHub
parent b91d9e9a72
commit 45b2f6e222
3 changed files with 73 additions and 0 deletions
+19
View File
@@ -1,4 +1,5 @@
import { randomString } from "@shared/random";
import { Scope } from "@shared/types";
import { buildApiKey } from "@server/test/factories";
import ApiKey from "./ApiKey";
@@ -110,5 +111,23 @@ describe("#ApiKey", () => {
expect(apiKey.canAccess("/api/documents.create")).toBe(false);
expect(apiKey.canAccess("/api/collections.create")).toBe(false);
});
it("should allow MCP access for scoped API keys", async () => {
const apiKey = await buildApiKey({
name: "Dev",
scope: [Scope.Read],
});
expect(apiKey.canAccess("/mcp")).toBe(true);
expect(apiKey.canAccess("/mcp/")).toBe(true);
});
it("should allow MCP access for unscoped API keys", async () => {
const apiKey = await buildApiKey({
name: "Dev",
});
expect(apiKey.canAccess("/mcp")).toBe(true);
});
});
});
+6
View File
@@ -176,6 +176,12 @@ class ApiKey extends ParanoidModel<
return true;
}
// MCP endpoint access is allowed if the key has any valid scope.
// Fine-grained scope enforcement happens at the tool level.
if (path.startsWith("/mcp")) {
return this.scope.length > 0;
}
return AuthenticationHelper.canAccess(path, this.scope);
};
}
@@ -0,0 +1,48 @@
import { Scope } from "@shared/types";
import { buildOAuthAuthentication, buildUser } from "@server/test/factories";
describe("OAuthAuthentication", () => {
describe("canAccess", () => {
it("should allow MCP access for scoped tokens", async () => {
const user = await buildUser();
const authentication = await buildOAuthAuthentication({
user,
scope: [Scope.Read],
});
expect(authentication.canAccess("/mcp")).toBe(true);
expect(authentication.canAccess("/mcp/")).toBe(true);
});
it("should deny MCP access for tokens with empty scope", async () => {
const user = await buildUser();
const authentication = await buildOAuthAuthentication({
user,
scope: [],
});
expect(authentication.canAccess("/mcp")).toBe(false);
});
it("should always allow the revoke endpoint", async () => {
const user = await buildUser();
const authentication = await buildOAuthAuthentication({
user,
scope: [Scope.Read],
});
expect(authentication.canAccess("/oauth/revoke")).toBe(true);
});
it("should check scopes for API paths", async () => {
const user = await buildUser();
const authentication = await buildOAuthAuthentication({
user,
scope: [Scope.Read],
});
expect(authentication.canAccess("/api/documents.list")).toBe(true);
expect(authentication.canAccess("/api/documents.update")).toBe(false);
});
});
});