15088: Improves federated linking logic and UI
authorEric Biagiotti <ebiagiotti@veritasgenetics.com>
Fri, 17 May 2019 19:04:17 +0000 (15:04 -0400)
committerEric Biagiotti <ebiagiotti@veritasgenetics.com>
Fri, 17 May 2019 19:04:17 +0000 (15:04 -0400)
Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti@veritasgenetics.com>

src/models/link-account.ts
src/routes/routes.ts
src/store/auth/auth-action.ts
src/store/link-account-panel/link-account-panel-actions.ts
src/views/link-account-panel/link-account-panel-root.tsx
src/views/link-account-panel/link-account-panel.tsx

index 1c2029cfac62471200305a30b4cfa1aad1f23be4..f5b60400dcbdd027ac624c5bf1fc578d3a1adb90 100644 (file)
@@ -10,7 +10,9 @@ export enum LinkAccountStatus {
 
 export enum LinkAccountType {
     ADD_OTHER_LOGIN,
-    ACCESS_OTHER_ACCOUNT
+    ADD_LOCAL_TO_REMOTE,
+    ACCESS_OTHER_ACCOUNT,
+    ACCESS_OTHER_REMOTE_ACCOUNT
 }
 
 export interface AccountToLink {
index 76f5c32dc192f9205674f5e52e6c88a1979cdd9a..7e6897a8991fe87593b60eb8cf10477db360c9e6 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { matchPath } from 'react-router';
+import { matchPath, Router } from 'react-router';
 import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
 import { getProjectUrl } from '~/models/project';
 import { getCollectionUrl } from '~/models/collection';
@@ -127,6 +127,9 @@ export const matchKeepServicesRoute = (route: string) =>
 export const matchTokenRoute = (route: string) =>
     matchPath(route, { path: Routes.TOKEN });
 
+export const matchFedTokenRoute = (route: string) =>
+    matchPath(route, {path: Routes.FED_LOGIN});
+
 export const matchUsersRoute = (route: string) =>
     matchPath(route, { path: Routes.USERS });
 
index 6ca7140339f86542632e01c7616b6cec0e92073a..87eb3f752a9fbaf733fda7664af48a654368eee2 100644 (file)
@@ -13,7 +13,7 @@ import { Session } from "~/models/session";
 import { getDiscoveryURL, Config } from '~/common/config';
 import { initSessions } from "~/store/auth/auth-action-session";
 import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
-import { matchTokenRoute } from '~/routes/routes';
+import { matchTokenRoute, matchFedTokenRoute } from '~/routes/routes';
 import Axios from "axios";
 import { AxiosError } from "axios";
 
@@ -54,7 +54,7 @@ export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () =>
     // Cancel any link account ops in progess unless the user has
     // just logged in or there has been a successful link operation
     const data = services.linkAccountService.getLinkOpStatus();
-    if (!matchTokenRoute(location.pathname) && data === undefined) {
+    if (!matchTokenRoute(location.pathname) && (!matchFedTokenRoute(location.pathname)) && data === undefined) {
         dispatch<any>(cancelLinking());
     }
 
index eec5bf3d883382e80f2ea340e689a08a734db3fb..7a92f4aaa990ca1bef6e6792ccd9b8d3d7ed6304 100644 (file)
@@ -99,8 +99,17 @@ export const linkFailed = () =>
 
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        // If there are remote hosts, set the initial selected cluster by getting the first cluster that isn't the local cluster
         if (getState().linkAccountPanel.selectedCluster === undefined) {
-            dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster: getState().auth.localCluster }));
+            const localCluster = getState().auth.localCluster;
+            let selectedCluster = localCluster;
+            for (const key in getState().auth.remoteHosts) {
+                if (key !== localCluster) {
+                    selectedCluster = key;
+                    break;
+                }
+            }
+            dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster }));
         }
 
         // First check if an account link operation has completed
@@ -124,7 +133,7 @@ export const loadLinkAccountPanel = () =>
                 dispatch(saveApiToken(curToken));
 
                 let params: any;
-                if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
+                if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
                     params = {
                         originatingUser: OriginatingUser.USER_TO_LINK,
                         targetUser: curUserResource,
@@ -133,7 +142,7 @@ export const loadLinkAccountPanel = () =>
                         userToLinkToken: linkAccountData.token
                     };
                 }
-                else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
+                else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN || linkAccountData.type === LinkAccountType.ADD_LOCAL_TO_REMOTE) {
                     params = {
                         originatingUser: OriginatingUser.TARGET_USER,
                         targetUser: savedUserResource,
@@ -176,9 +185,14 @@ export const startLinking = (t: LinkAccountType) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
         services.linkAccountService.saveAccountToLink(accountToLink);
+
         const auth = getState().auth;
-        const selectedCluster = getState().linkAccountPanel.selectedCluster;
-        const homeCluster = selectedCluster ? selectedCluster : auth.homeCluster;
+        const isLocalUser = auth.user!.uuid.substring(0,5) === auth.localCluster;
+        let homeCluster = auth.localCluster;
+        if (isLocalUser && t === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
+            homeCluster = getState().linkAccountPanel.selectedCluster!;
+        }
+
         dispatch(logout());
         dispatch(login(auth.localCluster, homeCluster, auth.remoteHosts));
     };
index 581e86dde429658256c85ed216c256ce69455de5..12dae426340d5758d8d179f62b5b362f06ad5a97 100644 (file)
@@ -33,6 +33,7 @@ export interface LinkAccountPanelRootDataProps {
     userToLink?: UserResource;
     remoteHosts:  { [key: string]: string };
     hasRemoteHosts: boolean;
+    localCluster: string;
     status : LinkAccountPanelStatus;
     error: LinkAccountPanelError;
     selectedCluster?: string;
@@ -58,55 +59,88 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false, showClu
     return disp;
 }
 
+function isLocalUser(uuid: string, localCluster: string) {
+    return uuid.substring(0, 5) === localCluster;
+}
+
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
     ({classes, targetUser, userToLink, status, error, startLinking, cancelLinking, linkAccount,
-      remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster}: LinkAccountPanelRootProps) => {
+      remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster, localCluster}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-            { status === LinkAccountPanelStatus.INITIAL && targetUser &&
-            <Grid container spacing={24}>
-                <Grid container item direction="column" spacing={24}>
-                    <Grid item>
-                        You are currently logged in as {displayUser(targetUser, true, hasRemoteHosts)}
+            { status === LinkAccountPanelStatus.INITIAL && targetUser && <div>
+                { isLocalUser(targetUser.uuid, localCluster) ? <Grid container spacing={24}>
+                    <Grid container item direction="column" spacing={24}>
+                        <Grid item>
+                            You are currently logged in as {displayUser(targetUser, true)}
+                        </Grid>
+                        <Grid item>
+                            You can link Arvados accounts. After linking, either login will take you to the same account.
+                        </Grid >
                     </Grid>
-                    <Grid item>
-                        You can link Arvados accounts. After linking, either login will take you to the same account.
-                    </Grid >
-                    {hasRemoteHosts && selectedCluster && <Grid item>
-                        Please select the cluster that hosts the account you want to link with:
-                            <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
-                                onChange={(event) => setSelectedCluster(event.target.value)}>
-                                {Object.keys(remoteHosts).map((k) => <option key={k} value={k}>{k}</option>)}
-                            </Select>
-                    </Grid> }
-                </Grid>
-                <Grid container item direction="row" spacing={24}>
-                    <Grid item>
-                        <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
-                            Add another login&nbsp;{hasRemoteHosts ? <label> from {selectedCluster} </label> : null}&nbsp;to this account
-                        </Button>
+                    <Grid container item direction="row" spacing={24}>
+                        <Grid item>
+                            <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
+                                Add another login to this account
+                            </Button>
+                        </Grid>
+                        <Grid item>
+                            <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
+                                Use this login to access another account
+                            </Button>
+                        </Grid>
                     </Grid>
-                    <Grid item>
-                        <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
-                            Use this login to access another account&nbsp;{hasRemoteHosts ? <label> on {selectedCluster} </label> : null}
-                        </Button>
+                    { hasRemoteHosts && selectedCluster && <Grid container item direction="column" spacing={24}>
+                        <Grid item>
+                            You can also link {displayUser(targetUser, false)} with an account from a remote cluster.
+                        </Grid>
+                        <Grid item>
+                            Please select the cluster that hosts the account you want to link with:
+                                <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
+                                    onChange={(event) => setSelectedCluster(event.target.value)}>
+                                    {Object.keys(remoteHosts).map((k) => k !== localCluster ? <option key={k} value={k}>{k}</option> : null)}
+                                </Select>
+                            </Grid>
+                        <Grid item>
+                            <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT)}>
+                                Link with an account on&nbsp;{hasRemoteHosts ? <label>{selectedCluster} </label> : null}
+                            </Button>
+                        </Grid>
+                    </Grid> }
+                </Grid> :
+                <Grid container spacing={24}>
+                    <Grid container item direction="column" spacing={24}>
+                        <Grid item>
+                            You are currently logged in as {displayUser(targetUser, true, true)}
+                        </Grid>
+                        <Grid item>
+                            This a remote account. You can link a local Arvados account to this one. After linking, you can access the local account's data by logging into the <b>{localCluster}</b> cluster with the <b>{targetUser.email}</b> account.
+                        </Grid >
+                        <Grid item>
+                            <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>
+                                Link an account from {localCluster} to this account
+                            </Button>
+                        </Grid>
                     </Grid>
-                </Grid>
-            </Grid> }
+                </Grid>}
+            </div> }
             { (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && targetUser &&
             <Grid container spacing={24}>
                 { status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        Clicking 'Link accounts' will link {displayUser(userToLink, true, hasRemoteHosts)} to {displayUser(targetUser, true, hasRemoteHosts)}.
+                        Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}.
                     </Grid>
-                    <Grid item>
+                    { (isLocalUser(targetUser.uuid, localCluster)) && <Grid item>
                         After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}.
-                    </Grid>
+                    </Grid> }
                     <Grid item>
-                       Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
+                        Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
                     </Grid>
+                    { !isLocalUser(targetUser.uuid, localCluster) && <Grid item>
+                        You can access <b>{userToLink.email}</b> data by logging into <b>{localCluster}</b> with the <b>{targetUser.email}</b> account.
+                    </Grid> }
                 </Grid> }
                 { error === LinkAccountPanelError.NON_ADMIN && <Grid item>
                     Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(targetUser)}.
index 0635911129a6e66bd0ef5f58ed04958ff380ccb4..3bdfbe43aa41ad3390e016f02fdc3dc157bacdbf 100644 (file)
@@ -18,6 +18,7 @@ const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
         remoteHosts: state.auth.remoteHosts,
         hasRemoteHosts: Object.keys(state.auth.remoteHosts).length > 1,
         selectedCluster: state.linkAccountPanel.selectedCluster,
+        localCluster: state.auth.localCluster,
         targetUser: state.linkAccountPanel.targetUser,
         userToLink: state.linkAccountPanel.userToLink,
         status: state.linkAccountPanel.status,