254 lines
7.9 KiB
TypeScript
254 lines
7.9 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import {
|
|
ALL_FEATURES,
|
|
canUserAccessFeature,
|
|
createUser,
|
|
deleteUser,
|
|
getUserByUsername,
|
|
getUserFeaturePermissions,
|
|
resetUserFeaturePermissions,
|
|
setUserFeaturePermission,
|
|
setUserFeaturePermissions,
|
|
type FeatureName,
|
|
} from "./users";
|
|
|
|
const createdUserIds: number[] = [];
|
|
|
|
async function createTempUser(role: "viewer" | "operator" | "admin") {
|
|
const username = `fp_${role.slice(0, 3)}_${Date.now().toString(36)}${Math.floor(Math.random() * 1000).toString(36)}`;
|
|
const result = await createUser(username, "Aa1!VeryLongTestPassword_2026", role, "test");
|
|
expect(result.success).toBe(true);
|
|
const user = getUserByUsername(username);
|
|
expect(user).not.toBeNull();
|
|
createdUserIds.push(user!.id);
|
|
return user!;
|
|
}
|
|
|
|
function cleanupCreatedUsers() {
|
|
while (createdUserIds.length > 0) {
|
|
const id = createdUserIds.pop();
|
|
if (typeof id === "number") {
|
|
deleteUser(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
describe("feature permissions", () => {
|
|
test("ALL_FEATURES contains the expected feature keys", () => {
|
|
expect(ALL_FEATURES).toEqual([
|
|
"console",
|
|
"remote_desktop",
|
|
"backstage",
|
|
"webcam",
|
|
"file_browser",
|
|
"processes",
|
|
"keylogger",
|
|
"voice",
|
|
"disconnect",
|
|
"reconnect",
|
|
"uninstall",
|
|
"client_metadata",
|
|
]);
|
|
});
|
|
|
|
test("admin always has every feature regardless of stored overrides", async () => {
|
|
try {
|
|
const admin = await createTempUser("admin");
|
|
|
|
for (const f of ALL_FEATURES) {
|
|
expect(canUserAccessFeature(admin.id, admin.role, f)).toBe(true);
|
|
}
|
|
|
|
// Even an explicit deny is ignored for admin role.
|
|
setUserFeaturePermission(admin.id, "webcam", false);
|
|
expect(canUserAccessFeature(admin.id, "admin", "webcam")).toBe(true);
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("viewer is denied every feature regardless of stored overrides", async () => {
|
|
try {
|
|
const viewer = await createTempUser("viewer");
|
|
|
|
for (const f of ALL_FEATURES) {
|
|
expect(canUserAccessFeature(viewer.id, viewer.role, f)).toBe(false);
|
|
}
|
|
|
|
// Granting access to a viewer at the per-feature level does not promote them.
|
|
setUserFeaturePermission(viewer.id, "console", true);
|
|
expect(canUserAccessFeature(viewer.id, "viewer", "console")).toBe(false);
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("operator defaults to having every feature enabled", async () => {
|
|
try {
|
|
const operator = await createTempUser("operator");
|
|
|
|
for (const f of ALL_FEATURES) {
|
|
expect(canUserAccessFeature(operator.id, operator.role, f)).toBe(true);
|
|
}
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("setUserFeaturePermission disables a single feature for an operator", async () => {
|
|
try {
|
|
const operator = await createTempUser("operator");
|
|
|
|
const result = setUserFeaturePermission(operator.id, "webcam", false);
|
|
expect(result.success).toBe(true);
|
|
|
|
expect(canUserAccessFeature(operator.id, operator.role, "webcam")).toBe(false);
|
|
// Other features remain enabled.
|
|
expect(canUserAccessFeature(operator.id, operator.role, "console")).toBe(true);
|
|
expect(canUserAccessFeature(operator.id, operator.role, "file_browser")).toBe(true);
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("setUserFeaturePermission can re-enable a previously denied feature", async () => {
|
|
try {
|
|
const operator = await createTempUser("operator");
|
|
|
|
setUserFeaturePermission(operator.id, "backstage", false);
|
|
expect(canUserAccessFeature(operator.id, operator.role, "backstage")).toBe(false);
|
|
|
|
setUserFeaturePermission(operator.id, "backstage", true);
|
|
expect(canUserAccessFeature(operator.id, operator.role, "backstage")).toBe(true);
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("setUserFeaturePermissions applies a bulk update", async () => {
|
|
try {
|
|
const operator = await createTempUser("operator");
|
|
|
|
const result = setUserFeaturePermissions(operator.id, {
|
|
webcam: false,
|
|
file_browser: false,
|
|
keylogger: true,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
|
|
expect(canUserAccessFeature(operator.id, operator.role, "webcam")).toBe(false);
|
|
expect(canUserAccessFeature(operator.id, operator.role, "file_browser")).toBe(false);
|
|
expect(canUserAccessFeature(operator.id, operator.role, "keylogger")).toBe(true);
|
|
// Untouched features keep their default (allowed).
|
|
expect(canUserAccessFeature(operator.id, operator.role, "console")).toBe(true);
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("setUserFeaturePermissions ignores unknown feature keys", async () => {
|
|
try {
|
|
const operator = await createTempUser("operator");
|
|
|
|
const result = setUserFeaturePermissions(operator.id, {
|
|
webcam: false,
|
|
// @ts-expect-error intentionally unknown feature key
|
|
not_a_feature: true,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
|
|
const perms = getUserFeaturePermissions(operator.id);
|
|
expect(perms.webcam).toBe(false);
|
|
// Unknown keys never get persisted.
|
|
expect((perms as any).not_a_feature).toBeUndefined();
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("setUserFeaturePermission rejects invalid feature names", async () => {
|
|
try {
|
|
const operator = await createTempUser("operator");
|
|
|
|
const result = setUserFeaturePermission(
|
|
operator.id,
|
|
"not_a_feature" as FeatureName,
|
|
false,
|
|
);
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain("Invalid feature");
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("getUserFeaturePermissions returns role-derived values", async () => {
|
|
try {
|
|
const admin = await createTempUser("admin");
|
|
const operator = await createTempUser("operator");
|
|
const viewer = await createTempUser("viewer");
|
|
|
|
const adminPerms = getUserFeaturePermissions(admin.id);
|
|
const operatorPerms = getUserFeaturePermissions(operator.id);
|
|
const viewerPerms = getUserFeaturePermissions(viewer.id);
|
|
|
|
for (const f of ALL_FEATURES) {
|
|
expect(adminPerms[f]).toBe(true);
|
|
expect(operatorPerms[f]).toBe(true);
|
|
expect(viewerPerms[f]).toBe(false);
|
|
}
|
|
|
|
// After a deny, the operator's map reflects it.
|
|
setUserFeaturePermission(operator.id, "processes", false);
|
|
const updated = getUserFeaturePermissions(operator.id);
|
|
expect(updated.processes).toBe(false);
|
|
expect(updated.console).toBe(true);
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("resetUserFeaturePermissions restores defaults (all enabled for operator)", async () => {
|
|
try {
|
|
const operator = await createTempUser("operator");
|
|
|
|
setUserFeaturePermissions(operator.id, {
|
|
webcam: false,
|
|
file_browser: false,
|
|
keylogger: false,
|
|
});
|
|
expect(canUserAccessFeature(operator.id, operator.role, "webcam")).toBe(false);
|
|
|
|
const result = resetUserFeaturePermissions(operator.id);
|
|
expect(result.success).toBe(true);
|
|
|
|
for (const f of ALL_FEATURES) {
|
|
expect(canUserAccessFeature(operator.id, operator.role, f)).toBe(true);
|
|
}
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
|
|
test("getUserFeaturePermissions for a missing user returns all-false", () => {
|
|
const perms = getUserFeaturePermissions(0);
|
|
for (const f of ALL_FEATURES) {
|
|
expect(perms[f]).toBe(false);
|
|
}
|
|
});
|
|
|
|
test("feature denials are scoped to the affected user", async () => {
|
|
try {
|
|
const a = await createTempUser("operator");
|
|
const b = await createTempUser("operator");
|
|
|
|
setUserFeaturePermission(a.id, "webcam", false);
|
|
|
|
expect(canUserAccessFeature(a.id, a.role, "webcam")).toBe(false);
|
|
expect(canUserAccessFeature(b.id, b.role, "webcam")).toBe(true);
|
|
} finally {
|
|
cleanupCreatedUsers();
|
|
}
|
|
});
|
|
});
|