1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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, LinkAccountStatus } 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 { login, logout, setAuthorizationHeader } 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';
20 export const linkAccountPanelActions = unionize({
22 targetUser: UserResource | undefined }>(),
24 originatingUser: OriginatingUser | undefined,
25 targetUser: UserResource | undefined,
26 targetUserToken: string | undefined,
27 userToLink: UserResource | undefined,
28 userToLinkToken: string | undefined }>(),
29 LINK_INVALID: ofType<{
30 originatingUser: OriginatingUser | undefined,
31 targetUser: UserResource | undefined,
32 userToLink: UserResource | undefined,
33 error: LinkAccountPanelError }>(),
34 SET_SELECTED_CLUSTER: ofType<{
35 selectedCluster: string }>(),
36 SET_IS_PROCESSING: ofType<{
37 isProcessing: boolean}>(),
41 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
43 function validateLink(userToLink: UserResource, targetUser: UserResource) {
44 if (userToLink.uuid === targetUser.uuid) {
45 return LinkAccountPanelError.SAME_USER;
47 else if (userToLink.isAdmin && !targetUser.isAdmin) {
48 return LinkAccountPanelError.NON_ADMIN;
50 else if (!targetUser.isActive) {
51 return LinkAccountPanelError.INACTIVE;
53 return LinkAccountPanelError.NONE;
56 export const checkForLinkStatus = () =>
57 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
58 const status = services.linkAccountService.getLinkOpStatus();
59 if (status !== undefined) {
61 let msgKind: SnackbarKind;
62 if (status.valueOf() === LinkAccountStatus.CANCELLED) {
63 msg = "Account link cancelled!", msgKind = SnackbarKind.INFO;
65 else if (status.valueOf() === LinkAccountStatus.FAILED) {
66 msg = "Account link failed!", msgKind = SnackbarKind.ERROR;
68 else if (status.valueOf() === LinkAccountStatus.SUCCESS) {
69 msg = "Account link success!", msgKind = SnackbarKind.SUCCESS;
72 msg = "Unknown Error!", msgKind = SnackbarKind.ERROR;
74 dispatch(snackbarActions.OPEN_SNACKBAR({ message: msg, kind: msgKind, hideDuration: 3000 }));
75 services.linkAccountService.removeLinkOpStatus();
79 export const switchUser = (user: UserResource, token: string) =>
80 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
81 dispatch(saveUser(user));
82 dispatch(saveApiToken(token));
85 export const linkFailed = () =>
86 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
87 // If the link fails, switch to the user account that originated the link operation
88 const linkState = getState().linkAccountPanel;
89 if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
90 if (linkState.originatingUser === OriginatingUser.TARGET_USER) {
91 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
93 else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
94 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
97 services.linkAccountService.removeAccountToLink();
98 services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.FAILED);
102 export const loadLinkAccountPanel = () =>
103 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
105 // If there are remote hosts, set the initial selected cluster by getting the first cluster that isn't the local cluster
106 if (getState().linkAccountPanel.selectedCluster === undefined) {
107 const localCluster = getState().auth.localCluster;
108 let selectedCluster = localCluster;
109 for (const key in getState().auth.remoteHosts) {
110 if (key !== localCluster) {
111 selectedCluster = key;
115 dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster }));
118 // First check if an account link operation has completed
119 dispatch(checkForLinkStatus());
121 // Continue loading the link account panel
122 dispatch(setBreadcrumbs([{ label: 'Link account'}]));
123 const curUser = getState().auth.user;
124 const curToken = getState().auth.apiToken;
125 if (curUser && curToken) {
127 // If there is link account session data, then the user has logged in a second time
128 const linkAccountData = services.linkAccountService.getAccountToLink();
129 if (linkAccountData) {
131 dispatch(linkAccountPanelActions.SET_IS_PROCESSING({ isProcessing: true }));
132 const curUserResource = await services.userService.get(curUser.uuid);
134 // Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
135 // issues since a user will always be able to query the api server for their own user data.
136 setAuthorizationHeader(services, linkAccountData.token);
137 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
138 setAuthorizationHeader(services, curToken);
141 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
143 originatingUser: OriginatingUser.USER_TO_LINK,
144 targetUser: curUserResource,
145 targetUserToken: curToken,
146 userToLink: savedUserResource,
147 userToLinkToken: linkAccountData.token
150 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN || linkAccountData.type === LinkAccountType.ADD_LOCAL_TO_REMOTE) {
152 originatingUser: OriginatingUser.TARGET_USER,
153 targetUser: savedUserResource,
154 targetUserToken: linkAccountData.token,
155 userToLink: curUserResource,
156 userToLinkToken: curToken
160 throw new Error("Unknown link account type");
163 dispatch(switchUser(params.targetUser, params.targetUserToken));
164 const error = validateLink(params.userToLink, params.targetUser);
165 if (error === LinkAccountPanelError.NONE) {
166 dispatch(linkAccountPanelActions.LINK_LOAD(params));
169 dispatch(linkAccountPanelActions.LINK_INVALID({
170 originatingUser: params.originatingUser,
171 targetUser: params.targetUser,
172 userToLink: params.userToLink,
178 // If there is no link account session data, set the state to invoke the initial UI
179 const curUserResource = await services.userService.get(curUser.uuid);
180 dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
186 dispatch(linkFailed());
189 dispatch(linkAccountPanelActions.SET_IS_PROCESSING({ isProcessing: false }));
193 export const startLinking = (t: LinkAccountType) =>
194 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
195 const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
196 services.linkAccountService.saveAccountToLink(accountToLink);
198 const auth = getState().auth;
199 const isLocalUser = auth.user!.uuid.substring(0,5) === auth.localCluster;
200 let homeCluster = auth.localCluster;
201 if (isLocalUser && t === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
202 homeCluster = getState().linkAccountPanel.selectedCluster!;
206 dispatch(login(auth.localCluster, homeCluster, auth.remoteHosts));
209 export const getAccountLinkData = () =>
210 (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
211 return services.linkAccountService.getAccountToLink();
214 export const cancelLinking = (reload: boolean = false) =>
215 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
216 let user: UserResource | undefined;
218 // When linking is cancelled switch to the originating user (i.e. the user saved in session data)
219 dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
220 const linkAccountData = services.linkAccountService.getAccountToLink();
221 if (linkAccountData) {
222 services.linkAccountService.removeAccountToLink();
223 setAuthorizationHeader(services, linkAccountData.token);
224 user = await services.userService.get(linkAccountData.userUuid);
225 dispatch(switchUser(user, linkAccountData.token));
226 services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.CANCELLED);
234 dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
239 export const linkAccount = () =>
240 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
241 const linkState = getState().linkAccountPanel;
242 if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
244 // First create a project owned by the target user
245 const projectName = `Migrated from ${linkState.userToLink.email} (${linkState.userToLink.uuid})`;
246 let newGroup: GroupResource;
248 newGroup = await services.projectService.create({
250 ensure_unique_name: true
254 dispatch(linkFailed());
259 // The merge api links the user sending the request into the user
260 // specified in the request, so change the authorization header accordingly
261 setAuthorizationHeader(services, linkState.userToLinkToken);
262 await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
263 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
264 services.linkAccountService.removeAccountToLink();
265 services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.SUCCESS);
269 // If the link operation fails, delete the previously made project
271 setAuthorizationHeader(services, linkState.targetUserToken);
272 await services.projectService.delete(newGroup.uuid);
275 dispatch(linkFailed());