Gateway: align pairing scope checks for read access

This commit is contained in:
Shakker
2026-02-20 04:51:36 +00:00
committed by Shakker
parent 86f207adb0
commit 525d6e0671
8 changed files with 256 additions and 16 deletions

View File

@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { roleScopesAllow } from "./operator-scope-compat.js";
describe("roleScopesAllow", () => {
it("treats operator.read as satisfied by read/write/admin scopes", () => {
expect(
roleScopesAllow({
role: "operator",
requestedScopes: ["operator.read"],
allowedScopes: ["operator.read"],
}),
).toBe(true);
expect(
roleScopesAllow({
role: "operator",
requestedScopes: ["operator.read"],
allowedScopes: ["operator.write"],
}),
).toBe(true);
expect(
roleScopesAllow({
role: "operator",
requestedScopes: ["operator.read"],
allowedScopes: ["operator.admin"],
}),
).toBe(true);
});
it("keeps non-read operator scopes explicit", () => {
expect(
roleScopesAllow({
role: "operator",
requestedScopes: ["operator.write"],
allowedScopes: ["operator.admin"],
}),
).toBe(false);
});
it("uses strict matching for non-operator roles", () => {
expect(
roleScopesAllow({
role: "node",
requestedScopes: ["system.run"],
allowedScopes: ["operator.admin", "system.run"],
}),
).toBe(true);
expect(
roleScopesAllow({
role: "node",
requestedScopes: ["system.run"],
allowedScopes: ["operator.admin"],
}),
).toBe(false);
});
});

View File

@@ -0,0 +1,46 @@
const OPERATOR_ROLE = "operator";
const OPERATOR_ADMIN_SCOPE = "operator.admin";
const OPERATOR_READ_SCOPE = "operator.read";
const OPERATOR_WRITE_SCOPE = "operator.write";
function normalizeScopeList(scopes: readonly string[]): string[] {
const out = new Set<string>();
for (const scope of scopes) {
const trimmed = scope.trim();
if (trimmed) {
out.add(trimmed);
}
}
return [...out];
}
function operatorScopeSatisfied(requestedScope: string, granted: Set<string>): boolean {
if (requestedScope === OPERATOR_READ_SCOPE) {
return (
granted.has(OPERATOR_READ_SCOPE) ||
granted.has(OPERATOR_WRITE_SCOPE) ||
granted.has(OPERATOR_ADMIN_SCOPE)
);
}
return granted.has(requestedScope);
}
export function roleScopesAllow(params: {
role: string;
requestedScopes: readonly string[];
allowedScopes: readonly string[];
}): boolean {
const requested = normalizeScopeList(params.requestedScopes);
if (requested.length === 0) {
return true;
}
const allowed = normalizeScopeList(params.allowedScopes);
if (allowed.length === 0) {
return false;
}
const allowedSet = new Set(allowed);
if (params.role.trim() !== OPERATOR_ROLE) {
return requested.every((scope) => allowedSet.has(scope));
}
return requested.every((scope) => operatorScopeSatisfied(scope, allowedSet));
}