mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
fix: read-only scoped API keys cannot access MCP (#11875)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user