Skip to content

Commit

Permalink
Display permission banner on Enroll Resource page (#50657)
Browse files Browse the repository at this point in the history
This adds an info banner to the Enroll Resource page if the user has no
permissions to add any resource kind.
  • Loading branch information
avatus authored Jan 3, 2025
1 parent c77f177 commit 2fc0ea5
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,32 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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 = () => {
Expand Down Expand Up @@ -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(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<SelectResource onSelect={() => {}} />
</ContextProvider>
</MemoryRouter>
);

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(
<MemoryRouter>
<ContextProvider ctx={ctx}>
<SelectResource onSelect={() => {}} />
</ContextProvider>
</MemoryRouter>
);

expect(
screen.queryByText(/You cannot add new resources./i)
).not.toBeInTheDocument();
});

describe('filterResources', () => {
it('filters out resources based on supportedPlatforms', () => {
const winAndLinux = makeResourceSpec({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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';
Expand Down Expand Up @@ -64,19 +69,49 @@ type UrlLocationState = {
searchKeywords: string;
};

function getDefaultResources(
includeEnterpriseResources: boolean
): ResourceSpec[] {
const RESOURCES = includeEnterpriseResources
? BASE_RESOURCES
: [...BASE_RESOURCES, ...SAML_APPLICATIONS];
return RESOURCES;
}

export function SelectResource({ onSelect }: SelectResourceProps) {
const ctx = useTeleport();
const location = useLocation<UrlLocationState>();
const history = useHistory();
const { preferences } = useUser();

const [search, setSearch] = useState('');
const [resources, setResources] = useState<ResourceSpec[]>([]);
const [defaultResources, setDefaultResources] = useState<ResourceSpec[]>([]);
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;
Expand All @@ -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 <specific-resource-type>` button.
// We sort the list by the specified resource type,
Expand All @@ -127,26 +145,32 @@ export function SelectResource({ onSelect }: SelectResourceProps) {
) {
const sortedResourcesByKind = sortResourcesByKind(
resourceKindSpecifiedByUrlLoc,
sortedResources
defaultResources
);
onSearch(resourceKindSpecifiedByUrlLoc, sortedResourcesByKind);
return;
}

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
}, []);

return (
<Box>
{!canAddResources && (
<Alert kind="info" mt={5}>
You cannot add new resources. Reach out to your Teleport administrator
for additional permissions.
</Alert>
)}
<FeatureHeader>
<FeatureHeaderTitle>Select Resource To Add</FeatureHeaderTitle>
</FeatureHeader>
Expand Down
5 changes: 4 additions & 1 deletion web/packages/teleport/src/features.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,10 @@ export class FeatureIntegrationEnroll implements TeleportFeature {
};

hasAccess(flags: FeatureFlags) {
return flags.enrollIntegrations;
if (cfg.hideInaccessibleFeatures) {
return flags.enrollIntegrations;
}
return true;
}

navigationItem = {
Expand Down

0 comments on commit 2fc0ea5

Please sign in to comment.