From f85216363afaa54061c02f8c4fd34d5b3dbd76ea Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Mon, 3 Nov 2025 14:49:50 +0100 Subject: [PATCH 1/6] UIE-9342 --- .../VPCs/VPCDetail/SubnetActionMenu.test.tsx | 40 ------------------- .../VPCs/VPCDetail/SubnetActionMenu.tsx | 9 +---- .../VPCDetail/SubnetAssignLinodesDrawer.tsx | 15 +------ 3 files changed, 3 insertions(+), 61 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetActionMenu.test.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetActionMenu.test.tsx index 06bf9522042..3a30c590f83 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetActionMenu.test.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetActionMenu.test.tsx @@ -10,8 +10,6 @@ import { SubnetActionMenu } from './SubnetActionMenu'; const queryMocks = vi.hoisted(() => ({ userPermissions: vi.fn(() => ({ data: { - update_linode: true, - delete_linode: true, update_vpc: true, delete_vpc: true, }, @@ -128,45 +126,9 @@ describe('SubnetActionMenu', () => { expect(props.handleAssignLinodes).toHaveBeenCalled(); }); - it('should disable the Assign Linodes button if user does not have update_linode permission', async () => { - queryMocks.userPermissions.mockReturnValue({ - data: { - update_linode: false, - delete_linode: false, - update_vpc: false, - delete_vpc: false, - }, - }); - const view = renderWithTheme(); - const actionMenu = view.getByLabelText(`Action menu for Subnet subnet-1`); - await userEvent.click(actionMenu); - - const assignButton = view.getByRole('menuitem', { name: 'Assign Linodes' }); - expect(assignButton).toHaveAttribute('aria-disabled', 'true'); - }); - - it('should enable the Assign Linodes button if user has update_linode and update_vpc permissions', async () => { - queryMocks.userPermissions.mockReturnValue({ - data: { - update_linode: true, - delete_linode: false, - update_vpc: true, - delete_vpc: false, - }, - }); - const view = renderWithTheme(); - const actionMenu = view.getByLabelText(`Action menu for Subnet subnet-1`); - await userEvent.click(actionMenu); - - const assignButton = view.getByRole('menuitem', { name: 'Assign Linodes' }); - expect(assignButton).not.toHaveAttribute('aria-disabled', 'true'); - }); - it('should disable the Edit button if user does not have update_vpc permission', async () => { queryMocks.userPermissions.mockReturnValue({ data: { - update_linode: false, - delete_linode: false, update_vpc: false, delete_vpc: false, }, @@ -182,8 +144,6 @@ describe('SubnetActionMenu', () => { it('should enable the Edit button if user has update_vpc permission', async () => { queryMocks.userPermissions.mockReturnValue({ data: { - update_linode: false, - delete_linode: false, update_vpc: true, delete_vpc: false, }, diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetActionMenu.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetActionMenu.tsx index 878e317956d..1fa8a73baa8 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetActionMenu.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetActionMenu.tsx @@ -40,6 +40,7 @@ export const SubnetActionMenu = (props: Props) => { ['update_vpc', 'delete_vpc'], vpcId ); + const canUpdateVPC = permissions?.update_vpc; const canDeleteVPC = permissions?.delete_vpc; @@ -49,20 +50,12 @@ export const SubnetActionMenu = (props: Props) => { handleAssignLinodes(subnet); }, title: 'Assign Linodes', - disabled: !canUpdateVPC, - tooltip: !canUpdateVPC - ? 'You do not have permission to assign Linode to this subnet.' - : undefined, }, { onClick: () => { handleUnassignLinodes(subnet); }, title: 'Unassign Linodes', - disabled: !canUpdateVPC, - tooltip: !canUpdateVPC - ? 'You do not have permission to unassign Linode from this subnet.' - : undefined, }, { onClick: () => { diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index c8ff57284f2..689625abe38 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -28,10 +28,7 @@ import { DownloadCSV } from 'src/components/DownloadCSV/DownloadCSV'; import { Link } from 'src/components/Link'; import { RemovableSelectionsListTable } from 'src/components/RemovableSelectionsList/RemovableSelectionsListTable'; import { FirewallSelect } from 'src/features/Firewalls/components/FirewallSelect'; -import { - usePermissions, - useQueryWithPermissions, -} from 'src/features/IAM/hooks/usePermissions'; +import { useQueryWithPermissions } from 'src/features/IAM/hooks/usePermissions'; import { getDefaultFirewallForInterfacePurpose } from 'src/features/Linodes/LinodeCreate/Networking/utilities'; import { REMOVABLE_SELECTIONS_LINODES_TABLE_HEADERS, @@ -164,7 +161,6 @@ export const SubnetAssignLinodesDrawer = ( csvRef.current.link.click(); }; - const { data: permissions } = usePermissions('vpc', ['update_vpc'], vpcId); // TODO: change update_linode to create_linode_config_profile_interface once it's available // TODO: change delete_linode to delete_linode_config_profile_interface once it's available // TODO: refactor useQueryWithPermissions once API filter is available @@ -176,8 +172,7 @@ export const SubnetAssignLinodesDrawer = ( open ); - const userCanAssignLinodes = - permissions?.update_vpc && filteredLinodes?.length > 0; + const userCanAssignLinodes = filteredLinodes?.length > 0; // We need to filter to the linodes from this region that are not already // assigned to this subnet const findUnassignedLinodes = React.useCallback(() => { @@ -594,12 +589,6 @@ export const SubnetAssignLinodesDrawer = ( open={open} title={`Assign Linodes to subnet: ${subnet?.label ?? 'Unknown'}`} > - {!userCanAssignLinodes && ( - - )} {assignLinodesErrors.none && ( )} From 97c2f2174d794aa58373d0e065504c01949e98af Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Mon, 3 Nov 2025 15:58:15 +0100 Subject: [PATCH 2/6] UIE-9341 --- .../VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx | 9 +-------- .../VPCDetail/SubnetUnassignLinodesDrawer.tsx | 15 ++------------- 2 files changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index 689625abe38..ae58b017e95 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -162,15 +162,8 @@ export const SubnetAssignLinodesDrawer = ( }; // TODO: change update_linode to create_linode_config_profile_interface once it's available - // TODO: change delete_linode to delete_linode_config_profile_interface once it's available - // TODO: refactor useQueryWithPermissions once API filter is available const { data: filteredLinodes, isLoading: isLoadingFilteredLinodes } = - useQueryWithPermissions( - query, - 'linode', - ['update_linode', 'delete_linode'], - open - ); + useQueryWithPermissions(query, 'linode', ['update_linode'], open); const userCanAssignLinodes = filteredLinodes?.length > 0; // We need to filter to the linodes from this region that are not already diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx index 7a39a12c019..324955b7b5b 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx @@ -15,10 +15,7 @@ import * as React from 'react'; import { DownloadCSV } from 'src/components/DownloadCSV/DownloadCSV'; import { RemovableSelectionsListTable } from 'src/components/RemovableSelectionsList/RemovableSelectionsListTable'; -import { - usePermissions, - useQueryWithPermissions, -} from 'src/features/IAM/hooks/usePermissions'; +import { useQueryWithPermissions } from 'src/features/IAM/hooks/usePermissions'; import { REMOVABLE_SELECTIONS_LINODES_TABLE_HEADERS } from 'src/features/VPCs/constants'; import { useUnassignLinode } from 'src/hooks/useUnassignLinode'; import { useVPCDualStack } from 'src/hooks/useVPCDualStack'; @@ -121,7 +118,6 @@ export const SubnetUnassignLinodesDrawer = React.memo( }); }, [linodes, subnetLinodeIds]); - const { data: permissions } = usePermissions('vpc', ['update_vpc'], vpcId); // TODO: change to 'delete_linode_config_profile_interface' once it's available const { data: filteredLinodes, isLoading: isLoadingFilteredLinodes } = useQueryWithPermissions( @@ -130,8 +126,7 @@ export const SubnetUnassignLinodesDrawer = React.memo( ['delete_linode'], open ); - const userCanUnassignLinodes = - permissions.update_vpc && filteredLinodes?.length > 0; + const userCanUnassignLinodes = filteredLinodes?.length > 0; React.useEffect(() => { if (linodes) { @@ -340,12 +335,6 @@ export const SubnetUnassignLinodesDrawer = React.memo( subnet?.ipv4 ?? subnet?.ipv6 ?? 'Unknown' })`} > - {!userCanUnassignLinodes && linodeOptionsToUnassign.length > 0 && ( - - )} {unassignLinodesErrors.length > 0 && ( )} From fbf60fe1771a4d112592e2c2c715c56fb4b78044 Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Mon, 3 Nov 2025 17:50:45 +0100 Subject: [PATCH 3/6] feat: [UIE-9341, UIE-9342, UIE-9521] - IAM: fix perm for vpc --- .../VPCDetail/SubnetAssignLinodesDrawer.tsx | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx index ae58b017e95..4b8df610e09 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetAssignLinodesDrawer.tsx @@ -166,9 +166,10 @@ export const SubnetAssignLinodesDrawer = ( useQueryWithPermissions(query, 'linode', ['update_linode'], open); const userCanAssignLinodes = filteredLinodes?.length > 0; - // We need to filter to the linodes from this region that are not already - // assigned to this subnet - const findUnassignedLinodes = React.useCallback(() => { + + const linodeOptionsToAssign = React.useMemo(() => { + // We need to filter to the linodes from this region that are not already + // assigned to this subnet if (!filteredLinodes) return []; return filteredLinodes?.filter((linode) => { @@ -176,19 +177,6 @@ export const SubnetAssignLinodesDrawer = ( }); }, [subnet, filteredLinodes]); - const [linodeOptionsToAssign, setLinodeOptionsToAssign] = React.useState< - Linode[] - >([]); - - // Moved the list of linodes that are currently assignable to a subnet into a state variable (linodeOptionsToAssign) - // and update that list whenever this subnet or the list of all linodes in this subnet's region changes. This takes - // care of the MUI invalid value warning that was occurring before in the Linodes autocomplete [M3-6752] - React.useEffect(() => { - if (filteredLinodes) { - setLinodeOptionsToAssign(findUnassignedLinodes() ?? []); - } - }, [filteredLinodes, setLinodeOptionsToAssign, findUnassignedLinodes]); - // Determine the configId based on the number of configurations function getConfigId(inputs: { isLinodeInterface: boolean; From f8375c48891d8ded8008b42a150fffb6799d7c1b Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Mon, 3 Nov 2025 18:06:31 +0100 Subject: [PATCH 4/6] Added changeset: IAM: fix permissiom's check for vpc for assigning/unassigning linodes --- .../manager/.changeset/pr-13050-changed-1762189590928.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-13050-changed-1762189590928.md diff --git a/packages/manager/.changeset/pr-13050-changed-1762189590928.md b/packages/manager/.changeset/pr-13050-changed-1762189590928.md new file mode 100644 index 00000000000..32de1a27641 --- /dev/null +++ b/packages/manager/.changeset/pr-13050-changed-1762189590928.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +IAM: fix permissiom's check for vpc for assigning/unassigning linodes ([#13050](https://github.com/linode/manager/pull/13050)) From f0aaafeeb9e99d6cb4c24e429824f963f5a25f11 Mon Sep 17 00:00:00 2001 From: Anastasiia Alekseenko Date: Wed, 5 Nov 2025 14:58:20 +0100 Subject: [PATCH 5/6] filter linodes --- .../VPCDetail/SubnetUnassignLinodesDrawer.tsx | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx b/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx index 324955b7b5b..82fad4c4878 100644 --- a/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx +++ b/packages/manager/src/features/VPCs/VPCDetail/SubnetUnassignLinodesDrawer.tsx @@ -94,8 +94,6 @@ export const SubnetUnassignLinodesDrawer = React.memo( const hasError = React.useRef(false); // This flag is used to prevent the drawer from closing if an error occurs. - const [linodeOptionsToUnassign, setLinodeOptionsToUnassign] = - React.useState([]); const [interfacesToDelete, setInterfacesToDelete] = React.useState< DeleteInterfaceIds[] >([]); @@ -103,36 +101,30 @@ export const SubnetUnassignLinodesDrawer = React.memo( const { linodes: subnetLinodeIds } = subnet || {}; // 1. We need to get all the linodes. + // TODO: change to 'delete_linode_config_profile_interface' once it's available const { - data: linodes, + data: filteredLinodes, error: linodesError, + isLoading: isLoadingFilteredLinodes, refetch: getCSVData, - } = useAllLinodesQuery(); + } = useQueryWithPermissions( + useAllLinodesQuery({}, {}, open), + 'linode', + ['delete_linode'], + open + ); + const userCanUnassignLinodes = filteredLinodes?.length > 0; - // 2. We need to filter only the linodes that are assigned to the subnet. - const findAssignedLinodes = React.useCallback(() => { - return linodes?.filter((linode) => { + const linodeOptionsToUnassign = React.useMemo(() => { + // 2. We need to filter only the linodes that are assigned to the subnet. + if (!filteredLinodes) return []; + + return filteredLinodes?.filter((linode) => { return subnetLinodeIds?.some( (linodeInfo) => linodeInfo.id === linode.id ); }); - }, [linodes, subnetLinodeIds]); - - // TODO: change to 'delete_linode_config_profile_interface' once it's available - const { data: filteredLinodes, isLoading: isLoadingFilteredLinodes } = - useQueryWithPermissions( - useAllLinodesQuery({}, {}, open), - 'linode', - ['delete_linode'], - open - ); - const userCanUnassignLinodes = filteredLinodes?.length > 0; - - React.useEffect(() => { - if (linodes) { - setLinodeOptionsToUnassign(findAssignedLinodes() ?? []); - } - }, [linodes, setLinodeOptionsToUnassign, findAssignedLinodes]); + }, [subnetLinodeIds, filteredLinodes]); // 3. When a linode is selected, we need to get the VPC interface to unassign. const getVPCInterface = React.useCallback( From b529b424260c961a6eb80dc35291648f09ee4706 Mon Sep 17 00:00:00 2001 From: Alban Bailly Date: Thu, 6 Nov 2025 13:09:52 +0100 Subject: [PATCH 6/6] e2e deferred getLinodes call --- .../cypress/e2e/core/vpc/vpc-linodes-update.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts index 6a5615b8c00..8454fa75054 100644 --- a/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts +++ b/packages/manager/cypress/e2e/core/vpc/vpc-linodes-update.spec.ts @@ -119,7 +119,7 @@ describe('VPC assign/unassign flows', () => { .click(); }); - cy.wait(['@createSubnet', '@getVPC', '@getSubnets', '@getLinodes']); + cy.wait(['@createSubnet', '@getVPC', '@getSubnets']); mockGetSubnet(mockVPC.id, mockSubnet.id, mockSubnet); @@ -139,6 +139,8 @@ describe('VPC assign/unassign flows', () => { .should('be.visible') .click(); + cy.wait(['@getLinodes']); + ui.drawer .findByTitle(`Assign Linodes to subnet: ${mockSubnet.label}`) .should('be.visible') @@ -395,7 +397,7 @@ describe('VPC assign/unassign flows', () => { mockGetLinodes([mockLinode, mockSecondLinode]).as('getLinodes'); cy.visitWithLogin(`/vpcs/${mockVPC.id}`); - cy.wait(['@getVPC', '@getSubnets', '@getLinodes', '@getFeatureFlags']); + cy.wait(['@getVPC', '@getSubnets', '@getFeatureFlags']); // confirm that subnet should get displayed on VPC's detail page cy.findByText(mockVPC.label).should('be.visible'); @@ -415,6 +417,8 @@ describe('VPC assign/unassign flows', () => { .should('be.visible') .click(); + cy.wait(['@getLinodes']); + ui.drawer .findByTitle( `Unassign Linodes from subnet: ${mockSubnet.label} (0.0.0.0/0)`