mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(gateway): require admin for device role approvals (#87146)
* fix(gateway): require admin for device role approvals * fix(gateway): add trusted-proxy approval proof
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Block unsafe Node runtime env overrides [AI]. (#87308) Thanks @pgondhi987.
|
||||
- Telegram: route `sendMessage` action replies through durable outbound delivery so completed agent responses remain retryable when the gateway send path times out. (#87261) Thanks @mbelinky.
|
||||
- Gateway/security: require `operator.admin` for node and other non-operator device-role pairing approvals, including trusted-proxy sessions, while keeping pairing-only approvals available for operator-role requests. (#87146)
|
||||
|
||||
## 2026.5.26
|
||||
|
||||
|
||||
@@ -71,9 +71,10 @@ matching client IPs can be approved before they appear in this list. That policy
|
||||
is disabled by default and never applies to operator/browser clients or upgrade
|
||||
requests.
|
||||
|
||||
Approving a non-operator device role, such as `role: node`, requires
|
||||
`operator.admin`. `operator.pairing` is enough for operator-device approvals
|
||||
only when the requested operator scopes stay within the caller's own scopes.
|
||||
Approving node or other non-operator device roles requires `operator.admin`.
|
||||
`operator.pairing` is enough for operator-device approvals only when the
|
||||
requested operator scopes stay within the caller's own scopes. See
|
||||
[Operator scopes](/gateway/operator-scopes) for the approval-time checks.
|
||||
|
||||
```
|
||||
openclaw devices approve
|
||||
|
||||
@@ -70,7 +70,8 @@ When approving a device request:
|
||||
|
||||
- A request with no operator role does not need operator token scope approval.
|
||||
- A request for a non-operator device role, such as `node`, requires
|
||||
`operator.admin`.
|
||||
`operator.admin`, even when `device.pair.approve` is reachable with
|
||||
`operator.pairing`.
|
||||
- A request for `operator.read`, `operator.write`, `operator.approvals`,
|
||||
`operator.pairing`, or `operator.talk.secrets` requires the caller to hold
|
||||
those scopes, or `operator.admin`.
|
||||
@@ -79,9 +80,9 @@ When approving a device request:
|
||||
token scopes. If that existing token is admin-scoped, approval still requires
|
||||
`operator.admin`.
|
||||
|
||||
Non-admin sessions can approve operator-device requests only inside their own
|
||||
operator scopes. Approving non-operator roles is admin-only even for
|
||||
shared-secret or trusted-proxy sessions that can otherwise use
|
||||
Non-admin shared-secret and trusted-proxy sessions can approve operator-device
|
||||
requests only inside their own declared operator scopes. Approving non-operator
|
||||
roles is admin-only even when those sessions can otherwise use
|
||||
`operator.pairing`.
|
||||
|
||||
For paired-device token sessions, management is also self-scoped unless the
|
||||
|
||||
@@ -888,7 +888,7 @@ describe("deviceHandlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("allows non-device operator sessions to approve operator roles within caller scopes", async () => {
|
||||
it("allows shared-auth operator sessions to approve operator roles within caller scopes", async () => {
|
||||
getPendingDevicePairingMock.mockResolvedValue({
|
||||
requestId: "req-1",
|
||||
deviceId: "device-2",
|
||||
@@ -914,7 +914,7 @@ describe("deviceHandlers", () => {
|
||||
const opts = createOptions(
|
||||
"device.pair.approve",
|
||||
{ requestId: "req-1" },
|
||||
{ client: createClient(["operator.pairing"]) },
|
||||
{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: false }) },
|
||||
);
|
||||
|
||||
await deviceHandlers["device.pair.approve"](opts);
|
||||
@@ -941,11 +941,11 @@ describe("deviceHandlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects approving node roles from non-admin shared-auth sessions", async () => {
|
||||
it("rejects approving node roles for the caller device without admin scope", async () => {
|
||||
getPendingDevicePairingMock.mockResolvedValue({
|
||||
requestId: "req-1",
|
||||
deviceId: "device-2",
|
||||
publicKey: "pk-2",
|
||||
deviceId: " device-1 ",
|
||||
publicKey: "pk-1",
|
||||
role: "node",
|
||||
roles: ["node"],
|
||||
ts: 100,
|
||||
@@ -953,7 +953,28 @@ describe("deviceHandlers", () => {
|
||||
const opts = createOptions(
|
||||
"device.pair.approve",
|
||||
{ requestId: "req-1" },
|
||||
{ client: createClient(["operator.pairing"]) },
|
||||
{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
|
||||
);
|
||||
|
||||
await deviceHandlers["device.pair.approve"](opts);
|
||||
|
||||
expect(approveDevicePairingMock).not.toHaveBeenCalled();
|
||||
expectRespondedErrorMessage(opts, "device pairing approval denied");
|
||||
});
|
||||
|
||||
it("rejects approving node roles from non-admin shared-auth sessions", async () => {
|
||||
getPendingDevicePairingMock.mockResolvedValue({
|
||||
requestId: "req-1",
|
||||
deviceId: "device-1",
|
||||
publicKey: "pk-1",
|
||||
role: "node",
|
||||
roles: ["node"],
|
||||
ts: 100,
|
||||
});
|
||||
const opts = createOptions(
|
||||
"device.pair.approve",
|
||||
{ requestId: "req-1" },
|
||||
{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: false }) },
|
||||
);
|
||||
|
||||
await deviceHandlers["device.pair.approve"](opts);
|
||||
@@ -998,27 +1019,6 @@ describe("deviceHandlers", () => {
|
||||
expectRespondedErrorMessage(opts, "device pairing approval denied");
|
||||
});
|
||||
|
||||
it("rejects approving node roles for the caller device without admin scope", async () => {
|
||||
getPendingDevicePairingMock.mockResolvedValue({
|
||||
requestId: "req-1",
|
||||
deviceId: " device-1 ",
|
||||
publicKey: "pk-1",
|
||||
role: "node",
|
||||
roles: ["node"],
|
||||
ts: 100,
|
||||
});
|
||||
const opts = createOptions(
|
||||
"device.pair.approve",
|
||||
{ requestId: "req-1" },
|
||||
{ client: createClient(["operator.pairing"], "device-1", { isDeviceTokenAuth: true }) },
|
||||
);
|
||||
|
||||
await deviceHandlers["device.pair.approve"](opts);
|
||||
|
||||
expect(approveDevicePairingMock).not.toHaveBeenCalled();
|
||||
expectRespondedErrorMessage(opts, "device pairing approval denied");
|
||||
});
|
||||
|
||||
it("rejects rejecting another device from a non-admin device session", async () => {
|
||||
getPendingDevicePairingMock.mockResolvedValue({
|
||||
requestId: "req-2",
|
||||
|
||||
Reference in New Issue
Block a user