+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
import { Dispatch } from "redux";
import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
import { RootState } from "~/store/store";
import { ServiceRepository } from "~/services/services";
import Axios from "axios";
-import { getUserFullname } from "~/models/user";
+import { getUserFullname, User } from "~/models/user";
import { authActions } from "~/store/auth/auth-action";
import { Config, DISCOVERY_URL } from "~/common/config";
-import { Session } from "~/models/session";
+import { Session, SessionStatus } from "~/models/session";
import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
import { UserDetailsResponse } from "~/services/auth-service/auth-service";
+import * as jsSHA from "jssha";
-
-const getSessionOrigin = async (session: Session) => {
- let url = session.remoteHost;
+const getRemoteHostBaseUrl = async (remoteHost: string): Promise<string | null> => {
+ let url = remoteHost;
if (url.indexOf('://') < 0) {
url = 'https://' + url;
}
const origin = new URL(url).origin;
+ let baseUrl: string | null = null;
+
try {
const resp = await Axios.get<Config>(`${origin}/${DISCOVERY_URL}`);
- return resp.data.origin;
+ baseUrl = resp.data.baseUrl;
} catch (err) {
try {
const resp = await Axios.get<any>(`${origin}/status.json`);
- return resp.data.apiBaseURL;
+ baseUrl = resp.data.apiBaseURL;
} catch (err) {
}
}
- return null;
+
+ if (baseUrl && baseUrl[baseUrl.length - 1] === '/') {
+ baseUrl = baseUrl.substr(0, baseUrl.length - 1);
+ }
+
+ return baseUrl;
};
-const getUserDetails = async (origin: string, token: string): Promise<UserDetailsResponse> => {
- const resp = await Axios.get<UserDetailsResponse>(`${origin}/arvados/v1/users/current`, {
+const getUserDetails = async (baseUrl: string, token: string): Promise<UserDetailsResponse> => {
+ const resp = await Axios.get<UserDetailsResponse>(`${baseUrl}/users/current`, {
headers: {
Authorization: `OAuth2 ${token}`
}
return resp.data;
};
-const validateSessions = () =>
+const getTokenUuid = async (baseUrl: string, token: string): Promise<string> => {
+ if (token.startsWith("v2/")) {
+ const uuid = token.split("/")[1];
+ return Promise.resolve(uuid);
+ }
+
+ const resp = await Axios.get(`${baseUrl}/api_client_authorizations`, {
+ headers: {
+ Authorization: `OAuth2 ${token}`
+ },
+ data: {
+ filters: JSON.stringify([['api_token', '=', token]])
+ }
+ });
+
+ return resp.data.items[0].uuid;
+};
+
+const getSaltedToken = (clusterId: string, tokenUuid: string, token: string) => {
+ const shaObj = new jsSHA("SHA-1", "TEXT");
+ let secret = token;
+ if (token.startsWith("v2/")) {
+ secret = token.split("/")[2];
+ }
+ shaObj.setHMACKey(secret, "TEXT");
+ shaObj.update(clusterId);
+ const hmac = shaObj.getHMAC("HEX");
+ return `v2/${tokenUuid}/${hmac}`;
+};
+
+const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: Session): Promise<{user: User, token: string}> => {
+ const tokenUuid = await getTokenUuid(activeSession.baseUrl, activeSession.token);
+ console.log(">> Cluster", clusterId);
+ const saltedToken = getSaltedToken(clusterId, tokenUuid, activeSession.token);
+ console.log(">> Salted token", saltedToken);
+ const user = await getUserDetails(baseUrl, saltedToken);
+ return {
+ user: {
+ firstName: user.first_name,
+ lastName: user.last_name,
+ uuid: user.uuid,
+ ownerUuid: user.owner_uuid,
+ email: user.email,
+ isAdmin: user.is_admin
+ },
+ token: saltedToken
+ };
+};
+
+const getActiveSession = (sessions: Session[]): Session | undefined => sessions.find(s => s.active);
+
+export const validateCluster = async (remoteHost: string, clusterId: string, activeSession: Session): Promise<{ user: User; token: string, baseUrl: string }> => {
+ const baseUrl = await getRemoteHostBaseUrl(remoteHost);
+ if (!baseUrl) {
+ return Promise.reject(`Could not find base url for ${remoteHost}`);
+ }
+ const { user, token } = await clusterLogin(clusterId, baseUrl, activeSession);
+ return { baseUrl, user, token };
+};
+
+export const validateSession = (session: Session, activeSession: Session) =>
+ async (dispatch: Dispatch) => {
+ dispatch(authActions.UPDATE_SESSION({ ...session, status: SessionStatus.BEING_VALIDATED }));
+ session.loggedIn = false;
+ try {
+ const { baseUrl, user, token } = await validateCluster(session.remoteHost, session.clusterId, activeSession);
+ session.baseUrl = baseUrl;
+ session.token = token;
+ session.email = user.email;
+ session.username = getUserFullname(user);
+ session.loggedIn = true;
+ } catch {
+ session.loggedIn = false;
+ } finally {
+ session.status = SessionStatus.VALIDATED;
+ dispatch(authActions.UPDATE_SESSION(session));
+ }
+ return session;
+ };
+
+export const validateSessions = () =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const sessions = getState().auth.sessions;
- dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
- for (const session of sessions) {
- if (!session.validated) {
- const origin = await getSessionOrigin(session);
- const user = await getUserDetails(origin, session.token);
+ const activeSession = getActiveSession(sessions);
+ if (activeSession) {
+ dispatch(progressIndicatorActions.START_WORKING("sessionsValidation"));
+ for (const session of sessions) {
+ if (session.status === SessionStatus.INVALIDATED) {
+ await dispatch(validateSession(session, activeSession));
+ }
}
+ services.authService.saveSessions(sessions);
+ dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
}
- dispatch(progressIndicatorActions.STOP_WORKING("sessionsValidation"));
};
export const addSession = (remoteHost: string) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const user = getState().auth.user!;
- const clusterId = remoteHost.match(/^(\w+)\./)![1];
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const sessions = getState().auth.sessions;
+ const activeSession = getActiveSession(sessions);
+ if (activeSession) {
+ const clusterId = remoteHost.match(/^(\w+)\./)![1];
+ if (sessions.find(s => s.clusterId === clusterId)) {
+ return Promise.reject("Cluster already exists");
+ }
+ try {
+ const { baseUrl, user, token } = await dispatch(validateCluster(remoteHost, clusterId, activeSession));
+ const session = {
+ loggedIn: false,
+ status: SessionStatus.VALIDATED,
+ active: false,
+ email: user.email,
+ username: getUserFullname(user),
+ remoteHost,
+ baseUrl,
+ clusterId,
+ token
+ };
- dispatch(authActions.ADD_SESSION({
- loggedIn: false,
- validated: false,
- email: user.email,
- username: getUserFullname(user),
- remoteHost,
- clusterId,
- token: ''
- }));
+ dispatch(authActions.ADD_SESSION(session));
+ services.authService.saveSessions(getState().auth.sessions);
- services.authService.saveSessions(getState().auth.sessions);
+ return session;
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ debugger;
+ return Promise.reject("Could not validate cluster");
};
export const loadSiteManagerPanel = () =>
import * as React from 'react';
import {
Card,
- CardContent,
+ CardContent, CircularProgress,
Grid,
StyleRulesCallback,
Table,
withStyles
} from '@material-ui/core';
import { ArvadosTheme } from '~/common/custom-theme';
-import { Session } from "~/models/session";
+import { Session, SessionStatus } from "~/models/session";
import Button from "@material-ui/core/Button";
import { User } from "~/models/user";
import { compose } from "redux";
-import { Field, InjectedFormProps, reduxForm, reset } from "redux-form";
+import { Field, FormErrors, InjectedFormProps, reduxForm, reset, stopSubmit } from "redux-form";
import { TextField } from "~/components/text-field/text-field";
import { addSession } from "~/store/auth/auth-action-session";
import { SITE_MANAGER_REMOTE_HOST_VALIDATION } from "~/validators/validators";
+import {
+ RENAME_FILE_DIALOG,
+ RenameFileDialogData
+} from "~/store/collection-panel/collection-panel-files/collection-panel-files-actions";
type CssRules = 'root' | 'link' | 'buttonContainer' | 'table' | 'tableRow' | 'status' | 'remoteSiteInfo' | 'buttonAdd';
export const SiteManagerPanelRoot = compose(
reduxForm<{remoteHost: string}>({
form: SITE_MANAGER_FORM_NAME,
- onSubmit: (data, dispatch) => {
- dispatch(addSession(data.remoteHost));
- dispatch(reset(SITE_MANAGER_FORM_NAME));
+ onSubmit: async (data, dispatch) => {
+ try {
+ await dispatch(addSession(data.remoteHost));
+ dispatch(reset(SITE_MANAGER_FORM_NAME));
+ } catch (e) {
+ const errors = {
+ remoteHost: e
+ } as FormErrors;
+ dispatch(stopSubmit(SITE_MANAGER_FORM_NAME, errors));
+ }
+
}
}),
withStyles(styles))
</TableRow>
</TableHead>
<TableBody>
- {sessions.map((session, index) =>
- <TableRow key={index} className={classes.tableRow}>
+ {sessions.map((session, index) => {
+ const validating = session.status === SessionStatus.BEING_VALIDATED;
+ return <TableRow key={index} className={classes.tableRow}>
<TableCell>{session.clusterId}</TableCell>
- <TableCell>{session.username}</TableCell>
- <TableCell>{session.email}</TableCell>
+ <TableCell>{validating ? <CircularProgress size={20}/> : session.username}</TableCell>
+ <TableCell>{validating ? <CircularProgress size={20}/> : session.email}</TableCell>
<TableCell>
<div className={classes.status} style={{
color: session.loggedIn ? '#fff' : '#000',
{session.loggedIn ? "Logged in" : "Logged out"}
</div>
</TableCell>
- </TableRow>)}
+ </TableRow>;
+ })}
</TableBody>
</Table>}
</Grid>
component={TextField}
placeholder="zzzz.arvadosapi.com"
margin="normal"
- label="New cluster"/>
+ label="New cluster"
+ autoFocus/>
</Grid>
<Grid item xs={3}>
<Button type="submit" variant="contained" color="primary"