Merge branch '21666-provision-test-improvement'
[arvados.git] / services / workbench2 / 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 { getUserUuid } from "common/getuser";
8 import { ServiceRepository, createServices, setAuthorizationHeader } from "services/services";
9 import { setBreadcrumbs } from "store/breadcrumbs/breadcrumbs-actions";
10 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
11 import { LinkAccountType, AccountToLink, LinkAccountStatus } from "models/link-account";
12 import { authActions, getConfig } from "store/auth/auth-action";
13 import { unionize, ofType, UnionOf } from 'common/unionize';
14 import { UserResource } from "models/user";
15 import { GroupResource } from "models/group";
16 import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
17 import { login, logout } from "store/auth/auth-action";
18 import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
19 import { WORKBENCH_LOADING_SCREEN } from 'store/workbench/workbench-actions';
20
21 export const linkAccountPanelActions = unionize({
22     LINK_INIT: ofType<{
23         targetUser: UserResource | undefined
24     }>(),
25     LINK_LOAD: ofType<{
26         originatingUser: OriginatingUser | undefined,
27         targetUser: UserResource | undefined,
28         targetUserToken: string | undefined,
29         userToLink: UserResource | undefined,
30         userToLinkToken: string | undefined
31     }>(),
32     LINK_INVALID: ofType<{
33         originatingUser: OriginatingUser | undefined,
34         targetUser: UserResource | undefined,
35         userToLink: UserResource | undefined,
36         error: LinkAccountPanelError
37     }>(),
38     SET_SELECTED_CLUSTER: ofType<{
39         selectedCluster: string
40     }>(),
41     SET_IS_PROCESSING: ofType<{
42         isProcessing: boolean
43     }>(),
44     HAS_SESSION_DATA: {}
45 });
46
47 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
48
49 function validateLink(userToLink: UserResource, targetUser: UserResource) {
50     if (userToLink.uuid === targetUser.uuid) {
51         return LinkAccountPanelError.SAME_USER;
52     }
53     else if (userToLink.isAdmin && !targetUser.isAdmin) {
54         return LinkAccountPanelError.NON_ADMIN;
55     }
56     else if (!targetUser.isActive) {
57         return LinkAccountPanelError.INACTIVE;
58     }
59     return LinkAccountPanelError.NONE;
60 }
61
62 const newServices = (dispatch: Dispatch<any>, token: string) => {
63     const config = dispatch<any>(getConfig);
64     const svc = createServices(config, { progressFn: () => { }, errorFn: () => { } });
65     setAuthorizationHeader(svc, token);
66     return svc;
67 };
68
69 export const checkForLinkStatus = () =>
70     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
71         const status = services.linkAccountService.getLinkOpStatus();
72         if (status !== undefined) {
73             let msg: string;
74             let msgKind: SnackbarKind;
75             if (status.valueOf() === LinkAccountStatus.CANCELLED) {
76                 msg = "Account link cancelled!";
77                 msgKind = SnackbarKind.INFO;
78             }
79             else if (status.valueOf() === LinkAccountStatus.FAILED) {
80                 msg = "Account link failed!";
81                 msgKind = SnackbarKind.ERROR;
82             }
83             else if (status.valueOf() === LinkAccountStatus.SUCCESS) {
84                 msg = "Account link success!";
85                 msgKind = SnackbarKind.SUCCESS;
86             }
87             else {
88                 msg = "Unknown Error!";
89                 msgKind = SnackbarKind.ERROR;
90             }
91             dispatch(snackbarActions.OPEN_SNACKBAR({ message: msg, kind: msgKind, hideDuration: 3000 }));
92             services.linkAccountService.removeLinkOpStatus();
93         }
94     };
95
96 export const switchUser = (user: UserResource, token: string) =>
97     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
98         dispatch(authActions.INIT_USER({ user, token }));
99     };
100
101 export const linkFailed = () =>
102     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
103         // If the link fails, switch to the user account that originated the link operation
104         const linkState = getState().linkAccountPanel;
105         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
106             if (linkState.originatingUser === OriginatingUser.TARGET_USER) {
107                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
108             }
109             else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
110                 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
111             }
112         }
113         services.linkAccountService.removeAccountToLink();
114         services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.FAILED);
115         window.location.reload();
116     };
117
118 export const loadLinkAccountPanel = () =>
119     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
120         try {
121             // If there are remote hosts, set the initial selected cluster by getting the first cluster that isn't the local cluster
122             if (getState().linkAccountPanel.selectedCluster === undefined) {
123                 const localCluster = getState().auth.localCluster;
124                 let selectedCluster = localCluster;
125                 for (const key in getState().auth.remoteHosts) {
126                     if (key !== localCluster) {
127                         selectedCluster = key;
128                         break;
129                     }
130                 }
131                 dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster }));
132             }
133
134             // First check if an account link operation has completed
135             dispatch(checkForLinkStatus());
136
137             // Continue loading the link account panel
138             dispatch(setBreadcrumbs([{ label: 'Link account' }]));
139             const curUser = getState().auth.user;
140             const curToken = getState().auth.apiToken;
141             if (curUser && curToken) {
142
143                 // If there is link account session data, then the user has logged in a second time
144                 const linkAccountData = services.linkAccountService.getAccountToLink();
145                 if (linkAccountData) {
146
147                     dispatch(linkAccountPanelActions.SET_IS_PROCESSING({ isProcessing: true }));
148                     const curUserResource = await services.userService.get(curUser.uuid);
149
150                     // Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
151                     // issues since a user will always be able to query the api server for their own user data.
152                     const svc = newServices(dispatch, linkAccountData.token);
153                     const savedUserResource = await svc.userService.get(linkAccountData.userUuid);
154
155                     let params: any;
156                     if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
157                         params = {
158                             originatingUser: OriginatingUser.USER_TO_LINK,
159                             targetUser: curUserResource,
160                             targetUserToken: curToken,
161                             userToLink: savedUserResource,
162                             userToLinkToken: linkAccountData.token
163                         };
164                     }
165                     else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN || linkAccountData.type === LinkAccountType.ADD_LOCAL_TO_REMOTE) {
166                         params = {
167                             originatingUser: OriginatingUser.TARGET_USER,
168                             targetUser: savedUserResource,
169                             targetUserToken: linkAccountData.token,
170                             userToLink: curUserResource,
171                             userToLinkToken: curToken
172                         };
173                     }
174                     else {
175                         throw new Error("Unknown link account type");
176                     }
177
178                     dispatch(switchUser(params.targetUser, params.targetUserToken));
179                     const error = validateLink(params.userToLink, params.targetUser);
180                     if (error === LinkAccountPanelError.NONE) {
181                         dispatch(linkAccountPanelActions.LINK_LOAD(params));
182                     }
183                     else {
184                         dispatch(linkAccountPanelActions.LINK_INVALID({
185                             originatingUser: params.originatingUser,
186                             targetUser: params.targetUser,
187                             userToLink: params.userToLink,
188                             error
189                         }));
190                         return;
191                     }
192                 }
193                 else {
194                     // If there is no link account session data, set the state to invoke the initial UI
195                     const curUserResource = await services.userService.get(curUser.uuid);
196                     dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
197                     return;
198                 }
199             }
200         }
201         catch (e) {
202             dispatch(linkFailed());
203         }
204         finally {
205             dispatch(linkAccountPanelActions.SET_IS_PROCESSING({ isProcessing: false }));
206         }
207     };
208
209 export const startLinking = (t: LinkAccountType) =>
210     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
211         const userUuid = getUserUuid(getState());
212         if (!userUuid) { return; }
213         const accountToLink = { type: t, userUuid, token: services.authService.getApiToken() } as AccountToLink;
214         services.linkAccountService.saveAccountToLink(accountToLink);
215
216         const auth = getState().auth;
217         const isLocalUser = auth.user!.uuid.substring(0, 5) === auth.localCluster;
218         let homeCluster = auth.localCluster;
219         if (isLocalUser && t === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
220             homeCluster = getState().linkAccountPanel.selectedCluster!;
221         }
222
223         dispatch(logout());
224         dispatch(login(auth.localCluster, homeCluster, auth.loginCluster, auth.remoteHosts));
225     };
226
227 export const getAccountLinkData = () =>
228     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
229         return services.linkAccountService.getAccountToLink();
230     };
231
232 export const cancelLinking = (reload: boolean = false) =>
233     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
234         let user: UserResource | undefined;
235         try {
236             // When linking is cancelled switch to the originating user (i.e. the user saved in session data)
237             dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
238             const linkAccountData = services.linkAccountService.getAccountToLink();
239             if (linkAccountData) {
240                 services.linkAccountService.removeAccountToLink();
241                 const svc = newServices(dispatch, linkAccountData.token);
242                 user = await svc.userService.get(linkAccountData.userUuid);
243                 dispatch(switchUser(user, linkAccountData.token));
244                 services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.CANCELLED);
245             }
246         }
247         finally {
248             if (reload) {
249                 window.location.reload();
250             }
251             else {
252                 dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
253             }
254         }
255     };
256
257 export const linkAccount = () =>
258     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
259         const linkState = getState().linkAccountPanel;
260         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
261
262             // First create a project owned by the target user
263             const projectName = `Migrated from ${linkState.userToLink.email} (${linkState.userToLink.uuid})`;
264             let newGroup: GroupResource;
265             try {
266                 newGroup = await services.projectService.create({
267                     name: projectName,
268                     ensure_unique_name: true
269                 });
270             }
271             catch (e) {
272                 dispatch(linkFailed());
273                 throw e;
274             }
275
276             try {
277                 // The merge api links the user sending the request into the user
278                 // specified in the request, so change the authorization header accordingly
279                 const svc = newServices(dispatch, linkState.userToLinkToken);
280                 await svc.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
281                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
282                 services.linkAccountService.removeAccountToLink();
283                 services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.SUCCESS);
284                 window.location.reload();
285             }
286             catch (e) {
287                 // If the link operation fails, delete the previously made project
288                 try {
289                     const svc = newServices(dispatch, linkState.targetUserToken);
290                     await svc.projectService.delete(newGroup.uuid);
291                 }
292                 finally {
293                     dispatch(linkFailed());
294                 }
295                 throw e;
296             }
297         }
298     };