mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -110,6 +110,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/device auth: reuse cached device-token scopes only for cached-token reconnects, while keeping explicit `deviceToken` scope requests and empty-cache fallbacks intact so reconnects preserve `operator.read` without breaking explicit auth flows. (#46032) Thanks @caicongyang.
|
||||
- Mattermost/config schema: accept `groups.*.requireMention` again so existing Mattermost configs no longer fail strict validation after upgrade. (#58271) Thanks @MoerAI.
|
||||
- Providers/OpenRouter failover: classify `403 "Key limit exceeded"` spending-limit responses as billing so model fallback continues instead of stopping on generic auth. (#59892) Thanks @rockcent.
|
||||
- Device pairing/security: keep non-operator device scope checks bound to the requested role prefix so bootstrap verification cannot redeem `operator.*` scopes through `node` auth. (#57258) Thanks @jlapenna.
|
||||
|
||||
## 2026.4.2
|
||||
|
||||
|
||||
@@ -286,6 +286,21 @@ describe("device bootstrap tokens", () => {
|
||||
).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("rejects cross-role scope escalation (node role requesting operator scopes)", async () => {
|
||||
const baseDir = await createTempDir();
|
||||
const issued = await issueDeviceBootstrapToken({ baseDir });
|
||||
|
||||
await expect(
|
||||
verifyBootstrapToken(baseDir, issued.token, {
|
||||
role: "node",
|
||||
scopes: ["operator.read"],
|
||||
}),
|
||||
).resolves.toEqual({ ok: false, reason: "bootstrap_token_invalid" });
|
||||
|
||||
const raw = await fs.readFile(resolveBootstrapPath(baseDir), "utf8");
|
||||
expect(raw).toContain(issued.token);
|
||||
});
|
||||
|
||||
it("supports explicitly bound bootstrap profiles", async () => {
|
||||
const baseDir = await createTempDir();
|
||||
const issued = await issueDeviceBootstrapToken({
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user