refactor(security): unify gateway scope authorization flows

This commit is contained in:
Peter Steinberger
2026-02-19 15:06:28 +01:00
parent f4b288b8f7
commit 2777d8ad93
14 changed files with 202 additions and 86 deletions

View File

@@ -75,7 +75,8 @@ vi.mock("./client.js", () => ({
},
}));
const { buildGatewayConnectionDetails, callGateway } = await import("./call.js");
const { buildGatewayConnectionDetails, callGateway, callGatewayCli, callGatewayScoped } =
await import("./call.js");
function resetGatewayCallMocks() {
loadConfig.mockReset();
@@ -198,13 +199,23 @@ describe("callGateway url resolution", () => {
expect(lastClientOptions?.token).toBe("explicit-token");
});
it("keeps legacy admin scopes when call scopes are omitted", async () => {
it("uses least-privilege scopes by default for non-CLI callers", async () => {
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "loopback" } });
resolveGatewayPort.mockReturnValue(18789);
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
await callGateway({ method: "health" });
expect(lastClientOptions?.scopes).toEqual(["operator.read"]);
});
it("keeps legacy admin scopes for explicit CLI callers", async () => {
loadConfig.mockReturnValue({ gateway: { mode: "local", bind: "loopback" } });
resolveGatewayPort.mockReturnValue(18789);
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
await callGatewayCli({ method: "health" });
expect(lastClientOptions?.scopes).toEqual([
"operator.admin",
"operator.approvals",
@@ -217,10 +228,10 @@ describe("callGateway url resolution", () => {
resolveGatewayPort.mockReturnValue(18789);
pickPrimaryTailnetIPv4.mockReturnValue(undefined);
await callGateway({ method: "health", scopes: ["operator.read"] });
await callGatewayScoped({ method: "health", scopes: ["operator.read"] });
expect(lastClientOptions?.scopes).toEqual(["operator.read"]);
await callGateway({ method: "health", scopes: [] });
await callGatewayScoped({ method: "health", scopes: [] });
expect(lastClientOptions?.scopes).toEqual([]);
});
});