From 5cc8b4c30140c862105eab6a826778ee75f8db8a Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Thu, 7 May 2026 10:21:26 +0800 Subject: [PATCH] fix: some actions should be hidden for read-only user (#855) * fix: some actions limited for virt-viewer Signed-off-by: Andy Lee * refactor: based on schema collectionMethods Signed-off-by: Andy Lee * fix: conditionally add migrate action to available actions * fix: update collectionMethods check to use find for case-insensitive matching * fix: update canEditClusterMembers method to use schema for collectionMethods * fix: update canCreateImage check to use case-insensitive matching --------- Signed-off-by: Andy Lee --- pkg/harvester/components/Members.vue | 2 +- pkg/harvester/config/harvester-cluster.js | 2 +- ...sterhci.io.networkattachmentdefinition.vue | 1 - .../devices.harvesterhci.io.pcidevice.js | 5 ++-- pkg/harvester/models/harvester/namespace.js | 12 +++++++++- pkg/harvester/models/harvester/secret.js | 14 +++++++++++ .../harvester/storage.k8s.io.storageclass.js | 9 ++++++++ pkg/harvester/models/harvesterhci.io.addon.js | 3 ++- .../models/harvesterhci.io.keypair.js | 14 +++++++++++ .../harvesterhci.io.schedulevmbackup.js | 6 +++-- .../harvesterhci.io.virtualmachineimage.js | 5 ++-- .../models/kubevirt.io.virtualmachine.js | 4 +++- .../models/management.cattle.io.project.js | 19 +++++++++++++++ .../network.harvesterhci.io.vlanconfig.js | 6 ++++- .../store/harvester-store/getters.ts | 23 ++++++++++++++++++- 15 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 pkg/harvester/models/management.cattle.io.project.js diff --git a/pkg/harvester/components/Members.vue b/pkg/harvester/components/Members.vue index 14332594..8f3848e2 100644 --- a/pkg/harvester/components/Members.vue +++ b/pkg/harvester/components/Members.vue @@ -122,7 +122,7 @@ export default { return this.$store.getters['currentCluster'].isLocal; }, canEditClusterMembers() { - return this.normanClusterRTBSchema?.collectionMethods.find((x) => x.toLowerCase() === 'post'); + return this.schema?.collectionMethods.find((x) => x.toLowerCase() === 'post'); }, }, }; diff --git a/pkg/harvester/config/harvester-cluster.js b/pkg/harvester/config/harvester-cluster.js index 6041e780..d1287541 100644 --- a/pkg/harvester/config/harvester-cluster.js +++ b/pkg/harvester/config/harvester-cluster.js @@ -686,7 +686,7 @@ export function init($plugin, store) { }, resource: NETWORK_ATTACHMENT, resourceDetail: HCI.NETWORK_ATTACHMENT, - resourceEdit: HCI.NETWORK_ATTACHMENT + resourceEdit: HCI.NETWORK_ATTACHMENT, }); virtualType({ diff --git a/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue b/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue index 1dc8d429..ac2ec748 100644 --- a/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue +++ b/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue @@ -3,7 +3,6 @@ import { Banner } from '@components/Banner'; import Loading from '@shell/components/Loading'; import ResourceTable from '@shell/components/ResourceTable'; import BadgeState from '@shell/components/formatter/BadgeStateFormatter'; - import { NAME, AGE, NAMESPACE, STATE } from '@shell/config/table-headers'; import { NETWORK_ATTACHMENT, SCHEMA } from '@shell/config/types'; import { allHash } from '@shell/utils/promise'; diff --git a/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js b/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js index 9c488790..cea2e8a0 100644 --- a/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js +++ b/pkg/harvester/models/devices.harvesterhci.io.pcidevice.js @@ -30,11 +30,12 @@ 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, + enabled: !this.isEnabling && !this.isvGPUDevice && canUpdate, icon: 'icon icon-fw icon-dot', label: 'Enable Passthrough', bulkable: true, @@ -43,7 +44,7 @@ export default class PCIDevice extends SteveModel { }, { action: 'disablePassthrough', - enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice, + enabled: this.isEnabling && this.claimedByMe && !this.isvGPUDevice && canUpdate, icon: 'icon icon-fw icon-dot-open', label: 'Disable Passthrough', bulkable: true, diff --git a/pkg/harvester/models/harvester/namespace.js b/pkg/harvester/models/harvester/namespace.js index e15bedbe..f2f15134 100644 --- a/pkg/harvester/models/harvester/namespace.js +++ b/pkg/harvester/models/harvester/namespace.js @@ -24,7 +24,7 @@ const OBSCURE_NAMESPACE_PREFIX = [ export default class HciNamespace extends namespace { get _availableActions() { - const out = super._availableActions; + let out = super._availableActions; const remove = out.findIndex((a) => a.action === 'promptRemove'); const promptRemove = { @@ -53,6 +53,16 @@ export default class HciNamespace extends namespace { insertAt(out, out.length - 1, promptRemove); insertAt(out, out.length - 5, editQuotaAction); + const canUpdate = !!this.linkFor('update'); + + out = out.map((action) => { + if (['move'].includes(action.action)) { + return { ...action, enabled: action.enabled && canUpdate }; + } + + return action; + }); + return out; } diff --git a/pkg/harvester/models/harvester/secret.js b/pkg/harvester/models/harvester/secret.js index e8b2ccc6..c81c02d6 100644 --- a/pkg/harvester/models/harvester/secret.js +++ b/pkg/harvester/models/harvester/secret.js @@ -5,6 +5,20 @@ import Secret from '@shell/models/secret'; import { NAMESPACE } from '@shell/config/types'; export default class HciSecret extends Secret { + get _availableActions() { + let out = super._availableActions; + + out = out.map((action) => { + if (['download'].includes(action.action)) { + return { ...action, enabled: !!this.linkFor('update') }; + } + + return action; + }); + + return out; + } + // prevent harvester secret detail page be overridden. // See isFullPageOverride in https://github.com/rancher/dashboard/blob/master/shell/components/ResourceDetail/index.vue get fullDetailPageOverride() { diff --git a/pkg/harvester/models/harvester/storage.k8s.io.storageclass.js b/pkg/harvester/models/harvester/storage.k8s.io.storageclass.js index 6101dd10..a26694f5 100644 --- a/pkg/harvester/models/harvester/storage.k8s.io.storageclass.js +++ b/pkg/harvester/models/harvester/storage.k8s.io.storageclass.js @@ -106,6 +106,15 @@ export default class HciStorageClass extends StorageClass { get availableActions() { let out = super.availableActions || []; + const canUpdate = !!this.linkFor('update'); + + out = out.map((action) => { + if (['setDefault', 'setAsDefault', 'resetDefault'].includes(action.action)) { + return { ...action, enabled: canUpdate }; + } + + return action; + }); if (this.isInternalStorageClass()) { out = out.filter((action) => { diff --git a/pkg/harvester/models/harvesterhci.io.addon.js b/pkg/harvester/models/harvesterhci.io.addon.js index 138493b1..1ba90060 100644 --- a/pkg/harvester/models/harvesterhci.io.addon.js +++ b/pkg/harvester/models/harvesterhci.io.addon.js @@ -21,9 +21,10 @@ export default class HciAddonConfig extends HarvesterResource { out.push(rancherDashboard); } + const canUpdate = !!this.linkFor('update'); const toggleAddon = { action: 'toggleAddon', - enabled: true, + enabled: canUpdate, icon: this.spec.enabled ? 'icon icon-pause' : 'icon icon-play', label: this.spec.enabled ? this.t('generic.disable') : this.t('generic.enable'), }; diff --git a/pkg/harvester/models/harvesterhci.io.keypair.js b/pkg/harvester/models/harvesterhci.io.keypair.js index 917f390b..f2d8c224 100644 --- a/pkg/harvester/models/harvesterhci.io.keypair.js +++ b/pkg/harvester/models/harvesterhci.io.keypair.js @@ -3,6 +3,20 @@ import { findBy } from '@shell/utils/array'; import HarvesterResource from './harvester'; export default class HciKeypair extends HarvesterResource { + get _availableActions() { + let out = super._availableActions; + + out = out.map((action) => { + if (['download'].includes(action.action)) { + return { ...action, enabled: !!this.linkFor('update') }; + } + + return action; + }); + + return out; + } + get stateDisplay() { const conditions = get(this, 'status.conditions'); const status = (findBy(conditions, 'type', 'validated') || {}).status ; diff --git a/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js index 7fedea76..9f5b9beb 100644 --- a/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js +++ b/pkg/harvester/models/harvesterhci.io.schedulevmbackup.js @@ -19,16 +19,18 @@ export default class ScheduleVmBackup extends HarvesterResource { } }); + const canUpdate = !!this.linkFor('update'); + return [ { action: 'resumeSchedule', - enabled: ucFirst(this.state) === STATES.suspended.label, + enabled: canUpdate && ucFirst(this.state) === STATES.suspended.label, icon: 'icons icon-play', label: this.t('harvester.action.resumeSchedule'), }, { action: 'suspendSchedule', - enabled: ucFirst(this.state) === STATES.active.label, + enabled: canUpdate && ucFirst(this.state) === STATES.active.label, icon: 'icons icon-pause', label: this.t('harvester.action.suspendSchedule'), }, diff --git a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js index 9d3ee7c9..bb8de3b4 100644 --- a/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js +++ b/pkg/harvester/models/harvesterhci.io.virtualmachineimage.js @@ -52,6 +52,7 @@ export default class HciVmImage extends HarvesterResource { canCreateVM = false; } + const canCreateImage = !!this.$getters?.['schemaFor']?.(HCI.IMAGE)?.collectionMethods?.some((method) => method.toLowerCase() === 'post'); const customActions = this.isReady ? [ { action: 'createFromImage', @@ -61,13 +62,13 @@ export default class HciVmImage extends HarvesterResource { }, { action: 'encryptImage', - enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted, + enabled: this.volumeEncryptionFeatureEnabled && !this.isEncrypted && canCreateImage, icon: 'icon icon-lock', label: this.t('harvester.action.encryptImage'), }, { action: 'decryptImage', - enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted, + enabled: this.volumeEncryptionFeatureEnabled && this.isEncrypted && canCreateImage, icon: 'icon icon-unlock', label: this.t('harvester.action.decryptImage'), }, diff --git a/pkg/harvester/models/kubevirt.io.virtualmachine.js b/pkg/harvester/models/kubevirt.io.virtualmachine.js index 8c99ce67..d418e191 100644 --- a/pkg/harvester/models/kubevirt.io.virtualmachine.js +++ b/pkg/harvester/models/kubevirt.io.virtualmachine.js @@ -130,6 +130,8 @@ export default class VirtVm extends HarvesterResource { clone.action = 'goToCloneVM'; } + const canCreateVMSSchedule = !!this.$getters?.['schemaFor']?.(HCI.SCHEDULE_VM_BACKUP)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase())); + return [ { action: 'stopVM', @@ -207,7 +209,7 @@ export default class VirtVm extends HarvesterResource { }, { action: 'createSchedule', - enabled: this.schedulingVMBackupFeatureEnabled, + enabled: canCreateVMSSchedule && this.schedulingVMBackupFeatureEnabled, icon: 'icon icon-history', label: this.t('harvester.action.createSchedule') }, diff --git a/pkg/harvester/models/management.cattle.io.project.js b/pkg/harvester/models/management.cattle.io.project.js new file mode 100644 index 00000000..daa0fb58 --- /dev/null +++ b/pkg/harvester/models/management.cattle.io.project.js @@ -0,0 +1,19 @@ + +import shellProject from '@shell/models/management.cattle.io.project'; + +// This model controls `Project / Namespace` page in rancher integration mode +// Extend management.cattle.io.project model from shell +export default class Project extends shellProject { + get _availableActions() { + const canUpdate = !!this.linkFor('update'); + + // disable `Edit Config` action if user does not have update permission. + return super._availableActions.map((action) => { + if (action.action === 'goToEdit') { + return { ...action, enabled: canUpdate }; + } + + return action; + }); + } +} diff --git a/pkg/harvester/models/network.harvesterhci.io.vlanconfig.js b/pkg/harvester/models/network.harvesterhci.io.vlanconfig.js index 2e0dfd27..5e769f5f 100644 --- a/pkg/harvester/models/network.harvesterhci.io.vlanconfig.js +++ b/pkg/harvester/models/network.harvesterhci.io.vlanconfig.js @@ -57,7 +57,11 @@ export default class HciVlanConfig extends HarvesterResource { get _availableActions() { const out = super._availableActions; - insertAt(out, 0, this.migrateAction); + const canMigrate = !!this.$getters?.['schemaFor']?.(HCI.VLAN_CONFIG)?.collectionMethods?.find((x) => ['post'].includes(x.toLowerCase())); + + if (canMigrate) { + insertAt(out, 0, this.migrateAction); + } return out; } diff --git a/pkg/harvester/store/harvester-store/getters.ts b/pkg/harvester/store/harvester-store/getters.ts index 2655cc0e..ccaf5768 100644 --- a/pkg/harvester/store/harvester-store/getters.ts +++ b/pkg/harvester/store/harvester-store/getters.ts @@ -123,5 +123,26 @@ export default { const clusterId = currentCluster.id; return projectsInAllClusters.filter((project: any) => project.spec.clusterName === clusterId && project.nameDisplay !== 'System'); - } + }, + + // Few harvester resources name and REAL resource are different. E.g. HCI.NETWORK_ATTACHMENT page resource is NETWORK_ATTACHMENT. + // Check in config/harvester-cluster.js for more details. + // We need to look up the schema by resource name, and fallback to find using real resource name + schemaFor: (state, getters, rootState, rootGetters) => (type, _fuzzy = false, _allowThrow = true) => { + // follow the same logic as type-map/schemaFor in /dashboard/shell/plugins/dashboard-store/getters.js + const normalizedType = getters.normalizeType(type); + const schemas = state.types['schema']; + const out = schemas?.map?.get(normalizedType); + + if (out) return out; + + // if not found, use the resource mapping in configureType for a second try + const resourceType = rootGetters['type-map/optionsFor'](type)?.resource; + if (resourceType && resourceType !== type) { + const normalizedResource = getters.normalizeType(resourceType); + return schemas?.map?.get(normalizedResource) || null; + } + + return null; + }, };