From ecdd95d01102492448e4d3a965f75497573eee39 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Fri, 20 Feb 2026 14:45:28 +0100 Subject: [PATCH 1/6] fix: [UIE-10253] - IAM Delegate: company name for users with no permissions --- .../manager/src/features/Account/SwitchAccountButton.tsx | 7 ++++++- .../manager/src/features/Account/SwitchAccountDrawer.tsx | 9 +++++++-- .../Account/SwitchAccounts/ChildAccountsTable.tsx | 3 +++ .../manager/src/features/TopMenu/UserMenu/UserMenu.tsx | 4 +++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/features/Account/SwitchAccountButton.tsx b/packages/manager/src/features/Account/SwitchAccountButton.tsx index ef6ce0568ef..c1f4a07f1b5 100644 --- a/packages/manager/src/features/Account/SwitchAccountButton.tsx +++ b/packages/manager/src/features/Account/SwitchAccountButton.tsx @@ -19,10 +19,15 @@ export const SwitchAccountButton = (props: ButtonProps) => { }, font: theme.tokens.alias.Typography.Label.Semibold.S, marginTop: theme.tokens.spacing.S4, + ...(isDelegateUserType && { + '&.MuiButton-root': { + textTransform: 'none', + }, + }), })} {...props} > - {isDelegateUserType ? 'Switch back to your account' : 'Switch Account'} + {isDelegateUserType ? 'Switch Back to Your Account' : 'Switch Account'} ); }; diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index 37ef1d6dbd4..56779570f2b 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -20,7 +20,7 @@ import { useSwitchToParentAccount } from 'src/features/Account/SwitchAccounts/us import { setTokenInLocalStorage } from 'src/features/Account/SwitchAccounts/utils'; import { useIsIAMDelegationEnabled } from 'src/features/IAM/hooks/useIsIAMEnabled'; import { sendSwitchToParentAccountEvent } from 'src/utilities/analytics/customEventAnalytics'; -import { getStorage, storage } from 'src/utilities/storage'; +import { getStorage, setStorage, storage } from 'src/utilities/storage'; import { ChildAccountList } from './SwitchAccounts/ChildAccountList'; import { ChildAccountsTable } from './SwitchAccounts/ChildAccountsTable'; @@ -28,6 +28,8 @@ import { updateParentTokenInLocalStorage } from './SwitchAccounts/utils'; import type { APIError, Filter, UserType } from '@linode/api-v4'; +const SWITCH_ACCOUNT_COMPANY_KEY = 'switch_account/company_name'; + interface Props { onClose: () => void; open: boolean; @@ -35,6 +37,7 @@ interface Props { } interface HandleSwitchToChildAccountProps { + company?: string; currentTokenWithBearer?: string; euuid: string; event: React.MouseEvent; @@ -128,11 +131,12 @@ export const SwitchAccountDrawer = (props: Props) => { event, onClose, userType, + company, }: HandleSwitchToChildAccountProps) => { const isProxyOrDelegateUserType = userType === 'proxy' || userType === 'delegate'; - try { + setStorage(SWITCH_ACCOUNT_COMPANY_KEY, company ?? ''); if (isProxyOrDelegateUserType) { // Revoke proxy token before switching accounts. await revokeToken().catch(() => { @@ -162,6 +166,7 @@ export const SwitchAccountDrawer = (props: Props) => { location.replace('/linodes'); } catch { // Error is handled by createTokenError. + setStorage(SWITCH_ACCOUNT_COMPANY_KEY, ''); } }, [createToken, isProxyUserType, updateCurrentToken, revokeToken] diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx index 31cd956fb29..57b1a662e78 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx @@ -26,8 +26,10 @@ export interface ChildAccountsTableProps { euuid, event, onClose, + company, userType, }: { + company?: string; currentTokenWithBearer?: string; euuid: string; event: React.MouseEvent; @@ -107,6 +109,7 @@ export const ChildAccountsTable = (props: ChildAccountsTableProps) => { onSwitchAccount({ currentTokenWithBearer, euuid: childAccount.euuid, + company: childAccount.company, event, onClose, userType, diff --git a/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx b/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx index 0b712bc1248..363ba3e34dc 100644 --- a/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx +++ b/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx @@ -43,8 +43,10 @@ export const UserMenu = React.memo(() => { const open = Boolean(anchorEl); const id = open ? 'user-menu-popover' : undefined; + const switchAccountCompanyName = getStorage('switch_account/company_name'); + const companyNameOrEmail = getCompanyNameOrEmail({ - company: account?.company, + company: account?.company ? account?.company : switchAccountCompanyName, profile, }); From e0e08204fc22d27e61c63ef86355f883305ca72a Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Tue, 24 Feb 2026 10:55:12 +0100 Subject: [PATCH 2/6] fix: [UIE-10253] - IAM Delegate: clear storage --- packages/manager/src/OAuth/oauth.ts | 4 +++- .../Account/SwitchAccounts/useSwitchToParentAccount.ts | 5 +++-- .../src/features/TopMenu/UserMenu/UserMenuPopover.tsx | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/manager/src/OAuth/oauth.ts b/packages/manager/src/OAuth/oauth.ts index 3d111dbecbe..151c894f748 100644 --- a/packages/manager/src/OAuth/oauth.ts +++ b/packages/manager/src/OAuth/oauth.ts @@ -5,7 +5,7 @@ import { } from '@linode/utilities'; import * as Sentry from '@sentry/react'; -import { clearUserInput, storage } from 'src/utilities/storage'; +import { clearStorage, clearUserInput, storage } from 'src/utilities/storage'; import { getAppRoot, getClientId, getLoginURL } from './constants'; import { generateCodeChallenge, generateCodeVerifier } from './pkce'; @@ -45,6 +45,7 @@ function clearNonceAndCodeVerifierFromLocalStorage() { function clearAllAuthDataFromLocalStorage() { clearNonceAndCodeVerifierFromLocalStorage(); clearAuthDataFromLocalStorage(); + clearStorage('switch_account/company_name'); } export function clearStorageAndRedirectToLogout() { @@ -105,6 +106,7 @@ export async function logout() { clearUserInput(); clearAuthDataFromLocalStorage(); + clearStorage('switch_account/company_name'); if (token) { const tokenWithoutPrefix = token.split(' ')[1]; diff --git a/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts b/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts index f9ea6ec5efb..08b8c94e87d 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts +++ b/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts @@ -1,7 +1,7 @@ import React from 'react'; import { PARENT_USER_SESSION_EXPIRED } from 'src/features/Account/constants'; -import { setStorage } from 'src/utilities/storage'; +import { clearStorage, setStorage } from 'src/utilities/storage'; import { useParentChildAuthentication } from './useParentChildAuthentication'; @@ -38,7 +38,8 @@ export const useSwitchToParentAccount = ({ // Flag to prevent multiple clicks on the switch account button. setSubmitting(true); - + // Clean up the company name in storage to prevent it from being used in the parent account after switching back from a child account. + clearStorage('switch_account/company_name'); try { // Revoke proxy or delegate token before switching to parent account. await revokeToken().catch(() => { diff --git a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx index 4c159f880f0..fd8871b1db0 100644 --- a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx +++ b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx @@ -121,8 +121,9 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { } : undefined; + const switchAccountCompanyName = getStorage('switch_account/company_name'); const companyNameOrEmail = getCompanyNameOrEmail({ - company: account?.company, + company: account?.company ? account?.company : switchAccountCompanyName, profile, }); const { data: parentProfile } = useProfile({ headers: proxyHeaders }); From 97fcf54133006a5451d2663cb021000ffa0cd0a7 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Wed, 25 Feb 2026 17:16:16 +0100 Subject: [PATCH 3/6] fix: [UIE-10253] - IAM Delegate: hide company name for users with no permission --- packages/manager/src/OAuth/oauth.ts | 4 +- .../features/Account/SwitchAccountDrawer.tsx | 101 +++++++++--------- .../SwitchAccounts/ChildAccountsTable.tsx | 3 - .../useSwitchToParentAccount.ts | 5 +- .../features/TopMenu/UserMenu/UserMenu.tsx | 11 +- .../TopMenu/UserMenu/UserMenuPopover.tsx | 14 +-- 6 files changed, 64 insertions(+), 74 deletions(-) diff --git a/packages/manager/src/OAuth/oauth.ts b/packages/manager/src/OAuth/oauth.ts index 151c894f748..3d111dbecbe 100644 --- a/packages/manager/src/OAuth/oauth.ts +++ b/packages/manager/src/OAuth/oauth.ts @@ -5,7 +5,7 @@ import { } from '@linode/utilities'; import * as Sentry from '@sentry/react'; -import { clearStorage, clearUserInput, storage } from 'src/utilities/storage'; +import { clearUserInput, storage } from 'src/utilities/storage'; import { getAppRoot, getClientId, getLoginURL } from './constants'; import { generateCodeChallenge, generateCodeVerifier } from './pkce'; @@ -45,7 +45,6 @@ function clearNonceAndCodeVerifierFromLocalStorage() { function clearAllAuthDataFromLocalStorage() { clearNonceAndCodeVerifierFromLocalStorage(); clearAuthDataFromLocalStorage(); - clearStorage('switch_account/company_name'); } export function clearStorageAndRedirectToLogout() { @@ -106,7 +105,6 @@ export async function logout() { clearUserInput(); clearAuthDataFromLocalStorage(); - clearStorage('switch_account/company_name'); if (token) { const tokenWithoutPrefix = token.split(' ')[1]; diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index 56779570f2b..b7d09f90a21 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -20,7 +20,7 @@ import { useSwitchToParentAccount } from 'src/features/Account/SwitchAccounts/us import { setTokenInLocalStorage } from 'src/features/Account/SwitchAccounts/utils'; import { useIsIAMDelegationEnabled } from 'src/features/IAM/hooks/useIsIAMEnabled'; import { sendSwitchToParentAccountEvent } from 'src/utilities/analytics/customEventAnalytics'; -import { getStorage, setStorage, storage } from 'src/utilities/storage'; +import { getStorage, storage } from 'src/utilities/storage'; import { ChildAccountList } from './SwitchAccounts/ChildAccountList'; import { ChildAccountsTable } from './SwitchAccounts/ChildAccountsTable'; @@ -28,8 +28,6 @@ import { updateParentTokenInLocalStorage } from './SwitchAccounts/utils'; import type { APIError, Filter, UserType } from '@linode/api-v4'; -const SWITCH_ACCOUNT_COMPANY_KEY = 'switch_account/company_name'; - interface Props { onClose: () => void; open: boolean; @@ -37,7 +35,6 @@ interface Props { } interface HandleSwitchToChildAccountProps { - company?: string; currentTokenWithBearer?: string; euuid: string; event: React.MouseEvent; @@ -131,12 +128,11 @@ export const SwitchAccountDrawer = (props: Props) => { event, onClose, userType, - company, }: HandleSwitchToChildAccountProps) => { const isProxyOrDelegateUserType = userType === 'proxy' || userType === 'delegate'; + try { - setStorage(SWITCH_ACCOUNT_COMPANY_KEY, company ?? ''); if (isProxyOrDelegateUserType) { // Revoke proxy token before switching accounts. await revokeToken().catch(() => { @@ -166,7 +162,6 @@ export const SwitchAccountDrawer = (props: Props) => { location.replace('/linodes'); } catch { // Error is handled by createTokenError. - setStorage(SWITCH_ACCOUNT_COMPANY_KEY, ''); } }, [createToken, isProxyUserType, updateCurrentToken, revokeToken] @@ -248,7 +243,7 @@ export const SwitchAccountDrawer = (props: Props) => { . - {hasError && ( + {hasError ? ( Unable to load data. @@ -265,8 +260,7 @@ export const SwitchAccountDrawer = (props: Props) => { Try again - )} - {!hasError && ( + ) : ( <> { No search results )} + + {isIAMDelegationEnabled && ( + + )} + {!isIAMDelegationEnabled && ( + + )} )} - {isIAMDelegationEnabled && ( - - )} - {!isIAMDelegationEnabled && ( - - )} ); }; diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx index 57b1a662e78..31cd956fb29 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx @@ -26,10 +26,8 @@ export interface ChildAccountsTableProps { euuid, event, onClose, - company, userType, }: { - company?: string; currentTokenWithBearer?: string; euuid: string; event: React.MouseEvent; @@ -109,7 +107,6 @@ export const ChildAccountsTable = (props: ChildAccountsTableProps) => { onSwitchAccount({ currentTokenWithBearer, euuid: childAccount.euuid, - company: childAccount.company, event, onClose, userType, diff --git a/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts b/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts index 08b8c94e87d..f9ea6ec5efb 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts +++ b/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts @@ -1,7 +1,7 @@ import React from 'react'; import { PARENT_USER_SESSION_EXPIRED } from 'src/features/Account/constants'; -import { clearStorage, setStorage } from 'src/utilities/storage'; +import { setStorage } from 'src/utilities/storage'; import { useParentChildAuthentication } from './useParentChildAuthentication'; @@ -38,8 +38,7 @@ export const useSwitchToParentAccount = ({ // Flag to prevent multiple clicks on the switch account button. setSubmitting(true); - // Clean up the company name in storage to prevent it from being used in the parent account after switching back from a child account. - clearStorage('switch_account/company_name'); + try { // Revoke proxy or delegate token before switching to parent account. await revokeToken().catch(() => { diff --git a/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx b/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx index 363ba3e34dc..9cae73e0e82 100644 --- a/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx +++ b/packages/manager/src/features/TopMenu/UserMenu/UserMenu.tsx @@ -43,10 +43,8 @@ export const UserMenu = React.memo(() => { const open = Boolean(anchorEl); const id = open ? 'user-menu-popover' : undefined; - const switchAccountCompanyName = getStorage('switch_account/company_name'); - const companyNameOrEmail = getCompanyNameOrEmail({ - company: account?.company ? account?.company : switchAccountCompanyName, + company: account?.company, profile, }); @@ -85,9 +83,10 @@ export const UserMenu = React.memo(() => { setStorage('is_delegate_user_type', 'true'); } - enqueueSnackbar(`Account switched to ${companyNameOrEmail}.`, { - variant: 'success', - }); + const message = companyNameOrEmail + ? `Account switched to ${companyNameOrEmail}.` + : 'Account switched.'; + enqueueSnackbar(message, { variant: 'success' }); } }, [ isProxyOrDelegateUserType, diff --git a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx index fd8871b1db0..d5b6997d94e 100644 --- a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx +++ b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx @@ -121,9 +121,8 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { } : undefined; - const switchAccountCompanyName = getStorage('switch_account/company_name'); const companyNameOrEmail = getCompanyNameOrEmail({ - company: account?.company ? account?.company : switchAccountCompanyName, + company: account?.company, profile, }); const { data: parentProfile } = useProfile({ headers: proxyHeaders }); @@ -237,8 +236,11 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { gap={(theme) => theme.tokens.spacing.S16} minWidth={250} > - theme.tokens.spacing.S8}> - {canSwitchBetweenParentOrProxyAccount && ( + (companyNameOrEmail ? theme.tokens.spacing.S8 : 0)} + > + {canSwitchBetweenParentOrProxyAccount && companyNameOrEmail && ( ({ color: theme.tokens.alias.Content.Text.Primary.Default, @@ -255,8 +257,8 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { overflowWrap: 'break-word', })} > - {canSwitchBetweenParentOrProxyAccount && companyNameOrEmail - ? companyNameOrEmail + {canSwitchBetweenParentOrProxyAccount + ? companyNameOrEmail || null : userName} {canSwitchBetweenParentOrProxyAccount && ( From a35e19034feefe9394d3519dbdc012d98715bf6a Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Thu, 26 Feb 2026 13:03:20 +0100 Subject: [PATCH 4/6] test fix --- .../src/features/Account/SwitchAccountDrawer.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index b7d09f90a21..1699e2aa5e2 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -159,12 +159,18 @@ export const SwitchAccountDrawer = (props: Props) => { userType: isIAMDelegationEnabled ? 'delegate' : 'proxy', }); onClose(event); - location.replace('/linodes'); + + // Only redirect to /linodes for IAM delegate users + if (isIAMDelegationEnabled) { + location.replace('/linodes'); + } else { + location.reload(); + } } catch { // Error is handled by createTokenError. } }, - [createToken, isProxyUserType, updateCurrentToken, revokeToken] + [createToken, updateCurrentToken, revokeToken, isIAMDelegationEnabled] ); const [isSwitchingChildAccounts, setIsSwitchingChildAccounts] = From ea623b22148cfab4048d723140ea447010eb1ea7 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Thu, 26 Feb 2026 15:34:39 +0100 Subject: [PATCH 5/6] mock delegation feature flag --- .../cypress/e2e/core/parentChild/account-switching.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts index 3d88c570b0d..768cd7071c4 100644 --- a/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts +++ b/packages/manager/cypress/e2e/core/parentChild/account-switching.spec.ts @@ -22,6 +22,7 @@ import { mockGetUser, } from 'support/intercepts/account'; import { mockGetEvents, mockGetNotifications } from 'support/intercepts/events'; +import { mockAppendFeatureFlags } from 'support/intercepts/feature-flags'; import { mockAllApiRequests } from 'support/intercepts/general'; import { mockGetRolePermissionsError, @@ -151,6 +152,10 @@ const mockAlternateChildAccountToken = appTokenFactory.build({ const mockErrorMessage = 'An unknown error has occurred.'; describe('Parent/Child account switching', () => { + beforeEach(() => { + // Disable IAM delegation to use legacy child accounts flow for all tests + mockAppendFeatureFlags({ iamDelegation: false }); + }); /* * Tests to confirm that Parent account users can switch to Child accounts as expected. */ From c1d0dd7d03893596bbd4f312149369568e784d0b Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Fri, 27 Feb 2026 16:06:57 +0100 Subject: [PATCH 6/6] UIE-10360: Navigate to Linodes landing page after switching back to parent account --- .../Account/SwitchAccounts/useSwitchToParentAccount.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts b/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts index f9ea6ec5efb..94a98755342 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts +++ b/packages/manager/src/features/Account/SwitchAccounts/useSwitchToParentAccount.ts @@ -55,7 +55,13 @@ export const useSwitchToParentAccount = ({ } onClose?.(); - location.reload(); + + // For switch back to parent, always redirect to /linodes for delegate users + if (isDelegateUserType) { + location.replace('/linodes'); + } else { + location.reload(); + } } catch (error) { setSubmitting(false); throw error;