Skip to content

Commit

Permalink
Merge branch 'dev' into UIE-162
Browse files Browse the repository at this point in the history
  • Loading branch information
cahrens authored Mar 25, 2024
2 parents e41f9c8 + e263fb5 commit 06d8cc9
Show file tree
Hide file tree
Showing 42 changed files with 587 additions and 159 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ jobs:
env: staging
integration-tests-staging:
executor: puppeteer
resource_class: medium+
parallelism: 4
steps:
- integration-tests:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/run-e2e-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: Run E2E integration Tests
#
# There are the required service accounts:
# lyle-user@terra-lyle.iam.gserviceaccount.com
# firecloud-alpha@broad-dsde-alpha.iam.gserviceaccount.com
# firecloud-dev@broad-dsde-dev.iam.gserviceaccount.com
#
# Instructions setting up Workload Identity Federation can be found here:
# https://docs.google.com/document/d/1bnhDmWQHAMat_Saoa_z28FHwXmGWw6kywjdbKf208h4/edit?usp=sharing
Expand Down Expand Up @@ -98,7 +98,7 @@ jobs:
with:
token_format: 'access_token'
workload_identity_provider: 'projects/1038484894585/locations/global/workloadIdentityPools/github-wi-pool/providers/github-wi-provider'
service_account: 'firecloud-alpha@broad-dsde-alpha.iam.gserviceaccount.com'
service_account: 'firecloud-dev@broad-dsde-dev.iam.gserviceaccount.com'
access_token_scopes: 'profile, email, openid'
access_token_subject: 'Scarlett.Flowerpicker@test.firecloud.org'
export_environment_variables: false
Expand Down
12 changes: 6 additions & 6 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion integration-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"scripts": {
"test": "jest",
"test-local": "TERRA_SA_KEY=$(vault read --format=json secret/dsde/firecloud/alpha/common/firecloud-account.json | jq .data) LYLE_SA_KEY=$(vault read --format=json secret/dsde/terra/envs/common/lyle-user-service-account-key | jq .data) TEST_URL=http://localhost:3000 yarn test",
"test-local": "TERRA_SA_KEY=$(vault read --format=json secret/dsde/firecloud/dev/common/firecloud-account.json | jq .data) LYLE_SA_KEY=$(vault read --format=json secret/dsde/terra/envs/common/lyle-user-service-account-key | jq .data) TEST_URL=http://localhost:3000 yarn test",
"test-flakes": "FLAKES=true yarn test-local --runInBand"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"keycode-js": "^3.1.0",
"lodash": "^4.17.21",
"marked": "^4.0.10",
"oidc-client-ts": "^2.0.4",
"oidc-client-ts": "^2.4.0",
"outdated-browser-rework": "^3.0.1",
"path-to-regexp": "^5.0.0",
"pluralize": "^8.0.0",
Expand Down
2 changes: 2 additions & 0 deletions src/auth/AuthContainer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ReactNode } from 'react';
import { h } from 'react-hyperscript-helpers';
import { SignOutPage } from 'src/auth/signout/SignOutPage';
import { fixedSpinnerOverlay } from 'src/components/common';
import { useRoute } from 'src/libs/nav';
import { useStore } from 'src/libs/react-utils';
Expand All @@ -22,6 +23,7 @@ const AuthContainer = ({ children }) => {
const authspinner = () => fixedSpinnerOverlay;

return Utils.cond<ReactNode>(
[name === 'signout-callback', () => h(SignOutPage)],
[signInStatus === 'uninitialized' && !isPublic, authspinner],
[signInStatus === 'signedOut' && !isPublic, () => h(SignIn)],
[userMustRegister, () => h(Register)],
Expand Down
85 changes: 2 additions & 83 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
import { DEFAULT, switchCase } from '@terra-ui-packages/core-utils';
import { parseJSON } from 'date-fns/fp';
import jwtDecode, { JwtPayload } from 'jwt-decode';
import _ from 'lodash/fp';
import { sessionTimedOutErrorMessage } from 'src/auth/auth-errors';
import {
B2cIdTokenClaims,
getCurrentOidcUser,
initializeOidcUserManager,
oidcSignIn,
OidcSignInArgs,
OidcUser,
revokeTokens,
} from 'src/auth/oidc-broker';
import { cookiesAcceptedKey } from 'src/components/CookieWarning';
import { Ajax } from 'src/libs/ajax';
import { fetchOk } from 'src/libs/ajax/ajax-common';
import { SamUserAttributes } from 'src/libs/ajax/User';
import { getSessionStorage } from 'src/libs/browser-storage';
import { withErrorIgnoring, withErrorReporting } from 'src/libs/error';
import Events, { captureAppcuesEvent, MetricsEventName } from 'src/libs/events';
import * as Nav from 'src/libs/nav';
import { clearNotification, notify, sessionTimeoutProps } from 'src/libs/notifications';
import { clearNotification, sessionTimeoutProps } from 'src/libs/notifications';
import { getLocalPref, getLocalPrefForUserId, setLocalPref } from 'src/libs/prefs';
import allProviders from 'src/libs/providers';
import {
asyncImportJobStore,
AuthState,
authStore,
azureCookieReadyStore,
cookieReadyStore,
getTerraUser,
MetricState,
metricStore,
oidcStore,
requesterPaysProjectStore,
Expand All @@ -42,6 +35,7 @@ import {
workspaceStore,
} from 'src/libs/state';
import * as Utils from 'src/libs/utils';
import { getTimestampMetricLabel } from 'src/libs/utils';
import { v4 as uuid } from 'uuid';

export const getAuthToken = (): string | undefined => {
Expand All @@ -56,42 +50,6 @@ export const getAuthTokenFromLocalStorage = async (): Promise<string | undefined
return oidcUser?.access_token;
};

export type SignOutCause =
| 'requested'
| 'disabled'
| 'declinedTos'
| 'expiredRefreshToken'
| 'errorRefreshingAuthToken'
| 'idleStatusMonitor'
| 'unspecified';

const sendSignOutMetrics = async (cause: SignOutCause): Promise<void> => {
const eventToFire: MetricsEventName = switchCase<SignOutCause, MetricsEventName>(
cause,
['requested', () => Events.user.signOut.requested],
['disabled', () => Events.user.signOut.disabled],
['declinedTos', () => Events.user.signOut.declinedTos],
['expiredRefreshToken', () => Events.user.signOut.expiredRefreshToken],
['errorRefreshingAuthToken', () => Events.user.signOut.errorRefreshingAuthToken],
['idleStatusMonitor', () => Events.user.signOut.idleStatusMonitor],
['unspecified', () => Events.user.signOut.unspecified],
[DEFAULT, () => Events.user.signOut.unspecified]
);
const sessionEndTime: number = Date.now();
const metricStoreState: MetricState = metricStore.get();
const tokenMetadata: TokenMetadata = metricStoreState.authTokenMetadata;

await Ajax().Metrics.captureEvent(eventToFire, {
sessionEndTime: Utils.makeCompleteDate(sessionEndTime),
sessionDurationInSeconds:
metricStoreState.sessionStartTime < 0 ? undefined : (sessionEndTime - metricStoreState.sessionStartTime) / 1000.0,
authTokenCreatedAt: getTimestampMetricLabel(tokenMetadata.createdAt),
authTokenExpiresAt: getTimestampMetricLabel(tokenMetadata.expiresAt),
totalAuthTokensUsedThisSession: metricStoreState.authTokenMetadata.totalTokensUsedThisSession,
totalAuthTokenLoadAttemptsThisSession: metricStoreState.authTokenMetadata.totalTokenLoadAttemptsThisSession,
});
};

export const sendRetryMetric = () => {
Ajax().Metrics.captureEvent(Events.user.authToken.load.retry, {});
};
Expand All @@ -100,42 +58,6 @@ export const sendAuthTokenDesyncMetric = () => {
Ajax().Metrics.captureEvent(Events.user.authToken.desync, {});
};

export const signOut = (cause: SignOutCause = 'unspecified'): void => {
sendSignOutMetrics(cause);
if (cause === 'expiredRefreshToken' || cause === 'errorRefreshingAuthToken') {
notify('info', sessionTimedOutErrorMessage, sessionTimeoutProps);
}
// TODO: invalidate runtime cookies https://broadworkbench.atlassian.net/browse/IA-3498
cookieReadyStore.reset();
azureCookieReadyStore.reset();
getSessionStorage().clear();

revokeTokens();

const { cookiesAccepted } = authStore.get();

authStore.reset();
authStore.update((state) => ({
...state,
signInStatus: 'signedOut',
// TODO: If allowed, this should be moved to the cookie store
// Load whether a user has input a cookie acceptance in a previous session on this system,
// or whether they input cookie acceptance previously in this session
cookiesAccepted,
}));
oidcStore.update((state) => ({
...state,
user: undefined,
}));
const anonymousId: string | undefined = metricStore.get().anonymousId;
metricStore.reset();
metricStore.update((state) => ({
...state,
anonymousId,
}));
userStore.reset();
};

export const signIn = async (includeBillingScope = false): Promise<OidcUser> => {
// Here, we update `userJustSignedIn` to true, so that we that the user became authenticated via the "Sign In" button.
// This is necessary to differentiate signing in vs reloading or opening a new tab.
Expand Down Expand Up @@ -325,9 +247,6 @@ const getOldAuthTokenLabels = (oldAuthTokenMetadata: TokenMetadata) => {
oldAuthTokenMetadata.createdAt < 0 ? undefined : Date.now() - oldAuthTokenMetadata.createdAt / 1000.0,
};
};
const getTimestampMetricLabel = (timestamp: number): string | undefined => {
return timestamp < 0 ? undefined : Utils.formatTimestampInSeconds(timestamp);
};

export const hasBillingScope = (): boolean => authStore.get().hasGcpBillingScopeThroughB2C === true;

Expand Down
3 changes: 2 additions & 1 deletion src/auth/oidc-broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const getOidcConfig = () => {
const metadata = {
authorization_endpoint: `${getConfig().orchestrationUrlRoot}/oauth2/authorize`,
token_endpoint: `${getConfig().orchestrationUrlRoot}/oauth2/token`,
end_session_endpoint: `${getConfig().orchestrationUrlRoot}/oauth2/logout`,
};
return {
authority: `${getConfig().orchestrationUrlRoot}/oauth2/authorize`,
Expand Down Expand Up @@ -105,7 +106,7 @@ export const oidcSignIn = async (args: OidcSignInArgs): Promise<OidcUser | null>
authInstance.signinSilent(extraArgs);
};

export const revokeTokens = async (): Promise<void> => {
export const removeUserFromLocalState = async (): Promise<void> => {
// send back auth instance, so we can use it for remove and clear stale state
const auth: AuthContextProps = getAuthInstance();
auth.removeUser();
Expand Down
41 changes: 41 additions & 0 deletions src/auth/signout/SignOutPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render } from '@testing-library/react';
import React from 'react';
import { OidcUser } from 'src/auth/oidc-broker';
import { SignOutPage } from 'src/auth/signout/SignOutPage';
import { authStore, azureCookieReadyStore, cookieReadyStore, metricStore, oidcStore, userStore } from 'src/libs/state';

type NavExports = typeof import('src/libs/nav');
jest.mock(
'src/libs/nav',
(): NavExports => ({
...jest.requireActual('src/libs/nav'),
getCurrentUrl: jest.fn().mockReturnValue(new URL('https://app.terra.bio')),
getLink: jest.fn().mockImplementation((_) => _),
goToPath: jest.fn(),
})
);

describe('SignOutPage', () => {
it('clears stores after being redirected to', () => {
// Arrange
cookieReadyStore.update(() => true);
azureCookieReadyStore.update((state) => ({ ...state, readyForRuntime: true }));
authStore.update((state) => ({ ...state, cookiesAccepted: true, nihStatusLoaded: true }));
oidcStore.update((state) => ({ ...state, user: {} as OidcUser }));
metricStore.update((state) => ({ ...state, anonymousId: '12345', sessionId: '67890' }));
userStore.update((state) => ({ ...state, enterpriseFeatures: ['github-account-linking'] }));
// Act
render(<SignOutPage />);
// Assert
expect(cookieReadyStore.get()).toBe(false);
expect(azureCookieReadyStore.get().readyForRuntime).toBe(false);
// logout preserves cookiesAccepted
expect(authStore.get().cookiesAccepted).toBe(true);
expect(authStore.get().nihStatusLoaded).toBe(false);
expect(oidcStore.get().user).toBeUndefined();
// logout preserves the anonymousId
expect(metricStore.get().anonymousId).toBe('12345');
expect(metricStore.get().sessionId).toBeUndefined();
expect(userStore.get().enterpriseFeatures).toEqual([]);
});
});
25 changes: 25 additions & 0 deletions src/auth/signout/SignOutPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useEffect } from 'react';
import { userSignedOut } from 'src/auth/signout/sign-out';
import * as Nav from 'src/libs/nav';

export const signOutCallbackLinkName = 'signout-callback';
export const SignOutPage = () => {
useEffect(() => {
try {
userSignedOut();
} catch (e) {
console.error(e);
}
Nav.goToPath('root');
}, []);
return null;
};

export const navPaths = [
{
name: signOutCallbackLinkName,
path: '/signout',
component: SignOutPage,
title: 'SignOut',
},
];
Loading

0 comments on commit 06d8cc9

Please sign in to comment.