[ARVADOS-WORKBENCH2] updated: 1.3.0-52-g2a4f0a7d
Git user
git at public.curoverse.com
Tue Dec 18 02:05:35 EST 2018
Summary of changes:
package.json | 2 +
src/components/text-field/text-field.tsx | 3 +-
src/models/session.ts | 14 +-
src/services/auth-service/auth-service.ts | 20 ++-
src/store/auth/auth-action-session.ts | 171 +++++++++++++++++----
src/store/auth/auth-action-ssh.ts | 4 +
src/store/auth/auth-action.ts | 3 +-
src/store/auth/auth-reducer.ts | 7 +
.../site-manager-panel/site-manager-panel-root.tsx | 39 +++--
yarn.lock | 10 ++
10 files changed, 221 insertions(+), 52 deletions(-)
via 2a4f0a7d69cb0cb94b43a05ddff91e4cd06c6c39 (commit)
from f4012790be2404ce2f5b2594338fac43b1b9c59b (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
commit 2a4f0a7d69cb0cb94b43a05ddff91e4cd06c6c39
Author: Daniel Kos <daniel.kos at contractors.roche.com>
Date: Tue Dec 18 08:03:33 2018 +0100
Add session validation
Feature #14478
Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos at contractors.roche.com>
diff --git a/package.json b/package.json
index 13326304..46b3ee4f 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"@material-ui/icons": "3.0.1",
"@types/debounce": "3.0.0",
"@types/js-yaml": "3.11.2",
+ "@types/jssha": "0.0.29",
"@types/lodash": "4.14.116",
"@types/react-copy-to-clipboard": "4.2.6",
"@types/react-dnd": "3.0.2",
@@ -21,6 +22,7 @@
"debounce": "1.2.0",
"is-image": "2.0.0",
"js-yaml": "3.12.0",
+ "jssha": "2.3.1",
"lodash": "4.17.11",
"react": "16.5.2",
"react-copy-to-clipboard": "5.0.1",
diff --git a/src/components/text-field/text-field.tsx b/src/components/text-field/text-field.tsx
index 0aeaeb85..0ebb46bc 100644
--- a/src/components/text-field/text-field.tsx
+++ b/src/components/text-field/text-field.tsx
@@ -26,7 +26,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
type TextFieldProps = WrappedFieldProps & WithStyles<CssRules>;
export const TextField = withStyles(styles)((props: TextFieldProps & {
- label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, children: React.ReactNode, margin?: Margin
+ label?: string, autoFocus?: boolean, required?: boolean, select?: boolean, children: React.ReactNode, margin?: Margin, placeholder?: string
}) =>
<MaterialTextField
helperText={props.meta.touched && props.meta.error}
@@ -41,6 +41,7 @@ export const TextField = withStyles(styles)((props: TextFieldProps & {
select={props.select}
children={props.children}
margin={props.margin}
+ placeholder={props.placeholder}
{...props.input}
/>);
diff --git a/src/models/session.ts b/src/models/session.ts
index 8a5002be..9a942967 100644
--- a/src/models/session.ts
+++ b/src/models/session.ts
@@ -1,9 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export enum SessionStatus {
+ INVALIDATED,
+ BEING_VALIDATED,
+ VALIDATED
+}
+
export interface Session {
clusterId: string;
remoteHost: string;
+ baseUrl: string;
username: string;
email: string;
token: string;
loggedIn: boolean;
- validated: boolean;
+ status: SessionStatus;
+ active: boolean;
}
diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts
index ffd81ef1..1492ef1c 100644
--- a/src/services/auth-service/auth-service.ts
+++ b/src/services/auth-service/auth-service.ts
@@ -6,9 +6,9 @@ import { getUserFullname, User } from "~/models/user";
import { AxiosInstance } from "axios";
import { ApiActions } from "~/services/api/api-actions";
import * as uuid from "uuid/v4";
-import { Session } from "~/models/session";
+import { Session, SessionStatus } from "~/models/session";
import { Config } from "~/common/config";
-import { merge, uniqWith, uniqBy } from "lodash";
+import { uniqBy } from "lodash";
export const API_TOKEN_KEY = 'apiToken';
export const USER_EMAIL_KEY = 'userEmail';
@@ -144,11 +144,14 @@ export class AuthService {
public buildSessions(cfg: Config, user?: User) {
const currentSession = {
clusterId: cfg.uuidPrefix,
- remoteHost: cfg.baseUrl,
+ remoteHost: cfg.rootUrl,
+ baseUrl: cfg.baseUrl,
username: getUserFullname(user),
email: user ? user.email : '',
token: this.getApiToken(),
- loggedIn: true
+ loggedIn: true,
+ active: true,
+ status: SessionStatus.VALIDATED
} as Session;
const localSessions = this.getSessions();
const cfgSessions = Object.keys(cfg.remoteHosts).map(clusterId => {
@@ -156,15 +159,18 @@ export class AuthService {
return {
clusterId,
remoteHost,
+ baseUrl: '',
username: '',
email: '',
token: '',
- loggedIn: false
+ loggedIn: false,
+ active: false,
+ status: SessionStatus.INVALIDATED
} as Session;
});
const sessions = [currentSession]
- .concat(cfgSessions)
- .concat(localSessions);
+ .concat(localSessions)
+ .concat(cfgSessions);
const uniqSessions = uniqBy(sessions, 'clusterId');
diff --git a/src/store/auth/auth-action-session.ts b/src/store/auth/auth-action-session.ts
index c70bcfbb..6ffca6b5 100644
--- a/src/store/auth/auth-action-session.ts
+++ b/src/store/auth/auth-action-session.ts
@@ -1,37 +1,48 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
import { Dispatch } from "redux";
import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
import { RootState } from "~/store/store";
import { ServiceRepository } from "~/services/services";
import Axios from "axios";
-import { getUserFullname } from "~/models/user";
+import { getUserFullname, User } from "~/models/user";
import { authActions } from "~/store/auth/auth-action";
import { Config, DISCOVERY_URL } from "~/common/config";
-import { Session } from "~/models/session";
+import { Session, SessionStatus } from "~/models/session";
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
import { UserDetailsResponse } from "~/services/auth-service/auth-service";
+import * as jsSHA from "jssha";
-
-const getSessionOrigin = async (session: Session) => {
- let url = session.remoteHost;
+const getRemoteHostBaseUrl = async (remoteHost: string): Promise<string | null> => {
+ let url = remoteHost;
if (url.indexOf('://') < 0) {
url = 'https://' + url;
}
const origin = new URL(url).origin;
+ let baseUrl: string | null = null;
+
try {
const resp = await Axios.get<Config>(`${origin}/${DISCOVERY_URL}`);
- return resp.data.origin;
+ baseUrl = resp.data.baseUrl;
} catch (err) {
try {
const resp = await Axios.get<any>(`${origin}/status.json`);
- return resp.data.apiBaseURL;
+ baseUrl = resp.data.apiBaseURL;
} catch (err) {
}
}
- return null;
+
+ if (baseUrl && baseUrl[baseUrl.length - 1] === '/') {
+ baseUrl = baseUrl.substr(0, baseUrl.length - 1);
+ }
+
+ return baseUrl;
};
-const getUserDetails = async (origin: string, token: string): Promise<UserDetailsResponse> => {
- const resp = await Axios.get<UserDetailsResponse>(`${origin}/arvados/v1/users/current`, {
+const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
+ const resp = await Axios.get<UserDetailsResponse>(`${baseUrl}/users/current`, {
headers: {
Authorization: `OAuth2 ${token}`
}
@@ -39,35 +50,135 @@ const getUserDetails = async (origin: string, token: string): Promise<UserDetail
return resp.data;
};
-const validateSessions = () =>
+const getTokenUuid = async (baseUrl: string, token: string): Promise<string> => {
+ if (token.startsWith("v2/")) {
+ const uuid = token.split("/")[1];
+ return Promise.resolve(uuid);
+ }
+
+ const resp = await Axios.get(`${baseUrl}/api_client_authorizations`, {
+ headers: {
+ Authorization: `OAuth2 ${token}`
+ },
+ data: {
+ filters: JSON.stringify([['api_token', '=', token]])
+ }
+ });
+
+ return resp.data.items[0].uuid;
+};
+
+const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
+ const shaObj = new jsSHA("SHA-1", "TEXT");
+ let secret = token;
+ if (token.startsWith("v2/")) {
+ secret = token.split("/")[2];
+ }
+ shaObj.setHMACKey(secret, "TEXT");
+ shaObj.update(clusterId);
+ const hmac = shaObj.getHMAC("HEX");
+ return `v2/${tokenUuid}/${hmac}`;
+};
+
+const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{user: User, token: string}> => {
+ const tokenUuid = await getTokenUuid(activeSession.baseUrl, activeSession.token);
+ console.log(">> Cluster", clusterId);
+ const saltedToken = getSaltedToken(clusterId, tokenUuid, activeSession.token);
+ console.log(">> Salted token", saltedToken);
+ const user = await getUserDetails(baseUrl, saltedToken);
+ return {
+ user: {
+ firstName: user.first_name,
+ lastName: user.last_name,
+ uuid: user.uuid,
+ ownerUuid: user.owner_uuid,
+ email: user.email,
+ isAdmin: user.is_admin
+ },
+ token: saltedToken
+ };
+};
+
+const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
+
+export const validateCluster = async (remoteHost: string, clusterId: string, activeSession: Session): Promise<{ user: User; token: string, baseUrl: string }> => {
+ const baseUrl = await getRemoteHostBaseUrl(remoteHost);
+ if (!baseUrl) {
+ return Promise.reject(`Could not find base url for ${remoteHost}`);
+ }
+ const { user, token } = await clusterLogin(clusterId, baseUrl, activeSession);
+ return { baseUrl, user, token };
+};
+
+export const validateSession = (session: Session, activeSession: Session) =>
+ async (dispatch: Dispatch) => {
+ dispatch(authActions.UPDATE_SESSION({ ...session, status: SessionStatus.BEING_VALIDATED }));
+ session.loggedIn = false;
+ try {
+ const { baseUrl, user, token } = await validateCluster(session.remoteHost, session.clusterId, activeSession);
+ session.baseUrl = baseUrl;
+ session.token = token;
+ session.email = user.email;
+ session.username = getUserFullname(user);
+ session.loggedIn = true;
+ } catch {
+ session.loggedIn = false;
+ } finally {
+ session.status = SessionStatus.VALIDATED;
+ dispatch(authActions.UPDATE_SESSION(session));
+ }
+ return session;
+ };
+
+export const validateSessions = () =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const sessions = getState().auth.sessions;
- dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
- for (const session of sessions) {
- if (!session.validated) {
- const origin = await getSessionOrigin(session);
- const user = await getUserDetails(origin, session.token);
+ const activeSession = getActiveSession(sessions);
+ if (activeSession) {
+ dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
+ for (const session of sessions) {
+ if (session.status === SessionStatus.INVALIDATED) {
+ await dispatch(validateSession(session, activeSession));
+ }
}
+ services.authService.saveSessions(sessions);
+ dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
}
- dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
};
export const addSession = (remoteHost: string) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const user = getState().auth.user!;
- const clusterId = remoteHost.match(/^(\w+)\./)![1];
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const sessions = getState().auth.sessions;
+ const activeSession = getActiveSession(sessions);
+ if (activeSession) {
+ const clusterId = remoteHost.match(/^(\w+)\./)![1];
+ if (sessions.find(s => s.clusterId === clusterId)) {
+ return Promise.reject("Cluster already exists");
+ }
+ try {
+ const { baseUrl, user, token } = await dispatch(validateCluster(remoteHost, clusterId, activeSession));
+ const session = {
+ loggedIn: false,
+ status: SessionStatus.VALIDATED,
+ active: false,
+ email: user.email,
+ username: getUserFullname(user),
+ remoteHost,
+ baseUrl,
+ clusterId,
+ token
+ };
- dispatch(authActions.ADD_SESSION({
- loggedIn: false,
- validated: false,
- email: user.email,
- username: getUserFullname(user),
- remoteHost,
- clusterId,
- token: ''
- }));
+ dispatch(authActions.ADD_SESSION(session));
+ services.authService.saveSessions(getState().auth.sessions);
- services.authService.saveSessions(getState().auth.sessions);
+ return session;
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ debugger;
+ return Promise.reject("Could not validate cluster");
};
export const loadSiteManagerPanel = () =>
diff --git a/src/store/auth/auth-action-ssh.ts b/src/store/auth/auth-action-ssh.ts
index 4175d295..2c3a2722 100644
--- a/src/store/auth/auth-action-ssh.ts
+++ b/src/store/auth/auth-action-ssh.ts
@@ -1,3 +1,7 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
import { dialogActions } from "~/store/dialog/dialog-actions";
import { Dispatch } from "redux";
import { RootState } from "~/store/store";
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index 8c1673d7..ed998ab5 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -24,7 +24,8 @@ export const authActions = unionize({
REMOVE_SSH_KEY: ofType<string>(),
SET_SESSIONS: ofType<Session[]>(),
ADD_SESSION: ofType<Session>(),
- REMOVE_SESSION: ofType<string>()
+ REMOVE_SESSION: ofType<string>(),
+ UPDATE_SESSION: ofType<Session>()
});
function setAuthorizationHeader(services: ServiceRepository, token: string) {
diff --git a/src/store/auth/auth-reducer.ts b/src/store/auth/auth-reducer.ts
index 1edcedee..a2822f10 100644
--- a/src/store/auth/auth-reducer.ts
+++ b/src/store/auth/auth-reducer.ts
@@ -61,6 +61,13 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat
session => session.clusterId !== clusterId
)};
},
+ UPDATE_SESSION: (session: Session) => {
+ return {
+ ...state,
+ sessions: state.sessions.map(
+ s => s.clusterId === session.clusterId ? session : s
+ )};
+ },
default: () => state
});
};
diff --git a/src/views/site-manager-panel/site-manager-panel-root.tsx b/src/views/site-manager-panel/site-manager-panel-root.tsx
index 29969fc5..a64fdb25 100644
--- a/src/views/site-manager-panel/site-manager-panel-root.tsx
+++ b/src/views/site-manager-panel/site-manager-panel-root.tsx
@@ -5,7 +5,7 @@
import * as React from 'react';
import {
Card,
- CardContent,
+ CardContent, CircularProgress,
Grid,
StyleRulesCallback,
Table,
@@ -18,14 +18,18 @@ import {
withStyles
} from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
-import { Session } from "~/models/session";
+import { Session, SessionStatus } from "~/models/session";
import Button from "@material-ui/core/Button";
import { User } from "~/models/user";
import { compose } from "redux";
-import { Field, InjectedFormProps, reduxForm, reset } from "redux-form";
+import { Field, FormErrors, InjectedFormProps, reduxForm, reset, stopSubmit } from "redux-form";
import { TextField } from "~/components/text-field/text-field";
import { addSession } from "~/store/auth/auth-action-session";
import { SITE_MANAGER_REMOTE_HOST_VALIDATION } from "~/validators/validators";
+import {
+ RENAME_FILE_DIALOG,
+ RenameFileDialogData
+} from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
type CssRules = 'root' | 'link' | 'buttonContainer' | 'table' | 'tableRow' | 'status' | 'remoteSiteInfo' | 'buttonAdd';
@@ -80,9 +84,17 @@ const SITE_MANAGER_FORM_NAME = 'siteManagerForm';
export const SiteManagerPanelRoot = compose(
reduxForm<{remoteHost: string}>({
form: SITE_MANAGER_FORM_NAME,
- onSubmit: (data, dispatch) => {
- dispatch(addSession(data.remoteHost));
- dispatch(reset(SITE_MANAGER_FORM_NAME));
+ onSubmit: async (data, dispatch) => {
+ try {
+ await dispatch(addSession(data.remoteHost));
+ dispatch(reset(SITE_MANAGER_FORM_NAME));
+ } catch (e) {
+ const errors = {
+ remoteHost: e
+ } as FormErrors;
+ dispatch(stopSubmit(SITE_MANAGER_FORM_NAME, errors));
+ }
+
}
}),
withStyles(styles))
@@ -107,11 +119,12 @@ export const SiteManagerPanelRoot = compose(
</TableRow>
</TableHead>
<TableBody>
- {sessions.map((session, index) =>
- <TableRow key={index} className={classes.tableRow}>
+ {sessions.map((session, index) => {
+ const validating = session.status === SessionStatus.BEING_VALIDATED;
+ return <TableRow key={index} className={classes.tableRow}>
<TableCell>{session.clusterId}</TableCell>
- <TableCell>{session.username}</TableCell>
- <TableCell>{session.email}</TableCell>
+ <TableCell>{validating ? <CircularProgress size={20}/> : session.username}</TableCell>
+ <TableCell>{validating ? <CircularProgress size={20}/> : session.email}</TableCell>
<TableCell>
<div className={classes.status} style={{
color: session.loggedIn ? '#fff' : '#000',
@@ -120,7 +133,8 @@ export const SiteManagerPanelRoot = compose(
{session.loggedIn ? "Logged in" : "Logged out"}
</div>
</TableCell>
- </TableRow>)}
+ </TableRow>;
+ })}
</TableBody>
</Table>}
</Grid>
@@ -138,7 +152,8 @@ export const SiteManagerPanelRoot = compose(
component={TextField}
placeholder="zzzz.arvadosapi.com"
margin="normal"
- label="New cluster"/>
+ label="New cluster"
+ autoFocus/>
</Grid>
<Grid item xs={3}>
<Button type="submit" variant="contained" color="primary"
diff --git a/yarn.lock b/yarn.lock
index d3d6396d..b642d71a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -130,6 +130,11 @@
csstype "^2.0.0"
indefinite-observable "^1.0.1"
+"@types/jssha at 0.0.29":
+ version "0.0.29"
+ resolved "https://registry.yarnpkg.com/@types/jssha/-/jssha-0.0.29.tgz#95e83dba98787ff796d2d5f37a1925abf41bc9cb"
+ integrity sha1-leg9uph4f/eW0tXzehklq/Qbycs=
+
"@types/lodash at 4.14.116":
version "4.14.116"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.116.tgz#5ccf215653e3e8c786a58390751033a9adca0eb9"
@@ -5540,6 +5545,11 @@ jss@^9.3.3:
symbol-observable "^1.1.0"
warning "^3.0.0"
+jssha at 2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/jssha/-/jssha-2.3.1.tgz#147b2125369035ca4b2f7d210dc539f009b3de9a"
+ integrity sha1-FHshJTaQNcpLL30hDcU58Amz3po=
+
keycode@^2.1.9:
version "2.2.0"
resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04"
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list