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_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";
18 const getRemoteHostBaseUrl = async (remoteHost: string): Promise<string | null> => {
20 if (url.indexOf('://') < 0) {
21 url = 'https://' + url;
23 const origin = new URL(url).origin;
24 let baseUrl: string | null = null;
27 const resp = await Axios.get<ClusterConfigJSON>(`${origin}/${CLUSTER_CONFIG_URL}`);
28 baseUrl = `${resp.data.Services.Controller.ExternalURL}/${ARVADOS_API_PATH}`;
31 const resp = await Axios.get<any>(`${origin}/status.json`);
32 baseUrl = resp.data.apiBaseURL;
37 if (baseUrl && baseUrl[baseUrl.length - 1] === '/') {
38 baseUrl = baseUrl.substr(0, baseUrl.length - 1);
44 const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
45 const resp = await Axios.get<UserDetailsResponse>(`${baseUrl}/users/current`, {
47 Authorization: `OAuth2 ${token}`
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);
59 const resp = await Axios.get(`${baseUrl}api_client_authorizations`, {
61 Authorization: `OAuth2 ${token}`
64 filters: JSON.stringify([['api_token', '=', token]])
68 return resp.data.items[0].uuid;
71 export const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
72 const shaObj = new jsSHA("SHA-1", "TEXT");
74 if (token.startsWith("v2/")) {
75 secret = token.split("/")[2];
77 shaObj.setHMACKey(secret, "TEXT");
78 shaObj.update(clusterId);
79 const hmac = shaObj.getHMAC("HEX");
80 return `v2/${tokenUuid}/${hmac}`;
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);
89 firstName: user.first_name,
90 lastName: user.last_name,
92 ownerUuid: user.owner_uuid,
94 isAdmin: user.is_admin,
95 isActive: user.is_active,
96 username: user.username,
103 export const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
105 export const validateCluster = async (remoteHost: string, clusterId: string, activeSession: Session): Promise<{ user: User; token: string, baseUrl: string }> => {
106 const baseUrl = await getRemoteHostBaseUrl(remoteHost);
108 return Promise.reject(`Could not find base url for ${remoteHost}`);
110 const { user, token } = await clusterLogin(clusterId, baseUrl, activeSession);
111 return { baseUrl, user, token };
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;
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;
127 session.loggedIn = false;
129 session.status = SessionStatus.VALIDATED;
130 dispatch(authActions.UPDATE_SESSION(session));
135 export const validateSessions = () =>
136 async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
137 const sessions = getState().auth.sessions;
138 const activeSession = getActiveSession(sessions);
140 dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
141 for (const session of sessions) {
142 if (session.status === SessionStatus.INVALIDATED) {
143 await dispatch(validateSession(session, activeSession));
146 services.authService.saveSessions(sessions);
147 dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
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);
156 const clusterId = remoteHost.match(/^(\w+)\./)![1];
157 if (sessions.find(s => s.clusterId === clusterId)) {
158 return Promise.reject("Cluster already exists");
161 const { baseUrl, user, token } = await validateCluster(remoteHost, clusterId, activeSession);
164 status: SessionStatus.VALIDATED,
167 name: getUserFullname(user),
175 dispatch(authActions.ADD_SESSION(session));
176 services.authService.saveSessions(getState().auth.sessions);
182 return Promise.reject("Could not validate cluster");
185 export const toggleSession = (session: Session) =>
186 async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
187 let s = { ...session };
189 if (session.loggedIn) {
192 const sessions = getState().auth.sessions;
193 const activeSession = getActiveSession(sessions);
195 s = await dispatch<any>(validateSession(s, activeSession)) as Session;
199 dispatch(authActions.UPDATE_SESSION(s));
200 services.authService.saveSessions(getState().auth.sessions);
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());
211 export const loadSiteManagerPanel = () =>
212 async (dispatch: Dispatch<any>) => {
214 dispatch(setBreadcrumbs([{ label: 'Site Manager' }]));
215 dispatch(validateSessions());