15088: Adds logout before logging in to second account
[arvados-workbench2.git] / src / store / link-account-panel / link-account-panel-actions.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import { Dispatch } from "redux";
6 import { RootState } from "~/store/store";
7 import { ServiceRepository } from "~/services/services";
8 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
9 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
10 import { LinkAccountType, AccountToLink } from "~/models/link-account";
11 import { saveApiToken, saveUser } from "~/store/auth/auth-action";
12 import { unionize, ofType, UnionOf } from '~/common/unionize';
13 import { UserResource } from "~/models/user";
14 import { GroupResource } from "~/models/group";
15 import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
16 import { navigateToRootProject } from "../navigation/navigation-action";
17 import { login, logout } from "~/store/auth/auth-action";
18
19 export const linkAccountPanelActions = unionize({
20     LINK_INIT: ofType<{ targetUser: UserResource | undefined }>(),
21     LINK_LOAD: ofType<{
22         originatingUser: OriginatingUser | undefined,
23         targetUser: UserResource | undefined,
24         targetUserToken: string | undefined,
25         userToLink: UserResource | undefined,
26         userToLinkToken: string | undefined }>(),
27     LINK_INVALID: ofType<{
28         originatingUser: OriginatingUser | undefined,
29         targetUser: UserResource | undefined,
30         userToLink: UserResource | undefined,
31         error: LinkAccountPanelError }>(),
32     HAS_SESSION_DATA: {}
33 });
34
35 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
36
37 function validateLink(userToLink: UserResource, targetUser: UserResource) {
38     if (userToLink.uuid === targetUser.uuid) {
39         return LinkAccountPanelError.SAME_USER;
40     }
41     else if (userToLink.isAdmin && !targetUser.isAdmin) {
42         return LinkAccountPanelError.NON_ADMIN;
43     }
44     else if (!targetUser.isActive) {
45         return LinkAccountPanelError.INACTIVE;
46     }
47     return LinkAccountPanelError.NONE;
48 }
49
50 export const switchUser = (user: UserResource, token: string) =>
51     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
52         dispatch(saveUser(user));
53         dispatch(saveApiToken(token));
54     };
55
56 export const linkFailed = () =>
57     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
58         // If the link fails, switch to the user account that originated the link operation
59         const linkState = getState().linkAccountPanel;
60         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
61             if (linkState.originatingUser === OriginatingUser.TARGET_USER) {
62                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
63                 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
64             }
65             else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
66                 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
67                 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.userToLink}));
68             }
69             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
70         }
71         services.linkAccountService.removeFromSession();
72     };
73
74 export const loadLinkAccountPanel = () =>
75     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
76         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
77         const curUser = getState().auth.user;
78         const curToken = getState().auth.apiToken;
79         if (curUser && curToken) {
80             const curUserResource = await services.userService.get(curUser.uuid);
81             const linkAccountData = services.linkAccountService.getFromSession();
82
83             // If there is link account session data, then the user has logged in a second time
84             if (linkAccountData) {
85
86                 // If the window is refreshed after the second login, cancel the linking
87                 if (window.performance) {
88                     if (performance.navigation.type === PerformanceNavigation.TYPE_BACK_FORWARD ||
89                         performance.navigation.type === PerformanceNavigation.TYPE_RELOAD) {
90                         dispatch(cancelLinking());
91                         return;
92                     }
93                 }
94
95                 // Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
96                 // issues since a user will always be able to query the api server for their own user data.
97                 dispatch(saveApiToken(linkAccountData.token));
98                 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
99                 dispatch(saveApiToken(curToken));
100
101                 let params: any;
102                 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
103                     params = {
104                         originatingUser: OriginatingUser.USER_TO_LINK,
105                         targetUser: curUserResource,
106                         targetUserToken: curToken,
107                         userToLink: savedUserResource,
108                         userToLinkToken: linkAccountData.token
109                     };
110                 }
111                 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
112                     params = {
113                         originatingUser: OriginatingUser.TARGET_USER,
114                         targetUser: savedUserResource,
115                         targetUserToken: linkAccountData.token,
116                         userToLink: curUserResource,
117                         userToLinkToken: curToken
118                     };
119                 }
120                 else {
121                     // This should never really happen, but just in case, switch to the user that
122                     // originated the linking operation (i.e. the user saved in session data)
123                     dispatch(switchUser(savedUserResource, linkAccountData.token));
124                     dispatch(linkAccountPanelActions.LINK_INIT({targetUser: savedUserResource}));
125                     throw new Error("Invalid link account type.");
126                 }
127
128                 dispatch(switchUser(params.targetUser, params.targetUserToken));
129                 const error = validateLink(params.userToLink, params.targetUser);
130                 if (error === LinkAccountPanelError.NONE) {
131                     dispatch(linkAccountPanelActions.LINK_LOAD(params));
132                 }
133                 else {
134                     dispatch(linkAccountPanelActions.LINK_INVALID({
135                         originatingUser: params.originatingUser,
136                         targetUser: params.targetUser,
137                         userToLink: params.userToLink,
138                         error}));
139                     return;
140                 }
141             }
142             else {
143                 // If there is no link account session data, set the state to invoke the initial UI
144                 dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
145                 return;
146             }
147         }
148     };
149
150 export const saveAccountLinkData = (t: LinkAccountType) =>
151     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
152         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
153         services.linkAccountService.saveToSession(accountToLink);
154         const auth = getState().auth;
155         dispatch(logout());
156         dispatch(login(auth.localCluster, auth.remoteHosts[auth.homeCluster]));
157     };
158
159 export const getAccountLinkData = () =>
160     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
161         return services.linkAccountService.getFromSession();
162     };
163
164 export const cancelLinking = () =>
165     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
166         let user: UserResource | undefined;
167         try {
168             // When linking is cancelled switch to the originating user (i.e. the user saved in session data)
169             const linkAccountData = services.linkAccountService.getFromSession();
170             if (linkAccountData) {
171                 dispatch(saveApiToken(linkAccountData.token));
172                 user = await services.userService.get(linkAccountData.userUuid);
173                 dispatch(switchUser(user, linkAccountData.token));
174             }
175         }
176         finally {
177             services.linkAccountService.removeFromSession();
178             dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: user }));
179         }
180     };
181
182 export const linkAccount = () =>
183     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
184         const linkState = getState().linkAccountPanel;
185         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
186
187             // First create a project owned by the target user
188             const projectName = `Migrated from ${linkState.userToLink.email} (${linkState.userToLink.uuid})`;
189             let newGroup: GroupResource;
190             try {
191                 newGroup = await services.projectService.create({
192                     name: projectName,
193                     ensure_unique_name: true
194                 });
195             }
196             catch (e) {
197                 dispatch(linkFailed());
198                 throw e;
199             }
200
201             try {
202                 // The merge api links the user sending the request into the user
203                 // specified in the request, so switch users for this api call
204                 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
205                 await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
206                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
207                 dispatch(navigateToRootProject);
208                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link success!', kind: SnackbarKind.SUCCESS, hideDuration: 3000 }));
209                 dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
210             }
211             catch(e) {
212                 // If the link operation fails, delete the previously made project
213                 try {
214                     dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
215                     await services.projectService.delete(newGroup.uuid);
216                 }
217                 finally {
218                     dispatch(linkFailed());
219                 }
220                 throw e;
221             }
222             finally {
223                 services.linkAccountService.removeFromSession();
224             }
225         }
226     };