16812: Handoff token using query param
authorPeter Amstutz <peter.amstutz@curii.com>
Thu, 15 Oct 2020 21:21:27 +0000 (17:21 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Thu, 15 Oct 2020 21:21:27 +0000 (17:21 -0400)
Need to pass the token to keep-web without it being "sticky" in the
URL bar.  Using a query param accomplishes this, because keep-web
knows to strip the api_token query parameter and respond with redirect
and a cookie which the browser can use to fetch the file safely.

Also distinguish between KeepWebService (now the download service) and
KeepWebInlineService (the one that will serve content that can be
displayed inline in the browser if it is safe to do so).

Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

src/common/config.ts
src/common/redirect-to.test.ts
src/common/redirect-to.ts
src/store/auth/auth-action-session.ts
src/store/store.ts
src/views-components/context-menu/actions/collection-file-viewer-action.tsx
src/views-components/context-menu/actions/download-action.tsx
src/views-components/context-menu/actions/download-collection-file-action.tsx
src/views-components/context-menu/actions/file-viewer-action.tsx
src/views-components/context-menu/actions/helpers.ts

index afbeb5aecce884a1ba98a9c505ce7d2fdd299c13..146ca90acf62de05f7c4a0214f74701eb6c97f0f 100644 (file)
@@ -89,6 +89,7 @@ export interface ClusterConfigJSON {
 export class Config {
     baseUrl: string;
     keepWebServiceUrl: string;
+    keepWebInlineServiceUrl: string;
     remoteHosts: {
         [key: string]: string
     };
@@ -114,6 +115,7 @@ export const buildConfig = (clusterConfig: ClusterConfigJSON): Config => {
     config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
     config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
     config.keepWebServiceUrl = clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
+    config.keepWebInlineServiceUrl = clusterConfigJSON.Services.WebDAV.ExternalURL;
     config.loginCluster = clusterConfigJSON.Login.LoginCluster;
     config.clusterConfig = clusterConfigJSON;
     config.apiRevision = 0;
@@ -249,6 +251,7 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
 export const mockConfig = (config: Partial<Config>): Config => ({
     baseUrl: "",
     keepWebServiceUrl: "",
+    keepWebInlineServiceUrl: "",
     remoteHosts: {},
     rootUrl: "",
     uuidPrefix: "",
index 177e16566896e6f6d723d457ce7850d6b70b3444..c7d3a84e8cec8ca42e4c0233117a760478b0524c 100644 (file)
@@ -19,9 +19,9 @@ describe('redirect-to', () => {
         port: '80',
         protocol: 'http',
         search: '',
-        reload: () => {},
-        replace: () => {},
-        assign: () => {},
+        reload: () => { },
+        replace: () => { },
+        assign: () => { },
         ancestorOrigins: [],
         href: '',
     };
@@ -72,10 +72,10 @@ describe('redirect-to', () => {
 
         it('should redirect to page when it is present in session storage', () => {
             // when
-            handleRedirects(config);
+            handleRedirects("abcxyz", config);
 
             // then
-            expect(window.location.href).toBe(`${config.keepWebServiceUrl}${redirectTo}`);
+            expect(window.location.href).toBe(`${config.keepWebServiceUrl}${redirectTo}?api_token=abcxyz`);
         });
     });
-});
\ No newline at end of file
+});
index 86fac71cbae8ab7adb27354ee7a351e34c7c72e3..7cb0d5805577794fae00148c5fd42f24d360cc8e 100644 (file)
@@ -10,20 +10,23 @@ export const storeRedirects = () => {
     if (window.location.href.indexOf(REDIRECT_TO_KEY) > -1) {
         const { location: { href }, sessionStorage } = window;
         const redirectUrl = href.split(`${REDIRECT_TO_KEY}=`)[1];
-        
+
         if (sessionStorage) {
             sessionStorage.setItem(REDIRECT_TO_KEY, redirectUrl);
         }
     }
 };
 
-export const handleRedirects = (config: Config) => {
+export const handleRedirects = (token: string, config: Config) => {
     const { sessionStorage } = window;
     const { keepWebServiceUrl } = config;
 
     if (sessionStorage && sessionStorage.getItem(REDIRECT_TO_KEY)) {
         const redirectUrl = sessionStorage.getItem(REDIRECT_TO_KEY);
         sessionStorage.removeItem(REDIRECT_TO_KEY);
-        window.location.href = `${keepWebServiceUrl}${redirectUrl}`;
+        if (redirectUrl) {
+            const sep = redirectUrl.indexOf("?") > -1 ? "&" : "?";
+            window.location.href = `${keepWebServiceUrl}${redirectUrl}${sep}api_token=${token}`;
+        }
     }
-};
\ No newline at end of file
+};
index ed2e18b2db39079705805501ed2cc4cc9bb18be6..a02f922df435d9f0b1c9065ef9a9e9f4a317ebd6 100644 (file)
@@ -27,6 +27,7 @@ const getClusterConfig = async (origin: string, apiClient: AxiosInstance): Promi
         configFromDD = {
             baseUrl: normalizeURLPath(dd.baseUrl),
             keepWebServiceUrl: dd.keepWebServiceUrl,
+            keepWebInlineServiceUrl: dd.keepWebInlineServiceUrl,
             remoteHosts: dd.remoteHosts,
             rootUrl: dd.rootUrl,
             uuidPrefix: dd.uuidPrefix,
index a6e0cbbe8556459a51abf0f004f6fd90145b9120..7beb099c1cf9c8f32ba818d064c6d2938b422b57 100644 (file)
@@ -136,7 +136,7 @@ export function configureStore(history: History, services: ServiceRepository, co
         const state = store.getState();
 
         if (state.auth && state.auth.apiToken) {
-            handleRedirects(config);
+            handleRedirects(state.auth.apiToken, config);
         }
 
         return next(action);
index cdc0c8294ca01f75e06ab59335633a1cab0ae304..f75da23869666e248fe1f04d91cd0e500f0e1a21 100644 (file)
@@ -15,15 +15,15 @@ const mapStateToProps = (state: RootState) => {
         const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
         if (file) {
             return {
-                href: file.url,
+                href: file.url.replace(state.auth.config.keepWebServiceUrl, state.auth.config.keepWebInlineServiceUrl),
                 kind: 'file',
                 currentCollectionUuid
             };
         }
     } else {
-        return ;
+        return;
     }
-    return ;
+    return;
 };
 
 export const CollectionFileViewerAction = connect(mapStateToProps)(FileViewerAction);
index 7468954fdd97d293837dd8edfebbc0d5a62a241a..224d43085d630c2734d78c12ae2c5c58ec507db0 100644 (file)
@@ -47,7 +47,7 @@ export const DownloadAction = (props: { href?: any, download?: any, onClick?: ()
     return props.href || props.kind === 'files'
         ? <a
             style={{ textDecoration: 'none' }}
-            href={props.kind === 'files' ? undefined : `${props.href}?disposition=attachment`}
+            href={props.kind === 'files' ? undefined : props.href}
             onClick={props.onClick}
             {...downloadProps}>
             <ListItem button onClick={() => props.kind === 'files' ? createZip(props.href, props.download) : undefined}>
@@ -61,4 +61,4 @@ export const DownloadAction = (props: { href?: any, download?: any, onClick?: ()
             </ListItem>
         </a>
         : null;
-};
\ No newline at end of file
+};
index 9332d170f82856bb714b7c05ff99c4377a6a67bf..437b22ed00cf5c81afc9525741c846be1eb112d0 100644 (file)
@@ -17,7 +17,7 @@ const mapStateToProps = (state: RootState) => {
         const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
         if (file) {
             return {
-                href: sanitizeToken(file.url, false),
+                href: sanitizeToken(file.url, true),
                 kind: 'file',
                 currentCollectionUuid
             };
@@ -25,7 +25,7 @@ const mapStateToProps = (state: RootState) => {
     } else {
         const files = filterCollectionFilesBySelection(state.collectionPanelFiles, true);
         return {
-            href: files.map(file => sanitizeToken(file.url, false)),
+            href: files.map(file => sanitizeToken(file.url, true)),
             kind: 'files',
             currentCollectionUuid
         };
index e58ea6a7888461599c8d45dd9dc663c0306b7710..9af2ef92042a39eb8905345c66af81af9438b6dc 100644 (file)
@@ -12,17 +12,17 @@ export const FileViewerAction = (props: { href?: any, download?: any, onClick?:
     return props.href
         ? <a
             style={{ textDecoration: 'none' }}
-            href={sanitizeToken(props.href, false)}
+            href={sanitizeToken(props.href, true)}
             target="_blank"
             onClick={props.onClick}>
             <ListItem button>
-                    <ListItemIcon>
-                        <OpenIcon />
-                    </ListItemIcon>
+                <ListItemIcon>
+                    <OpenIcon />
+                </ListItemIcon>
                 <ListItemText>
                     Open in new tab
-                </ListItemText>
+                 </ListItemText>
             </ListItem>
         </a>
         : null;
-};
\ No newline at end of file
+};
index 3836860626a8363554ae1df135975a7a07c204ba..419c796d1a769f6a156d2c7c9aabc16be293fb60 100644 (file)
@@ -6,7 +6,9 @@ export const sanitizeToken = (href: string, tokenAsQueryParam: boolean = true):
     const [prefix, suffix] = href.split('/t=');
     const [token, ...rest] = suffix.split('/');
 
-    return `${[prefix, ...rest].join('/')}${tokenAsQueryParam ? `?api_token=${token}` : ''}`;
+    const sep = href.indexOf("?") > -1 ? "&" : "?";
+
+    return `${[prefix, ...rest].join('/')}${tokenAsQueryParam ? `${sep}api_token=${token}` : ''}`;
 };
 
 export const getClipboardUrl = (href: string): string => {
@@ -14,4 +16,4 @@ export const getClipboardUrl = (href: string): string => {
     const url = sanitizeToken(href, false);
 
     return `${origin}?redirectTo=${url}`;
-};
\ No newline at end of file
+};