mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:00:42 +00:00
refactor: share scoped gateway http auth
This commit is contained in:
@@ -17,6 +17,7 @@ vi.mock("../config/config.js", () => ({
|
||||
|
||||
vi.mock("./http-common.js", () => ({
|
||||
sendGatewayAuthFailure: vi.fn(),
|
||||
sendJson: vi.fn(),
|
||||
}));
|
||||
|
||||
const { authorizeHttpGatewayConnect } = await import("./auth.js");
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
resolveDefaultModelForAgent,
|
||||
} from "../agents/model-selection.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { buildAgentMainSessionKey, normalizeAgentId } from "../routing/session-key.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
@@ -20,8 +21,9 @@ import {
|
||||
type GatewayAuthResult,
|
||||
type ResolvedGatewayAuth,
|
||||
} from "./auth.js";
|
||||
import { sendGatewayAuthFailure } from "./http-common.js";
|
||||
import { sendGatewayAuthFailure, sendJson } from "./http-common.js";
|
||||
import { ADMIN_SCOPE, CLI_DEFAULT_OPERATOR_SCOPES } from "./method-scopes.js";
|
||||
import { authorizeOperatorScopesForMethod } from "./method-scopes.js";
|
||||
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
|
||||
|
||||
export const OPENCLAW_MODEL_ID = "openclaw";
|
||||
@@ -119,6 +121,48 @@ export async function authorizeGatewayHttpRequestOrReply(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function authorizeScopedGatewayHttpRequestOrReply(params: {
|
||||
req: IncomingMessage;
|
||||
res: ServerResponse;
|
||||
auth: ResolvedGatewayAuth;
|
||||
trustedProxies?: string[];
|
||||
allowRealIpFallback?: boolean;
|
||||
rateLimiter?: AuthRateLimiter;
|
||||
operatorMethod: string;
|
||||
resolveOperatorScopes: (
|
||||
req: IncomingMessage,
|
||||
requestAuth: AuthorizedGatewayHttpRequest,
|
||||
) => string[];
|
||||
}): Promise<{ cfg: OpenClawConfig; requestAuth: AuthorizedGatewayHttpRequest } | null> {
|
||||
const cfg = loadConfig();
|
||||
const requestAuth = await authorizeGatewayHttpRequestOrReply({
|
||||
req: params.req,
|
||||
res: params.res,
|
||||
auth: params.auth,
|
||||
trustedProxies: params.trustedProxies ?? cfg.gateway?.trustedProxies,
|
||||
allowRealIpFallback: params.allowRealIpFallback ?? cfg.gateway?.allowRealIpFallback,
|
||||
rateLimiter: params.rateLimiter,
|
||||
});
|
||||
if (!requestAuth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestedScopes = params.resolveOperatorScopes(params.req, requestAuth);
|
||||
const scopeAuth = authorizeOperatorScopesForMethod(params.operatorMethod, requestedScopes);
|
||||
if (!scopeAuth.allowed) {
|
||||
sendJson(params.res, 403, {
|
||||
ok: false,
|
||||
error: {
|
||||
type: "forbidden",
|
||||
message: `missing scope: ${scopeAuth.missingScope}`,
|
||||
},
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return { cfg, requestAuth };
|
||||
}
|
||||
|
||||
export function isGatewayBearerHttpRequest(
|
||||
req: IncomingMessage,
|
||||
auth?: SharedSecretGatewayAuth,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import path from "node:path";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { loadSessionStore } from "../config/sessions.js";
|
||||
import { onSessionTranscriptUpdate } from "../sessions/transcript-events.js";
|
||||
import {
|
||||
@@ -17,11 +16,10 @@ import {
|
||||
setSseHeaders,
|
||||
} from "./http-common.js";
|
||||
import {
|
||||
authorizeGatewayHttpRequestOrReply,
|
||||
authorizeScopedGatewayHttpRequestOrReply,
|
||||
getHeader,
|
||||
resolveTrustedHttpOperatorScopes,
|
||||
} from "./http-utils.js";
|
||||
import { authorizeOperatorScopesForMethod } from "./method-scopes.js";
|
||||
import { DEFAULT_CHAT_HISTORY_TEXT_MAX_CHARS } from "./server-methods/chat.js";
|
||||
import { buildSessionHistorySnapshot, SessionHistorySseState } from "./session-history-state.js";
|
||||
import {
|
||||
@@ -108,33 +106,22 @@ export async function handleSessionHistoryHttpRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const requestAuth = await authorizeGatewayHttpRequestOrReply({
|
||||
// HTTP callers must declare the same least-privilege operator scopes they
|
||||
// intend to use over WS so both transport surfaces enforce the same gate.
|
||||
const authResult = await authorizeScopedGatewayHttpRequestOrReply({
|
||||
req,
|
||||
res,
|
||||
auth: opts.auth,
|
||||
trustedProxies: opts.trustedProxies ?? cfg.gateway?.trustedProxies,
|
||||
allowRealIpFallback: opts.allowRealIpFallback ?? cfg.gateway?.allowRealIpFallback,
|
||||
trustedProxies: opts.trustedProxies,
|
||||
allowRealIpFallback: opts.allowRealIpFallback,
|
||||
rateLimiter: opts.rateLimiter,
|
||||
operatorMethod: "chat.history",
|
||||
resolveOperatorScopes: resolveTrustedHttpOperatorScopes,
|
||||
});
|
||||
if (!requestAuth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// HTTP callers must declare the same least-privilege operator scopes they
|
||||
// intend to use over WS so both transport surfaces enforce the same gate.
|
||||
const requestedScopes = resolveTrustedHttpOperatorScopes(req, requestAuth);
|
||||
const scopeAuth = authorizeOperatorScopesForMethod("chat.history", requestedScopes);
|
||||
if (!scopeAuth.allowed) {
|
||||
sendJson(res, 403, {
|
||||
ok: false,
|
||||
error: {
|
||||
type: "forbidden",
|
||||
message: `missing scope: ${scopeAuth.missingScope}`,
|
||||
},
|
||||
});
|
||||
if (!authResult) {
|
||||
return true;
|
||||
}
|
||||
const { cfg } = authResult;
|
||||
|
||||
const target = resolveGatewaySessionStoreTarget({ cfg, key: sessionKey });
|
||||
const store = loadSessionStore(target.storePath);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { resolveToolLoopDetectionConfig } from "../agents/pi-tools.js";
|
||||
import { isKnownCoreToolId } from "../agents/tool-catalog.js";
|
||||
import { applyOwnerOnlyToolPolicy } from "../agents/tool-policy.js";
|
||||
import { ToolInputError, type AnyAgentTool } from "../agents/tools/common.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { resolveMainSessionKey } from "../config/sessions.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { logWarn } from "../logger.js";
|
||||
@@ -23,12 +22,11 @@ import {
|
||||
sendMethodNotAllowed,
|
||||
} from "./http-common.js";
|
||||
import {
|
||||
authorizeGatewayHttpRequestOrReply,
|
||||
authorizeScopedGatewayHttpRequestOrReply,
|
||||
getHeader,
|
||||
resolveOpenAiCompatibleHttpOperatorScopes,
|
||||
resolveOpenAiCompatibleHttpSenderIsOwner,
|
||||
} from "./http-utils.js";
|
||||
import { authorizeOperatorScopesForMethod } from "./method-scopes.js";
|
||||
import { resolveGatewayScopedTools } from "./tool-resolution.js";
|
||||
|
||||
const DEFAULT_BODY_BYTES = 2 * 1024 * 1024;
|
||||
@@ -155,34 +153,23 @@ export async function handleToolsInvokeHttpRequest(
|
||||
return true;
|
||||
}
|
||||
|
||||
const cfg = loadConfig();
|
||||
const requestAuth = await authorizeGatewayHttpRequestOrReply({
|
||||
req,
|
||||
res,
|
||||
auth: opts.auth,
|
||||
trustedProxies: opts.trustedProxies ?? cfg.gateway?.trustedProxies,
|
||||
allowRealIpFallback: opts.allowRealIpFallback ?? cfg.gateway?.allowRealIpFallback,
|
||||
rateLimiter: opts.rateLimiter,
|
||||
});
|
||||
if (!requestAuth) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// /tools/invoke intentionally uses the same shared-secret HTTP trust model as
|
||||
// the OpenAI-compatible APIs: token/password bearer auth is full operator
|
||||
// access for the gateway, not a narrower per-request scope boundary.
|
||||
const requestedScopes = resolveOpenAiCompatibleHttpOperatorScopes(req, requestAuth);
|
||||
const scopeAuth = authorizeOperatorScopesForMethod("agent", requestedScopes);
|
||||
if (!scopeAuth.allowed) {
|
||||
sendJson(res, 403, {
|
||||
ok: false,
|
||||
error: {
|
||||
type: "forbidden",
|
||||
message: `missing scope: ${scopeAuth.missingScope}`,
|
||||
},
|
||||
});
|
||||
const authResult = await authorizeScopedGatewayHttpRequestOrReply({
|
||||
req,
|
||||
res,
|
||||
auth: opts.auth,
|
||||
trustedProxies: opts.trustedProxies,
|
||||
allowRealIpFallback: opts.allowRealIpFallback,
|
||||
rateLimiter: opts.rateLimiter,
|
||||
operatorMethod: "agent",
|
||||
resolveOperatorScopes: resolveOpenAiCompatibleHttpOperatorScopes,
|
||||
});
|
||||
if (!authResult) {
|
||||
return true;
|
||||
}
|
||||
const { cfg, requestAuth } = authResult;
|
||||
|
||||
const bodyUnknown = await readJsonBodyOrError(req, res, opts.maxBodyBytes ?? DEFAULT_BODY_BYTES);
|
||||
if (bodyUnknown === undefined) {
|
||||
|
||||
Reference in New Issue
Block a user