16848: Adds an extra token to be displayed on the 'get token' dialog.
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 16 Feb 2021 23:00:57 +0000 (20:00 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 16 Feb 2021 23:00:57 +0000 (20:00 -0300)
Also, adds a "Get new token" button to the dialog to allow the user to
request new tokens.
These extra tokens won't be expired on logout, so they're suitable for use
on S3 URLs and other tasks the user may need.

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

src/store/auth/auth-action.ts
src/store/auth/auth-reducer.ts
src/store/token-dialog/token-dialog-actions.tsx
src/views-components/token-dialog/token-dialog.tsx

index 15fe3d4d591da9e00ef356ac591726060a8e76a3..04d1287a9ca76d9e46a4daf870e91e89280afd3a 100644 (file)
@@ -21,6 +21,7 @@ export const authActions = unionize({
     LOGIN: {},
     LOGOUT: ofType<{ deleteLinkData: boolean }>(),
     SET_CONFIG: ofType<{ config: Config }>(),
+    SET_EXTRA_TOKEN: ofType<{ extraToken: string }>(),
     INIT_USER: ofType<{ user: User, token: string }>(),
     USER_DETAILS_REQUEST: {},
     USER_DETAILS_SUCCESS: ofType<User>(),
@@ -86,11 +87,28 @@ export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: ()
     setAuthorizationHeader(svc, token);
     return svc.authService.getUserDetails().then((user: User) => {
         dispatch(authActions.INIT_USER({ user, token }));
+        // Upon user init, request an extra token that won't be expired on logout
+        // for other uses like the "get token" dialog, or S3 URL building.
+        dispatch<any>(getNewExtraToken());
     }).catch(() => {
         dispatch(authActions.LOGOUT({ deleteLinkData: false }));
     });
 };
 
+export const getNewExtraToken = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const user = getState().auth.user;
+        if (user === undefined) { return; }
+        try {
+            const aca = await services.apiClientAuthorizationService.create();
+            const newExtraToken = `v2/${aca.uuid}/${aca.apiToken}`;
+            dispatch(authActions.SET_EXTRA_TOKEN({ extraToken: newExtraToken }));
+            return newExtraToken;
+        } catch {
+            return;
+        }
+    };
+
 export const login = (uuidPrefix: string, homeCluster: string, loginCluster: string,
     remoteHosts: { [key: string]: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         services.authService.login(uuidPrefix, homeCluster, loginCluster, remoteHosts);
index 946407fe24172610fbc3aaf9cff7b95052a43af8..d55e8301df50713625a1cf13861018779661b0db 100644 (file)
@@ -12,6 +12,7 @@ import { Config, mockConfig } from '~/common/config';
 export interface AuthState {
     user?: User;
     apiToken?: string;
+    extraApiToken?: string;
     sshKeys: SshKeyResource[];
     sessions: Session[];
     localCluster: string;
@@ -25,6 +26,7 @@ export interface AuthState {
 const initialState: AuthState = {
     user: undefined,
     apiToken: undefined,
+    extraApiToken: undefined,
     sshKeys: [],
     sessions: [],
     localCluster: "",
@@ -54,6 +56,7 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat
                 remoteHostsConfig: { ...state.remoteHostsConfig, [config.uuidPrefix]: config },
             };
         },
+        SET_EXTRA_TOKEN: ({ extraToken }) => ({ ...state, extraApiToken: extraToken }),
         INIT_USER: ({ user, token }) => {
             return { ...state, user, apiToken: token, homeCluster: user.uuid.substr(0, 5) };
         },
index 08a45992e00a18726ac3842c48cc3c332ea8139f..656f532b5a5908a8f11a3892794822a74ef3337a 100644 (file)
@@ -20,7 +20,7 @@ export const setTokenDialogApiHost = (apiHost: string) =>
 
 export const getTokenDialogData = (state: RootState): TokenDialogData => ({
     apiHost: getProperty<string>(API_HOST_PROPERTY_NAME)(state.properties) || '',
-    token: state.auth.apiToken || '',
+    token: state.auth.extraApiToken || state.auth.apiToken || '',
 });
 
 export const openTokenDialog = dialogActions.OPEN_DIALOG({ id: TOKEN_DIALOG_NAME, data: {} });
index b0d5c67ef8c5943a9b8e0475deffef8b42c98a20..ed155541e41edea8402cdb284696fbb8d0fd2ab0 100644 (file)
@@ -3,17 +3,32 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Dialog, DialogActions, DialogTitle, DialogContent, WithStyles, withStyles, StyleRulesCallback, Button, Typography } from '@material-ui/core';
+import {
+    Dialog,
+    DialogActions,
+    DialogTitle,
+    DialogContent,
+    WithStyles,
+    withStyles,
+    StyleRulesCallback,
+    Button,
+    Typography
+} from '@material-ui/core';
 import * as CopyToClipboard from 'react-copy-to-clipboard';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { withDialog } from '~/store/dialog/with-dialog';
 import { WithDialogProps } from '~/store/dialog/with-dialog';
 import { connect, DispatchProp } from 'react-redux';
-import { TokenDialogData, getTokenDialogData, TOKEN_DIALOG_NAME } from '~/store/token-dialog/token-dialog-actions';
+import {
+    TokenDialogData,
+    getTokenDialogData,
+    TOKEN_DIALOG_NAME,
+} from '~/store/token-dialog/token-dialog-actions';
 import { DefaultCodeSnippet } from '~/components/default-code-snippet/default-code-snippet';
 import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { getNewExtraToken } from '~/store/auth/auth-action';
 
-type CssRules = 'link' | 'paper' | 'button' | 'copyButton';
+type CssRules = 'link' | 'paper' | 'button' | 'actionButton';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     link: {
@@ -31,10 +46,11 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         fontSize: '0.8125rem',
         fontWeight: 600
     },
-    copyButton: {
+    actionButton: {
         boxShadow: 'none',
         marginTop: theme.spacing.unit * 2,
         marginBottom: theme.spacing.unit * 2,
+        marginRight: theme.spacing.unit * 2,
     }
 });
 
@@ -49,6 +65,17 @@ export class TokenDialogComponent extends React.Component<TokenDialogProps> {
         }));
     }
 
+    onGetNewToken = async () => {
+        const newToken = await this.props.dispatch<any>(getNewExtraToken());
+        if (newToken) {
+            this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: 'New token retrieved',
+                hideDuration: 2000,
+                kind: SnackbarKind.SUCCESS
+            }));
+        }
+    }
+
     getSnippet = ({ apiHost, token }: TokenDialogData) =>
         `HISTIGNORE=$HISTIGNORE:'export ARVADOS_API_TOKEN=*'
 export ARVADOS_API_TOKEN=${token}
@@ -83,11 +110,20 @@ unset ARVADOS_API_HOST_INSECURE`
                         color="primary"
                         size="small"
                         variant="contained"
-                        className={classes.copyButton}
+                        className={classes.actionButton}
                     >
                         COPY TO CLIPBOARD
                     </Button>
                 </CopyToClipboard>
+                <Button
+                    onClick={() => this.onGetNewToken()}
+                    color="primary"
+                    size="small"
+                    variant="contained"
+                    className={classes.actionButton}
+                >
+                    GET NEW TOKEN
+                </Button>
                 <Typography >
                     Arvados
                             <a href='http://doc.arvados.org/user/reference/api-tokens.html' target='blank' className={classes.link}>virtual machines</a>