diff --git a/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/services/DatastreamPermissionUtil.java b/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/services/DatastreamPermissionUtil.java index ff69854f65..81a72d3da0 100644 --- a/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/services/DatastreamPermissionUtil.java +++ b/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/services/DatastreamPermissionUtil.java @@ -30,8 +30,8 @@ public class DatastreamPermissionUtil { DS_PERMISSION_MAP.put(DatastreamType.ORIGINAL_FILE, Permission.viewOriginal); DS_PERMISSION_MAP.put(DatastreamType.TECHNICAL_METADATA, Permission.viewHidden); DS_PERMISSION_MAP.put(DatastreamType.TECHNICAL_METADATA_HISTORY, Permission.viewHidden); - DS_PERMISSION_MAP.put(DatastreamType.THUMBNAIL_SMALL, Permission.viewMetadata); - DS_PERMISSION_MAP.put(DatastreamType.THUMBNAIL_LARGE, Permission.viewMetadata); + DS_PERMISSION_MAP.put(DatastreamType.THUMBNAIL_SMALL, Permission.viewAccessCopies); + DS_PERMISSION_MAP.put(DatastreamType.THUMBNAIL_LARGE, Permission.viewAccessCopies); } private DatastreamPermissionUtil() { diff --git a/static/js/vue-cdr-access/src/components/full_record/fileList.vue b/static/js/vue-cdr-access/src/components/full_record/fileList.vue index d9ee7435f5..7cdfc09f17 100644 --- a/static/js/vue-cdr-access/src/components/full_record/fileList.vue +++ b/static/js/vue-cdr-access/src/components/full_record/fileList.vue @@ -135,7 +135,7 @@ export default { render: (data, type, row) => { let img; - if ('thumbnail_url' in row) { + if ('thumbnail_url' in row && this.hasPermission(row,'viewAccessCopies')) { const thumbnail_title = this.$t('full_record.thumbnail_title', { title: row.title }) img = `${thumbnail_title}`; diff --git a/static/js/vue-cdr-access/src/components/full_record/thumbnail.vue b/static/js/vue-cdr-access/src/components/full_record/thumbnail.vue index 27c8718719..8ec1150b82 100644 --- a/static/js/vue-cdr-access/src/components/full_record/thumbnail.vue +++ b/static/js/vue-cdr-access/src/components/full_record/thumbnail.vue @@ -105,7 +105,7 @@ export default { }, src() { - if (this.objectData.thumbnail_url !== undefined) { + if (this.objectData.thumbnail_url !== undefined && this.hasPermission(this.objectData, 'viewAccessCopies')) { return this.objectData.thumbnail_url; } diff --git a/static/js/vue-cdr-access/tests/unit/thumbnail.spec.js b/static/js/vue-cdr-access/tests/unit/thumbnail.spec.js index 0d1f4f0f24..313c46f57f 100644 --- a/static/js/vue-cdr-access/tests/unit/thumbnail.spec.js +++ b/static/js/vue-cdr-access/tests/unit/thumbnail.spec.js @@ -128,12 +128,21 @@ describe('thumbnail.vue', () => { }); }); - it('displays a thumbnail, if present', () => { + it('displays a thumbnail, if present and user has viewAccessCopies permissions', () => { expect(wrapper.find('.thumbnail .thumbnail-viewer').exists()).toBe(true); + expect(wrapper.find('i.placeholder').exists()).toBe(false); expect(wrapper.find('a').attributes('class')) .toEqual('thumbnail thumbnail-size-large has_tooltip') }); + it('does not display a thumbnail if user does not have viewAccessCopies permissions', async () => { + let updatedRecordData = cloneDeep(recordData); + updatedRecordData.briefObject.permissions = ['viewMetadata']; + await wrapper.setProps({ thumbnailData: updatedRecordData }); + expect(wrapper.find('i.placeholder').exists()).toBe(true); + expect(wrapper.find('.thumbnail .thumbnail-viewer').exists()).toBe(false); + }); + it('displays a placeholder, if no thumbnail', async () => { let updatedRecordData = cloneDeep(recordData); updatedRecordData.briefObject.thumbnail_url = undefined; diff --git a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/AccessCopiesService.java b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/AccessCopiesService.java index 235ee9504c..c96eb0a3b0 100644 --- a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/AccessCopiesService.java +++ b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/AccessCopiesService.java @@ -130,12 +130,17 @@ public String getDownloadUrl(ContentObjectRecord contentObjectRecord, AccessGrou */ public String getThumbnailId(ContentObjectRecord contentObjectRecord, AccessGroupSet principals, boolean checkChildren) { + if (!permissionsHelper.hasThumbnailAccess(principals, contentObjectRecord)) { + return null; + } + // Find thumbnail datastream recorded directly on the object, if present var thumbId = DatastreamUtil.getThumbnailOwnerId(contentObjectRecord); if (thumbId != null) { log.debug("Found thumbnail object directly assigned to object {}", thumbId); return thumbId; } + // Don't need to check any further if object isn't a work or doesn't contain files with thumbnails if (!ResourceType.Work.name().equals(contentObjectRecord.getResourceType()) || contentObjectRecord.getFileFormatCategory() == null diff --git a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/PermissionsHelper.java b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/PermissionsHelper.java index 29218f80b4..5f1b3ba2c5 100644 --- a/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/PermissionsHelper.java +++ b/web-common/src/main/java/edu/unc/lib/boxc/web/common/services/PermissionsHelper.java @@ -8,6 +8,7 @@ import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY; import static edu.unc.lib.boxc.model.api.DatastreamType.MD_DESCRIPTIVE; import static edu.unc.lib.boxc.model.api.DatastreamType.ORIGINAL_FILE; +import static edu.unc.lib.boxc.model.api.DatastreamType.THUMBNAIL_LARGE; import static edu.unc.lib.boxc.model.api.DatastreamType.THUMBNAIL_SMALL; import static org.springframework.util.Assert.notNull; @@ -57,7 +58,8 @@ public boolean hasOriginalAccess(AccessGroupSet principals, ContentObjectRecord * @return */ public boolean hasThumbnailAccess(AccessGroupSet principals, ContentObjectRecord metadata) { - return hasDatastreamAccess(principals, THUMBNAIL_SMALL, metadata); + return hasThumbnailPreviewAccess(principals, THUMBNAIL_SMALL, metadata) || + hasThumbnailPreviewAccess(principals, THUMBNAIL_LARGE, metadata); } /** @@ -107,6 +109,16 @@ public boolean hasDatastreamAccess(AccessGroupSet principals, DatastreamType dat return accessControlService.hasAccess(metadata.getPid(), principals, permission); } + public boolean hasThumbnailPreviewAccess(AccessGroupSet principals, DatastreamType datastream, + ContentObjectRecord metadata) { + notNull(principals, "Requires agent principals"); + notNull(datastream, "Requires datastream type"); + notNull(metadata, "Requires metadata object"); + + Permission permission = getPermissionForDatastream(datastream); + return accessControlService.hasAccess(metadata.getPid(), principals, permission); + } + private static boolean containsDatastream(ContentObjectRecord metadata, String datastream) { return metadata.getDatastreamObjects().stream() .anyMatch(d -> d.getName().equals(datastream)); diff --git a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/AccessCopiesServiceTest.java b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/AccessCopiesServiceTest.java index fc17173f61..772d6fcce9 100644 --- a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/AccessCopiesServiceTest.java +++ b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/AccessCopiesServiceTest.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.UUID; +import static edu.unc.lib.boxc.auth.api.Permission.viewAccessCopies; import static edu.unc.lib.boxc.auth.api.Permission.viewOriginal; import static edu.unc.lib.boxc.model.api.DatastreamType.ORIGINAL_FILE; import static edu.unc.lib.boxc.model.api.DatastreamType.TECHNICAL_METADATA; @@ -137,6 +138,7 @@ private ContentObjectSolrRecord createImgObject(ResourceType resourceType) { List imgDatastreams = Arrays.asList( ORIGINAL_FILE.getId() + "|image/png|file.png|png|766|urn:sha1:checksum|", DatastreamType.THUMBNAIL_LARGE.getId() + "|image/png|thumb|png|55||", + DatastreamType.THUMBNAIL_SMALL.getId() + "|image/png|thumb|png|15||", DatastreamType.JP2_ACCESS_COPY.getId() + "|image/jp2|thumb|jp2|555||"); mdObjectImg.setFileFormatCategory(Collections.singletonList(ContentCategory.image.getDisplayName())); mdObjectImg.setFileFormatType(Collections.singletonList("image/png")); @@ -236,6 +238,14 @@ public void primaryObjThumbnail() { assertEquals(mdObjectImg.getId(), accessCopiesService.getThumbnailId(mdObjectImg, principals, true)); } + @Test + public void primaryObjThumbnailMetadataOnlyPermissions() { + hasPermissions(mdObjectImg, true, false); + + assertNull(accessCopiesService.getThumbnailId(mdObjectImg, principals, false)); + assertNull(accessCopiesService.getThumbnailId(mdObjectImg, principals, true)); + } + @Test public void noPrimaryObjThumbnailMultipleFiles() { hasPermissions(noOriginalFileObj, true); @@ -253,6 +263,21 @@ public void noPrimaryObjThumbnailMultipleFiles() { assertSortType("default"); } + @Test + public void noPrimaryObjThumbnailMultipleFilesMetadataOnlyPermissions() { + hasPermissions(noOriginalFileObj, true, false); + hasPermissions(mdObjectXml, false); + hasPermissions(mdObjectImg, false); + noOriginalFileObj.setFileFormatCategory(Collections.singletonList(ContentCategory.image.getDisplayName())); + noOriginalFileObj.setFileFormatType(Collections.singletonList("png")); + populateResultList(mdObjectImg); + when(searchResultResponse.getResultCount()).thenReturn(2l); + + assertNull(accessCopiesService.getThumbnailId(noOriginalFileObj, principals, false)); + // Gets the ID of the specific child with a thumbnail + assertNull(accessCopiesService.getThumbnailId(noOriginalFileObj, principals, true)); + } + @Test public void noPrimaryObjNoThumbnail() { hasPermissions(noOriginalFileObj, true); @@ -273,7 +298,8 @@ public void getThumbnailIdNoPrimaryMultipleImages() { mdObjectImg2.setId(UUID.randomUUID().toString()); var imgDatastreams = Arrays.asList( ORIGINAL_FILE.getId() + "|image/jpg|file2.png|png|555|urn:sha1:checksum|", - DatastreamType.THUMBNAIL_LARGE.getId() + "|image/png|thumb|png|55||"); + DatastreamType.THUMBNAIL_LARGE.getId() + "|image/png|thumb|png|55||", + DatastreamType.THUMBNAIL_SMALL.getId() + "|image/png|thumb|png|15||"); mdObjectImg2.setFileFormatCategory(Collections.singletonList(ContentCategory.image.getDisplayName())); mdObjectImg2.setFileFormatType(Collections.singletonList("png")); mdObjectImg2.setDatastream(imgDatastreams); @@ -294,6 +320,33 @@ public void getThumbnailIdNoPrimaryMultipleImages() { assertSortType("default"); } + @Test + public void getThumbnailIdNoPrimaryMultipleImagesMetadataOnlyPermissions() { + var mdObjectImg2 = new ContentObjectSolrRecord(); + mdObjectImg2.setResourceType(ResourceType.File.name()); + mdObjectImg2.setId(UUID.randomUUID().toString()); + var imgDatastreams = Arrays.asList( + ORIGINAL_FILE.getId() + "|image/jpg|file2.png|png|555|urn:sha1:checksum|", + DatastreamType.THUMBNAIL_LARGE.getId() + "|image/png|thumb|png|55||", + DatastreamType.THUMBNAIL_SMALL.getId() + "|image/png|thumb|png|15||"); + mdObjectImg2.setFileFormatCategory(Collections.singletonList(ContentCategory.image.getDisplayName())); + mdObjectImg2.setFileFormatType(Collections.singletonList("png")); + mdObjectImg2.setDatastream(imgDatastreams); + + hasPermissions(noOriginalFileObj, true, false); + hasPermissions(mdObjectImg2, true, false); + hasPermissions(mdObjectImg, true, false); + noOriginalFileObj.setFileFormatCategory(Collections.singletonList(ContentCategory.image.getDisplayName())); + noOriginalFileObj.setFileFormatType(Collections.singletonList("png")); + populateResultList(mdObjectImg2); + when(searchResultResponse.getResultCount()).thenReturn(2l); + + assertNull(accessCopiesService.getThumbnailId(noOriginalFileObj, principals, false)); + + // Gets the ID of the specific child with a thumbnail + assertNull(accessCopiesService.getThumbnailId(noOriginalFileObj, principals, true)); + } + private void assertRequestedDatastreamFilter(DatastreamType expectedType) { var searchState = searchRequestCaptor.getValue().getSearchState(); var queryFilter = (NamedDatastreamFilter) searchState.getFilters().get(0); @@ -366,6 +419,15 @@ public void hasViewableFilesImageWorkTest() { private void hasPermissions(ContentObjectSolrRecord contentObject, boolean hasAccess) { when(accessControlService.hasAccess(contentObject.getPid(), principals, viewOriginal)).thenReturn(hasAccess); + when(accessControlService.hasAccess(contentObject.getPid(), principals, viewAccessCopies)).thenReturn(hasAccess); + } + + private void hasPermissions(ContentObjectSolrRecord contentObject, boolean hasAccessOriginal, + boolean hasAccessThumb) { + when(accessControlService.hasAccess(contentObject.getPid(), principals, viewOriginal)) + .thenReturn(hasAccessOriginal); + when(accessControlService.hasAccess(contentObject.getPid(), principals, viewAccessCopies)) + .thenReturn(hasAccessThumb); } private void populateResultList(ContentObjectRecord... objects) { diff --git a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/PermissionsHelperTest.java b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/PermissionsHelperTest.java index 68ec7c9622..9ca62e8454 100644 --- a/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/PermissionsHelperTest.java +++ b/web-common/src/test/java/edu/unc/lib/boxc/web/common/services/PermissionsHelperTest.java @@ -6,6 +6,8 @@ import static edu.unc.lib.boxc.auth.api.Permission.viewOriginal; import static edu.unc.lib.boxc.model.api.DatastreamType.JP2_ACCESS_COPY; import static edu.unc.lib.boxc.model.api.DatastreamType.ORIGINAL_FILE; +import static edu.unc.lib.boxc.model.api.DatastreamType.THUMBNAIL_LARGE; +import static edu.unc.lib.boxc.model.api.DatastreamType.THUMBNAIL_SMALL; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Matchers.any; @@ -61,7 +63,9 @@ public void init() { mdObject.setRoleGroup(roleGroups); List datastreams = Arrays.asList( ORIGINAL_FILE.getId() + "|application/pdf|file.pdf|pdf|766|urn:sha1:checksum|", - JP2_ACCESS_COPY.getId() + "|application/jp2|file.jp2|jp2|884||"); + JP2_ACCESS_COPY.getId() + "|application/jp2|file.jp2|jp2|884||", + THUMBNAIL_LARGE.getId() + "|image/png|thumb|png|55||", + THUMBNAIL_SMALL.getId() + "|image/png|thumb|png|15||"); mdObject.setDatastream(datastreams); principals = new AccessGroupSetImpl("group"); @@ -110,6 +114,20 @@ public void testPermitDerivativeAccess() { assertTrue(helper.hasDatastreamAccess(principals, JP2_ACCESS_COPY, mdObject)); } + @Test + public void testAllowsThumbnailAccess() { + assignPermission(viewAccessCopies, true); + + assertTrue(helper.hasThumbnailAccess(principals, mdObject), "Thumbnail should have full patron access"); + } + + @Test + public void testDisAllowsThumbnailAccess() { + assignPermission(viewMetadata, true); + + assertFalse(helper.hasThumbnailAccess(principals, mdObject), "Thumbnail must not have full patron access"); + } + @Test public void testDenyOriginalAccess() { assignPermission(viewOriginal, false);