}).as('sharedGroup').then(function () {
// Creates the collection using the admin token so we can set up
// a bogus manifest text without block signatures.
- cy.createCollection(adminUser.token, {
- name: 'Test collection',
- owner_uuid: this.sharedGroup.uuid,
- properties: { someKey: 'someValue' },
- manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`
- })
+ cy.doRequest('GET', '/arvados/v1/config', null, null)
+ .its('body').should((clusterConfig) => {
+ expect(clusterConfig.Collections, "clusterConfig").to.have.property("TrustAllContent", false);
+ expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAV").have.property("ExternalURL");
+ expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAVDownload").have.property("ExternalURL");
+ const inlineUrl = clusterConfig.Services.WebDAV.ExternalURL !== ""
+ ? clusterConfig.Services.WebDAV.ExternalURL
+ : clusterConfig.Services.WebDAVDownload.ExternalURL;
+ expect(inlineUrl).to.not.contain("*");
+ })
+ .createCollection(adminUser.token, {
+ name: 'Test collection',
+ owner_uuid: this.sharedGroup.uuid,
+ properties: { someKey: 'someValue' },
+ manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`
+ })
.as('testCollection').then(function () {
// Share the group with active user.
cy.createLink(adminUser.token, {
.contains(fileName).rightclick({ force: true });
cy.get('[data-cy=context-menu]')
.should('contain', 'Download')
- .and('contain', 'Open in new tab')
+ .and('not.contain', 'Open in new tab')
.and('contain', 'Copy to clipboard')
.and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
.and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
.contains(subDirName).rightclick({ force: true });
cy.get('[data-cy=context-menu]')
.should('not.contain', 'Download')
- .and('contain', 'Open in new tab')
+ .and('not.contain', 'Open in new tab')
.and('contain', 'Copy to clipboard')
.and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
.and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
Value: string,
Protected?: boolean,
}
- }
+ },
+ TrustAllContent: boolean
};
Volumes: {
[key: string]: {
},
Collections: {
ForwardSlashNameSubstitution: "",
+ TrustAllContent: false,
},
Volumes: {},
...config
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { mount, configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import configureMockStore from 'redux-mock-store'
+import { Provider } from 'react-redux';
+import { CollectionFileViewerAction } from './collection-file-viewer-action';
+import { ContextMenuKind } from 'views-components/context-menu/context-menu';
+import { createTree, initTreeNode, setNode, getNodeValue } from "models/tree";
+import { getInlineFileUrl, sanitizeToken } from "./helpers";
+
+const middlewares = [];
+const mockStore = configureMockStore(middlewares);
+
+configure({ adapter: new Adapter() });
+
+describe('CollectionFileViewerAction', () => {
+ let defaultStore;
+ const fileUrl = "https://download.host:12345/c=abcde-4zz18-abcdefghijklmno/t=v2/token2/token3/cat.jpg";
+ const insecureKeepInlineUrl = "https://download.host:12345/";
+ const secureKeepInlineUrl = "https://*.collections.host:12345/";
+
+ beforeEach(() => {
+ let filesTree = createTree();
+ let data = {id: "000", value: {"url": fileUrl}};
+ filesTree = setNode(initTreeNode(data))(filesTree);
+
+ defaultStore = {
+ auth: {
+ config: {
+ keepWebServiceUrl: "https://download.host:12345/",
+ keepWebInlineServiceUrl: insecureKeepInlineUrl,
+ clusterConfig: {
+ Collections: {
+ TrustAllContent: false
+ }
+ }
+ }
+ },
+ contextMenu: {
+ resource: {
+ uuid: "000",
+ menuKind: ContextMenuKind.COLLECTION_FILE_ITEM,
+ }
+ },
+ collectionPanel: {
+ item: {
+ uuid: ""
+ }
+ },
+ collectionPanelFiles: filesTree
+ };
+ });
+
+ it('should hide open in new tab when unsafe', () => {
+ // given
+ const store = mockStore(defaultStore);
+
+ // when
+ const wrapper = mount(<Provider store={store}>
+ <CollectionFileViewerAction />
+ </Provider>);
+
+ // then
+ expect(wrapper).not.toBeUndefined();
+
+ // and
+ expect(wrapper.find("a")).toHaveLength(0);
+ });
+
+ it('should show open in new tab when TrustAllContent=true', () => {
+ // given
+ let initialState = defaultStore;
+ initialState.auth.config.clusterConfig.Collections.TrustAllContent = true;
+ const store = mockStore(initialState);
+
+ // when
+ const wrapper = mount(<Provider store={store}>
+ <CollectionFileViewerAction />
+ </Provider>);
+
+ // then
+ expect(wrapper).not.toBeUndefined();
+
+ // and
+ expect(wrapper.find("a").prop("href"))
+ .toEqual(sanitizeToken(getInlineFileUrl(fileUrl,
+ initialState.auth.config.keepWebServiceUrl,
+ initialState.auth.config.keepWebInlineServiceUrl))
+ );
+ });
+
+ it('should show open in new tab when inline url is secure', () => {
+ // given
+ let initialState = defaultStore;
+ initialState.auth.config.keepWebInlineServiceUrl = secureKeepInlineUrl;
+ const store = mockStore(initialState);
+
+ // when
+ const wrapper = mount(<Provider store={store}>
+ <CollectionFileViewerAction />
+ </Provider>);
+
+ // then
+ expect(wrapper).not.toBeUndefined();
+
+ // and
+ expect(wrapper.find("a").prop("href"))
+ .toEqual(sanitizeToken(getInlineFileUrl(fileUrl,
+ initialState.auth.config.keepWebServiceUrl,
+ initialState.auth.config.keepWebInlineServiceUrl))
+ );
+ });
+});
import { FileViewerAction } from 'views-components/context-menu/actions/file-viewer-action';
import { getNodeValue } from "models/tree";
import { ContextMenuKind } from 'views-components/context-menu/context-menu';
-import { getInlineFileUrl, sanitizeToken } from "./helpers";
+import { getInlineFileUrl, sanitizeToken, isInlineFileUrlSafe } from "./helpers";
const mapStateToProps = (state: RootState) => {
const { resource } = state.contextMenu;
ContextMenuKind.COLLECTION_DIRECTORY_ITEM,
ContextMenuKind.READONLY_COLLECTION_DIRECTORY_ITEM ].indexOf(resource.menuKind as ContextMenuKind) > -1) {
const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
- if (file) {
+ const shouldShowInlineUrl = isInlineFileUrlSafe(
+ file ? file.url : "",
+ state.auth.config.keepWebServiceUrl,
+ state.auth.config.keepWebInlineServiceUrl
+ ) || state.auth.config.clusterConfig.Collections.TrustAllContent;
+ if (file && shouldShowInlineUrl) {
const fileUrl = sanitizeToken(getInlineFileUrl(
file.url,
state.auth.config.keepWebServiceUrl,
inlineUrl = inlineUrl.replace(`/c=${collMatch[1]}`, '');
}
return inlineUrl;
-};
\ No newline at end of file
+};
+
+export const isInlineFileUrlSafe = (url: string, keepWebSvcUrl: string, keepWebInlineSvcUrl: string): boolean => {
+ let inlineUrl = keepWebInlineSvcUrl !== ""
+ ? url.replace(keepWebSvcUrl, keepWebInlineSvcUrl)
+ : url;
+ return inlineUrl.indexOf('*.') > -1;
+}
return ['Details', 'Versions'];
}
- getDetails(tabNumber: number) {
- switch (tabNumber) {
+ getDetails({tabNr}) {
+ switch (tabNr) {
case 0:
return this.getCollectionInfo();
case 1:
import React from 'react';
import { DetailsResource } from "models/details";
+interface GetDetailsParams {
+ tabNr?: number
+ showPreview?: boolean
+}
+
export abstract class DetailsData<T extends DetailsResource = DetailsResource> {
constructor(protected item: T) { }
}
abstract getIcon(className?: string): React.ReactElement<any>;
- abstract getDetails(tabNr?: number): React.ReactElement<any>;
+ abstract getDetails({tabNr, showPreview}: GetDetailsParams): React.ReactElement<any>;
}
import { EmptyDetails } from "./empty-details";
import { DetailsData } from "./details-data";
import { DetailsResource } from "models/details";
+import { Config } from 'common/config';
+import { isInlineFileUrlSafe } from "../context-menu/actions/helpers";
import { getResource } from 'store/resources/resources';
import { toggleDetailsPanel, SLIDE_TIMEOUT, openDetailsPanel } from 'store/details-panel/details-panel-action';
import { FileDetails } from 'views-components/details-panel/file-details';
}
};
-const mapStateToProps = ({ detailsPanel, resources, collectionPanelFiles }: RootState) => {
+const mapStateToProps = ({ auth, detailsPanel, resources, collectionPanelFiles }: RootState) => {
const resource = getResource(detailsPanel.resourceUuid)(resources) as DetailsResource | undefined;
const file = resource
? undefined
: getNode(detailsPanel.resourceUuid)(collectionPanelFiles);
return {
+ authConfig: auth.config,
isOpened: detailsPanel.isOpened,
tabNr: detailsPanel.tabNr,
res: resource || (file && file.value) || EMPTY_RESOURCE,
export interface DetailsPanelDataProps {
onCloseDrawer: () => void;
setActiveTab: (tabNr: number) => void;
+ authConfig: Config;
isOpened: boolean;
tabNr: number;
res: DetailsResource;
}
renderContent() {
- const { classes, onCloseDrawer, res, tabNr } = this.props;
+ const { classes, onCloseDrawer, res, tabNr, authConfig } = this.props;
+
+ let shouldShowInlinePreview = false;
+ if (!('kind' in res)) {
+ shouldShowInlinePreview = isInlineFileUrlSafe(
+ res ? res.url : "",
+ authConfig.keepWebServiceUrl,
+ authConfig.keepWebInlineServiceUrl
+ ) || authConfig.clusterConfig.Collections.TrustAllContent;
+ }
+
const item = getItem(res);
return <Grid
container
</Tabs>
</Grid>
<Grid item xs className={this.props.classes.tabContainer} >
- {item.getDetails(tabNr)}
+ {item.getDetails({tabNr, showPreview: shouldShowInlinePreview})}
</Grid>
</Grid >;
}
return <Icon className={className} />;
}
- getDetails() {
+ getDetails({showPreview}) {
const { item } = this;
return item.type === CollectionFileType.FILE
? <>
<DetailsAttribute label='Size' value={formatFileSize(item.size)} />
{
- isImage(item.url) && <>
+ isImage(item.url) && showPreview && <>
<DetailsAttribute label='Preview' />
<FileThumbnail file={item} />
</>
CollectionVersioning: true
PreserveVersionIfIdle: -1s
BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
- TrustAllContent: true
+ TrustAllContent: false
ForwardSlashNameSubstitution: /
ManagedProperties:
original_owner_uuid: {Function: original_owner, Protected: true}