Add Corporate Tools tab, Dell Command actions, and user-context refresh

Corporate Tools tab on PC Details, gated by endpoints.deploy and liveness.
Starts already-registered scheduled tasks; the agent's ScheduledTaskPolicy
allowlist remains the authority on what may actually run.

Dell Command | Update split into two actions (Scan, Apply Updates) because
they have very different consequences. Apply Updates carries a persistent
breakglass warning in the card naming Tanium as the primary update utility,
rendered between the row title and description so it is visible at all times
rather than only in the confirm dialog. Destructive actions keep the typed
RUN confirmation.

Fix scheduledtask_run being stripped by Configuration Policy: the
intersection in ALLOWED_ACTIONS_FOR dropped it on any endpoint whose
effective policy was non-empty, so every Corporate Tools click failed with
"not permitted by policy" regardless of the caller's role. Corporate tools
are role-gated only, via POLICY_EXEMPT_ACTIONS; policy keeps authority over
monitors and service/process primitives.

Fix blank PC Details page: watch(detailtabs) evaluates its source during
setup and read canDeploy before its const declaration, throwing a TDZ
ReferenceError that aborted setup with no API request made.

Mapped-drive refresh moves to the tray. SYSTEM cannot probe user-scoped
drives, so the service requests a re-probe over a named event and waits for
the spool collectedat stamp to advance before collecting. The probe task is
registered as a ShopFloor user principal; the Users-group principal remains
only as a degraded fallback since a group member cannot start it.
This commit is contained in:
benjamin.baker
2026-09-06 05:22:05 -04:00
parent f832c376f1
commit cd705563d0
25 changed files with 1756 additions and 58 deletions

View File

@@ -423,6 +423,42 @@
<div v-show="activegroup === 'activity'" class="detail-full">
<PluginAssetPanels :assetid="computer.assetid" group="activity" />
</div>
<!-- Corporate Tools: fixed triggers for scheduled tasks already
registered on the endpoint. The agent only STARTS a task it already
holds and that clears its approved-task allowlist - it never defines
one - so the endpoint stays the authority on what actually runs. -->
<div v-show="activegroup === 'corptools'" class="detail-full">
<div class="card">
<h3>Corporate Tools</h3>
<p class="corptools-intro">
Starts a scheduled task that is already registered on this computer.
Each task runs in the security context its own definition specifies.
</p>
<!-- Persistent by design: not a dismissible toast and not shown only
on click. These tasks run real corporate tooling as SYSTEM on a
machine someone may be sitting at, and the warning has to still
be on screen at the moment an operator decides to press Run. -->
<p class="corptools-warning">
These actions take effect immediately on the live computer and
cannot be undone from here. A Dell update run can reboot the machine
and may interrupt the person using it.
</p>
<div class="corptools-list">
<div v-for="tool in corporateTools" :key="tool.task" class="corptools-row">
<div class="corptools-text">
<div class="corptools-name">{{ tool.label }}</div>
<p v-if="tool.warning" class="corptools-rowwarning">{{ tool.warning }}</p>
<div class="corptools-desc">{{ tool.description }}</div>
</div>
<button
class="btn btn-warning btn-sm"
:disabled="firingTask === tool.task"
@click="confirmRunTask(tool)"
>{{ firingTask === tool.task ? 'Starting...' : 'Run' }}</button>
</div>
</div>
</div>
</div>
<!-- Audit Footer -->
<div class="audit-footer">
@@ -575,6 +611,13 @@ const hasCloudIdentity = computed(() => portalTemplatesLoaded.value && Boolean(
// populatedGroups comes from PluginAssetPanels, which knows which panels
// actually returned data; Software is additionally true when core has an
// installed-applications list of its own.
// Gates the Corporate Tools tab. endpoints.deploy currently bundles
// scheduledtask_run with run_script/update_app - see the note beside the
// permission declaration in plugins/endpoints/plugin.py if that gets split.
// Declared ahead of detailtabs because the watch below evaluates that computed
// during setup, which reads canDeploy before this line would otherwise run.
const canDeploy = computed(() => authStore.hasPermission('endpoints.deploy'))
const detailtabs = computed(() => {
const filled = assetPanels.value?.populatedGroups ?? new Set()
const has = (key) => filled.has ? filled.has(key) : false
@@ -595,6 +638,13 @@ const detailtabs = computed(() => {
tabs.push({ key: 'software', label: 'Software', count: installedApps.value.length || null })
}
if (has('activity')) tabs.push({ key: 'activity', label: 'Activity' })
// Corporate Tools is permission-gated rather than data-gated: it is a set of
// fixed actions, not a rendering of collected telemetry, so there is no
// populatedGroup to key off. Hidden without endpoints.deploy because every
// button here is an elevated command; the server re-checks regardless.
if (canDeploy.value && liveness.value) {
tabs.push({ key: 'corptools', label: 'Corporate Tools' })
}
return tabs
})
@@ -805,6 +855,93 @@ async function confirmRestart() {
}
}
// Fixed set, deliberately not user-editable from this page: these are the task
// paths the agent ships as its approved list (ScheduledTaskPolicy). Adding one
// here without adding it to the agent's allowlist gets a clean rejection from
// the endpoint rather than an execution, which is the intended failure mode.
const corporateTools = [
{
task: '\\ReportIP',
label: 'Report IP',
description: 'Re-runs the corporate IP reporting agent on this computer.',
},
{
task: '\\SFLD-ApplyDSCConfig',
label: 'Apply DSC Configuration',
description: 'Re-applies the shop floor DSC baseline. Runs as SYSTEM and '
+ 'may change system configuration.',
},
{
task: '\\SFLD - Consume Credentials',
label: 'Consume Credentials',
description: 'Runs the credential consumption task in the logged-on '
+ 'user\'s session. Requires a user to be signed in.',
},
// Dell Command | Update, split into two entries because they are two
// utilities with very different consequences: the scan only reports, the
// update installs and may reboot. Keeping them as separate buttons means the
// destructive one is never a field edit away from the safe one.
{
task: '\\ShopDB-DellCommandScan',
label: 'Dell Command: Scan',
description: 'Scans for available Dell driver and firmware updates. '
+ 'Reports only - installs nothing and does not reboot.',
},
{
task: '\\ShopDB-DellCommandUpdate',
label: 'Dell Command: Apply Updates',
// Persistent breakglass warning. Tanium is the primary update utility for
// this fleet; this tool exists only for when Tanium has failed. The text is
// rendered in the card at all times (not behind a hover or the confirm
// dialog) so the operator cannot reach Run without having seen it.
warning: 'These updates are meant to be executed by the primary utility - '
+ 'Tanium. This tool should only be used in the event of a failed '
+ 'update or a problem with Tanium. This is a breakglass update '
+ 'utility to account for critical failures in the primary '
+ 'management solution.',
description: 'Installs available Dell driver and firmware updates. Runs as '
+ 'SYSTEM and MAY REBOOT this computer when it finishes.',
destructive: true,
},
]
const firingTask = ref('')
// Elevated: queues a scheduledtask_run. The confirm gate mirrors
// confirmRestart - these start real work on a live machine and some of them
// change system state - and the server re-checks endpoints.deploy regardless.
//
// Tools marked destructive get a TYPED confirmation rather than an OK button,
// for the same reason Restart has its own gate: applying firmware updates can
// reboot a machine that may be mid-cycle on the shop floor, and a misclick
// should not be able to do that.
async function confirmRunTask(tool) {
const label = computer.value?.assetnumber || computer.value?.name || 'this PC'
if (tool.destructive) {
const typed = prompt(`${tool.label} on ${label}\n\n`
+ `${tool.description}\n\n`
+ 'Type RUN to confirm.')
if (typed !== 'RUN') return
} else if (!confirm(`Run "${tool.label}" on ${label}?\n\n`
+ `${tool.description}\n\n`
+ 'The task starts immediately on the endpoint.')) {
return
}
firingTask.value = tool.task
try {
await endpointsApi.enqueueCommand(
computer.value.assetid, 'scheduledtask_run', tool.task)
// "Started", not "finished": the agent reports success as soon as Task
// Scheduler accepts the task, so this never means the work is done. Any
// result the task produces comes back through telemetry, not this command.
toast.success(`${tool.label} started on ${label}`)
} catch (error) {
toast.error(apiError(error, `Failed to queue ${tool.label}`))
} finally {
firingTask.value = ''
}
}
// Retire, not delete: the server deactivates the asset, so the PC
// leaves every list and report while its history and relationships stay put.
// Reversible by an admin setting isactive back to true.
@@ -995,6 +1132,60 @@ async function confirmRetire() {
}
.triage-chip.ok .triage-dot { background: var(--success); }
.triage-chip.warning .triage-dot { background: var(--warning); }
/* Corporate Tools: a plain action list, not a data table - each row is a
trigger with the consequence stated next to it, so the operator reads what
it does before the confirm gate rather than after. */
.corptools-intro {
margin: 0 0 1rem;
color: var(--text-secondary);
font-size: 0.875rem;
}
/* Warning weight without alert-banner styling: it is permanent, so a loud
dismissible-looking banner would be tuned out within a day. The left rule and
warning tint keep it legible as a standing caution. */
.corptools-warning {
margin: 0 0 1rem;
padding: 0.5rem 0.75rem;
border-left: 3px solid var(--warning, #d97706);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 0.875rem;
}
.corptools-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.corptools-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 4px;
}
.corptools-name {
font-weight: 600;
}
.corptools-desc {
color: var(--text-secondary);
font-size: 0.8125rem;
margin-top: 0.125rem;
}
/* Row-level breakglass notice. Carries the same left-rule treatment as the
card-level warning so it reads as a caution and not as body copy, but sits
inside the row it applies to. */
.corptools-rowwarning {
margin: 0.25rem 0 0.375rem;
padding: 0.375rem 0.625rem;
border-left: 3px solid var(--danger, #dc2626);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 0.8125rem;
font-weight: 500;
}
.triage-chip.critical .triage-dot { background: var(--danger); }
.triage-chip.ok { color: var(--success-dark); }
.triage-chip.warning { color: var(--warning-dark); }