From 91232beffcdc3539933ac5203d4fcb3d87882826 Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Fri, 22 May 2026 11:56:44 +0800 Subject: [PATCH] fix: vGPU and USB enable/disable actions needs to be hidden for read only users (#877) * fix: vGPU / USB enable/disable actions needs to be hidden for read only users Signed-off-by: Andy Lee * style: add scoped styles for group actions in DeviceList component Signed-off-by: Andy Lee * style: remove width property from group actions in DeviceList components Signed-off-by: Andy Lee * fix: update logging type in init function and improve SideNav visibility handling Signed-off-by: Andy Lee * fix: ensure canManageGroup only returns true for non-empty rows with updatable permissions Signed-off-by: Andy Lee --------- Signed-off-by: Andy Lee --- pkg/harvester/config/harvester-cluster.js | 8 ++++ .../VirtualMachinePciDevices/DeviceList.vue | 45 ++++++++++++------ .../VirtualMachineUSBDevices/DeviceList.vue | 46 +++++++++++++------ ...evices.harvesterhci.io.migconfiguration.js | 5 +- .../devices.harvesterhci.io.pcidevice.js | 9 ++-- .../devices.harvesterhci.io.sriovgpudevice.js | 8 +++- .../devices.harvesterhci.io.usbdevice.js | 8 +++- .../devices.harvesterhci.io.vgpudevice.js | 5 +- ...terhci.io.virtualmachinetemplateversion.js | 8 ++-- pkg/harvester/utils/dynamic-nav.js | 44 +++++++++++++----- 10 files changed, 129 insertions(+), 57 deletions(-) diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js index d1287541..575c2c33 100644 --- a/pkg/harvester/config/harvester-cluster.js +++ b/pkg/harvester/config/harvester-cluster.js @@ -237,6 +237,7 @@ export function init($plugin, store) { labelKey: 'harvester.addons.vmImport.labels.vmimport', group: 'vmimport', namespaced: true, + ifHaveType: HCI.VMIMPORT, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, params: { resource: HCI.VMIMPORT } @@ -266,6 +267,7 @@ export function init($plugin, store) { labelKey: 'harvester.addons.vmImport.labels.vmimportSourceVMWare', group: 'vmimport', namespaced: true, + ifHaveType: HCI.VMIMPORT_SOURCE_V, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, params: { resource: HCI.VMIMPORT_SOURCE_V } @@ -295,6 +297,7 @@ export function init($plugin, store) { labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOpenStack', group: 'vmimport', namespaced: true, + ifHaveType: HCI.VMIMPORT_SOURCE_O, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, params: { resource: HCI.VMIMPORT_SOURCE_O } @@ -323,6 +326,7 @@ export function init($plugin, store) { labelKey: 'harvester.addons.vmImport.labels.vmimportSourceOVA', group: 'vmimport', namespaced: true, + ifHaveType: HCI.VMIMPORT_SOURCE_OVA, route: { name: `${ PRODUCT_NAME }-c-cluster-resource`, params: { resource: HCI.VMIMPORT_SOURCE_OVA } @@ -484,6 +488,7 @@ export function init($plugin, store) { }); virtualType({ + ifHaveType: LOGGING.CLUSTER_FLOW, labelKey: 'harvester.logging.clusterFlow.label', name: HCI.CLUSTER_FLOW, namespaced: true, @@ -507,6 +512,7 @@ export function init($plugin, store) { }); virtualType({ + ifHaveType: LOGGING.CLUSTER_OUTPUT, labelKey: 'harvester.logging.clusterOutput.label', name: HCI.CLUSTER_OUTPUT, namespaced: true, @@ -530,6 +536,7 @@ export function init($plugin, store) { }); virtualType({ + ifHaveType: LOGGING.FLOW, labelKey: 'harvester.logging.flow.label', name: HCI.FLOW, namespaced: true, @@ -553,6 +560,7 @@ export function init($plugin, store) { }); virtualType({ + ifHaveType: LOGGING.OUTPUT, labelKey: 'harvester.logging.output.label', name: HCI.OUTPUT, namespaced: true, diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/DeviceList.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/DeviceList.vue index 9ae59260..8139f281 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/DeviceList.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachinePciDevices/DeviceList.vue @@ -152,6 +152,10 @@ export default { return !rows.find((device) => !device.passthroughClaim); }, + canManageGroup(rows = []) { + return rows.length > 0 && rows.every((row) => row.canUpdate === true); + }, + changeRows(filterRows, parentSriov) { this['filterRows'] = filterRows; this['parentSriov'] = parentSriov; @@ -185,22 +189,27 @@ export default { v-trim-whitespace class="group-tab" > - - + + + @@ -232,3 +241,9 @@ export default { + + diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList.vue index 76b74d19..10087ad9 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineUSBDevices/DeviceList.vue @@ -113,6 +113,11 @@ export default { } }); }, + + canManageGroup(rows = []) { + return rows.every((row) => row.canUpdate === true); + }, + groupIsAllEnabled(rows = []) { return !rows.find((device) => !device.passthroughClaim); }, @@ -153,22 +158,27 @@ export default { v-trim-whitespace class="group-tab" > - - + + + @@ -181,3 +191,9 @@ export default { + + diff --git a/pkg/harvester/models/devices.harvesterhci.io.migconfiguration.js b/pkg/harvester/models/devices.harvesterhci.io.migconfiguration.js index 2ff1cefa..f0119e6a 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.migconfiguration.js +++ b/pkg/harvester/models/devices.harvesterhci.io.migconfiguration.js @@ -9,6 +9,7 @@ import HarvesterResource from './harvester'; export default class MIGCONFIGURATION extends HarvesterResource { get _availableActions() { let out = super._availableActions; + const canUpdate = !!this.linkFor('update'); out = out.map((action) => { if (action.action === 'showConfiguration') { @@ -26,13 +27,13 @@ export default class MIGCONFIGURATION extends HarvesterResource { out.push( { action: 'enableConfig', - enabled: !this.isEnabled, + enabled: !this.isEnabled && canUpdate, icon: 'icon icon-fw icon-dot', label: 'Enable', }, { action: 'disableConfig', - enabled: this.isEnabled, + enabled: this.isEnabled && canUpdate, icon: 'icon icon-fw icon-dot-open', label: 'Disable', }, diff --git a/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js b/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js index cea2e8a0..d33096e0 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js +++ b/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js @@ -30,12 +30,11 @@ const STATUS_DISPLAY = { export default class PCIDevice extends SteveModel { get _availableActions() { const out = super._availableActions; - const canUpdate = !!this.linkFor('update'); out.push( { action: 'enablePassthroughBulk', - enabled: !this.isEnabling && !this.isvGPUDevice && canUpdate, + enabled: !this.isEnabling && !this.isvGPUDevice && this.canUpdate, icon: 'icon icon-fw icon-dot', label: 'Enable Passthrough', bulkable: true, @@ -44,7 +43,7 @@ export default class PCIDevice extends SteveModel { }, { action: 'disablePassthrough', - enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && canUpdate, + enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && this.canUpdate, icon: 'icon icon-fw icon-dot-open', label: 'Disable Passthrough', bulkable: true, @@ -55,6 +54,10 @@ export default class PCIDevice extends SteveModel { return out; } + get canUpdate() { + return !!this.linkFor('update'); + } + get isvGPUDevice() { if (!this.vGPUAsPCIDeviceFeatureEnabled) { return false; diff --git a/pkg/harvester/models/devices.harvesterhci.io.sriovgpudevice.js b/pkg/harvester/models/devices.harvesterhci.io.sriovgpudevice.js index 796a10b5..c216c9b3 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.sriovgpudevice.js +++ b/pkg/harvester/models/devices.harvesterhci.io.sriovgpudevice.js @@ -16,13 +16,13 @@ export default class SRIOVDevice extends SteveModel { out.push( { action: 'enableDevice', - enabled: !this.isEnabled, + enabled: !this.isEnabled && this.canUpdate, icon: 'icon icon-fw icon-dot', label: 'Enable', }, { action: 'disableDevice', - enabled: this.isEnabled, + enabled: this.isEnabled && this.canUpdate, icon: 'icon icon-fw icon-dot-open', label: 'Disable', }, @@ -31,6 +31,10 @@ export default class SRIOVDevice extends SteveModel { return out; } + get canUpdate() { + return !!this.linkFor('update'); + } + get canYaml() { return false; } diff --git a/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js b/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js index 8cc539de..bb6c94a4 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js +++ b/pkg/harvester/models/devices.harvesterhci.io.usbdevice.js @@ -33,7 +33,7 @@ export default class USBDevice extends SteveModel { out.push( { action: 'enablePassthroughBulk', - enabled: !this.passthroughClaim && !this.status.enabled, + enabled: !this.passthroughClaim && !this.status.enabled && this.canUpdate, icon: 'icon icon-fw icon-dot', label: 'Enable Passthrough', bulkable: true, @@ -42,7 +42,7 @@ export default class USBDevice extends SteveModel { }, { action: 'disablePassthrough', - enabled: this.status.enabled, + enabled: this.status.enabled && this.canUpdate, icon: 'icon icon-fw icon-dot-open', label: 'Disable Passthrough', bulkable: true, @@ -53,6 +53,10 @@ export default class USBDevice extends SteveModel { return out; } + get canUpdate() { + return !!this.linkFor('update'); + } + get canYaml() { return false; } diff --git a/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js b/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js index 3db4e2cb..91334519 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js +++ b/pkg/harvester/models/devices.harvesterhci.io.vgpudevice.js @@ -27,17 +27,18 @@ const STATUS_DISPLAY = { export default class VGpuDevice extends SteveModel { get _availableActions() { const out = super._availableActions; + const canUpdate = !!this.linkFor('update'); out.push( { action: 'enableVGpu', - enabled: !this.isEnabled, + enabled: !this.isEnabled && canUpdate, icon: 'icon icon-fw icon-dot', label: 'Enable', }, { action: 'disableVGpu', - enabled: this.isEnabled, + enabled: this.isEnabled && canUpdate, icon: 'icon icon-fw icon-dot-open', label: 'Disable', bulkable: true, diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachinetemplateversion.js b/pkg/harvester/models/harvesterhci.io.virtualmachinetemplateversion.js index afd7edb1..036a9e68 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachinetemplateversion.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachinetemplateversion.js @@ -23,17 +23,17 @@ export default class HciVmTemplateVersion extends HarvesterResource { }); const schema = this.$getters['schemaFor'](HCI.VM); - let canCreateVM = true; + let canCreateVM = false; - if ( schema && !schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) { - canCreateVM = false; + if (schema?.collectionMethods.find((x) => ['post'].includes(x.toLowerCase())) ) { + canCreateVM = true; } return [ { action: 'launchFromTemplate', icon: 'icon icon-spinner', - disabled: !canCreateVM || !this.isReady, + enabled: canCreateVM && this.isReady, label: this.t('harvester.action.launchFormTemplate'), }, { diff --git a/pkg/harvester/utils/dynamic-nav.js b/pkg/harvester/utils/dynamic-nav.js index 489b7a6f..8dd1af9a 100644 --- a/pkg/harvester/utils/dynamic-nav.js +++ b/pkg/harvester/utils/dynamic-nav.js @@ -31,22 +31,42 @@ export function registerAddonSideNav(store, productName, { }, 600); }; + const hasAccessibleSchema = (t) => { + try { + return !!store.getters[`${ productName }/schemaFor`]?.(t); + } catch (e) { + return false; + } + }; + + const showTypes = (visibleTypes) => { + store.commit('type-map/basicType', { + product: productName, + group: navGroup, + types: visibleTypes + }); + }; + + const hideTypes = () => { + const basicTypes = store.state['type-map'].basicTypes[productName]; + + if (basicTypes) { + types.forEach((t) => delete basicTypes[t]); + } + }; + // Adds or removes the resource IDs from the product visibility whitelist. const setMenuVisibility = (visible) => { - if (visible) { - store.commit('type-map/basicType', { - product: productName, - group: navGroup, - types - }); - } else { - // Manually delete the keys from the state object to hide them. - const basicTypes = store.state['type-map'].basicTypes[productName]; + const accessibleTypes = visible ? types.filter(hasAccessibleSchema) : []; - if (basicTypes) { - types.forEach((t) => delete basicTypes[t]); - } + // Always clear first to remove any previously-registered types that are + // no longer accessible (e.g. partial permission changes like types=[A,B] where B is dropped). + hideTypes(); + + if (accessibleTypes.length > 0) { + showTypes(accessibleTypes); } + kickSideNav(); };