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