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_AUTH_TYPE := $(if $(CXG_AUTH_TYPE),$(CXG_AUTH_TYPE),"test")
# Packaging
@@ -40,7 +39,7 @@ smoke-test:
start_server_and_test \
'CXG_OPTIONS="--config-file $(CXG_CONFIG)" $(MAKE) start-server' \
$(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
.PHONY: smoke-test-annotations
+17 -78
View File
@@ -13,11 +13,8 @@ import {
getTestClass,
getTestId,
isElementPresent,
goToPage,
} from "./puppeteerUtils";
import { appUrlBase, TEST_EMAIL, TEST_PASSWORD } from "./config";
export async function drag(testId, start, end, lasso = false) {
const layout = await waitByID(testId);
const elBox = await layout.boxModel();
@@ -61,7 +58,9 @@ export async function getAllHistograms(testclass, testIds) {
const allHistograms = await getAllByClass(testclass);
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-/, ""));
@@ -172,9 +171,9 @@ export async function createCategory(categoryName) {
await clickOn("submit-category");
}
/*
/*
GENESET
GENESET
*/
@@ -189,7 +188,9 @@ export async function colorByGene(gene) {
export async function assertColorLegendLabel(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);
}
@@ -246,12 +247,14 @@ export async function assertGenesetDoesNotExist(genesetName) {
export async function assertGenesetExists(genesetName) {
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);
}
/*
/*
GENE
@@ -276,7 +279,9 @@ export async function removeGene(geneSymbol) {
export async function assertGeneExistsInGeneset(geneSymbol) {
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);
}
@@ -291,9 +296,9 @@ export async function expandGene(geneSymbol) {
await clickOn(`maximize-${geneSymbol}`);
}
/*
/*
CATEGORY
CATEGORY
*/
@@ -437,70 +442,4 @@ export async function assertCategoryDoesNotExist(categoryName) {
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 */
+1 -1
View File
@@ -8,7 +8,7 @@ server:
# none: No authentication support, features like user_annotations must not be enabled.
# session: A session based userid is automatically generated. (no params needed)
# oauth: oauth2 is used for authentication; parameters are defined in params_oauth.
type: test
type: session
insecure_test_environment: true
dataset:
-21
View File
@@ -1,26 +1,5 @@
const {
SecretsManagerClient,
GetSecretValueCommand,
} = require("@aws-sdk/client-secrets-manager");
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 () => {
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"
],
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.13.0",
"@babel/eslint-parser": "^7.15.0",
"@blueprintjs/core": "^3.44.0",
"@blueprintjs/icons": "^3.19.0",
-8
View File
@@ -39,14 +39,6 @@ app.use(
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) => {
try {
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) {
/* request genesets ONLY if the backend supports the feature */
const defaultResponse = {
@@ -105,7 +94,6 @@ const doInitialDataLoad = () =>
configFetch(dispatch),
schemaFetch(dispatch),
userColorsFetchAndLoad(dispatch),
userInfoFetch(dispatch),
]);
genesetsFetch(dispatch, config);
@@ -14,8 +14,6 @@ import {
@connect((state) => ({
idhash: state.config?.parameters?.["annotations-user-data-idhash"] ?? null,
annotations: state.annotations,
auth: state.config?.authentication,
userInfo: state.userInfo,
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
writableGenesetsEnabled: !(
state.config?.parameters?.annotations_genesets_readonly ?? true
@@ -101,15 +99,13 @@ class FilenameDialog extends React.Component {
writableGenesetsEnabled,
annotations,
idhash,
userInfo,
} = this.props;
const { filenameText } = this.state;
return (writableCategoriesEnabled || writableGenesetsEnabled) &&
annotations.promptForFilename &&
!annotations.dataCollectionNameIsReadOnly &&
!annotations.dataCollectionName &&
userInfo.is_authenticated ? (
!annotations.dataCollectionName ? (
<Dialog
icon="tag"
title="User Generated Data Directory"
+4 -13
View File
@@ -13,7 +13,6 @@ import actions from "../../actions";
@connect((state) => ({
writableCategoriesEnabled: state.config?.parameters?.annotations ?? false,
schema: state.annoMatrix?.schema,
userInfo: state.userInfo,
}))
class Categories extends React.Component {
constructor(props) {
@@ -98,11 +97,8 @@ class Categories extends React.Component {
this.setState({ newCategoryText: name });
};
instruction = (name) => labelPrompt(
this.categoryNameError(name),
"New, unique category name",
":"
);
instruction = (name) =>
labelPrompt(this.categoryNameError(name), "New, unique category name", ":");
onExpansionChange = (catName) => {
const { expandedCats } = this.state;
@@ -124,7 +120,7 @@ class Categories extends React.Component {
newCategoryText,
expandedCats,
} = 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 */
const allCategoryNames =
ControlsHelpers.selectableCategoryNames(schema).sort();
@@ -174,11 +170,7 @@ class Categories extends React.Component {
{writableCategoriesEnabled ? (
<div style={{ marginBottom: 10 }}>
<Tooltip
content={
userInfo.is_authenticated
? "Create a new category"
: "You must be logged in to create new categorical fields"
}
content="Create a new category"
position={Position.RIGHT}
boundary="viewport"
hoverOpenDelay={globals.tooltipHoverOpenDelay}
@@ -192,7 +184,6 @@ class Categories extends React.Component {
data-testid="open-annotation-dialog"
onClick={this.handleEnableAnnoMode}
intent="primary"
disabled={!userInfo.is_authenticated}
>
Create new <strong>category</strong>
</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 Clip from "./clip";
import AuthButtons from "./authButtons";
import Subset from "./subset";
import UndoRedoReset from "./undoRedo";
import DiffexpButtons from "./diffexpButtons";
@@ -36,8 +35,6 @@ import { getEmbSubsetView } from "../../util/stateManager/viewStackHelpers";
scatterplotXXaccessor: state.controls.scatterplotXXaccessor,
scatterplotYYaccessor: state.controls.scatterplotYYaccessor,
libraryVersions: state.config?.library_versions,
auth: state.config?.authentication,
userInfo: state.userInfo,
undoDisabled: state["@@undoable/past"].length === 0,
redoDisabled: state["@@undoable/future"].length === 0,
aboutLink: state.config?.links?.["about-dataset"],
@@ -209,8 +206,6 @@ class MenuBar extends React.PureComponent {
colorAccessor,
subsetPossible,
subsetResetPossible,
userInfo,
auth,
} = this.props;
const { pendingClipPercentiles } = this.state;
@@ -236,7 +231,6 @@ class MenuBar extends React.PureComponent {
zIndex: 3,
}}
>
<AuthButtons {...{ auth, userInfo }} />
<UndoRedoReset
dispatch={dispatch}
undoDisabled={undoDisabled}
@@ -1,6 +1,5 @@
export const KEYS = {
COOKIE_DECISION: "cxg.cookieDecision",
LOGIN_PROMPT: "cxg.LOGIN_PROMPT",
};
export function storageGet(key, defaultValue = null) {
-2
View File
@@ -4,7 +4,6 @@ import thunk from "redux-thunk";
import cascadeReducers from "./cascade";
import undoable from "./undoable";
import config from "./config";
import userInfo from "./userInfo";
import annoMatrix from "./annoMatrix";
import obsCrossfilter from "./obsCrossfilter";
import categoricalSelection from "./categoricalSelection";
@@ -42,7 +41,6 @@ const Reducer = undoable(
["centroidLabels", centroidLabels],
["pointDilation", pointDialation],
["autosave", autosave],
["userInfo", userInfo],
]),
[
"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;