From 4f5ff7baa6b66001fe0490932f8d90d9441fa1a6 Mon Sep 17 00:00:00 2001 From: Trevyn Langsford Date: Fri, 15 Mar 2024 15:51:44 -0400 Subject: [PATCH] ID-1150 Hide GitHub If Not Available (#4717) --- src/auth/auth.ts | 5 +- src/enterprise-features/features.test.ts | 46 +++++++++++++ src/enterprise-features/features.ts | 17 +++++ src/libs/ajax/User.ts | 9 +++ src/libs/state.ts | 2 + .../ExternalIdentities.test.tsx | 65 +++++++++++++++++++ .../ExternalIdentities.tsx | 15 +++-- .../TermsOfServicePage.test.ts | 1 + 8 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 src/enterprise-features/features.test.ts create mode 100644 src/enterprise-features/features.ts create mode 100644 src/profile/external-identities/ExternalIdentities.test.tsx diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 151ad1ad0c..2e733ce4b3 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -572,17 +572,20 @@ export const loadTerraUser = async (): Promise => { const getAllowances = Ajax().User.getUserAllowances(); const getAttributes = Ajax().User.getUserAttributes(); const getTermsOfService = Ajax().TermsOfService.getUserTermsOfServiceDetails(); - const [profile, terraUserAllowances, terraUserAttributes, termsOfService] = await Promise.all([ + const getEnterpriseFeatures = Ajax().User.getEnterpriseFeatures(); + const [profile, terraUserAllowances, terraUserAttributes, termsOfService, enterpriseFeatures] = await Promise.all([ getProfile, getAllowances, getAttributes, getTermsOfService, + getEnterpriseFeatures, ]); clearNotification(sessionTimeoutProps.id); userStore.update((state: TerraUserState) => ({ ...state, profile, terraUserAttributes, + enterpriseFeatures, })); authStore.update((state: AuthState) => ({ ...state, diff --git a/src/enterprise-features/features.test.ts b/src/enterprise-features/features.test.ts new file mode 100644 index 0000000000..80dd9247a2 --- /dev/null +++ b/src/enterprise-features/features.test.ts @@ -0,0 +1,46 @@ +import { act } from '@testing-library/react'; +import { userHasAccessToEnterpriseFeature } from 'src/enterprise-features/features'; +import { TerraUserState, userStore } from 'src/libs/state'; + +describe('features', () => { + describe('when the user has access to an enterprise feature', () => { + it('says the user has access to that feature', async () => { + // Arrange + const userEnterpriseFeatures = ['github-account-linking']; + await act(async () => { + userStore.update((state: TerraUserState) => ({ ...state, enterpriseFeatures: userEnterpriseFeatures })); + }); + + // Act + const hasAccess = userHasAccessToEnterpriseFeature('github-account-linking'); + // Assert + expect(hasAccess).toBe(true); + }); + }); + describe('when the user does not have access to an enterprise feature', () => { + it('says the user does not have access to that feature', async () => { + // Arrange + const userEnterpriseFeatures = []; + await act(async () => { + userStore.update((state: TerraUserState) => ({ ...state, enterpriseFeatures: userEnterpriseFeatures })); + }); + + // Act + const hasAccess = userHasAccessToEnterpriseFeature('github-account-linking'); + // Assert + expect(hasAccess).toBe(false); + }); + }); + it('it raises an error if the feature is not found', async () => { + // Arrange + const userEnterpriseFeatures = []; + await act(async () => { + userStore.update((state: TerraUserState) => ({ ...state, enterpriseFeatures: userEnterpriseFeatures })); + }); + + // Act + const throws = () => userHasAccessToEnterpriseFeature('feature-not-found'); + // Assert + expect(throws).toThrow(Error); + }); +}); diff --git a/src/enterprise-features/features.ts b/src/enterprise-features/features.ts new file mode 100644 index 0000000000..0b98e254cc --- /dev/null +++ b/src/enterprise-features/features.ts @@ -0,0 +1,17 @@ +import { userStore } from 'src/libs/state'; + +type EnterpriseFeature = { + id: string; + name: string; +}; + +const allFeatures: EnterpriseFeature[] = [{ id: 'github-account-linking', name: 'GitHub Account Linking' }]; + +export const userHasAccessToEnterpriseFeature = (feature: string): boolean => { + const matchingFeature = allFeatures.find((f) => f.id === feature); + if (!matchingFeature) { + throw new Error(`Feature ${feature} not found`); + } + const userEnterpriseFeatures = userStore.get().enterpriseFeatures; + return userEnterpriseFeatures?.includes(matchingFeature.id) ?? false; +}; diff --git a/src/libs/ajax/User.ts b/src/libs/ajax/User.ts index 4f271ecf12..1b7e488816 100644 --- a/src/libs/ajax/User.ts +++ b/src/libs/ajax/User.ts @@ -216,6 +216,15 @@ export const User = (signal?: AbortSignal) => { return res.json(); }, + getEnterpriseFeatures: async (): Promise => { + const res = await fetchSam( + '/api/resources/v2?format=flat&resourceTypes=enterprise-feature&roles=user', + _.mergeAll([authOpts(), { signal }]) + ); + const json = await res.json(); + return json.resources.map((resource) => resource.resourceId); + }, + registerWithProfile: async ( acceptsTermsOfService: boolean, profile: CreateTerraUserProfileRequest diff --git a/src/libs/state.ts b/src/libs/state.ts index 8806082c07..3272b049b0 100644 --- a/src/libs/state.ts +++ b/src/libs/state.ts @@ -132,6 +132,7 @@ export interface TerraUserState { profile: TerraUserProfile; terraUser: TerraUser; terraUserAttributes: SamUserAttributes; + enterpriseFeatures: string[]; } /** @@ -165,6 +166,7 @@ export const userStore: Atom = atom({ terraUserAttributes: { marketingConsent: true, }, + enterpriseFeatures: [], }); export const getTerraUser = (): TerraUser => userStore.get().terraUser; diff --git a/src/profile/external-identities/ExternalIdentities.test.tsx b/src/profile/external-identities/ExternalIdentities.test.tsx new file mode 100644 index 0000000000..db0285484e --- /dev/null +++ b/src/profile/external-identities/ExternalIdentities.test.tsx @@ -0,0 +1,65 @@ +import { asMockedFn } from '@terra-ui-packages/test-utils'; +import { act, screen } from '@testing-library/react'; +import React from 'react'; +import { getConfig } from 'src/libs/config'; +import { TerraUserState, userStore } from 'src/libs/state'; +import { ExternalIdentities } from 'src/profile/external-identities/ExternalIdentities'; +import { renderWithAppContexts as render } from 'src/testing/test-utils'; + +jest.mock('src/libs/config', () => ({ + ...jest.requireActual('src/libs/config'), + getConfig: jest.fn().mockReturnValue({}), +})); + +jest.mock('src/profile/external-identities/OAuth2Link', () => ({ + ...jest.requireActual('src/profile/external-identities/OAuth2Link'), + OAuth2Link: jest.fn((props) =>
{props.provider.name}
), +})); + +jest.mock('src/profile/external-identities/OAuth2Link', () => ({ + ...jest.requireActual('src/profile/external-identities/OAuth2Link'), + OAuth2Link: jest.fn((props) =>
{props.provider.name}
), +})); + +jest.mock('src/profile/external-identities/FenceAccount', () => ({ + ...jest.requireActual('src/profile/external-identities/FenceAccount'), + FenceAccount: jest.fn((props) =>
{props.provider.name}
), +})); + +jest.mock('src/profile/external-identities/NihAccount', () => ({ + ...jest.requireActual('src/profile/external-identities/NihAccount'), + NihAccount: jest.fn(() =>
Nih Account
), +})); +describe('ExternalIdentities', () => { + beforeEach(() => + asMockedFn(getConfig).mockReturnValue({ externalCreds: { providers: ['github'], urlRoot: 'https/foo.bar.com' } }) + ); + describe('when the user has access to GitHub Account Linking', () => { + it('shows the GitHub Account Linking card', async () => { + // Arrange + await act(async () => { + userStore.update((state: TerraUserState) => ({ ...state, enterpriseFeatures: ['github-account-linking'] })); + }); + + // Act + render(); + + // Assert + screen.getByText('GitHub'); + }); + }); + describe('when the user does not have access to GitHub Account Linking', () => { + it('hides the GitHub Account Linking card', async () => { + // Arrange + await act(async () => { + userStore.update((state: TerraUserState) => ({ ...state, enterpriseFeatures: [] })); + }); + + // Act + render(); + + // Assert + expect(screen.queryByText('GitHub')).toBeNull(); + }); + }); +}); diff --git a/src/profile/external-identities/ExternalIdentities.tsx b/src/profile/external-identities/ExternalIdentities.tsx index 1ea4cb5c84..a220a24bf6 100644 --- a/src/profile/external-identities/ExternalIdentities.tsx +++ b/src/profile/external-identities/ExternalIdentities.tsx @@ -1,6 +1,7 @@ import _ from 'lodash/fp'; import React, { ReactNode } from 'react'; import { PageBox, PageBoxVariants } from 'src/components/PageBox'; +import { userHasAccessToEnterpriseFeature } from 'src/enterprise-features/features'; import { getConfig } from 'src/libs/config'; import allProviders from 'src/libs/providers'; import { FenceAccount } from 'src/profile/external-identities/FenceAccount'; @@ -24,13 +25,13 @@ export const ExternalIdentities = (props: ExternalIdentitiesProps): ReactNode => ), allProviders )} - {getConfig().externalCreds?.providers.map((providerKey) => ( - - ))} + {getConfig().externalCreds?.providers.includes('ras') && ( + + )} + {getConfig().externalCreds?.providers.includes('github') && + userHasAccessToEnterpriseFeature('github-account-linking') && ( + + )} ); }; diff --git a/src/registration/terms-of-service/TermsOfServicePage.test.ts b/src/registration/terms-of-service/TermsOfServicePage.test.ts index a9a10882bd..c45877408f 100644 --- a/src/registration/terms-of-service/TermsOfServicePage.test.ts +++ b/src/registration/terms-of-service/TermsOfServicePage.test.ts @@ -74,6 +74,7 @@ const setupMockAjax = async ( User: { getUserAttributes: jest.fn().mockResolvedValue({ marketingConsent: true }), getUserAllowances: jest.fn().mockResolvedValue(terraUserAllowances), + getEnterpriseFeatures: jest.fn().mockResolvedValue([]), profile: { get: jest.fn().mockResolvedValue({ keyValuePairs: [] }), update: jest.fn().mockResolvedValue({ keyValuePairs: [] }),