small bug fixes

This commit is contained in:
KingKDot
2026-07-22 21:10:56 -05:00
parent c8f6a8742e
commit dbe0e8ee21
6 changed files with 201 additions and 5 deletions
@@ -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 });