fix(gateway): allow health method for all authenticated roles (#19699)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: b9764432672d15d63061df2d2e58542e5c777479
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:
Nachx639
2026-02-20 18:48:44 +01:00
committed by GitHub
parent c8ee33c162
commit 868fe48d58
4 changed files with 30 additions and 7 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
### 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/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.

View File

@@ -39,6 +39,9 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c
if (!client?.connect) {
return null;
}
if (method === "health") {
return null;
}
const role = client.connect.role ?? "operator";
const scopes = client.connect.scopes ?? [];
if (isNodeRoleMethod(method)) {

View File

@@ -111,9 +111,9 @@ async function expectMissingScopeAfterConnect(
try {
const res = await connectReq(ws, opts);
expect(res.ok).toBe(true);
const health = await rpcReq(ws, "health");
expect(health.ok).toBe(false);
expect(health.error?.message).toContain("missing scope");
const status = await rpcReq(ws, "status");
expect(status.ok).toBe(false);
expect(status.error?.message).toContain("missing scope");
} finally {
ws.close();
}
@@ -363,6 +363,18 @@ describe("gateway server auth/connect", () => {
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 () => {
const ws = await openWs(port);
const token = resolveGatewayTokenOrEnv();
@@ -400,9 +412,11 @@ describe("gateway server auth/connect", () => {
expect(presenceScopes).toEqual([]);
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");
expect(health.ok).toBe(false);
expect(health.error?.message).toContain("missing scope");
expect(health.ok).toBe(true);
ws.close();
});
@@ -680,9 +694,11 @@ describe("gateway server auth/connect", () => {
const ws = await openTailscaleWs(port);
const res = await connectReq(ws, { token: "secret", device: null });
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");
expect(health.ok).toBe(false);
expect(health.error?.message).toContain("missing scope");
expect(health.ok).toBe(true);
ws.close();
});
});

View File

@@ -96,6 +96,9 @@ describe("gateway role enforcement", () => {
const statusRes = await rpcReq(nodeWs, "status", {});
expect(statusRes.ok).toBe(false);
expect(statusRes.error?.message ?? "").toContain("unauthorized role");
const healthRes = await rpcReq(nodeWs, "health", {});
expect(healthRes.ok).toBe(true);
} finally {
nodeWs.close();
}