[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