1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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_PATH, DISCOVERY_DOC_PATH, ARVADOS_API_PATH } from "~/common/config";
13 import { normalizeURLPath } from "~/common/url";
14 import { Session, SessionStatus } from "~/models/session";
15 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
16 import { AuthService, UserDetailsResponse } from "~/services/auth-service/auth-service";
17 import * as jsSHA from "jssha";
19 const getClusterInfo = async (origin: string): Promise<{ clusterId: string, baseUrl: string } | null> => {
20 // Try the new public config endpoint
22 const config = (await Axios.get<ClusterConfigJSON>(`${origin}/${CLUSTER_CONFIG_PATH}`)).data;
24 clusterId: config.ClusterID,
25 baseUrl: normalizeURLPath(`${config.Services.Controller.ExternalURL}/${ARVADOS_API_PATH}`)
29 // Fall back to discovery document
31 const config = (await Axios.get<any>(`${origin}/${DISCOVERY_DOC_PATH}`)).data;
33 clusterId: config.uuidPrefix,
34 baseUrl: normalizeURLPath(config.baseUrl)
41 interface RemoteHostInfo {
46 const getRemoteHostInfo = async (remoteHost: string): Promise<RemoteHostInfo | null> => {
48 if (url.indexOf('://') < 0) {
49 url = 'https://' + url;
51 const origin = new URL(url).origin;
53 // Maybe it is an API server URL, try fetching config and discovery doc
54 let r = getClusterInfo(origin);
59 // Maybe it is a Workbench2 URL, try getting config.json
61 r = getClusterInfo((await Axios.get<any>(`${origin}/config.json`)).data.API_HOST);
67 // Maybe it is a Workbench1 URL, try getting status.json
69 r = getClusterInfo((await Axios.get<any>(`${origin}/status.json`)).data.apiBaseURL);
78 const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
79 const resp = await Axios.get<UserDetailsResponse>(`${baseUrl}/users/current`, {
81 Authorization: `OAuth2 ${token}`
87 export const getSaltedToken = (clusterId: string, token: string) => {
88 const shaObj = new jsSHA("SHA-1", "TEXT");
89 const [ver, uuid, secret] = token.split("/");
91 throw new Error("Must be a v2 token");
94 if (uuid.substr(0, 5) !== clusterId) {
95 shaObj.setHMACKey(secret, "TEXT");
96 shaObj.update(clusterId);
97 salted = shaObj.getHMAC("HEX");
99 return `v2/${uuid}/${salted}`;
102 export const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
104 export const validateCluster = async (info: RemoteHostInfo, useToken: string):
105 Promise<{ user: User; token: string }> => {
107 const saltedToken = getSaltedToken(info.clusterId, useToken);
108 const user = await getUserDetails(info.baseUrl, saltedToken);
111 firstName: user.first_name,
112 lastName: user.last_name,
114 ownerUuid: user.owner_uuid,
116 isAdmin: user.is_admin,
117 isActive: user.is_active,
118 username: user.username,
125 export const validateSession = (session: Session, activeSession: Session) =>
126 async (dispatch: Dispatch): Promise<Session> => {
127 dispatch(authActions.UPDATE_SESSION({ ...session, status: SessionStatus.BEING_VALIDATED }));
128 session.loggedIn = false;
130 const setupSession = (baseUrl: string, user: User, token: string) => {
131 session.baseUrl = baseUrl;
132 session.token = token;
133 session.email = user.email;
134 session.uuid = user.uuid;
135 session.name = getUserFullname(user);
136 session.loggedIn = true;
139 const info = await getRemoteHostInfo(session.remoteHost);
141 throw new Error(`Could not get config for ${session.remoteHost}`);
144 const { user, token } = await validateCluster(info, session.token);
145 setupSession(info.baseUrl, user, token);
148 const { user, token } = await validateCluster(info, activeSession.token);
149 setupSession(info.baseUrl, user, token);
153 session.status = SessionStatus.VALIDATED;
154 dispatch(authActions.UPDATE_SESSION(session));
159 export const validateSessions = () =>
160 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
161 const sessions = getState().auth.sessions;
162 const activeSession = getActiveSession(sessions);
164 dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
165 for (const session of sessions) {
166 if (session.status === SessionStatus.INVALIDATED) {
167 await dispatch(validateSession(session, activeSession));
170 services.authService.saveSessions(sessions);
171 dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
175 export const addSession = (remoteHost: string, token?: string, sendToLogin?: boolean) =>
176 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
177 const sessions = getState().auth.sessions;
178 const activeSession = getActiveSession(sessions);
179 let useToken: string | null = null;
182 } else if (activeSession) {
183 useToken = activeSession.token;
187 const info = await getRemoteHostInfo(remoteHost);
189 return Promise.reject(`Could not get config for ${remoteHost}`);
193 const { user, token } = await validateCluster(info, useToken);
196 status: SessionStatus.VALIDATED,
199 name: getUserFullname(user),
201 baseUrl: info.baseUrl,
202 clusterId: info.clusterId,
207 if (sessions.find(s => s.clusterId === info.clusterId)) {
208 dispatch(authActions.UPDATE_SESSION(session));
210 dispatch(authActions.ADD_SESSION(session));
212 services.authService.saveSessions(getState().auth.sessions);
217 const rootUrl = new URL(info.baseUrl);
218 rootUrl.pathname = "";
219 window.location.href = `${rootUrl.toString()}/login?return_to=` + encodeURI(`${window.location.protocol}//${window.location.host}/add-session?baseURL=` + encodeURI(rootUrl.toString()));
224 return Promise.reject("Could not validate cluster");
227 export const toggleSession = (session: Session) =>
228 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
229 let s = { ...session };
231 if (session.loggedIn) {
234 const sessions = getState().auth.sessions;
235 const activeSession = getActiveSession(sessions);
237 s = await dispatch<any>(validateSession(s, activeSession)) as Session;
241 dispatch(authActions.UPDATE_SESSION(s));
242 services.authService.saveSessions(getState().auth.sessions);
245 export const initSessions = (authService: AuthService, config: Config, user: User) =>
246 (dispatch: Dispatch<any>) => {
247 const sessions = authService.buildSessions(config, user);
248 authService.saveSessions(sessions);
249 dispatch(authActions.SET_SESSIONS(sessions));
250 dispatch(validateSessions());
253 export const loadSiteManagerPanel = () =>
254 async (dispatch: Dispatch<any>) => {
256 dispatch(setBreadcrumbs([{ label: 'Site Manager' }]));
257 dispatch(validateSessions());