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