small bug fixes
This commit is contained in:
@@ -190,6 +190,12 @@ export async function handleClientCommandRoute(
|
||||
return Response.json({ ok: false, error: error.message || "macOS permission request failed" }, { status: 504 });
|
||||
}
|
||||
} else if (action === "script_exec") {
|
||||
try {
|
||||
requireFeatureAccess(user, "console");
|
||||
} catch (error) {
|
||||
if (error instanceof Response) return error;
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
const scriptContent = body?.script || "";
|
||||
const scriptType = body?.scriptType || "powershell";
|
||||
const cmdId = uuidv4();
|
||||
@@ -213,6 +219,12 @@ export async function handleClientCommandRoute(
|
||||
return Response.json({ ok: false, error: error.message }, { status: 500 });
|
||||
}
|
||||
} else if (action === "voice_capabilities") {
|
||||
try {
|
||||
requireFeatureAccess(user, "voice");
|
||||
} catch (error) {
|
||||
if (error instanceof Response) return error;
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
const cmdId = uuidv4();
|
||||
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
deleteUser,
|
||||
getUserById,
|
||||
setUserClientAccessScope,
|
||||
setUserClientAccessRule,
|
||||
getUserClientAccessScope,
|
||||
listUserClientRuleIdsByAccess,
|
||||
setUserFeaturePermission,
|
||||
} from "../../users";
|
||||
import { upsertClientRow, deleteClientRow } from "../../db";
|
||||
@@ -169,6 +172,75 @@ describe("remote desktop alternate command gates", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("alternate command feature gates", () => {
|
||||
test("operator with console feature OFF cannot execute a saved script through the HTTP command route", async () => {
|
||||
const auth = await makeUserWithToken("operator");
|
||||
const clientId = registerTestClient();
|
||||
setUserFeaturePermission(auth.user.id, "console", false);
|
||||
const res = await command(auth.token, clientId, "script_exec", { script: "echo blocked" });
|
||||
expect(res!.status).toBe(403);
|
||||
});
|
||||
|
||||
test("operator with voice feature OFF cannot probe voice capabilities through the HTTP command route", async () => {
|
||||
const auth = await makeUserWithToken("operator");
|
||||
const clientId = registerTestClient();
|
||||
setUserFeaturePermission(auth.user.id, "voice", false);
|
||||
const res = await command(auth.token, clientId, "voice_capabilities");
|
||||
expect(res!.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("client scope aggregation and bulk mutation gates", () => {
|
||||
test("country aggregation only includes clients in an operator's allowlist", async () => {
|
||||
const auth = await makeUserWithToken("operator", false);
|
||||
const allowedId = registerTestClient();
|
||||
const deniedId = registerTestClient();
|
||||
upsertClientRow({ id: allowedId, country: "Q1" });
|
||||
upsertClientRow({ id: deniedId, country: "Q2" });
|
||||
setUserClientAccessScope(auth.user.id, "allowlist");
|
||||
setUserClientAccessRule(auth.user.id, allowedId, "allow");
|
||||
expect(getUserClientAccessScope(auth.user.id)).toBe("allowlist");
|
||||
expect(listUserClientRuleIdsByAccess(auth.user.id, "allow")).toEqual([allowedId]);
|
||||
|
||||
const url = new URL("https://localhost/api/clients/countries");
|
||||
const res = await handleClientRoutes(
|
||||
new Request(url, { headers: { Authorization: `Bearer ${auth.token}` } }),
|
||||
url,
|
||||
mockServer,
|
||||
deps,
|
||||
);
|
||||
const body = await res!.json();
|
||||
expect(body.countries).toEqual([{ code: "Q1", count: 1 }]);
|
||||
});
|
||||
|
||||
test("bulk offline cleanup cannot delete a client outside an operator's allowlist", async () => {
|
||||
const auth = await makeUserWithToken("operator", false);
|
||||
const allowedId = registerTestClient();
|
||||
const deniedId = registerTestClient();
|
||||
clientManager.deleteClient(allowedId);
|
||||
clientManager.deleteClient(deniedId);
|
||||
upsertClientRow({ id: allowedId, online: 0 });
|
||||
upsertClientRow({ id: deniedId, online: 0 });
|
||||
setUserClientAccessScope(auth.user.id, "allowlist");
|
||||
setUserClientAccessRule(auth.user.id, allowedId, "allow");
|
||||
expect(getUserClientAccessScope(auth.user.id)).toBe("allowlist");
|
||||
expect(listUserClientRuleIdsByAccess(auth.user.id, "allow")).toEqual([allowedId]);
|
||||
|
||||
const url = new URL("https://localhost/api/clients/offline");
|
||||
const res = await handleClientRoutes(
|
||||
new Request(url, { method: "DELETE", headers: { Authorization: `Bearer ${auth.token}` } }),
|
||||
url,
|
||||
mockServer,
|
||||
deps,
|
||||
);
|
||||
expect(res!.status).toBe(200);
|
||||
expect((await res!.json()).count).toBeGreaterThan(0);
|
||||
const { clientExists } = await import("../../db");
|
||||
expect(clientExists(allowedId)).toBe(false);
|
||||
expect(clientExists(deniedId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clients:reconnect gates", () => {
|
||||
test("operator (default) can reconnect", async () => {
|
||||
const auth = await makeUserWithToken("operator");
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
clientExists,
|
||||
deleteClientRow,
|
||||
deleteOfflineClientRows,
|
||||
deleteOfflineClientRowsForScope,
|
||||
getClientOnlineState,
|
||||
getClientIp,
|
||||
isIpBanned,
|
||||
@@ -150,7 +151,16 @@ export async function handleClientRoutes(
|
||||
if (url.pathname === "/api/clients/countries") {
|
||||
const user = await authenticateRequest(req);
|
||||
if (!user) return new Response("Unauthorized", { status: 401 });
|
||||
return Response.json({ countries: listDistinctCountries() }, { headers: deps.CORS_HEADERS });
|
||||
if (user.role === "admin") {
|
||||
return Response.json({ countries: listDistinctCountries() }, { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
const scope = getUserClientAccessScope(user.userId);
|
||||
const allowedClientIds = scope === "allowlist" ? listUserClientRuleIdsByAccess(user.userId, "allow") : scope === "none" ? [] : undefined;
|
||||
const deniedClientIds = scope === "denylist" ? listUserClientRuleIdsByAccess(user.userId, "deny") : undefined;
|
||||
return Response.json(
|
||||
{ countries: listDistinctCountries(allowedClientIds, deniedClientIds) },
|
||||
{ headers: deps.CORS_HEADERS },
|
||||
);
|
||||
}
|
||||
|
||||
const logsMatch = url.pathname.match(/^\/api\/clients\/(.+)\/logs$/);
|
||||
@@ -331,7 +341,15 @@ export async function handleClientRoutes(
|
||||
if (error instanceof Response) return error;
|
||||
return new Response("Forbidden", { status: 403 });
|
||||
}
|
||||
const count = deleteOfflineClientRows();
|
||||
let count: number;
|
||||
if (user.role === "admin") {
|
||||
count = deleteOfflineClientRows();
|
||||
} else {
|
||||
const scope = getUserClientAccessScope(user.userId);
|
||||
const allowedClientIds = scope === "allowlist" ? listUserClientRuleIdsByAccess(user.userId, "allow") : scope === "none" ? [] : undefined;
|
||||
const deniedClientIds = scope === "denylist" ? listUserClientRuleIdsByAccess(user.userId, "deny") : undefined;
|
||||
count = deleteOfflineClientRowsForScope(allowedClientIds, deniedClientIds);
|
||||
}
|
||||
notifyDashboardViewers();
|
||||
const ip = server.requestIP(req)?.address || "unknown";
|
||||
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, details: `wipe_offline_clients: removed ${count}`, success: true });
|
||||
|
||||
Reference in New Issue
Block a user