diff --git a/packages/manager/.changeset/pr-13266-tech-stories-1768401757846.md b/packages/manager/.changeset/pr-13266-tech-stories-1768401757846.md new file mode 100644 index 00000000000..ed228c7281c --- /dev/null +++ b/packages/manager/.changeset/pr-13266-tech-stories-1768401757846.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Tech Stories +--- + +IAM - Clean up beta flag + BETA/LA logic ([#13266](https://github.com/linode/manager/pull/13266)) diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx index de1aa70d4ec..05870b0fede 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.test.tsx @@ -22,7 +22,6 @@ const queryString = 'menu-item-Managed'; const queryMocks = vi.hoisted(() => ({ useIsIAMEnabled: vi.fn(() => ({ - isIAMBeta: false, isIAMEnabled: false, })), usePreferences: vi.fn().mockReturnValue({}), @@ -497,7 +496,6 @@ describe('PrimaryNav', () => { it('should show Administration links', async () => { const flags: Partial = { iam: { - beta: true, enabled: true, }, limitsEvolution: { @@ -508,7 +506,6 @@ describe('PrimaryNav', () => { }; queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMBeta: true, isIAMEnabled: true, }); @@ -544,13 +541,11 @@ describe('PrimaryNav', () => { it('should hide Identity & Access link for non beta users', async () => { const flags: Partial = { iam: { - beta: true, enabled: false, }, }; queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMBeta: true, isIAMEnabled: false, }); diff --git a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx index 1aa77fc982f..ebe54255732 100644 --- a/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx +++ b/packages/manager/src/components/PrimaryNav/PrimaryNav.tsx @@ -119,7 +119,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { const { isPlacementGroupsEnabled } = useIsPlacementGroupsEnabled(); const { isDatabasesEnabled, isDatabasesV2Beta } = useIsDatabasesEnabled(); - const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled(); + const { isIAMEnabled } = useIsIAMEnabled(); const showLimitedAvailabilityBadges = flags.iamLimitedAvailabilityBadges; const { isNetworkLoadBalancerEnabled } = useIsNetworkLoadBalancerEnabled(); @@ -297,8 +297,7 @@ export const PrimaryNav = (props: PrimaryNavProps) => { display: 'Identity & Access', hide: !isIAMEnabled, to: '/iam', - isBeta: isIAMBeta, - isNew: !isIAMBeta && showLimitedAvailabilityBadges, + isNew: isIAMEnabled && showLimitedAvailabilityBadges, }, { display: 'Quotas', @@ -352,7 +351,6 @@ export const PrimaryNav = (props: PrimaryNavProps) => { isACLPEnabled, isACLPLogsBeta, isACLPLogsEnabled, - isIAMBeta, isIAMEnabled, isMarketplaceV2FeatureEnabled, isNetworkLoadBalancerEnabled, diff --git a/packages/manager/src/featureFlags.ts b/packages/manager/src/featureFlags.ts index b0445bf0f18..2792798740e 100644 --- a/packages/manager/src/featureFlags.ts +++ b/packages/manager/src/featureFlags.ts @@ -223,7 +223,7 @@ export interface Flags { gecko2: GeckoFeatureFlag; generationalPlansv2: GenerationalPlansFlag; gpuv2: GpuV2; - iam: BetaFeatureFlag; + iam: BaseFeatureFlag; iamDelegation: BaseFeatureFlag; iamLimitedAvailabilityBadges: boolean; ipv6Sharing: boolean; diff --git a/packages/manager/src/features/IAM/IAMLanding.tsx b/packages/manager/src/features/IAM/IAMLanding.tsx index 06ab02a1018..930b2b5131f 100644 --- a/packages/manager/src/features/IAM/IAMLanding.tsx +++ b/packages/manager/src/features/IAM/IAMLanding.tsx @@ -19,9 +19,9 @@ import { IAM_DOCS_LINK, ROLES_LEARN_MORE_LINK } from './Shared/constants'; export const IdentityAccessLanding = React.memo(() => { const flags = useFlags(); - const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled(); + const { isIAMEnabled } = useIsIAMEnabled(); const showLimitedAvailabilityBadges = - flags.iamLimitedAvailabilityBadges && isIAMEnabled && !isIAMBeta; + flags.iamLimitedAvailabilityBadges && isIAMEnabled; const location = useLocation(); const navigate = useNavigate(); const { isParentAccount } = useDelegationRole(); diff --git a/packages/manager/src/features/IAM/Roles/Defaults/DefaultsLanding.tsx b/packages/manager/src/features/IAM/Roles/Defaults/DefaultsLanding.tsx index 18cbf8d3a6e..792dcc47ac4 100644 --- a/packages/manager/src/features/IAM/Roles/Defaults/DefaultsLanding.tsx +++ b/packages/manager/src/features/IAM/Roles/Defaults/DefaultsLanding.tsx @@ -17,9 +17,9 @@ export const DefaultsLanding = () => { const location = useLocation(); const navigate = useNavigate(); const flags = useFlags(); - const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled(); + const { isIAMEnabled } = useIsIAMEnabled(); const showLimitedAvailabilityBadges = - flags.iamLimitedAvailabilityBadges && isIAMEnabled && !isIAMBeta; + flags.iamLimitedAvailabilityBadges && isIAMEnabled; const { tabs, tabIndex, handleTabChange } = useTabs([ { diff --git a/packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx b/packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx index 24e44c98a70..d0500deca04 100644 --- a/packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx +++ b/packages/manager/src/features/IAM/Users/UserDetailsLanding.tsx @@ -23,9 +23,9 @@ import { export const UserDetailsLanding = () => { const flags = useFlags(); - const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled(); + const { isIAMEnabled } = useIsIAMEnabled(); const showLimitedAvailabilityBadges = - flags.iamLimitedAvailabilityBadges && isIAMEnabled && !isIAMBeta; + flags.iamLimitedAvailabilityBadges && isIAMEnabled; const { username } = useParams({ from: '/iam/users/$username' }); const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled(); const { isParentAccount } = useDelegationRole(); diff --git a/packages/manager/src/features/IAM/hooks/useGetAllUserEntitiesByPermission.ts b/packages/manager/src/features/IAM/hooks/useGetAllUserEntitiesByPermission.ts index b1b6eab9076..289642f293c 100644 --- a/packages/manager/src/features/IAM/hooks/useGetAllUserEntitiesByPermission.ts +++ b/packages/manager/src/features/IAM/hooks/useGetAllUserEntitiesByPermission.ts @@ -11,10 +11,6 @@ import { import { entityPermissionMapFrom } from './adapters/permissionAdapters'; import { useIsIAMEnabled } from './useIsIAMEnabled'; -import { - BETA_ACCESS_TYPE_SCOPE, - LA_ACCOUNT_ADMIN_PERMISSIONS_TO_EXCLUDE, -} from './usePermissions'; import type { APIError, @@ -115,7 +111,7 @@ export const useGetAllUserEntitiesByPermission = ({ filter = {}, params = {}, }: UseGetEntitiesByPermissionProps) => { - const { isIAMBeta, isIAMEnabled, profile } = useIsIAMEnabled(); + const { isIAMEnabled, profile } = useIsIAMEnabled(); // Get entities by permission (Restricted IAM users only) const { @@ -129,24 +125,11 @@ export const useGetAllUserEntitiesByPermission = ({ enabled: enabled && isIAMEnabled, }); - /** - * Determine if we should use IAM permissions or legacy permissions - */ - const useBetaPermissions = - isIAMEnabled && - isIAMBeta && - BETA_ACCESS_TYPE_SCOPE.includes(entityType) && - LA_ACCOUNT_ADMIN_PERMISSIONS_TO_EXCLUDE.some((blacklistedPermission) => - permission.includes(blacklistedPermission) - ) === false; - const useLAPermissions = isIAMEnabled && !isIAMBeta; - const shouldUseIAMPermissions = useBetaPermissions || useLAPermissions; - /** * Extract entity IDs from the entities by permission data */ const entityIds = - shouldUseIAMPermissions && profile?.restricted + isIAMEnabled && profile?.restricted ? entitiesByPermission?.map((e) => e.id) : undefined; @@ -159,7 +142,7 @@ export const useGetAllUserEntitiesByPermission = ({ * Legacy grants for non-IAM users */ const { data: grants, isLoading: grantsLoading } = useGrants( - !shouldUseIAMPermissions && profile?.restricted && enabled + !isIAMEnabled && profile?.restricted && enabled ); /** @@ -168,7 +151,7 @@ export const useGetAllUserEntitiesByPermission = ({ * In case a filter was used, we also return it to be used for client-side filtering * ex: we also pass this filter to the LinodeSelect to avoid caching two different queries (with and without filter) */ - if (shouldUseIAMPermissions) { + if (isIAMEnabled) { return { ...entityQuery, filter, diff --git a/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.test.ts b/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.test.ts index b4c671a46a6..f17859b4370 100644 --- a/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.test.ts +++ b/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.test.ts @@ -43,7 +43,6 @@ describe('useIsIAMEnabled', () => { }); await waitFor(() => { - expect(result.current.isIAMBeta).toBe(true); expect(result.current.isIAMEnabled).toBe(true); }); }); @@ -67,8 +66,6 @@ describe('useIsIAMEnabled', () => { }); await waitFor(() => { - expect(result.current.isIAMBeta).toBe(false); - // eslint-disable-next-line testing-library/no-wait-for-multiple-assertions expect(result.current.isIAMEnabled).toBe(true); // eslint-disable-next-line testing-library/no-wait-for-multiple-assertions expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(true); @@ -94,8 +91,6 @@ describe('useIsIAMEnabled', () => { }); await waitFor(() => { - expect(result.current.isIAMBeta).toBe(false); - // eslint-disable-next-line testing-library/no-wait-for-multiple-assertions expect(result.current.isIAMEnabled).toBe(false); // eslint-disable-next-line testing-library/no-wait-for-multiple-assertions expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(false); @@ -120,8 +115,6 @@ describe('useIsIAMEnabled', () => { }); await waitFor(() => { - expect(result.current.isIAMBeta).toBe(true); - // eslint-disable-next-line testing-library/no-wait-for-multiple-assertions expect(result.current.isIAMEnabled).toBe(false); // eslint-disable-next-line testing-library/no-wait-for-multiple-assertions expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(true); diff --git a/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts b/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts index 8d5fc172f1e..9802fe6195f 100644 --- a/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts +++ b/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts @@ -28,7 +28,6 @@ export const useIsIAMEnabled = () => { useUserAccountPermissions(flags?.iam?.enabled === true); return { - isIAMBeta: flags.iam?.beta, isIAMEnabled: flags?.iam?.enabled && Boolean(roles || permissions), isLoading: isLoadingRoles || isLoadingPermissions, accountRoles: roles, diff --git a/packages/manager/src/features/IAM/hooks/usePermissions.test.ts b/packages/manager/src/features/IAM/hooks/usePermissions.test.ts index 1fed21f3720..58e65619f5f 100644 --- a/packages/manager/src/features/IAM/hooks/usePermissions.test.ts +++ b/packages/manager/src/features/IAM/hooks/usePermissions.test.ts @@ -7,9 +7,7 @@ import { usePermissions } from './usePermissions'; import type { AccessType, PermissionType } from '@linode/api-v4'; const queryMocks = vi.hoisted(() => ({ - useIsIAMEnabled: vi - .fn() - .mockReturnValue({ isIAMEnabled: true, isIAMBeta: true }), + useIsIAMEnabled: vi.fn().mockReturnValue({ isIAMEnabled: true }), useUserAccountPermissions: vi.fn().mockReturnValue({ data: ['cancel_account', 'create_linode'], }), @@ -75,7 +73,7 @@ describe('usePermissions', () => { }); it('returns correct map when IAM is enabled (account)', () => { - const flags = { iam: { beta: true, enabled: true } }; + const flags = { iam: { enabled: true } }; renderHook( () => usePermissions('account', ['cancel_account', 'create_linode']), @@ -94,7 +92,7 @@ describe('usePermissions', () => { }); it('returns correct map when IAM is enabled (entity)', () => { - const flags = { iam: { beta: true, enabled: true } }; + const flags = { iam: { enabled: true } }; renderHook( () => usePermissions('linode', ['reboot_linode', 'view_linode'], 123), @@ -120,7 +118,7 @@ describe('usePermissions', () => { data: { global: { add_linode: true } }, }); - const flags = { iam: { beta: false, enabled: false } }; + const flags = { iam: { enabled: false } }; renderHook( () => usePermissions('account', ['cancel_account', 'create_linode']), { @@ -136,83 +134,4 @@ describe('usePermissions', () => { false ); }); - - it('returns correct map when IAM beta is false', () => { - queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMEnabled: true, - isIAMBeta: false, - }); - const flags = { iam: { beta: false, enabled: true } }; - - renderHook(() => usePermissions('account', ['create_linode']), { - wrapper: (ui) => wrapWithTheme(ui, { flags }), - }); - - expect(queryMocks.useGrants).toHaveBeenCalledWith(false); - expect(queryMocks.useUserEntityPermissions).toHaveBeenCalledWith( - 'account', - undefined, - true - ); - }); - - it('returns correct map when beta is true and neither the access type nor the permissions are in the limited availability scope', () => { - const flags = { iam: { beta: true, enabled: true } }; - queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMEnabled: true, - isIAMBeta: true, - }); - - renderHook(() => usePermissions('linode', ['update_linode'], 123), { - wrapper: (ui) => wrapWithTheme(ui, { flags }), - }); - - expect(queryMocks.useGrants).toHaveBeenCalledWith(false); - expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(false); - expect(queryMocks.useUserEntityPermissions).toHaveBeenCalledWith( - 'linode', - 123, - true - ); - }); - - it('returns correct map when beta is true and the access type is in the limited availability scope', () => { - const flags = { iam: { beta: true, enabled: true } }; - queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMEnabled: true, - isIAMBeta: true, - }); - - renderHook(() => usePermissions('volume', ['resize_volume'], 123), { - wrapper: (ui) => wrapWithTheme(ui, { flags }), - }); - - expect(queryMocks.useGrants).toHaveBeenCalledWith(true); - expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(false); - expect(queryMocks.useUserEntityPermissions).toHaveBeenCalledWith( - 'volume', - 123, - false - ); - }); - - it('returns correct map when beta is true and one of the permissions is in the limited availability scope', () => { - const flags = { iam: { beta: true, enabled: true } }; - queryMocks.useIsIAMEnabled.mockReturnValue({ - isIAMEnabled: true, - isIAMBeta: true, - }); - - renderHook(() => usePermissions('account', ['create_volume']), { - wrapper: (ui) => wrapWithTheme(ui, { flags }), - }); - - expect(queryMocks.useGrants).toHaveBeenCalledWith(true); - expect(queryMocks.useUserAccountPermissions).toHaveBeenCalledWith(false); - expect(queryMocks.useUserEntityPermissions).toHaveBeenCalledWith( - 'account', - undefined, - false - ); - }); }); diff --git a/packages/manager/src/features/IAM/hooks/usePermissions.ts b/packages/manager/src/features/IAM/hooks/usePermissions.ts index b184155af59..f64e0c5e6e6 100644 --- a/packages/manager/src/features/IAM/hooks/usePermissions.ts +++ b/packages/manager/src/features/IAM/hooks/usePermissions.ts @@ -34,19 +34,6 @@ import type { } from '@linode/api-v4'; import type { UseQueryResult } from '@linode/queries'; -export const BETA_ACCESS_TYPE_SCOPE: AccessType[] = [ - 'account', - 'linode', - 'firewall', -]; -export const LA_ACCOUNT_ADMIN_PERMISSIONS_TO_EXCLUDE = [ - 'create_image', - 'upload_image', - 'create_vpc', - 'create_volume', - 'create_nodebalancer', -]; - type EntityPermission = | FirewallAdmin | FirewallContributor @@ -151,7 +138,7 @@ export function usePermissions< entityId?: number | string, enabled: boolean = true ): PermissionsResult { - const { isIAMBeta, isIAMEnabled } = useIsIAMEnabled(); + const { isIAMEnabled } = useIsIAMEnabled(); const { data: profile } = useProfile(); const _entityId = @@ -159,31 +146,8 @@ export function usePermissions< ? entityId.split('/')[1] : entityId; - /** - * BETA and LA features should use the new permission model. - * However, beta features are limited to a subset of AccessTypes and account permissions. - * - Use Beta Permissions if: - * - The feature is beta - * - The access type is in the BETA_ACCESS_TYPE_SCOPE - * - The account permission is not in the LA_ACCOUNT_ADMIN_PERMISSIONS_TO_EXCLUDE - * - Use LA Permissions if: - * - The feature is not beta - */ - const useBetaPermissions = - isIAMEnabled && - isIAMBeta && - BETA_ACCESS_TYPE_SCOPE.includes(accessType) && - LA_ACCOUNT_ADMIN_PERMISSIONS_TO_EXCLUDE.some( - (blacklistedPermission) => - permissionsToCheck.includes( - blacklistedPermission as AllowedPermissionsFor - ) // some of the account admin in the blacklist have not been added yet - ) === false; - const useLAPermissions = isIAMEnabled && !isIAMBeta; - const shouldUsePermissionMap = useBetaPermissions || useLAPermissions; - const { data: grants, isLoading: isGrantsLoading } = useGrants( - (!isIAMEnabled || !shouldUsePermissionMap) && enabled + !isIAMEnabled && enabled ); const { @@ -191,23 +155,19 @@ export function usePermissions< isLoading: isUserAccountPermissionsLoading, ...restAccountPermissions } = useUserAccountPermissions( - shouldUsePermissionMap && accessType === 'account' && enabled + isIAMEnabled && accessType === 'account' && enabled ); const { data: userEntityPermissions, isLoading: isUserEntityPermissionsLoading, ...restEntityPermissions - } = useUserEntityPermissions( - accessType, - _entityId!, - shouldUsePermissionMap && enabled - ); + } = useUserEntityPermissions(accessType, _entityId!, isIAMEnabled && enabled); const usersPermissions = accessType === 'account' ? userAccountPermissions : userEntityPermissions; - const permissionMap = shouldUsePermissionMap + const permissionMap = isIAMEnabled ? toPermissionMap( permissionsToCheck, Array.isArray(usersPermissions) ? usersPermissions : [], @@ -224,7 +184,7 @@ export function usePermissions< return { data: permissionMap, - isLoading: shouldUsePermissionMap + isLoading: isIAMEnabled ? isUserAccountPermissionsLoading || isUserEntityPermissionsLoading : isGrantsLoading, ...restAccountPermissions, diff --git a/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.test.tsx b/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.test.tsx index 47aade6506a..7010b5eb7d4 100644 --- a/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.test.tsx +++ b/packages/manager/src/features/Linodes/LinodeEntityDetailFooter.test.tsx @@ -53,7 +53,7 @@ describe('LinodeEntityDetailFooter', () => { const { getByRole } = renderWithTheme( , - { flags: { iam: { enabled: true, beta: true } } } + { flags: { iam: { enabled: true } } } ); await waitFor(() => { diff --git a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx index d1a3e9035bd..2b874778d16 100644 --- a/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx +++ b/packages/manager/src/features/TopMenu/UserMenu/UserMenuPopover.tsx @@ -51,7 +51,7 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { const { data: account } = useAccount(); const { data: profile } = useProfile(); - const { isIAMEnabled, isIAMBeta } = useIsIAMEnabled(); + const { isIAMEnabled } = useIsIAMEnabled(); const isChildAccountAccessRestricted = useRestrictedGlobalGrantCheck({ globalGrantType: 'child_account_access', @@ -118,8 +118,7 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { { display: isIAMEnabled ? 'Identity & Access' : 'Users & Grants', to: isIAMEnabled ? '/iam' : '/users', - isBeta: isIAMEnabled && isIAMBeta, - isNew: isIAMEnabled && !isIAMBeta && iamLimitedAvailabilityBadges, + isNew: isIAMEnabled && iamLimitedAvailabilityBadges, }, { display: 'Quotas', @@ -143,7 +142,7 @@ export const UserMenuPopover = (props: UserMenuPopoverProps) => { to: '/account-settings', }, ], - [isIAMEnabled, limitsEvolution, iamLimitedAvailabilityBadges, isIAMBeta] + [isIAMEnabled, limitsEvolution, iamLimitedAvailabilityBadges] ); const renderLink = (link: MenuLink) => {