better permissions mac

This commit is contained in:
test
2026-06-04 20:41:10 -05:00
parent 4e267f4d36
commit a35de5d129
15 changed files with 256 additions and 10 deletions
+11
View File
@@ -2589,6 +2589,13 @@ tbody tr:hover {
gap: 6px;
flex-wrap: wrap;
}
.cv-perm-summary {
width: 100%;
font-size: 11px;
line-height: 1.25;
}
.cv-perm-summary.is-ok { color: #86efac; }
.cv-perm-summary.is-warn { color: #fcd34d; }
.cv-perm-chip {
display: inline-flex;
align-items: center;
@@ -2599,7 +2606,11 @@ tbody tr:hover {
border: 1px solid transparent;
font-size: 11px;
white-space: nowrap;
cursor: pointer;
font: inherit;
appearance: none;
}
.cv-perm-chip:hover { filter: brightness(1.08); }
.cv-perm-chip.is-ok { background: rgba(16, 185, 129, 0.1); color: #86efac; border-color: rgba(16, 185, 129, 0.25); }
.cv-perm-chip.is-missing { background: rgba(244, 63, 94, 0.1); color: #fda4af; border-color: rgba(244, 63, 94, 0.25); }
+65
View File
@@ -75,6 +75,7 @@ let contextCard = null;
let availableOsList = new Set();
let rendererInitialized = false;
let lastKnownTotalPages = 1;
let currentServerVersion = "";
window.setDashboardPageBounds = (currentPage, totalPages) => {
lastKnownTotalPages = Math.max(1, Number(totalPages) || 1);
@@ -114,8 +115,14 @@ async function loadServerVersion() {
const version = typeof payload?.version === "string" && payload.version.trim()
? payload.version.trim()
: "unknown";
currentServerVersion = version;
setServerVersionLabel(version, version === "unknown" ? "warn" : "ok");
if (rendererInitialized) {
state.lastDigest = "";
renderCachedClients({ force: true });
}
} catch {
currentServerVersion = "unavailable";
setServerVersionLabel("unavailable", "bad");
}
}
@@ -173,6 +180,62 @@ function getClientCard(clientId) {
return document.querySelector(`[data-client-row][data-id="${selectorId}"]`);
}
function macPermissionLabels(keys = []) {
const labels = {
accessibility: "Accessibility",
screenRecording: "Screen Recording",
fullDiskAccess: "Full Disk Access",
};
return keys.map((key) => labels[key] || key).join(", ");
}
async function requestMacPermissions(clientId, permissionKey = "") {
if (!clientId) return;
if (!isClientOnline(clientId)) {
alert("Client is offline. macOS permissions can only be requested while the client is online.");
return;
}
if (detectClientPlatform(clientId) !== "mac") {
alert("This permission request is only available for macOS clients.");
return;
}
const requested = permissionKey ? [permissionKey] : ["accessibility", "screenRecording", "fullDiskAccess"];
const label = macPermissionLabels(requested) || "macOS permissions";
const proceed = confirm(
`Request ${label} from ${clientId}?\n\n` +
"This can show a macOS prompt on the target Mac. " +
"Full Disk Access must be granted in System Settings; the agent will open the Privacy pane when possible.",
);
if (!proceed) return;
try {
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "darwin_request_permissions",
permissions: requested,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
alert(data.error || "macOS permission request failed.");
return;
}
const missing = Array.isArray(data.missing) ? data.missing : [];
const stillMissingRequested = missing.filter((key) => requested.includes(key));
if (stillMissingRequested.length) {
alert(`Permission request completed, but still missing: ${macPermissionLabels(stillMissingRequested)}`);
} else {
alert(`${label} is granted.`);
}
setTimeout(() => loadWithOptions({ force: true }), 300);
} catch (err) {
alert("macOS permission request failed: " + err.message);
}
}
window.__uninstallingClientIds = window.__uninstallingClientIds || new Set();
function flipFallAndRemove(clientId, { delayMs = 0 } = {}) {
@@ -846,7 +909,9 @@ function initializeRenderer() {
requestPreview,
requestThumbnail,
pingClient: (id) => sendCommand(id, "ping"),
onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey),
userRole: currentUser?.role,
getServerVersion: () => currentServerVersion,
getDisplayFields: () => displayFields,
});
rendererSetLayout = rSetLayout;
+28 -5
View File
@@ -232,12 +232,14 @@ function macPermissionBadgeHtml(client) {
function macPermissionDetailHtml(client) {
const state = macPermissionState(client);
if (!state) return "";
const ok = state.missing.length === 0;
const summary = ok ? "All required permissions granted" : `Missing: ${state.missing.map((item) => item.label).join(", ")}`;
const chips = state.items.map((item) => `
<span class="cv-perm-chip ${item.granted ? "is-ok" : "is-missing"}">
<button type="button" class="cv-perm-chip ${item.granted ? "is-ok" : "is-missing"}" data-mac-permission-key="${escapeHtml(item.key)}" title="Request ${escapeHtml(item.label)} permission">
<i class="fa-solid ${item.granted ? "fa-check" : "fa-xmark"}"></i>${escapeHtml(item.label)}
</span>
</button>
`).join("");
return `<div class="cv-field cv-field-wide"><span class="cv-field-label">macOS Permissions</span><span class="cv-field-value cv-perm-list">${chips}</span></div>`;
return `<div class="cv-field cv-field-wide cv-field-macperms"><span class="cv-field-label">macOS Permissions</span><span class="cv-field-value cv-perm-list"><span class="cv-perm-summary ${ok ? "is-ok" : "is-warn"}">${escapeHtml(summary)}</span>${chips}</span></div>`;
}
function shortOsLabel(osRaw = "") {
@@ -259,6 +261,17 @@ function shortOsLabel(osRaw = "") {
return osRaw || "?";
}
function normalizeVersion(version) {
return String(version || "").trim().replace(/^v/i, "");
}
function isClientVersionCurrent(clientVersion, serverVersion) {
const client = normalizeVersion(clientVersion);
const server = normalizeVersion(serverVersion);
if (!client || !server || server === "unknown" || server === "unavailable") return true;
return client === server;
}
export function createRenderer({
grid,
totalPill,
@@ -268,7 +281,9 @@ export function createRenderer({
requestPreview,
requestThumbnail,
pingClient,
onMacPermissionRequest,
userRole,
getServerVersion,
getDisplayFields,
}) {
const isViewer = userRole === "viewer";
@@ -367,6 +382,14 @@ export function createRenderer({
return;
}
const macPermBtn = e.target.closest(".cv-perm-chip[data-mac-permission-key]");
if (macPermBtn) {
e.preventDefault();
e.stopPropagation();
if (onMacPermissionRequest) onMacPermissionRequest(clientId, card, macPermBtn.dataset.macPermissionKey);
return;
}
const copyBtn = e.target.closest(".copy-id-btn");
if (copyBtn) {
e.stopPropagation();
@@ -803,7 +826,7 @@ export function createRenderer({
const showBattery = showField("battery");
const cpuHtml = client.cpu ? cpuBadgeHtml(client.cpu) : "—";
const batteryIndicator = showBattery ? batteryHtml(client) : "";
const verLatest = String(client.version || "").startsWith("2.0.");
const verLatest = isClientVersionCurrent(client.version, typeof getServerVersion === "function" ? getServerVersion() : "");
const metaParts = [
showField("system") ? `<span class="cv-os cv-tone-${os.tone}"><i class="fa ${os.icon}"></i> ${escapeHtml(shortOsLabel(client.os))}</span>` : "",
@@ -956,7 +979,7 @@ export function createRenderer({
const customTag = String(client.customTag || "").trim();
const displayName = nickname || client.host || deviceId;
const userLine = client.user || "unknown";
const verLatest = String(client.version || "").startsWith("2.0.");
const verLatest = isClientVersionCurrent(client.version, typeof getServerVersion === "function" ? getServerVersion() : "");
const hasGroup = !!String(client.groupName || "").trim();
const showHost = nickname && client.host && nickname !== client.host;
const showHardware = showField("hardware");
+1
View File
@@ -119,6 +119,7 @@ export type CommandType =
| "desktop_select_display"
| "desktop_enable_mouse"
| "desktop_enable_keyboard"
| "darwin_request_permissions"
| "webrtc_publish"
| "webrtc_stop"
| "webrtc_p2p_offer"
@@ -2,7 +2,7 @@ import { v4 as uuidv4 } from "uuid";
import { authenticateRequest } from "../../auth";
import { AuditAction, logAudit } from "../../auditLog";
import * as clientManager from "../../clientManager";
import { deleteClientRow } from "../../db";
import { deleteClientRow, upsertClientRow } from "../../db";
import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
import { requireClientAccess, requireFeatureAccess, requirePermission } from "../../rbac";
@@ -106,6 +106,46 @@ export async function handleClientCommandRoute(
} else if (action === "desktop_start") {
target.ws.send(encodeMessage({ type: "command", commandType: "desktop_start", id: uuidv4() }));
metrics.recordCommand("desktop_start");
} else if (action === "darwin_request_permissions") {
const targetOs = String(target.os || "").toLowerCase();
if (!targetOs.includes("darwin") && !targetOs.includes("mac")) {
return Response.json({ ok: false, error: "macOS permission requests are only available for macOS clients" }, { status: 400 });
}
const requested = Array.isArray(body?.permissions)
? body.permissions.filter((p: unknown) => typeof p === "string").slice(0, 8)
: [];
const cmdId = uuidv4();
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
deps.pendingCommandReplies.delete(cmdId);
reject(new Error("macOS permission request timed out"));
}, 45_000);
deps.pendingCommandReplies.set(cmdId, { resolve, reject, timeout, clientId: targetId });
});
target.ws.send(encodeMessage({ type: "command", commandType: "darwin_request_permissions", id: cmdId, payload: { permissions: requested } } as any));
metrics.recordCommand("darwin_request_permissions");
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, targetClientId: targetId, success: true, details: "darwin_request_permissions" });
try {
const result = await replyPromise;
let detail: any = {};
if (result.message) {
try { detail = JSON.parse(result.message); } catch { detail = {}; }
}
if (detail.permissions && typeof detail.permissions === "object" && !Array.isArray(detail.permissions)) {
target.permissions = detail.permissions;
upsertClientRow({ id: targetId, permissions: target.permissions, lastSeen: target.lastSeen, online: target.online ? 1 : 0 });
}
return Response.json({
ok: result.ok,
permissions: detail.permissions || null,
missing: Array.isArray(detail.missing) ? detail.missing : [],
message: result.message || "",
}, { headers: deps.CORS_HEADERS });
} catch (error: any) {
return Response.json({ ok: false, error: error.message || "macOS permission request failed" }, { status: 504 });
}
} else if (action === "script_exec") {
const scriptContent = body?.script || "";
const scriptType = body?.scriptType || "powershell";