mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-21 06:51:01 +00:00
refactor(device): share missing-scope helper
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
summarizeDeviceTokens,
|
||||
} from "../../infra/device-pairing.js";
|
||||
import { normalizeDeviceAuthScopes } from "../../shared/device-auth.js";
|
||||
import { roleScopesAllow } from "../../shared/operator-scope-compat.js";
|
||||
import { resolveMissingRequestedScope } from "../../shared/operator-scope-compat.js";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
@@ -37,25 +37,6 @@ function redactPairedDevice(
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMissingRequestedScope(params: {
|
||||
role: string;
|
||||
requestedScopes: readonly string[];
|
||||
callerScopes: readonly string[];
|
||||
}): string | null {
|
||||
for (const scope of params.requestedScopes) {
|
||||
if (
|
||||
!roleScopesAllow({
|
||||
role: params.role,
|
||||
requestedScopes: [scope],
|
||||
allowedScopes: params.callerScopes,
|
||||
})
|
||||
) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function logDeviceTokenRotationDenied(params: {
|
||||
log: { warn: (message: string) => void };
|
||||
deviceId: string;
|
||||
@@ -234,7 +215,7 @@ export const deviceHandlers: GatewayRequestHandlers = {
|
||||
const missingScope = resolveMissingRequestedScope({
|
||||
role,
|
||||
requestedScopes,
|
||||
callerScopes,
|
||||
allowedScopes: callerScopes,
|
||||
});
|
||||
if (missingScope) {
|
||||
logDeviceTokenRotationDenied({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { normalizeDeviceAuthScopes } from "../shared/device-auth.js";
|
||||
import { roleScopesAllow } from "../shared/operator-scope-compat.js";
|
||||
import { resolveMissingRequestedScope, roleScopesAllow } from "../shared/operator-scope-compat.js";
|
||||
import {
|
||||
createAsyncLock,
|
||||
pruneExpiredPending,
|
||||
@@ -256,25 +256,6 @@ function scopesWithinApprovedDeviceBaseline(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveMissingRequestedScope(params: {
|
||||
role: string;
|
||||
requestedScopes: readonly string[];
|
||||
callerScopes: readonly string[];
|
||||
}): string | null {
|
||||
for (const scope of params.requestedScopes) {
|
||||
if (
|
||||
!roleScopesAllow({
|
||||
role: params.role,
|
||||
requestedScopes: [scope],
|
||||
allowedScopes: params.callerScopes,
|
||||
})
|
||||
) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function listDevicePairing(baseDir?: string): Promise<DevicePairingList> {
|
||||
const state = await loadState(baseDir);
|
||||
const pending = Object.values(state.pendingById).toSorted((a, b) => b.ts - a.ts);
|
||||
@@ -377,7 +358,7 @@ export async function approveDevicePairing(
|
||||
const missingScope = resolveMissingRequestedScope({
|
||||
role: pending.role,
|
||||
requestedScopes: normalizeDeviceAuthScopes(pending.scopes),
|
||||
callerScopes: options.callerScopes,
|
||||
allowedScopes: options.callerScopes,
|
||||
});
|
||||
if (missingScope) {
|
||||
return { status: "forbidden", missingScope };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { roleScopesAllow } from "./operator-scope-compat.js";
|
||||
import { resolveMissingRequestedScope, roleScopesAllow } from "./operator-scope-compat.js";
|
||||
|
||||
describe("roleScopesAllow", () => {
|
||||
it("allows empty requested scope lists regardless of granted scopes", () => {
|
||||
@@ -130,4 +130,24 @@ describe("roleScopesAllow", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns the first missing requested scope with operator compatibility", () => {
|
||||
expect(
|
||||
resolveMissingRequestedScope({
|
||||
role: "operator",
|
||||
requestedScopes: ["operator.read", "operator.write", "operator.approvals"],
|
||||
allowedScopes: ["operator.write"],
|
||||
}),
|
||||
).toBe("operator.approvals");
|
||||
});
|
||||
|
||||
it("returns null when all requested scopes are satisfied", () => {
|
||||
expect(
|
||||
resolveMissingRequestedScope({
|
||||
role: "node",
|
||||
requestedScopes: ["system.run"],
|
||||
allowedScopes: ["system.run", "operator.admin"],
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,3 +47,22 @@ export function roleScopesAllow(params: {
|
||||
}
|
||||
return requested.every((scope) => operatorScopeSatisfied(scope, allowedSet));
|
||||
}
|
||||
|
||||
export function resolveMissingRequestedScope(params: {
|
||||
role: string;
|
||||
requestedScopes: readonly string[];
|
||||
allowedScopes: readonly string[];
|
||||
}): string | null {
|
||||
for (const scope of params.requestedScopes) {
|
||||
if (
|
||||
!roleScopesAllow({
|
||||
role: params.role,
|
||||
requestedScopes: [scope],
|
||||
allowedScopes: params.allowedScopes,
|
||||
})
|
||||
) {
|
||||
return scope;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user