190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
|
|
let runtimeRoot = "";
|
|
let data: typeof import("../src/lib/boss-data");
|
|
let permissions: typeof import("../src/lib/boss-permissions");
|
|
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data")["readState"]>>;
|
|
|
|
async function setup() {
|
|
if (!runtimeRoot) {
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-rbac-permissions-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
}
|
|
if (!data) {
|
|
data = await import("../src/lib/boss-data.ts");
|
|
baseState = structuredClone(await data.readState());
|
|
}
|
|
if (!permissions) {
|
|
permissions = await import("../src/lib/boss-permissions.ts");
|
|
}
|
|
}
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) {
|
|
await rm(runtimeRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test.beforeEach(async () => {
|
|
await setup();
|
|
await data.writeState({
|
|
...structuredClone(baseState),
|
|
accountDeviceGrants: [],
|
|
accountProjectGrants: [],
|
|
accountSkillGrants: [],
|
|
skillCatalog: [],
|
|
permissionAuditLogs: [],
|
|
});
|
|
});
|
|
|
|
test("highest admin can access every device and project without explicit grants", async () => {
|
|
const state = await data.readState();
|
|
const session = {
|
|
account: "krisolo",
|
|
role: "highest_admin" as const,
|
|
displayName: "Boss 超级管理员",
|
|
};
|
|
|
|
assert.equal(permissions.canAccessDevice(state, session, "mac-studio", "device.view"), true);
|
|
assert.equal(permissions.canAccessProject(state, session, "audit-collab", "project.view"), true);
|
|
});
|
|
|
|
test("device.view grant gives project read visibility but not thread chat", async () => {
|
|
const state = await data.readState();
|
|
state.accountDeviceGrants = [
|
|
{
|
|
grantId: "grant-device-view",
|
|
account: "worker@example.com",
|
|
deviceId: "mac-studio",
|
|
permissions: ["device.view"],
|
|
grantedBy: "krisolo",
|
|
grantedAt: "2026-04-26T12:00:00+08:00",
|
|
},
|
|
];
|
|
await data.writeState(state);
|
|
|
|
const next = await data.readState();
|
|
const session = {
|
|
account: "worker@example.com",
|
|
role: "member" as const,
|
|
displayName: "Worker",
|
|
};
|
|
|
|
assert.equal(permissions.canAccessDevice(next, session, "mac-studio", "device.view"), true);
|
|
assert.equal(permissions.canAccessProject(next, session, "master-agent", "project.view"), true);
|
|
assert.equal(permissions.canAccessProject(next, session, "master-agent", "thread.chat"), false);
|
|
});
|
|
|
|
test("explicit project thread.chat grant allows posting to that project", async () => {
|
|
const state = await data.readState();
|
|
state.accountProjectGrants = [
|
|
{
|
|
grantId: "grant-thread-chat",
|
|
account: "worker@example.com",
|
|
projectId: "master-agent",
|
|
permissions: ["project.view", "thread.chat", "master_agent.ask"],
|
|
grantedBy: "krisolo",
|
|
grantedAt: "2026-04-26T12:00:00+08:00",
|
|
},
|
|
];
|
|
await data.writeState(state);
|
|
|
|
const next = await data.readState();
|
|
const session = {
|
|
account: "worker@example.com",
|
|
role: "member" as const,
|
|
displayName: "Worker",
|
|
};
|
|
|
|
assert.equal(permissions.canAccessProject(next, session, "master-agent", "project.view"), true);
|
|
assert.equal(permissions.canAccessProject(next, session, "master-agent", "thread.chat"), true);
|
|
assert.equal(permissions.canAccessProject(next, session, "master-agent", "computer.control"), false);
|
|
});
|
|
|
|
test("expired grants are ignored", async () => {
|
|
const state = await data.readState();
|
|
state.accountDeviceGrants = [
|
|
{
|
|
grantId: "expired-device-grant",
|
|
account: "worker@example.com",
|
|
deviceId: "mac-studio",
|
|
permissions: ["device.view"],
|
|
grantedBy: "krisolo",
|
|
grantedAt: "2026-04-25T12:00:00+08:00",
|
|
expiresAt: "2000-01-01T00:00:00.000Z",
|
|
},
|
|
];
|
|
await data.writeState(state);
|
|
|
|
const next = await data.readState();
|
|
const session = {
|
|
account: "worker@example.com",
|
|
role: "member" as const,
|
|
displayName: "Worker",
|
|
};
|
|
|
|
assert.equal(permissions.canAccessDevice(next, session, "mac-studio", "device.view"), false);
|
|
});
|
|
|
|
test("legacy device account ownership remains a compatibility fallback", async () => {
|
|
const state = await data.readState();
|
|
state.devices.push({
|
|
id: "worker-mac",
|
|
name: "Worker Mac",
|
|
avatar: "W",
|
|
account: "worker@example.com",
|
|
source: "production",
|
|
status: "online",
|
|
projects: ["worker-project"],
|
|
quota5h: 0,
|
|
quota7d: 0,
|
|
lastSeenAt: "2026-04-26T12:00:00+08:00",
|
|
preferredExecutionMode: "cli",
|
|
});
|
|
state.projects.push({
|
|
id: "worker-project",
|
|
name: "Worker Project",
|
|
pinned: false,
|
|
systemPinned: false,
|
|
deviceIds: ["worker-mac"],
|
|
preview: "Owned by worker.",
|
|
updatedAt: "2026-04-26T12:00:00+08:00",
|
|
lastMessageAt: "2026-04-26T12:00:00+08:00",
|
|
isGroup: false,
|
|
threadMeta: {
|
|
projectId: "worker-project",
|
|
threadId: "thread-worker-project",
|
|
threadDisplayName: "Worker Project",
|
|
folderName: "Worker",
|
|
activityIconCount: 0,
|
|
updatedAt: "2026-04-26T12:00:00+08:00",
|
|
codexThreadRef: "thread-worker-project",
|
|
codexFolderRef: "worker",
|
|
},
|
|
groupMembers: [],
|
|
createdByAgent: true,
|
|
collaborationMode: "development",
|
|
approvalState: "not_required",
|
|
unreadCount: 0,
|
|
riskLevel: "low",
|
|
messages: [],
|
|
goals: [],
|
|
versions: [],
|
|
});
|
|
await data.writeState(state);
|
|
const next = await data.readState();
|
|
const session = {
|
|
account: "worker@example.com",
|
|
role: "member" as const,
|
|
displayName: "Worker",
|
|
};
|
|
|
|
assert.equal(permissions.canAccessDevice(next, session, "worker-mac", "device.view"), true);
|
|
assert.equal(permissions.canAccessProject(next, session, "worker-project", "project.view"), true);
|
|
});
|