a23fb2ffea3d5f049551b1ffadb3a09cad4b1057
[arvados-workbench2.git] / src / store / auth / auth-action-session.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 { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
7 import { RootState } from "~/store/store";
8 import { ServiceRepository } from "~/services/services";
9 import Axios from "axios";
10 import { getUserFullname, User } from "~/models/user";
11 import { authActions } from "~/store/auth/auth-action";
12 import { Config, ClusterConfigJSON, CLUSTER_CONFIG_URL, ARVADOS_API_PATH } from "~/common/config";
13 import { Session, SessionStatus } from "~/models/session";
14 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
15 import { AuthService, UserDetailsResponse } from "~/services/auth-service/auth-service";
16 import * as jsSHA from "jssha";
17
18 const getRemoteHostBaseUrl = async (remoteHost: string): Promise<string | null> => {
19     let url = remoteHost;
20     if (url.indexOf('://') < 0) {
21         url = 'https://' + url;
22     }
23     const origin = new URL(url).origin;
24     let baseUrl: string | null = null;
25
26     try {
27         const resp = await Axios.get<ClusterConfigJSON>(`${origin}/${CLUSTER_CONFIG_URL}`);
28         baseUrl = `${resp.data.Services.Controller.ExternalURL}/${ARVADOS_API_PATH}`;
29     } catch (err) {
30         try {
31             const resp = await Axios.get<any>(`${origin}/status.json`);
32             baseUrl = resp.data.apiBaseURL;
33         } catch (err) {
34         }
35     }
36
37     if (baseUrl && baseUrl[baseUrl.length - 1] === '/') {
38         baseUrl = baseUrl.substr(0, baseUrl.length - 1);
39     }
40
41     return baseUrl;
42 };
43
44 const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
45     const resp = await Axios.get<UserDetailsResponse>(`${baseUrl}/users/current`, {
46         headers: {
47             Authorization: `OAuth2 ${token}`
48         }
49     });
50     return resp.data;
51 };
52
53 const getTokenUuid = async (baseUrl: string, token: string): Promise<string> => {
54     if (token.startsWith("v2/")) {
55         const uuid = token.split("/")[1];
56         return Promise.resolve(uuid);
57     }
58
59     const resp = await Axios.get(`${baseUrl}api_client_authorizations`, {
60         headers: {
61             Authorization: `OAuth2 ${token}`
62         },
63         data: {
64             filters: JSON.stringify([['api_token', '=', token]])
65         }
66     });
67
68     return resp.data.items[0].uuid;
69 };
70
71 export const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
72     const shaObj = new jsSHA("SHA-1", "TEXT");
73     let secret = token;
74     if (token.startsWith("v2/")) {
75         secret = token.split("/")[2];
76     }
77     shaObj.setHMACKey(secret, "TEXT");
78     shaObj.update(clusterId);
79     const hmac = shaObj.getHMAC("HEX");
80     return `v2/${tokenUuid}/${hmac}`;
81 };
82
83 const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{ user: User, token: string }> => {
84     const tokenUuid = await getTokenUuid(activeSession.baseUrl, activeSession.token);
85     const saltedToken = getSaltedToken(clusterId, tokenUuid, activeSession.token);
86     const user = await getUserDetails(baseUrl, saltedToken);
87     return {
88         user: {
89             firstName: user.first_name,
90             lastName: user.last_name,
91             uuid: user.uuid,
92             ownerUuid: user.owner_uuid,
93             email: user.email,
94             isAdmin: user.is_admin,
95             isActive: user.is_active,
96             username: user.username,
97             prefs: user.prefs
98         },
99         token: saltedToken
100     };
101 };
102
103 export const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
104
105 export const validateCluster = async (remoteHost: string, clusterId: string, activeSession: Session): Promise<{ user: User; token: string, baseUrl: string }> => {
106     const baseUrl = await getRemoteHostBaseUrl(remoteHost);
107     if (!baseUrl) {
108         return Promise.reject(`Could not find base url for ${remoteHost}`);
109     }
110     const { user, token } = await clusterLogin(clusterId, baseUrl, activeSession);
111     return { baseUrl, user, token };
112 };
113
114 export const validateSession = (session: Session, activeSession: Session) =>
115     async (dispatch: Dispatch): Promise<Session> => {
116         dispatch(authActions.UPDATE_SESSION({ ...session, status: SessionStatus.BEING_VALIDATED }));
117         session.loggedIn = false;
118         try {
119             const { baseUrl, user, token } = await validateCluster(session.remoteHost, session.clusterId, activeSession);
120             session.baseUrl = baseUrl;
121             session.token = token;
122             session.email = user.email;
123             session.uuid = user.uuid;
124             session.name = getUserFullname(user);
125             session.loggedIn = true;
126         } catch {
127             session.loggedIn = false;
128         } finally {
129             session.status = SessionStatus.VALIDATED;
130             dispatch(authActions.UPDATE_SESSION(session));
131         }
132         return session;
133     };
134
135 export const validateSessions = () =>
136     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
137         const sessions = getState().auth.sessions;
138         const activeSession = getActiveSession(sessions);
139         if (activeSession) {
140             dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
141             for (const session of sessions) {
142                 if (session.status === SessionStatus.INVALIDATED) {
143                     await dispatch(validateSession(session, activeSession));
144                 }
145             }
146             services.authService.saveSessions(sessions);
147             dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
148         }
149     };
150
151 export const addSession = (remoteHost: string) =>
152     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
153         const sessions = getState().auth.sessions;
154         const activeSession = getActiveSession(sessions);
155         if (activeSession) {
156             const clusterId = remoteHost.match(/^(\w+)\./)![1];
157             if (sessions.find(s => s.clusterId === clusterId)) {
158                 return Promise.reject("Cluster already exists");
159             }
160             try {
161                 const { baseUrl, user, token } = await validateCluster(remoteHost, clusterId, activeSession);
162                 const session = {
163                     loggedIn: true,
164                     status: SessionStatus.VALIDATED,
165                     active: false,
166                     email: user.email,
167                     name: getUserFullname(user),
168                     uuid: user.uuid,
169                     remoteHost,
170                     baseUrl,
171                     clusterId,
172                     token
173                 };
174
175                 dispatch(authActions.ADD_SESSION(session));
176                 services.authService.saveSessions(getState().auth.sessions);
177
178                 return session;
179             } catch (e) {
180             }
181         }
182         return Promise.reject("Could not validate cluster");
183     };
184
185 export const toggleSession = (session: Session) =>
186     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
187         let s = { ...session };
188
189         if (session.loggedIn) {
190             s.loggedIn = false;
191         } else {
192             const sessions = getState().auth.sessions;
193             const activeSession = getActiveSession(sessions);
194             if (activeSession) {
195                 s = await dispatch<any>(validateSession(s, activeSession)) as Session;
196             }
197         }
198
199         dispatch(authActions.UPDATE_SESSION(s));
200         services.authService.saveSessions(getState().auth.sessions);
201     };
202
203 export const initSessions = (authService: AuthService, config: Config, user: User) =>
204     (dispatch: Dispatch<any>) => {
205         const sessions = authService.buildSessions(config, user);
206         authService.saveSessions(sessions);
207         dispatch(authActions.SET_SESSIONS(sessions));
208         dispatch(validateSessions());
209     };
210
211 export const loadSiteManagerPanel = () =>
212     async (dispatch: Dispatch<any>) => {
213         try {
214             dispatch(setBreadcrumbs([{ label: 'Site Manager' }]));
215             dispatch(validateSessions());
216         } catch (e) {
217             return;
218         }
219     };