diff --git a/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx b/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx index 23c8ff9ab5967..496f7481a6e62 100644 --- a/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx +++ b/web/packages/teleport/src/Discover/SelectResource/SelectResource.test.tsx @@ -16,18 +16,32 @@ * along with this program. If not, see . */ +import { MemoryRouter } from 'react-router'; + import { Platform, UserAgent } from 'design/platform'; +import { render, screen, waitFor } from 'design/utils/testing'; import { OnboardUserPreferences, Resource, } from 'gen-proto-ts/teleport/userpreferences/v1/onboard_pb'; +import { ContextProvider } from 'teleport/index'; +import { + allAccessAcl, + createTeleportContext, + noAccess, +} from 'teleport/mocks/contexts'; import { OnboardDiscover } from 'teleport/services/user'; import { makeDefaultUserPreferences } from 'teleport/services/userPreferences/userPreferences'; +import * as userUserContext from 'teleport/User/UserContext'; import { ResourceKind } from '../Shared'; import { resourceKindToPreferredResource } from '../Shared/ResourceKind'; -import { filterResources, sortResources } from './SelectResource'; +import { + filterResources, + SelectResource, + sortResources, +} from './SelectResource'; import { ResourceSpec } from './types'; const setUp = () => { @@ -1112,6 +1126,56 @@ describe('sorting Connect My Computer', () => { }); }); +test('displays an info banner if lacking "all" permissions to add resources', async () => { + jest.spyOn(userUserContext, 'useUser').mockReturnValue({ + preferences: makeDefaultUserPreferences(), + updatePreferences: () => null, + updateClusterPinnedResources: () => null, + getClusterPinnedResources: () => null, + }); + + const ctx = createTeleportContext(); + ctx.storeUser.setState({ acl: { ...allAccessAcl, tokens: noAccess } }); + + render( + + + {}} /> + + + ); + + await waitFor(() => { + expect( + screen.getByText(/You cannot add new resources./i) + ).toBeInTheDocument(); + }); +}); + +test('does not display erorr banner if user has "some" permissions to add', async () => { + jest.spyOn(userUserContext, 'useUser').mockReturnValue({ + preferences: makeDefaultUserPreferences(), + updatePreferences: () => null, + updateClusterPinnedResources: () => null, + getClusterPinnedResources: () => null, + }); + + const ctx = createTeleportContext(); + ctx.storeUser.setState({ acl: { ...allAccessAcl } }); + + render( + + + {}} /> + + + ); + + expect( + screen.queryByText(/You cannot add new resources./i) + ).not.toBeInTheDocument(); +}); + describe('filterResources', () => { it('filters out resources based on supportedPlatforms', () => { const winAndLinux = makeResourceSpec({ diff --git a/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx b/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx index 37a764b01cd31..34b0d848933b8 100644 --- a/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx +++ b/web/packages/teleport/src/Discover/SelectResource/SelectResource.tsx @@ -16,11 +16,16 @@ * along with this program. If not, see . */ -import { useEffect, useState, type ComponentPropsWithoutRef } from 'react'; +import { + useEffect, + useMemo, + useState, + type ComponentPropsWithoutRef, +} from 'react'; import { useHistory, useLocation } from 'react-router'; import styled from 'styled-components'; -import { Box, Flex, Link, P3, Text } from 'design'; +import { Alert, Box, Flex, Link, P3, Text } from 'design'; import * as Icons from 'design/Icon'; import { NewTab } from 'design/Icon'; import { getPlatform, Platform } from 'design/platform'; @@ -64,6 +69,15 @@ type UrlLocationState = { searchKeywords: string; }; +function getDefaultResources( + includeEnterpriseResources: boolean +): ResourceSpec[] { + const RESOURCES = includeEnterpriseResources + ? [...BASE_RESOURCES, ...SAML_APPLICATIONS] + : BASE_RESOURCES; + return RESOURCES; +} + export function SelectResource({ onSelect }: SelectResourceProps) { const ctx = useTeleport(); const location = useLocation(); @@ -71,12 +85,33 @@ export function SelectResource({ onSelect }: SelectResourceProps) { const { preferences } = useUser(); const [search, setSearch] = useState(''); - const [resources, setResources] = useState([]); - const [defaultResources, setDefaultResources] = useState([]); + const { acl, authType } = ctx.storeUser.state; + const platform = getPlatform(); + const defaultResources: ResourceSpec[] = useMemo( + () => + sortResources( + // Apply access check to each resource. + addHasAccessField( + acl, + filterResources( + platform, + authType, + getDefaultResources(cfg.isEnterprise) + ) + ), + preferences, + storageService.getOnboardDiscover() + ), + [acl, authType, platform, preferences] + ); + const [resources, setResources] = useState(defaultResources); + + // a user must be able to create tokens AND have access to create at least one + // type of resource in order to be considered eligible to "add resources" + const canAddResources = + acl.tokens.create && defaultResources.some(r => r.hasAccess); + const [showApp, setShowApp] = useState(false); - const RESOURCES = !cfg.isEnterprise - ? BASE_RESOURCES - : [...BASE_RESOURCES, ...SAML_APPLICATIONS]; function onSearch(s: string, customList?: ResourceSpec[]) { const list = customList || defaultResources; @@ -95,23 +130,6 @@ export function SelectResource({ onSelect }: SelectResourceProps) { } useEffect(() => { - // Apply access check to each resource. - const userContext = ctx.storeUser.state; - const { acl, authType } = userContext; - const platform = getPlatform(); - - const resources = addHasAccessField( - acl, - filterResources(platform, authType, RESOURCES) - ); - const onboardDiscover = storageService.getOnboardDiscover(); - const sortedResources = sortResources( - resources, - preferences, - onboardDiscover - ); - setDefaultResources(sortedResources); - // A user can come to this screen by clicking on // a `add ` button. // We sort the list by the specified resource type, @@ -127,7 +145,7 @@ export function SelectResource({ onSelect }: SelectResourceProps) { ) { const sortedResourcesByKind = sortResourcesByKind( resourceKindSpecifiedByUrlLoc, - sortedResources + defaultResources ); onSearch(resourceKindSpecifiedByUrlLoc, sortedResourcesByKind); return; @@ -135,11 +153,11 @@ export function SelectResource({ onSelect }: SelectResourceProps) { const searchKeywordSpecifiedByUrlLoc = location.state?.searchKeywords; if (searchKeywordSpecifiedByUrlLoc) { - onSearch(searchKeywordSpecifiedByUrlLoc, sortedResources); + onSearch(searchKeywordSpecifiedByUrlLoc, defaultResources); return; } - setResources(sortedResources); + setResources(defaultResources); // Processing of the lists should only happen once on init. // User perms remain static and URL loc state does not change. // eslint-disable-next-line react-hooks/exhaustive-deps @@ -147,6 +165,12 @@ export function SelectResource({ onSelect }: SelectResourceProps) { return ( + {!canAddResources && ( + + You cannot add new resources. Reach out to your Teleport administrator + for additional permissions. + + )} Select Resource To Add diff --git a/web/packages/teleport/src/features.tsx b/web/packages/teleport/src/features.tsx index 94b92b0150d97..e1af09b9b3dde 100644 --- a/web/packages/teleport/src/features.tsx +++ b/web/packages/teleport/src/features.tsx @@ -476,7 +476,10 @@ export class FeatureIntegrationEnroll implements TeleportFeature { }; hasAccess(flags: FeatureFlags) { - return flags.enrollIntegrations; + if (cfg.hideInaccessibleFeatures) { + return flags.enrollIntegrations; + } + return true; } navigationItem = {