Skip to content

Commit

Permalink
[v17] Display permission banner on Enroll Resource page (#50874)
Browse files Browse the repository at this point in the history
* Display permission banner on Enroll Resource page

This adds an info banner to the Enroll Resource page if the user has no
permissions to add any resource kind.

* Invert integration resource builder (#50746)

This was accidentally setting enterprise to base instead of enterprise
resources
  • Loading branch information
avatus authored Jan 8, 2025
1 parent 4fa7928 commit 146a2b8
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, ...SAML_APPLICATIONS]
: BASE_RESOURCES;
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 146a2b8

Please sign in to comment.