fix: constrain device bootstrap scope checks by role prefix (#57258) (thanks @jlapenna) (#57258)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Joe LaPenna
2026-04-04 07:21:01 -07:00
committed by GitHub
parent a2e0a094c1
commit bb82fe8f19
4 changed files with 37 additions and 10 deletions

View File

@@ -80,26 +80,33 @@ describe("roleScopesAllow", () => {
).toBe(false);
});
it("uses strict matching for non-operator roles", () => {
it("uses strict matching with role-prefix partitioning for non-operator roles", () => {
expect(
roleScopesAllow({
role: "node",
requestedScopes: ["system.run"],
allowedScopes: ["operator.admin", "system.run"],
requestedScopes: ["node.exec"],
allowedScopes: ["operator.admin", "node.exec"],
}),
).toBe(true);
expect(
roleScopesAllow({
role: "node",
requestedScopes: ["system.run"],
requestedScopes: ["node.exec"],
allowedScopes: ["operator.admin"],
}),
).toBe(false);
expect(
roleScopesAllow({
role: "node",
requestedScopes: ["operator.read"],
allowedScopes: ["operator.read", "node.exec"],
}),
).toBe(false);
expect(
roleScopesAllow({
role: " node ",
requestedScopes: [" system.run ", "system.run", " "],
allowedScopes: ["system.run", "operator.admin"],
requestedScopes: [" node.exec ", "node.exec", " "],
allowedScopes: ["node.exec", "operator.admin"],
}),
).toBe(true);
});
@@ -145,8 +152,8 @@ describe("roleScopesAllow", () => {
expect(
resolveMissingRequestedScope({
role: "node",
requestedScopes: ["system.run"],
allowedScopes: ["system.run", "operator.admin"],
requestedScopes: ["node.exec"],
allowedScopes: ["node.exec", "operator.admin"],
}),
).toBeNull();
});

View File

@@ -16,7 +16,10 @@ function normalizeScopeList(scopes: readonly string[]): string[] {
}
function operatorScopeSatisfied(requestedScope: string, granted: Set<string>): boolean {
if (granted.has(OPERATOR_ADMIN_SCOPE) && requestedScope.startsWith(OPERATOR_SCOPE_PREFIX)) {
if (!requestedScope.startsWith(OPERATOR_SCOPE_PREFIX)) {
return false;
}
if (granted.has(OPERATOR_ADMIN_SCOPE)) {
return true;
}
if (requestedScope === OPERATOR_READ_SCOPE) {
@@ -43,7 +46,8 @@ export function roleScopesAllow(params: {
}
const allowedSet = new Set(allowed);
if (params.role.trim() !== OPERATOR_ROLE) {
return requested.every((scope) => allowedSet.has(scope));
const prefix = `${params.role.trim()}.`;
return requested.every((scope) => scope.startsWith(prefix) && allowedSet.has(scope));
}
return requested.every((scope) => operatorScopeSatisfied(scope, allowedSet));
}