Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ private void validateInput(final QueryContext context, final GetPresignedUploadU
if (scenario == UploadDownloadScenario.ASSET_DOCUMENTATION) {
validateInputForAssetDocumentationScenario(context, input);
}

if (scenario == UploadDownloadScenario.ASSET_DOCUMENTATION_LINKS) {
validateInputForAssetDocumentationScenario(context, input);
}
Comment on lines +85 to +87
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay actually i do think we have slightly different privileges for adding links - we have the LinkUtils.isAuthorizedToUpdateLinks check in AddLinkResolver and also the GlossaryUtils.canUpdateGlossaryEntity check. so I suppose we should be consistent here

}

private void validateInputForAssetDocumentationScenario(
Expand Down Expand Up @@ -119,8 +123,12 @@ private String getS3Key(

if (scenario == UploadDownloadScenario.ASSET_DOCUMENTATION) {
return String.format("%s/%s", s3Configuration.getAssetPathPrefix(), fileId);
} else {
throw new IllegalArgumentException("Unsupported upload scenario: " + scenario);
}

if (scenario == UploadDownloadScenario.ASSET_DOCUMENTATION_LINKS) {
return String.format("%s/%s", s3Configuration.getAssetPathPrefix(), fileId);
}

throw new IllegalArgumentException("Unsupported upload scenario: " + scenario);
}
}
5 changes: 5 additions & 0 deletions datahub-graphql-core/src/main/resources/files.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ enum UploadDownloadScenario {
Upload for asset documentation.
"""
ASSET_DOCUMENTATION

"""
Upload for asset documentation links.
"""
ASSET_DOCUMENTATION_LINKS
}

type GetPresignedUploadUrlResponse {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,4 +768,179 @@ public void testGetPresignedUploadUrlWhenBothAssetUrnAndSchemaFieldUrnAreProvide
assertEquals(result.getFileId(), extractedFileId);
assertTrue(result.getFileId().contains(testFileName));
}

@Test
public void testGetPresignedUploadUrlWithAssetDocumentationLinksScenario() throws Exception {
String testFileName = "my_test_file.pdf";
GetPresignedUploadUrlInput input =
createInput(
UploadDownloadScenario.ASSET_DOCUMENTATION_LINKS,
TEST_ASSET_URN,
null,
TEST_CONTENT_TYPE,
testFileName);

when(mockEnv.getArgument("input")).thenReturn(input);
when(mockEnv.getContext()).thenReturn(mockQueryContext);

ArgumentCaptor<String> s3KeyCaptor = ArgumentCaptor.forClass(String.class);
when(mockS3Util.generatePresignedUploadUrl(
eq(TEST_BUCKET_NAME),
s3KeyCaptor.capture(),
eq(TEST_EXPIRATION_SECONDS),
eq(TEST_CONTENT_TYPE)))
.thenReturn(MOCKED_PRESIGNED_URL);

when(mockS3Configuration.getBucketName()).thenReturn(TEST_BUCKET_NAME);
when(mockS3Configuration.getPresignedUploadUrlExpirationSeconds())
.thenReturn(TEST_EXPIRATION_SECONDS);
when(mockS3Configuration.getAssetPathPrefix()).thenReturn(TEST_ASSET_PATH_PREFIX);

GetPresignedUploadUrlResolver resolver =
new GetPresignedUploadUrlResolver(mockS3Util, mockS3Configuration);
CompletableFuture<GetPresignedUploadUrlResponse> future = resolver.get(mockEnv);
GetPresignedUploadUrlResponse result = future.get();

assertNotNull(result);
assertEquals(result.getUrl(), MOCKED_PRESIGNED_URL);
assertNotNull(result.getFileId());

// Verify that asset documentation authorization is called for ASSET_DOCUMENTATION_LINKS
// scenario
descriptionUtilsMockedStatic.verify(
() ->
DescriptionUtils.isAuthorizedToUpdateDescription(
any(QueryContext.class), any(Urn.class)));

String capturedS3Key = s3KeyCaptor.getValue();
assertTrue(capturedS3Key.startsWith(TEST_ASSET_PATH_PREFIX + "/"));

// Extract fileId from s3Key
String expectedFileIdPrefix = TEST_ASSET_PATH_PREFIX + "/";
String extractedFileId = capturedS3Key.substring(expectedFileIdPrefix.length());

assertEquals(result.getFileId(), extractedFileId);
assertTrue(result.getFileId().contains(testFileName));
}

@Test
public void testGetPresignedUploadUrlWithAssetDocumentationLinksScenarioAndSchemaField()
throws Exception {
String testFileName = "my_test_file.pdf";
String schemaFieldUrn =
"urn:li:schemaField:(urn:li:dataset:(urn:li:dataPlatform:hive,my-dataset,PROD),myField)";
GetPresignedUploadUrlInput input =
createInput(
UploadDownloadScenario.ASSET_DOCUMENTATION_LINKS,
TEST_ASSET_URN,
schemaFieldUrn,
TEST_CONTENT_TYPE,
testFileName);

when(mockEnv.getArgument("input")).thenReturn(input);
when(mockEnv.getContext()).thenReturn(mockQueryContext);

ArgumentCaptor<String> s3KeyCaptor = ArgumentCaptor.forClass(String.class);
when(mockS3Util.generatePresignedUploadUrl(
eq(TEST_BUCKET_NAME),
s3KeyCaptor.capture(),
eq(TEST_EXPIRATION_SECONDS),
eq(TEST_CONTENT_TYPE)))
.thenReturn(MOCKED_PRESIGNED_URL);

when(mockS3Configuration.getBucketName()).thenReturn(TEST_BUCKET_NAME);
when(mockS3Configuration.getPresignedUploadUrlExpirationSeconds())
.thenReturn(TEST_EXPIRATION_SECONDS);
when(mockS3Configuration.getAssetPathPrefix()).thenReturn(TEST_ASSET_PATH_PREFIX);

GetPresignedUploadUrlResolver resolver =
new GetPresignedUploadUrlResolver(mockS3Util, mockS3Configuration);
CompletableFuture<GetPresignedUploadUrlResponse> future = resolver.get(mockEnv);
GetPresignedUploadUrlResponse result = future.get();

assertNotNull(result);
assertEquals(result.getUrl(), MOCKED_PRESIGNED_URL);
assertNotNull(result.getFileId());

// Verify that schema field documentation authorization is called for ASSET_DOCUMENTATION_LINKS
// scenario
// when schema field URN is provided
descriptionUtilsMockedStatic.verify(
() ->
DescriptionUtils.isAuthorizedToUpdateFieldDescription(
any(QueryContext.class), any(Urn.class)));

String capturedS3Key = s3KeyCaptor.getValue();
assertTrue(capturedS3Key.startsWith(TEST_ASSET_PATH_PREFIX + "/"));

// Extract fileId from s3Key
String expectedFileIdPrefix = TEST_ASSET_PATH_PREFIX + "/";
String extractedFileId = capturedS3Key.substring(expectedFileIdPrefix.length());

assertEquals(result.getFileId(), extractedFileId);
assertTrue(result.getFileId().contains(testFileName));
}

@Test
public void testGetPresignedUploadUrlWithNullAssetUrnForAssetDocumentationLinks()
throws Exception {
GetPresignedUploadUrlInput input =
createInput(
UploadDownloadScenario.ASSET_DOCUMENTATION_LINKS,
null,
null,
TEST_CONTENT_TYPE,
"test.png");

when(mockEnv.getArgument("input")).thenReturn(input);
when(mockEnv.getContext()).thenReturn(mockQueryContext);

when(mockS3Configuration.getBucketName()).thenReturn(TEST_BUCKET_NAME);
when(mockS3Configuration.getPresignedUploadUrlExpirationSeconds())
.thenReturn(TEST_EXPIRATION_SECONDS);
when(mockS3Configuration.getAssetPathPrefix()).thenReturn(TEST_ASSET_PATH_PREFIX);

GetPresignedUploadUrlResolver resolver =
new GetPresignedUploadUrlResolver(mockS3Util, mockS3Configuration);
assertThrows(
"assetUrn is required for ASSET_DOCUMENTATION scenario",
IllegalArgumentException.class,
() -> resolver.get(mockEnv).get());
}

@Test
public void testGetPresignedUploadUrlWithAssetDocumentationLinksScenarioAuthorizationFailure()
throws Exception {
String testFileName = "my_test_file.pdf";
GetPresignedUploadUrlInput input =
createInput(
UploadDownloadScenario.ASSET_DOCUMENTATION_LINKS,
TEST_ASSET_URN,
null,
TEST_CONTENT_TYPE,
testFileName);

when(mockEnv.getArgument("input")).thenReturn(input);
when(mockEnv.getContext()).thenReturn(mockQueryContext);

// Mock asset description authorization to return false
descriptionUtilsMockedStatic
.when(
() ->
DescriptionUtils.isAuthorizedToUpdateDescription(
any(QueryContext.class), any(Urn.class)))
.thenReturn(false);

when(mockS3Configuration.getBucketName()).thenReturn(TEST_BUCKET_NAME);
when(mockS3Configuration.getPresignedUploadUrlExpirationSeconds())
.thenReturn(TEST_EXPIRATION_SECONDS);
when(mockS3Configuration.getAssetPathPrefix()).thenReturn(TEST_ASSET_PATH_PREFIX);

GetPresignedUploadUrlResolver resolver =
new GetPresignedUploadUrlResolver(mockS3Util, mockS3Configuration);
assertThrows(
"Unauthorized to edit documentation for asset: " + TEST_ASSET_URN,
AuthorizationException.class,
() -> resolver.get(mockEnv).get());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { NodeViewComponentProps } from '@remirror/react';
import { Typography } from 'antd';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import styled from 'styled-components';

Expand All @@ -11,19 +10,12 @@ import {
FileNodeAttributes,
TEXT_FILE_TYPES_TO_PREVIEW,
getExtensionFromFileName,
getFileIconFromExtension,
getFileTypeFromFilename,
handleFileDownload,
} from '@components/components/Editor/extensions/fileDragDrop/fileUtils';
import { Icon } from '@components/components/Icon';
import { FileNode } from '@components/components/FileNode/FileNode';
import { colors } from '@components/theme';

import Loading from '@app/shared/Loading';

const StyledIcon = styled(Icon)`
flex-shrink: 0;
`;

const FileContainer = styled.div<{ $isInline?: boolean }>`
display: inline-block;

Expand All @@ -47,21 +39,8 @@ const FileContainer = styled.div<{ $isInline?: boolean }>`
color: ${({ theme }) => theme.styles['primary-color']};
`;

const FileDetails = styled.span`
const StyledFileNode = styled(FileNode)`
max-width: 350px;
display: flex;
gap: 4px;
align-items: center;
font-weight: 600;
width: max-content;
padding: 4px;
`;

const FileName = styled(Typography.Text)`
color: ${({ theme }) => theme.styles['primary-color']};
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;

const StyledSyntaxHighlighter = styled(SyntaxHighlighter)`
Expand Down Expand Up @@ -127,7 +106,6 @@ export const FileNodeView: React.FC<FileNodeViewProps> = ({ node, onFileDownload
const { url, name, type, size, id } = node.attrs;
const extension = getExtensionFromFileName(name);
const fileType = type || getFileTypeFromFilename(name);
const icon = getFileIconFromExtension(extension || '');
const shouldWrap = extension === 'txt';
const isPdf = fileType === 'application/pdf';
const isVideo = fileType.startsWith('video/');
Expand Down Expand Up @@ -184,39 +162,32 @@ export const FileNodeView: React.FC<FileNodeViewProps> = ({ node, onFileDownload
}
}, [url, hasError, shouldShowPreview, isTextFile]);

const clickHandler = useCallback(() => {
onFileDownloadView?.(fileType, size);
handleFileDownload(url, name);
}, [fileType, size, url, name, onFileDownloadView]);

// Show loading state if no URL yet (file is being uploaded)
if (!url) {
return (
<FileContainer {...containerProps} $isInline>
<FileDetails>
<Loading height={18} width={20} marginTop={0} />
<FileName>Uploading {name}...</FileName>
</FileDetails>
<StyledFileNode fileName={name} loading />
</FileContainer>
);
}

const fileNode = (
<FileDetails
onClick={(e) => {
e.stopPropagation();
// Track file download/view event
onFileDownloadView?.(fileType, size);
handleFileDownload(url, name);
}}
>
<StyledIcon icon={icon} size="lg" source="phosphor" />
<FileName ellipsis={{ tooltip: name }}>{name}</FileName>
</FileDetails>
);

const fileNodeWithButton = (
<FileNameButtonWrapper>
{fileNode}
<Button
icon={{ source: 'phosphor', icon: isPreviewVisible ? 'CaretDown' : 'CaretRight' }}
variant="text"
onClick={() => setIsPreviewVisible(!isPreviewVisible)}
<StyledFileNode
fileName={name}
onClick={clickHandler}
extraRightContent={
<Button
icon={{ source: 'phosphor', icon: isPreviewVisible ? 'CaretDown' : 'CaretRight' }}
variant="text"
onClick={() => setIsPreviewVisible(!isPreviewVisible)}
/>
}
/>
</FileNameButtonWrapper>
);
Expand Down Expand Up @@ -284,7 +255,7 @@ export const FileNodeView: React.FC<FileNodeViewProps> = ({ node, onFileDownload
// Other files
return (
<FileContainer {...containerProps} $isInline>
{fileNode}
<StyledFileNode fileName={name} onClick={clickHandler} />
</FileContainer>
);
};
Loading
Loading