From c791399a61264615f25b5e047e5e4c4eea3a8b91 Mon Sep 17 00:00:00 2001 From: Yi-Ya Chen Date: Mon, 13 Jan 2025 12:41:08 +0800 Subject: [PATCH 1/4] fix: disable delete action Signed-off-by: Yi-Ya Chen --- pkg/harvester/list/harvesterhci.io.host.vue | 36 ++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/pkg/harvester/list/harvesterhci.io.host.vue b/pkg/harvester/list/harvesterhci.io.host.vue index 6ad60496..4bee3f4c 100644 --- a/pkg/harvester/list/harvesterhci.io.host.vue +++ b/pkg/harvester/list/harvesterhci.io.host.vue @@ -3,7 +3,7 @@ import ResourceTable from '@shell/components/ResourceTable'; import Loading from '@shell/components/Loading'; import { STATE, NAME, AGE } from '@shell/config/table-headers'; import { - CAPI, METRIC, NODE, SCHEMA, LONGHORN, POD + CAPI, METRIC, NODE, SCHEMA, LONGHORN, POD, MANAGEMENT, NORMAN } from '@shell/config/types'; import { allHash } from '@shell/utils/promise'; import metricPoller from '@shell/mixins/metric-poller'; @@ -62,8 +62,42 @@ export default { _hash.machines = this.$store.dispatch(`${ inStore }/findAll`, { type: CAPI.MACHINE }); } + if ( + this.$store.getters['rancher/schemaFor'](NORMAN.PRINCIPAL) && + this.$store.getters['rancher/schemaFor'](NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING) + ) { + _hash.normanPrincipal = this.$store.dispatch('rancher/findAll', { type: NORMAN.PRINCIPAL }); + _hash.clusterRoleTemplateBinding = this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING }); + } + const hash = await allHash(_hash); + // Remove delete action if current user role is cluster member + if (hash.normanPrincipal && hash.clusterRoleTemplateBinding) { + const role = hash.clusterRoleTemplateBinding.find( + (template) => template.userPrincipalName === hash.normanPrincipal[0]?.id + ); + const isClusterMember = role?.roleTemplateName === 'cluster-member'; + + if (isClusterMember) { + hash.nodes = hash.nodes.map((node) => { + const updatedActions = node.availableActions.map((action) => { + return action.action === 'promptRemove' ? { ...action, enabled: false } : action; + }); + + // keep availableActions non-enumerable + Object.defineProperty(node, 'availableActions', { + value: updatedActions, + writable: true, + enumerable: false, + configurable: true, + }); + + return node; + }); + } + } + this.rows = hash.nodes; }, From 43b10b250ef1dc89982a54677c53de040b009f4d Mon Sep 17 00:00:00 2001 From: Yi-Ya Chen Date: Tue, 14 Jan 2025 16:16:56 +0800 Subject: [PATCH 2/4] refactor: move logic to node model Signed-off-by: Yi-Ya Chen --- pkg/harvester/list/harvesterhci.io.host.vue | 36 +--------------- pkg/harvester/models/harvester/node.js | 48 ++++++++++++++++++++- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/pkg/harvester/list/harvesterhci.io.host.vue b/pkg/harvester/list/harvesterhci.io.host.vue index 4bee3f4c..6ad60496 100644 --- a/pkg/harvester/list/harvesterhci.io.host.vue +++ b/pkg/harvester/list/harvesterhci.io.host.vue @@ -3,7 +3,7 @@ import ResourceTable from '@shell/components/ResourceTable'; import Loading from '@shell/components/Loading'; import { STATE, NAME, AGE } from '@shell/config/table-headers'; import { - CAPI, METRIC, NODE, SCHEMA, LONGHORN, POD, MANAGEMENT, NORMAN + CAPI, METRIC, NODE, SCHEMA, LONGHORN, POD } from '@shell/config/types'; import { allHash } from '@shell/utils/promise'; import metricPoller from '@shell/mixins/metric-poller'; @@ -62,42 +62,8 @@ export default { _hash.machines = this.$store.dispatch(`${ inStore }/findAll`, { type: CAPI.MACHINE }); } - if ( - this.$store.getters['rancher/schemaFor'](NORMAN.PRINCIPAL) && - this.$store.getters['rancher/schemaFor'](NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING) - ) { - _hash.normanPrincipal = this.$store.dispatch('rancher/findAll', { type: NORMAN.PRINCIPAL }); - _hash.clusterRoleTemplateBinding = this.$store.dispatch(`management/findAll`, { type: MANAGEMENT.CLUSTER_ROLE_TEMPLATE_BINDING }); - } - const hash = await allHash(_hash); - // Remove delete action if current user role is cluster member - if (hash.normanPrincipal && hash.clusterRoleTemplateBinding) { - const role = hash.clusterRoleTemplateBinding.find( - (template) => template.userPrincipalName === hash.normanPrincipal[0]?.id - ); - const isClusterMember = role?.roleTemplateName === 'cluster-member'; - - if (isClusterMember) { - hash.nodes = hash.nodes.map((node) => { - const updatedActions = node.availableActions.map((action) => { - return action.action === 'promptRemove' ? { ...action, enabled: false } : action; - }); - - // keep availableActions non-enumerable - Object.defineProperty(node, 'availableActions', { - value: updatedActions, - writable: true, - enumerable: false, - configurable: true, - }); - - return node; - }); - } - } - this.rows = hash.nodes; }, diff --git a/pkg/harvester/models/harvester/node.js b/pkg/harvester/models/harvester/node.js index b265db35..dd7c9b69 100644 --- a/pkg/harvester/models/harvester/node.js +++ b/pkg/harvester/models/harvester/node.js @@ -1,5 +1,5 @@ import pickBy from 'lodash/pickBy'; -import { CAPI, LONGHORN, POD, NODE } from '@shell/config/types'; +import { CAPI, LONGHORN, POD, NODE, NORMAN } from '@shell/config/types'; import { CAPI as CAPI_ANNOTATIONS } from '@shell/config/labels-annotations.js'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { clone } from '@shell/utils/object'; @@ -25,7 +25,17 @@ const HEALTHY = 'healthy'; const WARNING = 'warning'; export default class HciNode extends HarvesterResource { + constructor(...args) { + super(...args); + this._roleBasedActions = []; + this._initialized = false; // flag to prevent repeated initialization + } + get _availableActions() { + if (!this._initialized) { + this.setupRoleBasedActions(); + } + const cordon = { action: 'cordon', enabled: this.hasAction('cordon') && !this.isCordoned, @@ -108,10 +118,44 @@ export default class HciNode extends HarvesterResource { shutDown, powerOn, reboot, - ...super._availableActions + ...this._roleBasedActions || [] ]; } + async setupRoleBasedActions() { + const baseActions = super._availableActions || []; + + // access control is only available on the multiple cluster Harvester + if (this.$rootGetters['isStandaloneHarvester']) { + this._roleBasedActions = baseActions; + } else { + this._roleBasedActions = await this._updateRoleBasedActions(baseActions); + } + + this._initialized = true; + } + + async _updateRoleBasedActions(actions) { + const hasSchema = (type) => this.$rootGetters['rancher/schemaFor'](type); + + if (!hasSchema(NORMAN.PRINCIPAL) || !hasSchema(NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING)) return actions; + + try { + const templates = await this.$dispatch('rancher/findAll', { type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING }, { root: true }); + const [currentUser] = this.$rootGetters['rancher/all'](NORMAN.PRINCIPAL) || []; + const userRole = templates.find((template) => template.userPrincipalId === currentUser?.id); + + if (userRole?.roleTemplateId === 'cluster-member') { + return actions.filter((action) => action.action !== 'promptRemove'); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error fetching role-based actions:', error); + } + + return actions; + } + promptRemove(resources = this) { this.$dispatch('promptModal', { resources, From 3565b44e3a10e1cb8410d6af5c595612a6781091 Mon Sep 17 00:00:00 2001 From: Yi-Ya Chen Date: Wed, 15 Jan 2025 10:12:38 +0800 Subject: [PATCH 3/4] refactor: fix lint warnings Signed-off-by: Yi-Ya Chen --- pkg/harvester/models/harvester/node.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/harvester/models/harvester/node.js b/pkg/harvester/models/harvester/node.js index dd7c9b69..cc7f9fb0 100644 --- a/pkg/harvester/models/harvester/node.js +++ b/pkg/harvester/models/harvester/node.js @@ -1,5 +1,7 @@ import pickBy from 'lodash/pickBy'; -import { CAPI, LONGHORN, POD, NODE, NORMAN } from '@shell/config/types'; +import { + CAPI, LONGHORN, POD, NODE, NORMAN +} from '@shell/config/types'; import { CAPI as CAPI_ANNOTATIONS } from '@shell/config/labels-annotations.js'; import { HCI as HCI_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations'; import { clone } from '@shell/utils/object'; From f5961f3a59edc02438f255a1871bdee30828d0af Mon Sep 17 00:00:00 2001 From: Yi-Ya Chen Date: Fri, 17 Jan 2025 16:45:27 +0800 Subject: [PATCH 4/4] fix: check user delete permission Signed-off-by: Yi-Ya Chen --- pkg/harvester/models/harvester/node.js | 73 ++++++++++---------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/pkg/harvester/models/harvester/node.js b/pkg/harvester/models/harvester/node.js index cc7f9fb0..c1d9abad 100644 --- a/pkg/harvester/models/harvester/node.js +++ b/pkg/harvester/models/harvester/node.js @@ -27,17 +27,7 @@ const HEALTHY = 'healthy'; const WARNING = 'warning'; export default class HciNode extends HarvesterResource { - constructor(...args) { - super(...args); - this._roleBasedActions = []; - this._initialized = false; // flag to prevent repeated initialization - } - get _availableActions() { - if (!this._initialized) { - this.setupRoleBasedActions(); - } - const cordon = { action: 'cordon', enabled: this.hasAction('cordon') && !this.isCordoned, @@ -120,44 +110,10 @@ export default class HciNode extends HarvesterResource { shutDown, powerOn, reboot, - ...this._roleBasedActions || [] + ...super._availableActions ]; } - async setupRoleBasedActions() { - const baseActions = super._availableActions || []; - - // access control is only available on the multiple cluster Harvester - if (this.$rootGetters['isStandaloneHarvester']) { - this._roleBasedActions = baseActions; - } else { - this._roleBasedActions = await this._updateRoleBasedActions(baseActions); - } - - this._initialized = true; - } - - async _updateRoleBasedActions(actions) { - const hasSchema = (type) => this.$rootGetters['rancher/schemaFor'](type); - - if (!hasSchema(NORMAN.PRINCIPAL) || !hasSchema(NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING)) return actions; - - try { - const templates = await this.$dispatch('rancher/findAll', { type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING }, { root: true }); - const [currentUser] = this.$rootGetters['rancher/all'](NORMAN.PRINCIPAL) || []; - const userRole = templates.find((template) => template.userPrincipalId === currentUser?.id); - - if (userRole?.roleTemplateId === 'cluster-member') { - return actions.filter((action) => action.action !== 'promptRemove'); - } - } catch (error) { - // eslint-disable-next-line no-console - console.error('Error fetching role-based actions:', error); - } - - return actions; - } - promptRemove(resources = this) { this.$dispatch('promptModal', { resources, @@ -513,10 +469,35 @@ export default class HciNode extends HarvesterResource { return parseSi(this.reserved.memory || '0'); } + // returns the user role, either 'cluster-owner' or 'cluster-member' + async _getRoleTemplateId() { + try { + const templates = await this.$dispatch('rancher/findAll', { type: NORMAN.CLUSTER_ROLE_TEMPLATE_BINDING }, { root: true }); + const currentUser = this.$rootGetters['rancher/all'](NORMAN.PRINCIPAL)?.[0]; + const userRole = templates.find((template) => template.userPrincipalId === currentUser?.id); + + return userRole?.roleTemplateId || 'cluster-member'; + } catch (error) { + return 'cluster-member'; + } + } + get canDelete() { const nodes = this.$rootGetters['harvester/all'](NODE) || []; + const isSingleNode = nodes.length === 1; + + if (isSingleNode) return false; + + // access control is unavailable in standalone harvester + if (this.$rootGetters['isStandaloneHarvester']) return true; + + if (this._canDelete === undefined) { + return this._getRoleTemplateId().then((id) => { + this._canDelete = id === 'cluster-owner'; + }); + } - return nodes.length > 1; + return this._canDelete; } get vlanStatuses() {