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 = {