mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 18:33:37 +00:00
fix(gateway): require read scope for models http (#55683)
This commit is contained in:
@@ -3,6 +3,8 @@ import { getFreePort, installGatewayTestHooks } from "./test-helpers.js";
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
const READ_SCOPE_HEADER = { "x-openclaw-scopes": "operator.read" };
|
||||
|
||||
let startGatewayServer: typeof import("./server.js").startGatewayServer;
|
||||
let enabledServer: Awaited<ReturnType<typeof startServer>>;
|
||||
let enabledPort: number;
|
||||
@@ -30,6 +32,7 @@ async function getModels(pathname: string, headers?: Record<string, string>) {
|
||||
return await fetch(`http://127.0.0.1:${enabledPort}${pathname}`, {
|
||||
headers: {
|
||||
authorization: "Bearer secret",
|
||||
...READ_SCOPE_HEADER,
|
||||
...headers,
|
||||
},
|
||||
});
|
||||
@@ -63,6 +66,49 @@ describe("OpenAI-compatible models HTTP API (e2e)", () => {
|
||||
expect(json.id).toBe(firstId);
|
||||
});
|
||||
|
||||
it("rejects operator scopes that lack read access", async () => {
|
||||
const res = await getModels("/v1/models", { "x-openclaw-scopes": "operator.approvals" });
|
||||
expect(res.status).toBe(403);
|
||||
await expect(res.json()).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
type: "forbidden",
|
||||
message: "missing scope: operator.read",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects requests with no declared operator scopes", async () => {
|
||||
const res = await getModels("/v1/models", { "x-openclaw-scopes": "" });
|
||||
expect(res.status).toBe(403);
|
||||
await expect(res.json()).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
type: "forbidden",
|
||||
message: "missing scope: operator.read",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects /v1/models/{id} without read access", async () => {
|
||||
const list = (await (await getModels("/v1/models")).json()) as {
|
||||
data?: Array<{ id?: string }>;
|
||||
};
|
||||
const firstId = list.data?.[0]?.id;
|
||||
expect(typeof firstId).toBe("string");
|
||||
const res = await getModels(`/v1/models/${encodeURIComponent(firstId!)}`, {
|
||||
"x-openclaw-scopes": "operator.approvals",
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
await expect(res.json()).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
type: "forbidden",
|
||||
message: "missing scope: operator.read",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects when disabled", async () => {
|
||||
const port = await getFreePort();
|
||||
const server = await startServer(port, { openAiChatCompletionsEnabled: false });
|
||||
|
||||
@@ -3,13 +3,17 @@ import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
||||
import type { ResolvedGatewayAuth } from "./auth.js";
|
||||
import { authorizeGatewayBearerRequestOrReply } from "./http-auth-helpers.js";
|
||||
import {
|
||||
authorizeGatewayBearerRequestOrReply,
|
||||
resolveGatewayRequestedOperatorScopes,
|
||||
} from "./http-auth-helpers.js";
|
||||
import { sendInvalidRequest, sendJson, sendMethodNotAllowed } from "./http-common.js";
|
||||
import {
|
||||
OPENCLAW_DEFAULT_MODEL_ID,
|
||||
OPENCLAW_MODEL_ID,
|
||||
resolveAgentIdFromModel,
|
||||
} from "./http-utils.js";
|
||||
import { authorizeOperatorScopesForMethod } from "./method-scopes.js";
|
||||
|
||||
type OpenAiModelsHttpOptions = {
|
||||
auth: ResolvedGatewayAuth;
|
||||
@@ -85,6 +89,19 @@ export async function handleOpenAiModelsHttpRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
const requestedScopes = resolveGatewayRequestedOperatorScopes(req);
|
||||
const scopeAuth = authorizeOperatorScopesForMethod("models.list", requestedScopes);
|
||||
if (!scopeAuth.allowed) {
|
||||
sendJson(res, 403, {
|
||||
ok: false,
|
||||
error: {
|
||||
type: "forbidden",
|
||||
message: `missing scope: ${scopeAuth.missingScope}`,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
const ids = loadAgentModelIds();
|
||||
if (requestPath === "/v1/models") {
|
||||
sendJson(res, 200, {
|
||||
|
||||
Reference in New Issue
Block a user