Gate operator reports behind their own grant
Operator report screenshots are an uncontrolled capture of whatever was on the operator's screen when they filed the report, so they can carry part numbers, customer names, or other shopfloor content. The rest of endpoints.view is uniform machine-collected telemetry. Reading a PC's CPU load does not imply a right to read someone's screen, so the reports and their images move to endpoints.operatorreports. The image route is the real control point, not the panel: the panel only decides whether a tab is drawn, while the bytes stay reachable by URL to anyone who can guess an id. Both GET routes now require the new grant. Migration 7d47 backfills the grant to everyone holding endpoints.view so nobody loses a panel they were already using; tightening from there is a per-account decision in the user editor. The grant is in the scope denylist, so it is a per-user direct grant and Reporter cannot hold it. Also adds test_user_editor_checkboxes_match_the_server_action_list. The SCOPE_ENDPOINT_ACTIONS comment claimed a sync guard kept the Vue checkbox list aligned with the Python tuple, but no such test existed. The lists had already drifted: endpoints.operatorreports reached a running container with no way to grant it through the UI. The guard catches drift in both directions - a name only in Python ships the feature dark, and a name only in Vue is worse, since the admin ticks a box and believes access was granted while the server refuses to store it.
This commit is contained in:
@@ -829,6 +829,13 @@ const hasCloudIdentity = computed(() => portalTemplatesLoaded.value && Boolean(
|
||||
// during setup, which reads canDeploy before this line would otherwise run.
|
||||
const canDeploy = computed(() => authStore.hasPermission('endpoints.corptools'))
|
||||
|
||||
// Operator report screenshots are an uncontrolled capture of whatever was on
|
||||
// the operator's screen, so they carry their own grant rather than riding
|
||||
// endpoints.view. Declared alongside canDeploy for the same reason: detailtabs
|
||||
// reads it during setup.
|
||||
const canViewOperatorReports = computed(
|
||||
() => authStore.hasPermission('endpoints.operatorreports'))
|
||||
|
||||
const detailtabs = computed(() => {
|
||||
const filled = assetPanels.value?.populatedGroups ?? new Set()
|
||||
const has = (key) => filled.has ? filled.has(key) : false
|
||||
@@ -844,7 +851,12 @@ const detailtabs = computed(() => {
|
||||
// Operator Reports earns a tab rather than a card in System Monitors: it is
|
||||
// what a person reported, not what the agent measured, and the thumbnail
|
||||
// grid is much bulkier than the fact cards around it.
|
||||
if (has('reports')) tabs.push({ key: 'reports', label: 'Operator Reports' })
|
||||
// Permission-gated as well as data-gated: the screenshots can carry shopfloor
|
||||
// content that endpoints.view does not imply a right to see. The panel and
|
||||
// image routes both re-check server-side; this only hides the tab.
|
||||
if (has('reports') && canViewOperatorReports.value) {
|
||||
tabs.push({ key: 'reports', label: 'Operator Reports' })
|
||||
}
|
||||
if (has('software') || installedApps.value.length > 0) {
|
||||
tabs.push({ key: 'software', label: 'Software', count: installedApps.value.length || null })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user