mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:41:23 +00:00
fix(gateway): allow health method for all authenticated roles (#19699)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: b976443267
Co-authored-by: Nachx639 <71144023+Nachx639@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
- Gateway/Auth: allow authenticated clients across roles/scopes to call `health` while preserving role and scope enforcement for non-health methods. (#19699) thanks @Nachx639.
|
||||||
- Gateway/Hooks: include transform export name in hook-transform cache keys so distinct exports from the same module do not reuse the wrong cached transform function. (#13855) thanks @mcaxtr.
|
- Gateway/Hooks: include transform export name in hook-transform cache keys so distinct exports from the same module do not reuse the wrong cached transform function. (#13855) thanks @mcaxtr.
|
||||||
- Gateway/Control UI: return 404 for missing static-asset paths instead of serving SPA fallback HTML, while preserving client-route fallback behavior for extensionless and non-asset dotted paths. (#12060) thanks @mcaxtr.
|
- Gateway/Control UI: return 404 for missing static-asset paths instead of serving SPA fallback HTML, while preserving client-route fallback behavior for extensionless and non-asset dotted paths. (#12060) thanks @mcaxtr.
|
||||||
- Gateway/Pairing: prevent device-token rotate scope escalation by enforcing an approved-scope baseline, preserving approved scopes across metadata updates, and rejecting rotate requests that exceed approved role scope implications. (#20703) thanks @coygeek.
|
- Gateway/Pairing: prevent device-token rotate scope escalation by enforcing an approved-scope baseline, preserving approved scopes across metadata updates, and rejecting rotate requests that exceed approved role scope implications. (#20703) thanks @coygeek.
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c
|
|||||||
if (!client?.connect) {
|
if (!client?.connect) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (method === "health") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const role = client.connect.role ?? "operator";
|
const role = client.connect.role ?? "operator";
|
||||||
const scopes = client.connect.scopes ?? [];
|
const scopes = client.connect.scopes ?? [];
|
||||||
if (isNodeRoleMethod(method)) {
|
if (isNodeRoleMethod(method)) {
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ async function expectMissingScopeAfterConnect(
|
|||||||
try {
|
try {
|
||||||
const res = await connectReq(ws, opts);
|
const res = await connectReq(ws, opts);
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
const health = await rpcReq(ws, "health");
|
const status = await rpcReq(ws, "status");
|
||||||
expect(health.ok).toBe(false);
|
expect(status.ok).toBe(false);
|
||||||
expect(health.error?.message).toContain("missing scope");
|
expect(status.error?.message).toContain("missing scope");
|
||||||
} finally {
|
} finally {
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
@@ -363,6 +363,18 @@ describe("gateway server auth/connect", () => {
|
|||||||
await expectMissingScopeAfterConnect(port, { device: null });
|
await expectMissingScopeAfterConnect(port, { device: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("allows health when scopes are empty", async () => {
|
||||||
|
const ws = await openWs(port);
|
||||||
|
try {
|
||||||
|
const res = await connectReq(ws, { scopes: [] });
|
||||||
|
expect(res.ok).toBe(true);
|
||||||
|
const health = await rpcReq(ws, "health");
|
||||||
|
expect(health.ok).toBe(true);
|
||||||
|
} finally {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("does not grant admin when scopes are omitted", async () => {
|
test("does not grant admin when scopes are omitted", async () => {
|
||||||
const ws = await openWs(port);
|
const ws = await openWs(port);
|
||||||
const token = resolveGatewayTokenOrEnv();
|
const token = resolveGatewayTokenOrEnv();
|
||||||
@@ -400,9 +412,11 @@ describe("gateway server auth/connect", () => {
|
|||||||
expect(presenceScopes).toEqual([]);
|
expect(presenceScopes).toEqual([]);
|
||||||
expect(presenceScopes).not.toContain("operator.admin");
|
expect(presenceScopes).not.toContain("operator.admin");
|
||||||
|
|
||||||
|
const status = await rpcReq(ws, "status");
|
||||||
|
expect(status.ok).toBe(false);
|
||||||
|
expect(status.error?.message).toContain("missing scope");
|
||||||
const health = await rpcReq(ws, "health");
|
const health = await rpcReq(ws, "health");
|
||||||
expect(health.ok).toBe(false);
|
expect(health.ok).toBe(true);
|
||||||
expect(health.error?.message).toContain("missing scope");
|
|
||||||
|
|
||||||
ws.close();
|
ws.close();
|
||||||
});
|
});
|
||||||
@@ -680,9 +694,11 @@ describe("gateway server auth/connect", () => {
|
|||||||
const ws = await openTailscaleWs(port);
|
const ws = await openTailscaleWs(port);
|
||||||
const res = await connectReq(ws, { token: "secret", device: null });
|
const res = await connectReq(ws, { token: "secret", device: null });
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
|
const status = await rpcReq(ws, "status");
|
||||||
|
expect(status.ok).toBe(false);
|
||||||
|
expect(status.error?.message).toContain("missing scope");
|
||||||
const health = await rpcReq(ws, "health");
|
const health = await rpcReq(ws, "health");
|
||||||
expect(health.ok).toBe(false);
|
expect(health.ok).toBe(true);
|
||||||
expect(health.error?.message).toContain("missing scope");
|
|
||||||
ws.close();
|
ws.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ describe("gateway role enforcement", () => {
|
|||||||
const statusRes = await rpcReq(nodeWs, "status", {});
|
const statusRes = await rpcReq(nodeWs, "status", {});
|
||||||
expect(statusRes.ok).toBe(false);
|
expect(statusRes.ok).toBe(false);
|
||||||
expect(statusRes.error?.message ?? "").toContain("unauthorized role");
|
expect(statusRes.error?.message ?? "").toContain("unauthorized role");
|
||||||
|
|
||||||
|
const healthRes = await rpcReq(nodeWs, "health", {});
|
||||||
|
expect(healthRes.ok).toBe(true);
|
||||||
} finally {
|
} finally {
|
||||||
nodeWs.close();
|
nodeWs.close();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user