mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 21:22:28 +03:00
fix(management): authorize mcp:connect-only keys on loopback/LAN when requireLogin is enabled (#9159)
Closes #9159 Refs: base-red #9737 fix/9159-mcp-connect-require-login-lo
This commit is contained in:
committed by
GitHub
parent
3cae1b1480
commit
9c343237d3
@@ -253,6 +253,39 @@ export const managementPolicy: RoutePolicy = {
|
||||
return allow({ kind: "management_key", id: "cli", label: "local-cli-token" });
|
||||
}
|
||||
|
||||
// MCP path carve-out (#9159): accept mcp:connect, manage, or admin
|
||||
// scope for /api/mcp/* from any origin (loopback, private LAN, or remote).
|
||||
// Loopback/LAN requests skip the Tier 1 bypass gate above, so with
|
||||
// requireLogin=true they would fall through to the generic API-key check
|
||||
// which only accepts manage/admin -- rejecting mcp:connect-only keys.
|
||||
// This carve-out mirrors the existing Tier 1 MCP check but without the
|
||||
// locality guard, so it catches the loopback/LAN requests that the Tier 1
|
||||
// gate does not reach.
|
||||
if (path.startsWith("/api/mcp/")) {
|
||||
const apiKey = extractApiKey(ctx.request as unknown as Request, { allowUrl: false });
|
||||
if (apiKey) {
|
||||
try {
|
||||
if (await isValidApiKey(apiKey)) {
|
||||
const meta = await getApiKeyMetadata(apiKey);
|
||||
if (meta && hasMcpConnectOrManageScope(meta.scopes)) {
|
||||
const grantedBy = meta.scopes.includes("admin")
|
||||
? "admin"
|
||||
: meta.scopes.includes("manage")
|
||||
? "manage"
|
||||
: "mcp-connect";
|
||||
return allow({
|
||||
kind: "management_key",
|
||||
id: meta.id,
|
||||
label: `api-key-${grantedBy}-scope-mcp-carve-out`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return reject(503, "AUTH_BACKEND_UNAVAILABLE", "Service temporarily unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 2: always-protected routes skip the requireLogin=false bypass.
|
||||
if (!isAlwaysProtectedPath(path) && !(await isAuthRequired(ctx.request))) {
|
||||
return allow({ kind: "anonymous", id: "anonymous", label: "auth-disabled" });
|
||||
|
||||
@@ -56,13 +56,14 @@ test.after(() => {
|
||||
else process.env.INITIAL_PASSWORD = ORIGINAL_INITIAL;
|
||||
});
|
||||
|
||||
function mgmtCtx(headers: Headers, method = "GET", pathname = "/api/keys") {
|
||||
function mgmtCtx(headers: Headers, method = "GET", pathname = "/api/keys", peerAddress?: string) {
|
||||
return {
|
||||
request: {
|
||||
method,
|
||||
headers,
|
||||
url: `http://localhost${pathname}`,
|
||||
nextUrl: { pathname },
|
||||
socket: peerAddress ? { remoteAddress: peerAddress } : undefined,
|
||||
},
|
||||
classification: {
|
||||
routeClass: "MANAGEMENT" as const,
|
||||
@@ -268,3 +269,37 @@ test("mcp:connect key is still rejected for the non-bypassable /api/cli-tools/ru
|
||||
assert.equal(out.code, "LOCAL_ONLY");
|
||||
}
|
||||
});
|
||||
|
||||
// ─── 7. #9159 — loopback/LAN mcp:connect with requireLogin ──────────────────
|
||||
|
||||
test("#9159 mcp:connect-only key must pass /api/mcp/ from loopback when login is required", async () => {
|
||||
await seedAuthRequired();
|
||||
const created = await apiKeysDb.createApiKey("mcp-loopback-only", "machine-mcp-loopback", [
|
||||
MCP_CONNECT_SCOPE,
|
||||
]);
|
||||
const out = await managementPolicy.evaluate(
|
||||
mgmtCtx(
|
||||
new Headers({ authorization: `Bearer ${created.key}` }),
|
||||
"POST",
|
||||
"/api/mcp/stream",
|
||||
"127.0.0.1"
|
||||
)
|
||||
);
|
||||
assert.equal(out.allow, true, JSON.stringify(out));
|
||||
});
|
||||
|
||||
test("#9159 mcp:connect-only key must pass /api/mcp/ from private LAN when login is required", async () => {
|
||||
await seedAuthRequired();
|
||||
const created = await apiKeysDb.createApiKey("mcp-lan-only", "machine-mcp-lan", [
|
||||
MCP_CONNECT_SCOPE,
|
||||
]);
|
||||
const out = await managementPolicy.evaluate(
|
||||
mgmtCtx(
|
||||
new Headers({ authorization: `Bearer ${created.key}` }),
|
||||
"POST",
|
||||
"/api/mcp/stream",
|
||||
"192.168.1.20"
|
||||
)
|
||||
);
|
||||
assert.equal(out.allow, true, JSON.stringify(out));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user