refactor: drop non-session auth from frontend (#2419)

This commit is contained in:
Severiano Badajoz
2021-09-14 15:55:46 -07:00
committed by GitHub
parent f9c744327a
commit 01013bcf04
15 changed files with 36 additions and 1798 deletions
+1 -2
View File
@@ -7,7 +7,6 @@ GENE_SETS_FILENAME := $(shell basename $(GENE_SETS))
CXG_CONFIG := $(if $(CXG_CONFIG),$(CXG_CONFIG),./__tests__/e2e/test_config.yaml) CXG_CONFIG := $(if $(CXG_CONFIG),$(CXG_CONFIG),./__tests__/e2e/test_config.yaml)
CXG_AUTH_TYPE := $(if $(CXG_AUTH_TYPE),$(CXG_AUTH_TYPE),"test")
# Packaging # Packaging
@@ -40,7 +39,7 @@ smoke-test:
start_server_and_test \ start_server_and_test \
'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \ 'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
$(CXG_SERVER_PORT) \ $(CXG_SERVER_PORT) \
'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" CXG_AUTH_TYPE=$(CXG_AUTH_TYPE) npm run e2e -- --verbose false' 'CXG_URL_BASE="http://localhost:$(CXG_SERVER_PORT)" npm run e2e -- --verbose false'
# start an instance of cellxgene and run the end-to-end annotations tests # start an instance of cellxgene and run the end-to-end annotations tests
.PHONY: smoke-test-annotations .PHONY: smoke-test-annotations
+17 -78
View File
@@ -13,11 +13,8 @@ import {
getTestClass, getTestClass,
getTestId, getTestId,
isElementPresent, isElementPresent,
goToPage,
} from "./puppeteerUtils"; } from "./puppeteerUtils";
import { appUrlBase, TEST_EMAIL, TEST_PASSWORD } from "./config";
export async function drag(testId, start, end, lasso = false) { export async function drag(testId, start, end, lasso = false) {
const layout = await waitByID(testId); const layout = await waitByID(testId);
const elBox = await layout.boxModel(); const elBox = await layout.boxModel();
@@ -61,7 +58,9 @@ export async function getAllHistograms(testclass, testIds) {
const allHistograms = await getAllByClass(testclass); const allHistograms = await getAllByClass(testclass);
const testIDs = await Promise.all( const testIDs = await Promise.all(
allHistograms.map((hist) => page.evaluate((elem) => elem.dataset.testid, hist)) allHistograms.map((hist) =>
page.evaluate((elem) => elem.dataset.testid, hist)
)
); );
return testIDs.map((id) => id.replace(/^histogram-/, "")); return testIDs.map((id) => id.replace(/^histogram-/, ""));
@@ -172,9 +171,9 @@ export async function createCategory(categoryName) {
await clickOn("submit-category"); await clickOn("submit-category");
} }
/* /*
GENESET GENESET
*/ */
@@ -189,7 +188,9 @@ export async function colorByGene(gene) {
export async function assertColorLegendLabel(label) { export async function assertColorLegendLabel(label) {
const handle = await waitByID("continuous_legend_color_by_label"); const handle = await waitByID("continuous_legend_color_by_label");
const result = await handle.evaluate((node) => node.getAttribute("aria-label")); const result = await handle.evaluate((node) =>
node.getAttribute("aria-label")
);
return expect(result).toBe(label); return expect(result).toBe(label);
} }
@@ -246,12 +247,14 @@ export async function assertGenesetDoesNotExist(genesetName) {
export async function assertGenesetExists(genesetName) { export async function assertGenesetExists(genesetName) {
const handle = await waitByID(`${genesetName}:geneset-name`); const handle = await waitByID(`${genesetName}:geneset-name`);
const result = await handle.evaluate((node) => node.getAttribute("aria-label")); const result = await handle.evaluate((node) =>
node.getAttribute("aria-label")
);
return expect(result).toBe(genesetName); return expect(result).toBe(genesetName);
} }
/* /*
GENE GENE
@@ -276,7 +279,9 @@ export async function removeGene(geneSymbol) {
export async function assertGeneExistsInGeneset(geneSymbol) { export async function assertGeneExistsInGeneset(geneSymbol) {
const handle = await waitByID(`${geneSymbol}:gene-label`); const handle = await waitByID(`${geneSymbol}:gene-label`);
const result = await handle.evaluate((node) => node.getAttribute("aria-label")); const result = await handle.evaluate((node) =>
node.getAttribute("aria-label")
);
return expect(result).toBe(geneSymbol); return expect(result).toBe(geneSymbol);
} }
@@ -291,9 +296,9 @@ export async function expandGene(geneSymbol) {
await clickOn(`maximize-${geneSymbol}`); await clickOn(`maximize-${geneSymbol}`);
} }
/* /*
CATEGORY CATEGORY
*/ */
@@ -437,70 +442,4 @@ export async function assertCategoryDoesNotExist(categoryName) {
await expect(result).toBe(false); await expect(result).toBe(false);
} }
export async function login() {
await goToPage(appUrlBase);
await clickOn("log-in");
// (thuang): Auth0 form is unstable and unsafe for input until verified
await waitUntilFormFieldStable('[name="email"]');
await expect(page).toFillForm("form", {
email: TEST_EMAIL,
password: TEST_PASSWORD,
});
await Promise.all([
page.waitForNavigation({ waitUntil: "networkidle0" }),
expect(page).toClick('[name="submit"]'),
]);
expect(page.url()).toContain(appUrlBase);
}
export async function logout() {
await clickOnUntil("user-info", async () => {
await waitByID("log-out");
await Promise.all([
page.waitForNavigation({ waitUntil: "networkidle0" }),
clickOn("log-out"),
]);
});
await waitByID("log-in");
}
async function waitUntilFormFieldStable(selector) {
const MAX_RETRY = 10;
const WAIT_FOR_MS = 200;
const EXPECTED_VALUE = "aaa";
let retry = 0;
while (retry < MAX_RETRY) {
try {
await expect(page).toFill(selector, EXPECTED_VALUE);
const fieldHandle = await expect(page).toMatchElement(selector);
const fieldValue = await page.evaluate(
(input) => input.value,
fieldHandle
);
expect(fieldValue).toBe(EXPECTED_VALUE);
break;
} catch (error) {
retry += 1;
await page.waitForTimeout(WAIT_FOR_MS);
}
}
if (retry === MAX_RETRY) {
throw Error("clickOnUntil() assertion failed!");
}
}
/* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */ /* eslint-enable no-await-in-loop -- await in loop is needed to emulate sequential user actions */
+1 -1
View File
@@ -8,7 +8,7 @@ server:
# none: No authentication support, features like user_annotations must not be enabled. # none: No authentication support, features like user_annotations must not be enabled.
# session: A session based userid is automatically generated. (no params needed) # session: A session based userid is automatically generated. (no params needed)
# oauth: oauth2 is used for authentication; parameters are defined in params_oauth. # oauth: oauth2 is used for authentication; parameters are defined in params_oauth.
type: test type: session
insecure_test_environment: true insecure_test_environment: true
dataset: dataset:
-21
View File
@@ -1,26 +1,5 @@
const {
SecretsManagerClient,
GetSecretValueCommand,
} = require("@aws-sdk/client-secrets-manager");
const { setup } = require("jest-environment-puppeteer"); const { setup } = require("jest-environment-puppeteer");
const client = new SecretsManagerClient({ region: "us-west-2" });
const deploymentStage = process.env.DEPLOYMENT_STAGE || "test";
const secretValueRequest = {
SecretId: `corpora/backend/${deploymentStage}/auth0-secret`,
};
const command = new GetSecretValueCommand(secretValueRequest);
module.exports = async () => { module.exports = async () => {
await setup(); await setup();
try {
const secret = JSON.parse((await client.send(command)).SecretString);
process.env.TEST_ACCOUNT_PASS = secret.test_account_password;
} catch (error) {
console.error(error);
}
}; };
+12 -1447
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -38,7 +38,6 @@
"not Safari > 0" "not Safari > 0"
], ],
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.13.0",
"@babel/eslint-parser": "^7.15.0", "@babel/eslint-parser": "^7.15.0",
"@blueprintjs/core": "^3.44.0", "@blueprintjs/core": "^3.44.0",
"@blueprintjs/icons": "^3.19.0", "@blueprintjs/icons": "^3.19.0",
-8
View File
@@ -39,14 +39,6 @@ app.use(
app.use(favicon("./favicon.png")); app.use(favicon("./favicon.png"));
app.get("/login", async (req, res) => {
try {
res.redirect(`${API.prefix}login?dataset=http://localhost:${CLIENT_PORT}`);
} catch (err) {
console.error(err);
}
});
app.get("/logout", async (req, res) => { app.get("/logout", async (req, res) => {
try { try {
res.redirect(`${API.prefix}logout?dataset=http://localhost:${CLIENT_PORT}`); res.redirect(`${API.prefix}logout?dataset=http://localhost:${CLIENT_PORT}`);
-12
View File
@@ -52,17 +52,6 @@ async function configFetch(dispatch) {
}); });
} }
async function userInfoFetch(dispatch) {
return fetchJson("userinfo").then((response) => {
const { userinfo: userInfo } = response || {};
dispatch({
type: "userInfo load complete",
userInfo,
});
return userInfo;
});
}
async function genesetsFetch(dispatch, config) { async function genesetsFetch(dispatch, config) {
/* request genesets ONLY if the backend supports the feature */ /* request genesets ONLY if the backend supports the feature */
const defaultResponse = { const defaultResponse = {
@@ -105,7 +94,6 @@ const doInitialDataLoad = () =>
configFetch(dispatch), configFetch(dispatch),
schemaFetch(dispatch), schemaFetch(dispatch),
userColorsFetchAndLoad(dispatch), userColorsFetchAndLoad(dispatch),
userInfoFetch(dispatch),
]); ]);
genesetsFetch(dispatch, config); genesetsFetch(dispatch, config);
@@ -14,8 +14,6 @@ import {
@connect((state) => ({ @connect((state) => ({
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null, idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
annotations: state.annotations, annotations: state.annotations,
auth: state.config?.authentication,
userInfo: state.userInfo,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
writableGenesetsEnabled: !( writableGenesetsEnabled: !(
state.config?.parameters?.annotations_genesets_readonly ?? true state.config?.parameters?.annotations_genesets_readonly ?? true
@@ -101,15 +99,13 @@ class FilenameDialog extends React.Component {
writableGenesetsEnabled, writableGenesetsEnabled,
annotations, annotations,
idhash, idhash,
userInfo,
} = this.props; } = this.props;
const { filenameText } = this.state; const { filenameText } = this.state;
return (writableCategoriesEnabled || writableGenesetsEnabled) && return (writableCategoriesEnabled || writableGenesetsEnabled) &&
annotations.promptForFilename && annotations.promptForFilename &&
!annotations.dataCollectionNameIsReadOnly && !annotations.dataCollectionNameIsReadOnly &&
!annotations.dataCollectionName && !annotations.dataCollectionName ? (
userInfo.is_authenticated ? (
<Dialog <Dialog
icon="tag" icon="tag"
title="User Generated Data Directory" title="User Generated Data Directory"
+4 -13
View File
@@ -13,7 +13,6 @@ import actions from "../../actions";
@connect((state) => ({ @connect((state) => ({
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false, writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
schema: state.annoMatrix?.schema, schema: state.annoMatrix?.schema,
userInfo: state.userInfo,
})) }))
class Categories extends React.Component { class Categories extends React.Component {
constructor(props) { constructor(props) {
@@ -98,11 +97,8 @@ class Categories extends React.Component {
this.setState({ newCategoryText: name }); this.setState({ newCategoryText: name });
}; };
instruction = (name) => labelPrompt( instruction = (name) =>
this.categoryNameError(name), labelPrompt(this.categoryNameError(name), "New, unique category name", ":");
"New, unique category name",
":"
);
onExpansionChange = (catName) => { onExpansionChange = (catName) => {
const { expandedCats } = this.state; const { expandedCats } = this.state;
@@ -124,7 +120,7 @@ class Categories extends React.Component {
newCategoryText, newCategoryText,
expandedCats, expandedCats,
} = this.state; } = this.state;
const { writableCategoriesEnabled, schema, userInfo } = this.props; const { writableCategoriesEnabled, schema } = this.props;
/* all names, sorted in display order. Will be rendered in this order */ /* all names, sorted in display order. Will be rendered in this order */
const allCategoryNames = const allCategoryNames =
ControlsHelpers.selectableCategoryNames(schema).sort(); ControlsHelpers.selectableCategoryNames(schema).sort();
@@ -174,11 +170,7 @@ class Categories extends React.Component {
{writableCategoriesEnabled ? ( {writableCategoriesEnabled ? (
<div style={{ marginBottom: 10 }}> <div style={{ marginBottom: 10 }}>
<Tooltip <Tooltip
content={ content="Create a new category"
userInfo.is_authenticated
? "Create a new category"
: "You must be logged in to create new categorical fields"
}
position={Position.RIGHT} position={Position.RIGHT}
boundary="viewport" boundary="viewport"
hoverOpenDelay={globals.tooltipHoverOpenDelay} hoverOpenDelay={globals.tooltipHoverOpenDelay}
@@ -192,7 +184,6 @@ class Categories extends React.Component {
data-testid="open-annotation-dialog" data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode} onClick={this.handleEnableAnnoMode}
intent="primary" intent="primary"
disabled={!userInfo.is_authenticated}
> >
Create new <strong>category</strong> Create new <strong>category</strong>
</AnchorButton> </AnchorButton>
@@ -1,175 +0,0 @@
import React, { useState } from "react";
import {
AnchorButton,
Button,
MenuItem,
Tooltip,
Popover,
Menu,
Elevation,
PopoverPosition,
Checkbox,
Card,
} from "@blueprintjs/core";
import { IconNames } from "@blueprintjs/icons";
import * as globals from "../../globals";
import styles from "./menubar.css";
import { storageGet, storageSet, KEYS } from "../util/localStorage";
const BASE_EMOJI = [0x1f9d1, 0x1f468, 0x1f469];
const SKIN_TONES = [0x1f3fb, 0x1f3fc, 0x1f3fd, 0x1f3fe, 0x1f3ff];
const MICROSCOPE = 0x1f52c;
const ZERO_WIDTH_JOINER = 0x0200d;
const LOGIN_PROMPT_OFF = "off";
const Auth = React.memo((props) => {
const [isPromptOpen, setIsPromptOpen] = useState(shouldShowPrompt());
const { auth, userInfo } = props;
const isAuthenticated = userInfo && userInfo.is_authenticated;
window.userInfo = userInfo;
const randomInt = Math.random() * 15;
const sexIndex = Math.floor(randomInt / 5);
const skinToneIndex = Math.floor(randomInt % 5);
const scientist = String.fromCodePoint(
BASE_EMOJI[sexIndex],
SKIN_TONES[skinToneIndex],
ZERO_WIDTH_JOINER,
MICROSCOPE
);
if (!shouldShowAuth()) return null;
if (isAuthenticated) {
const PopoverContent = (
<Menu>
<MenuItem
data-testid="user-email"
text={`Logged in as: ${userInfo.email}`}
/>
<MenuItem
data-testid="log-out"
text="Log Out"
href={auth.logout}
icon={IconNames.LOG_OUT}
/>
</Menu>
);
return (
<Popover content={PopoverContent}>
<Button
data-testid="user-info"
className={styles.menubarButton}
style={{ padding: 0 }}
>
{/* eslint-disable-next-line no-constant-condition -- disable profile picture until CSP is tweaked */}
{userInfo?.picture && false ? (
<img alt="profile" size="21px" src={userInfo?.picture} />
) : (
<span style={{ fontSize: "18px" }}>{scientist}</span>
)}
</Button>
</Popover>
);
}
const LoginButton = (
<Tooltip
content="Log in to cellxgene"
position="bottom"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
>
<AnchorButton
type="button"
data-testid="log-in"
href={auth.login}
className={styles.menubarButton}
>
Log In
</AnchorButton>
</Tooltip>
);
if (isPromptOpen) {
return (
<Popover
position={PopoverPosition.AUTO_END}
isOpen
content={<PromptContent setIsPromptOpen={setIsPromptOpen} />}
onInteraction={setIsPromptOpen}
>
{LoginButton}
</Popover>
);
}
return LoginButton;
function shouldShowAuth() {
return auth && auth.requires_client_login;
}
function shouldShowPrompt() {
if (storageGet(KEYS.LOGIN_PROMPT) === LOGIN_PROMPT_OFF) return false;
return shouldShowAuth && !isAuthenticated;
}
});
function PromptContent({ setIsPromptOpen }) {
const [isChecked, setIsChecked] = useState(false);
function handleOKClick() {
if (isChecked) {
storageSet(KEYS.LOGIN_PROMPT, LOGIN_PROMPT_OFF);
}
setIsPromptOpen(false);
}
function handleCheckboxChange() {
setIsChecked(!isChecked);
}
return (
<Card style={{ width: "500px" }} elevation={Elevation.TWO}>
<p>
Logging in will enable you to create your own categories and labels.
Logging in later will reset cellxgene to the default view and cause you
to lose progress.
</p>
<Checkbox
style={{ width: "230px" }}
checked={isChecked}
onChange={handleCheckboxChange}
data-testid="login-hint-do-not-show-again"
>
Do not show me this message again
</Checkbox>
<div
style={{ display: "flex", justifyContent: "flex-end", marginTop: 15 }}
>
<Button
onClick={handleOKClick}
intent="primary"
data-testid="login-hint-yes"
>
Acknowledge
</Button>
</div>
</Card>
);
}
export default Auth;
-6
View File
@@ -7,7 +7,6 @@ import styles from "./menubar.css";
import actions from "../../actions"; import actions from "../../actions";
import Clip from "./clip"; import Clip from "./clip";
import AuthButtons from "./authButtons";
import Subset from "./subset"; import Subset from "./subset";
import UndoRedoReset from "./undoRedo"; import UndoRedoReset from "./undoRedo";
import DiffexpButtons from "./diffexpButtons"; import DiffexpButtons from "./diffexpButtons";
@@ -36,8 +35,6 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
scatterplotXXaccessor: state.controls.scatterplotXXaccessor, scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor, scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
libraryVersions: state.config?.library_versions, libraryVersions: state.config?.library_versions,
auth: state.config?.authentication,
userInfo: state.userInfo,
undoDisabled: state["@@undoable/past"].length === 0, undoDisabled: state["@@undoable/past"].length === 0,
redoDisabled: state["@@undoable/future"].length === 0, redoDisabled: state["@@undoable/future"].length === 0,
aboutLink: state.config?.links?.["about-dataset"], aboutLink: state.config?.links?.["about-dataset"],
@@ -209,8 +206,6 @@ class MenuBar extends React.PureComponent {
colorAccessor, colorAccessor,
subsetPossible, subsetPossible,
subsetResetPossible, subsetResetPossible,
userInfo,
auth,
} = this.props; } = this.props;
const { pendingClipPercentiles } = this.state; const { pendingClipPercentiles } = this.state;
@@ -236,7 +231,6 @@ class MenuBar extends React.PureComponent {
zIndex: 3, zIndex: 3,
}} }}
> >
<AuthButtons {...{ auth, userInfo }} />
<UndoRedoReset <UndoRedoReset
dispatch={dispatch} dispatch={dispatch}
undoDisabled={undoDisabled} undoDisabled={undoDisabled}
@@ -1,6 +1,5 @@
export const KEYS = { export const KEYS = {
COOKIE_DECISION: "cxg.cookieDecision", COOKIE_DECISION: "cxg.cookieDecision",
LOGIN_PROMPT: "cxg.LOGIN_PROMPT",
}; };
export function storageGet(key, defaultValue = null) { export function storageGet(key, defaultValue = null) {
-2
View File
@@ -4,7 +4,6 @@ import thunk from "redux-thunk";
import cascadeReducers from "./cascade"; import cascadeReducers from "./cascade";
import undoable from "./undoable"; import undoable from "./undoable";
import config from "./config"; import config from "./config";
import userInfo from "./userInfo";
import annoMatrix from "./annoMatrix"; import annoMatrix from "./annoMatrix";
import obsCrossfilter from "./obsCrossfilter"; import obsCrossfilter from "./obsCrossfilter";
import categoricalSelection from "./categoricalSelection"; import categoricalSelection from "./categoricalSelection";
@@ -42,7 +41,6 @@ const Reducer = undoable(
["centroidLabels", centroidLabels], ["centroidLabels", centroidLabels],
["pointDilation", pointDialation], ["pointDilation", pointDialation],
["autosave", autosave], ["autosave", autosave],
["userInfo", userInfo],
]), ]),
[ [
"annoMatrix", "annoMatrix",
-26
View File
@@ -1,26 +0,0 @@
const UserInfo = (state = {}, action) => {
switch (action.type) {
case "initial data load start":
return {
...state,
loading: true,
error: null,
};
case "userInfo load complete":
return {
...state,
loading: false,
error: null,
...action.userInfo,
};
case "initial data load error":
return {
...state,
error: action.error,
};
default:
return state;
}
};
export default UserInfo;