mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
fix(pairing): require pairing scope for node approvals
This commit is contained in:
committed by
Peter Steinberger
parent
a90f3ffdac
commit
0089d0e2e6
@@ -211,7 +211,7 @@ describe("createNodesTool screen_record duration guardrails", () => {
|
||||
expect(JSON.stringify(result?.content ?? [])).not.toContain("MEDIA:");
|
||||
});
|
||||
|
||||
it("uses operator.admin to approve exec-capable node pair requests", async () => {
|
||||
it("uses operator.pairing plus operator.admin to approve exec-capable node pair requests", async () => {
|
||||
gatewayMocks.callGatewayTool.mockImplementation(async (method, _opts, params, extra) => {
|
||||
if (method === "node.pair.list") {
|
||||
return {
|
||||
@@ -247,11 +247,11 @@ describe("createNodesTool screen_record duration guardrails", () => {
|
||||
"node.pair.approve",
|
||||
{},
|
||||
{ requestId: "req-1" },
|
||||
{ scopes: ["operator.admin"] },
|
||||
{ scopes: ["operator.pairing", "operator.admin"] },
|
||||
);
|
||||
});
|
||||
|
||||
it("uses operator.write to approve non-exec node pair requests", async () => {
|
||||
it("uses operator.pairing plus operator.write to approve non-exec node pair requests", async () => {
|
||||
gatewayMocks.callGatewayTool.mockImplementation(async (method, _opts, params, extra) => {
|
||||
if (method === "node.pair.list") {
|
||||
return {
|
||||
@@ -287,11 +287,11 @@ describe("createNodesTool screen_record duration guardrails", () => {
|
||||
"node.pair.approve",
|
||||
{},
|
||||
{ requestId: "req-1" },
|
||||
{ scopes: ["operator.write"] },
|
||||
{ scopes: ["operator.pairing", "operator.write"] },
|
||||
);
|
||||
});
|
||||
|
||||
it("uses operator.write for commandless node pair requests", async () => {
|
||||
it("uses operator.pairing for commandless node pair requests", async () => {
|
||||
gatewayMocks.callGatewayTool.mockImplementation(async (method, _opts, params, extra) => {
|
||||
if (method === "node.pair.list") {
|
||||
return {
|
||||
@@ -319,7 +319,7 @@ describe("createNodesTool screen_record duration guardrails", () => {
|
||||
"node.pair.approve",
|
||||
{},
|
||||
{ requestId: "req-1" },
|
||||
{ scopes: ["operator.write"] },
|
||||
{ scopes: ["operator.pairing"] },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -49,12 +49,12 @@ function resolveApproveScopes(commands: unknown): OperatorScope[] {
|
||||
if (
|
||||
normalized.some((command) => NODE_SYSTEM_RUN_COMMANDS.some((allowed) => allowed === command))
|
||||
) {
|
||||
return ["operator.admin"];
|
||||
return ["operator.pairing", "operator.admin"];
|
||||
}
|
||||
if (normalized.length > 0) {
|
||||
return ["operator.write"];
|
||||
return ["operator.pairing", "operator.write"];
|
||||
}
|
||||
return ["operator.write"];
|
||||
return ["operator.pairing"];
|
||||
}
|
||||
|
||||
async function resolveNodePairApproveScopes(
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("method scope resolution", () => {
|
||||
["sessions.abort", ["operator.write"]],
|
||||
["sessions.messages.subscribe", ["operator.read"]],
|
||||
["sessions.messages.unsubscribe", ["operator.read"]],
|
||||
["node.pair.approve", ["operator.write"]],
|
||||
["node.pair.approve", ["operator.pairing"]],
|
||||
["poll", ["operator.write"]],
|
||||
["config.patch", ["operator.admin"]],
|
||||
["wizard.start", ["operator.admin"]],
|
||||
@@ -67,9 +67,15 @@ describe("operator scope authorization", () => {
|
||||
allowed: false,
|
||||
missingScope: "operator.write",
|
||||
});
|
||||
});
|
||||
|
||||
it("requires pairing scope for node pairing approvals", () => {
|
||||
expect(authorizeOperatorScopesForMethod("node.pair.approve", ["operator.pairing"])).toEqual({
|
||||
allowed: true,
|
||||
});
|
||||
expect(authorizeOperatorScopesForMethod("node.pair.approve", ["operator.write"])).toEqual({
|
||||
allowed: false,
|
||||
missingScope: "operator.write",
|
||||
missingScope: "operator.pairing",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
|
||||
"node.pair.list",
|
||||
"node.pair.reject",
|
||||
"node.pair.verify",
|
||||
"node.pair.approve",
|
||||
"device.pair.list",
|
||||
"device.pair.approve",
|
||||
"device.pair.reject",
|
||||
@@ -112,7 +113,6 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
|
||||
"tts.setProvider",
|
||||
"voicewake.set",
|
||||
"node.invoke",
|
||||
"node.pair.approve",
|
||||
"chat.send",
|
||||
"chat.abort",
|
||||
"sessions.create",
|
||||
|
||||
@@ -40,7 +40,7 @@ async function connectNodeClient(params: {
|
||||
}
|
||||
|
||||
describe("gateway node pairing authorization", () => {
|
||||
test("requires operator.write before node pairing approvals", async () => {
|
||||
test("requires operator.admin for exec-capable node pairing approvals", async () => {
|
||||
const started = await startServerWithClient("secret");
|
||||
const approver = await issueOperatorToken({
|
||||
name: "node-pair-approve-pairing-only",
|
||||
@@ -70,7 +70,7 @@ describe("gateway node pairing authorization", () => {
|
||||
requestId: request.request.requestId,
|
||||
});
|
||||
expect(approve.ok).toBe(false);
|
||||
expect(approve.error?.message).toBe("missing scope: operator.write");
|
||||
expect(approve.error?.message).toBe("missing scope: operator.admin");
|
||||
|
||||
await expect(
|
||||
import("../infra/node-pairing.js").then((m) => m.getPairedNode("node-approve-target")),
|
||||
@@ -83,7 +83,7 @@ describe("gateway node pairing authorization", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("rejects approving exec-capable node commands above the caller session scopes", async () => {
|
||||
test("requires operator.pairing before node pairing approvals", async () => {
|
||||
const started = await startServerWithClient("secret");
|
||||
const approver = await issueOperatorToken({
|
||||
name: "node-pair-approve-attacker",
|
||||
@@ -113,7 +113,7 @@ describe("gateway node pairing authorization", () => {
|
||||
requestId: request.request.requestId,
|
||||
});
|
||||
expect(approve.ok).toBe(false);
|
||||
expect(approve.error?.message).toBe("missing scope: operator.admin");
|
||||
expect(approve.error?.message).toBe("missing scope: operator.pairing");
|
||||
|
||||
await expect(
|
||||
import("../infra/node-pairing.js").then((m) => m.getPairedNode("node-approve-target")),
|
||||
|
||||
Reference in New Issue
Block a user