From 4b6d92d1134457ea49924d98e35e922e6c417e5a Mon Sep 17 00:00:00 2001 From: Ben Pennell Date: Fri, 12 Jan 2024 08:01:40 -0500 Subject: [PATCH] BXC-4216 - Patron Role/Permission for accessing reduced quality images (#1653) * Add viewReducedResolutionImages permission and canViewReducedQuality role. Remove unused methods. Add test coverage. Refactor roles to inherit permissions from each other, since almost all of them build off another role * Remove unused PermissionHelper methods. Add test coverage for still used methods * Shorten name * Require new permission for image downloads below full resolution * Only show reduced resolution image downloads in dropdown when viewReducedResImages is granted, and only show full size and original when viewOriginals granted. Update tests to include new permission * Allow new role to be added in admin ui * Add new permission to list of permitted patron roles/permissions, add CdrAcl property. Add additional tests to verify new permission works during evaluation and setting * For file record pages, if the user can access any download options (low res or original) then the download button will appear instead of the restricted content block. Add additional restrictedContent tests to verify this, check the dropdown has the right options, and some refactoring to DRY up code * Fix dropdown references now that there is an extra option. Add an extra test to verify new permission works in patron settings * Disable enum naming rule * Fix that collections were disallowing canViewReducedQuality. Reduce number of templates in restrictedContent.vue based on feedback --- .codeclimate.yml | 4 + .../edu/unc/lib/boxc/auth/api/Permission.java | 15 +- .../edu/unc/lib/boxc/auth/api/UserRole.java | 99 ++--- .../unc/lib/boxc/auth/api/UserRoleTest.java | 141 +++++++ ...ntentObjectAccessRestrictionValidator.java | 3 + .../InheritedPermissionEvaluator.java | 1 + ...tObjectAccessRestrictionValidatorTest.java | 12 +- .../InheritedPermissionEvaluatorTest.java | 15 + .../unc/lib/boxc/model/api/rdf/CdrAcl.java | 7 + .../PatronAccessAssignmentRouterTest.java | 23 ++ .../src/components/patronRoles.vue | 1 + .../src/mixins/patronHelpers.js | 1 + .../tests/unit/patronRoles.spec.js | 35 +- .../full_record/aggregateRecord.vue | 2 +- .../full_record/restrictedContent.vue | 14 +- .../src/mixins/fileDownloadUtils.js | 16 +- .../src/mixins/fullRecordUtils.js | 9 +- .../src/mixins/permissionUtils.js | 12 + .../tests/unit/adminUnit.spec.js | 1 + .../tests/unit/aggregateRecord.spec.js | 5 + .../tests/unit/analyticsUtils.spec.js | 1 + .../tests/unit/collectionFolder.spec.js | 1 + .../tests/unit/fileList.spec.js | 2 + .../tests/unit/fileRecord.spec.js | 3 + .../tests/unit/permissionUtils.spec.js | 24 ++ .../tests/unit/restrictedContent.spec.js | 362 +++++------------- .../tests/unit/thumbnail.spec.js | 1 + .../common/services/PermissionsHelper.java | 93 +---- .../services/PermissionsHelperTest.java | 95 ++--- .../rest/DownloadImageController.java | 2 +- .../rest/DownloadImageControllerIT.java | 6 +- 31 files changed, 460 insertions(+), 546 deletions(-) create mode 100644 auth-api/src/test/java/edu/unc/lib/boxc/auth/api/UserRoleTest.java diff --git a/.codeclimate.yml b/.codeclimate.yml index eedcc142ae..2e2e99007c 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -50,6 +50,10 @@ plugins: file: "common-utils/src/main/resources/checkstyle/checkstyle.xml" sonar-java: enabled: true + checks: + # Disable enforcement of rule that enum values must be all caps, since we have many that don't adhere + java:S115: + enabled: false pmd: enabled: true # duplication plugin affects similar-code and identical-code diff --git a/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/Permission.java b/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/Permission.java index d34b32fa00..eea6700253 100644 --- a/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/Permission.java +++ b/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/Permission.java @@ -9,8 +9,9 @@ public enum Permission { viewMetadata, viewAccessCopies, + viewReducedResImages, viewOriginal, - // TODO replaces viewAdminUI and viewEmbargoed + // Staff Permissions viewHidden, editDescription, bulkUpdateDescription, @@ -28,16 +29,4 @@ public enum Permission { editResourceType, runEnhancements, reindex; - - private Permission() { - } - - public static Permission getPermission(String permissionName) { - for (Permission permission: Permission.values()) { - if (permission.name().equals(permissionName)) { - return permission; - } - } - return null; - } } \ No newline at end of file diff --git a/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/UserRole.java b/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/UserRole.java index 92d403464b..6f0bfd3b21 100644 --- a/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/UserRole.java +++ b/auth-api/src/main/java/edu/unc/lib/boxc/auth/api/UserRole.java @@ -3,10 +3,8 @@ import static edu.unc.lib.boxc.auth.api.Permission.assignStaffRoles; import static edu.unc.lib.boxc.auth.api.Permission.bulkUpdateDescription; import static edu.unc.lib.boxc.auth.api.Permission.changePatronAccess; -import static edu.unc.lib.boxc.auth.api.Permission.createAdminUnit; import static edu.unc.lib.boxc.auth.api.Permission.createCollection; import static edu.unc.lib.boxc.auth.api.Permission.destroy; -import static edu.unc.lib.boxc.auth.api.Permission.destroyUnit; import static edu.unc.lib.boxc.auth.api.Permission.editDescription; import static edu.unc.lib.boxc.auth.api.Permission.editResourceType; import static edu.unc.lib.boxc.auth.api.Permission.ingest; @@ -14,21 +12,17 @@ import static edu.unc.lib.boxc.auth.api.Permission.markForDeletionUnit; import static edu.unc.lib.boxc.auth.api.Permission.move; import static edu.unc.lib.boxc.auth.api.Permission.orderMembers; -import static edu.unc.lib.boxc.auth.api.Permission.reindex; -import static edu.unc.lib.boxc.auth.api.Permission.runEnhancements; import static edu.unc.lib.boxc.auth.api.Permission.viewAccessCopies; import static edu.unc.lib.boxc.auth.api.Permission.viewHidden; import static edu.unc.lib.boxc.auth.api.Permission.viewMetadata; import static edu.unc.lib.boxc.auth.api.Permission.viewOriginal; +import static edu.unc.lib.boxc.auth.api.Permission.viewReducedResImages; import static java.util.Arrays.asList; import static java.util.stream.Collectors.toSet; import static org.apache.jena.rdf.model.ResourceFactory.createProperty; -import java.net.URI; -import java.net.URISyntaxException; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.EnumMap; import java.util.HashSet; import java.util.List; @@ -48,40 +42,35 @@ * */ public enum UserRole { - list("list", new Permission[] {}), // Patron roles - none("none", false), - canDiscover("canDiscover", false), - canViewMetadata("canViewMetadata", false, viewMetadata), - canViewAccessCopies("canViewAccessCopies", false, viewMetadata, viewAccessCopies), - canViewOriginals("canViewOriginals", false, viewMetadata, viewAccessCopies, viewOriginal), + none("none", false, null), + canDiscover("canDiscover", false, null), + canViewMetadata("canViewMetadata", false, canDiscover, viewMetadata), + canViewAccessCopies("canViewAccessCopies", false, canViewMetadata, viewAccessCopies), + canViewReducedQuality("canViewReducedQuality", false, canViewAccessCopies, + viewReducedResImages), + canViewOriginals("canViewOriginals", false, canViewReducedQuality, viewOriginal), // Staff roles - canAccess("canAccess", true, viewHidden, viewMetadata, viewAccessCopies, viewOriginal), - canIngest("canIngest", true, viewHidden, viewMetadata, viewAccessCopies, viewOriginal, - ingest), - canDescribe("canDescribe", true, viewHidden, viewMetadata, viewAccessCopies, viewOriginal, - editDescription, bulkUpdateDescription), - canProcess("canProcess", true, viewHidden, viewMetadata, viewAccessCopies, viewOriginal, - editDescription, bulkUpdateDescription, move, orderMembers, markForDeletion, changePatronAccess), - canManage("canManage", true, viewHidden, viewMetadata, viewAccessCopies, viewOriginal, - ingest, editDescription, bulkUpdateDescription, move, orderMembers, markForDeletion, - changePatronAccess, editResourceType, createCollection), - unitOwner("unitOwner", true, viewHidden, viewMetadata, viewAccessCopies, viewOriginal, - ingest, editDescription, bulkUpdateDescription, move, orderMembers, markForDeletion, markForDeletionUnit, - changePatronAccess, editResourceType, destroy, createCollection, assignStaffRoles), - administrator("administrator", true, viewHidden, viewMetadata, viewAccessCopies, viewOriginal, - ingest, editDescription, bulkUpdateDescription, move, orderMembers, markForDeletion, markForDeletionUnit, - changePatronAccess, editResourceType, destroy, destroyUnit, createCollection, - createAdminUnit, assignStaffRoles, runEnhancements, reindex); + canAccess("canAccess", true, canViewOriginals, viewHidden), + canIngest("canIngest", true, canAccess, ingest), + canDescribe("canDescribe", true, canAccess, editDescription, bulkUpdateDescription), + canProcess("canProcess", true, canDescribe, + move, orderMembers, markForDeletion, changePatronAccess), + canManage("canManage", true, canProcess, + ingest, editResourceType, createCollection), + unitOwner("unitOwner", true, canManage, + markForDeletionUnit, destroy, assignStaffRoles), + // Admin role receives all permissions + administrator("administrator", true, null, Permission.values()); public static final List PATRON_ROLE_PRECEDENCE = asList( UserRole.none.getPropertyString(), UserRole.canViewMetadata.getPropertyString(), UserRole.canViewAccessCopies.getPropertyString(), + UserRole.canViewReducedQuality.getPropertyString(), UserRole.canViewOriginals.getPropertyString() ); - private URI uri; private String predicate; private String propertyString; private Property property; @@ -94,56 +83,22 @@ public enum UserRole { private static Map> permissionToRoles; - UserRole(String predicate, boolean isStaffRole, Permission... perms) { + UserRole(String predicate, boolean isStaffRole, UserRole inheritPermsFrom, Permission... perms) { this.predicate = predicate; this.propertyString = CdrAcl.getURI() + predicate; this.property = createProperty(propertyString); - this.uri = URI.create(propertyString); this.isStaffRole = isStaffRole; this.permissions = new HashSet<>(Arrays.asList(perms)); - this.permissionNames = permissions.stream().map(p -> p.name()).collect(toSet()); - } - - @Deprecated - UserRole(String predicate, Permission[] perms) { - try { - this.predicate = predicate; - this.uri = new URI(CdrAcl.getURI() + predicate); - this.propertyString = ""; - HashSet mypermissions = new HashSet<>(perms.length); - Collections.addAll(mypermissions, perms); - this.permissions = Collections.unmodifiableSet(mypermissions); - } catch (URISyntaxException e) { - Error x = new ExceptionInInitializerError("Cannot initialize ContentModelHelper"); - x.initCause(e); - throw x; + if (inheritPermsFrom != null) { + this.permissions.addAll(inheritPermsFrom.getPermissions()); } - } - - @Deprecated - public static boolean matchesMemberURI(String test) { - for (UserRole r : UserRole.values()) { - if (r.getURI().toString().equals(test)) { - return true; - } - } - return false; - } - - @Deprecated - public static UserRole getUserRole(String roleUri) { - for (UserRole r : UserRole.values()) { - if (r.propertyString.equals(roleUri)) { - return r; - } - } - return null; + this.permissionNames = permissions.stream().map(p -> p.name()).collect(toSet()); } /** * Return a list of all user roles which have the specified permission * - * @param permission + * @param inPermissions * @return */ public static Set getUserRoles(Collection inPermissions) { @@ -233,10 +188,6 @@ public Property getProperty() { return property; } - public URI getURI() { - return this.uri; - } - public Set getPermissions() { return permissions; } diff --git a/auth-api/src/test/java/edu/unc/lib/boxc/auth/api/UserRoleTest.java b/auth-api/src/test/java/edu/unc/lib/boxc/auth/api/UserRoleTest.java new file mode 100644 index 0000000000..4dab3526be --- /dev/null +++ b/auth-api/src/test/java/edu/unc/lib/boxc/auth/api/UserRoleTest.java @@ -0,0 +1,141 @@ +package edu.unc.lib.boxc.auth.api; + +import edu.unc.lib.boxc.model.api.rdf.CdrAcl; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author bbpennel + */ +public class UserRoleTest { + @Test + public void canViewReducedQualityPermissionsTest() { + var subject = UserRole.canViewReducedQuality; + var expectedPermissions = Set.of( + Permission.viewMetadata, Permission.viewAccessCopies, Permission.viewReducedResImages); + assertSetMatchesExactly(expectedPermissions, subject.getPermissions()); + } + + @Test + public void canViewReducedQualityPermissionNamesTest() { + var subject = UserRole.canViewReducedQuality; + var expectedNames = Set.of(Permission.viewMetadata.name(), Permission.viewAccessCopies.name(), + Permission.viewReducedResImages.name()); + assertSetMatchesExactly(expectedNames, subject.getPermissionNames()); + } + + @Test + public void administratorPermissionsTest() { + var subject = UserRole.administrator; + var expectedPermissions = Set.of(Permission.values()); + assertSetMatchesExactly(expectedPermissions, subject.getPermissions()); + } + + @Test + public void getUserRolesWithNoPermissionsTest() { + // Listing no permissions returns all user roles + assertSetMatchesExactly(Set.of(UserRole.values()), UserRole.getUserRoles(Collections.emptyList())); + } + + @Test + public void getUserRolesMatchesMultipleRolesTest() { + var expected = Set.of(UserRole.canIngest, UserRole.canManage, + UserRole.unitOwner, UserRole.administrator); + var result = UserRole.getUserRoles(Arrays.asList(Permission.viewAccessCopies, Permission.ingest)); + assertSetMatchesExactly(expected, result); + } + + @Test + public void getUserRolesWithPermissionOrderMembersTest() { + var expected = Set.of(UserRole.canProcess, UserRole.canManage, + UserRole.unitOwner, UserRole.administrator); + var result = UserRole.getUserRolesWithPermission(Permission.orderMembers); + assertSetMatchesExactly(expected, result); + } + + @Test + public void getStaffRolesTest() { + var expected = Arrays.asList(UserRole.canAccess, UserRole.canIngest, UserRole.canDescribe, + UserRole.canProcess, UserRole.canManage, UserRole.unitOwner, UserRole.administrator); + assertIterableEquals(expected, UserRole.getStaffRoles()); + } + + @Test + public void getPatronRolesTest() { + var expected = Arrays.asList(UserRole.none, UserRole.canDiscover, UserRole.canViewMetadata, + UserRole.canViewAccessCopies, UserRole.canViewReducedQuality, UserRole.canViewOriginals); + assertIterableEquals(expected, UserRole.getPatronRoles()); + } + + @Test + public void getRoleByPropertyValidTest() { + assertEquals(UserRole.canAccess, UserRole.getRoleByProperty(CdrAcl.canAccess.getURI())); + } + + @Test + public void getRoleByPropertyNotFoundTest() { + assertNull(UserRole.getRoleByProperty("http://example.com/ohno")); + } + + @Test + public void getPredicateTest() { + assertEquals("canManage", UserRole.canManage.getPredicate()); + } + + @Test + public void getPropertyTest() { + assertEquals(CdrAcl.canManage, UserRole.canManage.getProperty()); + } + + @Test + public void getURITest() { + assertEquals(CdrAcl.canManage, UserRole.canManage.getProperty()); + } + + @Test + public void isStaffRoleTrueTest() { + assertTrue(UserRole.canManage.isStaffRole()); + } + + @Test + public void isStaffRoleFalseTest() { + assertFalse(UserRole.canViewReducedQuality.isStaffRole()); + } + + @Test + public void isPatronRoleFalseTest() { + assertFalse(UserRole.canManage.isPatronRole()); + } + + @Test + public void isPatronRoleTrueTest() { + assertTrue(UserRole.canViewReducedQuality.isPatronRole()); + } + + @Test + public void equalsTrueTest() { + assertTrue(UserRole.none.equals(CdrAcl.none.getURI())); + } + + @Test + public void equalsFalseTest() { + assertFalse(UserRole.none.equals("hello")); + } + + // Compare that two sets are exactly equal, order insensitive. + // junits assertIterableEquals is not reliable with sets since it depends on order, and we aren't importing hamcrest + private void assertSetMatchesExactly(Set expected, Set actual) { + var message = "Actual set values did not match expected:\nActual: " + actual + "\nExpected: " + expected; + assertTrue(actual.containsAll(expected), message); + assertEquals(expected.size(), actual.size(), message); + } +} diff --git a/auth-fcrepo/src/main/java/edu/unc/lib/boxc/auth/fcrepo/services/ContentObjectAccessRestrictionValidator.java b/auth-fcrepo/src/main/java/edu/unc/lib/boxc/auth/fcrepo/services/ContentObjectAccessRestrictionValidator.java index 8b28171499..3c5830c89b 100644 --- a/auth-fcrepo/src/main/java/edu/unc/lib/boxc/auth/fcrepo/services/ContentObjectAccessRestrictionValidator.java +++ b/auth-fcrepo/src/main/java/edu/unc/lib/boxc/auth/fcrepo/services/ContentObjectAccessRestrictionValidator.java @@ -8,6 +8,7 @@ import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.canViewAccessCopies; import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.canViewMetadata; import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.canViewOriginals; +import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.canViewReducedQuality; import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.embargoUntil; import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.markedForDeletion; import static edu.unc.lib.boxc.model.api.rdf.CdrAcl.none; @@ -44,6 +45,7 @@ public class ContentObjectAccessRestrictionValidator { private static final Set collectionProperties = new HashSet<>(Arrays.asList( canViewMetadata, canViewAccessCopies, + canViewReducedQuality, canViewOriginals, canAccess, canDescribe, @@ -66,6 +68,7 @@ public class ContentObjectAccessRestrictionValidator { private static final Set contentProperties = new HashSet<>(Arrays.asList( canViewMetadata, canViewAccessCopies, + canViewReducedQuality, canViewOriginals, none, embargoUntil, diff --git a/auth-fcrepo/src/main/java/edu/unc/lib/boxc/auth/fcrepo/services/InheritedPermissionEvaluator.java b/auth-fcrepo/src/main/java/edu/unc/lib/boxc/auth/fcrepo/services/InheritedPermissionEvaluator.java index 739e3bd8c6..9b1f6343cb 100644 --- a/auth-fcrepo/src/main/java/edu/unc/lib/boxc/auth/fcrepo/services/InheritedPermissionEvaluator.java +++ b/auth-fcrepo/src/main/java/edu/unc/lib/boxc/auth/fcrepo/services/InheritedPermissionEvaluator.java @@ -256,6 +256,7 @@ private List getObjectPath(PID pid) { private boolean isPatronPermission(Permission permission) { return permission.equals(Permission.viewMetadata) || permission.equals(Permission.viewAccessCopies) + || permission.equals(Permission.viewReducedResImages) || permission.equals(Permission.viewOriginal); } diff --git a/auth-fcrepo/src/test/java/edu/unc/lib/boxc/auth/fcrepo/services/ContentObjectAccessRestrictionValidatorTest.java b/auth-fcrepo/src/test/java/edu/unc/lib/boxc/auth/fcrepo/services/ContentObjectAccessRestrictionValidatorTest.java index 129d44a629..9f6be22074 100644 --- a/auth-fcrepo/src/test/java/edu/unc/lib/boxc/auth/fcrepo/services/ContentObjectAccessRestrictionValidatorTest.java +++ b/auth-fcrepo/src/test/java/edu/unc/lib/boxc/auth/fcrepo/services/ContentObjectAccessRestrictionValidatorTest.java @@ -72,7 +72,8 @@ public void workNoAclsTest() throws Exception { public void validWorkTest() throws Exception { model.add(resc, RDF.type, Cdr.Work); model.add(resc, CdrAcl.embargoUntil, model.createTypedLiteral(Calendar.getInstance())); - model.add(resc, CdrAcl.canViewOriginals, PUBLIC_PRINC); + model.add(resc, CdrAcl.canViewReducedQuality, PUBLIC_PRINC); + model.add(resc, CdrAcl.canViewOriginals, AUTHENTICATED_PRINC); model.add(resc, CdrAcl.markedForDeletion, model.createTypedLiteral(false)); validator.validate(resc); @@ -154,6 +155,15 @@ public void validCollectionTest() throws Exception { validator.validate(resc); } + @Test + public void validateCollectionWithCanViewReducedQualityTest() throws Exception { + model.add(resc, RDF.type, Cdr.Collection); + model.add(resc, CdrAcl.canViewReducedQuality, PUBLIC_PRINC); + model.add(resc, CdrAcl.canViewReducedQuality, AUTHENTICATED_PRINC); + + validator.validate(resc); + } + @Test public void invalidPatronPrincipalCollectionTest() throws Exception { Assertions.assertThrows(InvalidAssignmentException.class, () -> { diff --git a/auth-fcrepo/src/test/java/edu/unc/lib/boxc/auth/fcrepo/services/InheritedPermissionEvaluatorTest.java b/auth-fcrepo/src/test/java/edu/unc/lib/boxc/auth/fcrepo/services/InheritedPermissionEvaluatorTest.java index 06bf7cd661..377bcccb48 100644 --- a/auth-fcrepo/src/test/java/edu/unc/lib/boxc/auth/fcrepo/services/InheritedPermissionEvaluatorTest.java +++ b/auth-fcrepo/src/test/java/edu/unc/lib/boxc/auth/fcrepo/services/InheritedPermissionEvaluatorTest.java @@ -8,6 +8,7 @@ import static edu.unc.lib.boxc.auth.api.UserRole.canViewAccessCopies; import static edu.unc.lib.boxc.auth.api.UserRole.canViewMetadata; import static edu.unc.lib.boxc.auth.api.UserRole.canViewOriginals; +import static edu.unc.lib.boxc.auth.api.UserRole.canViewReducedQuality; import static edu.unc.lib.boxc.model.api.ids.RepositoryPathConstants.CONTENT_ROOT_ID; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -401,6 +402,20 @@ public void contentRegularPatronsNoneGroupReUpped() { assertTrue(evaluator.hasPermission(pid, PATRON_GROUP_PRINCIPLES, Permission.viewOriginal)); } + @Test + public void contentPatronHasPermissionViewReducedTest() { + addPidToAncestors(); + PID collectionPid = addPidToAncestors(); + + mockFactoryPrincipalRoles(collectionPid, PUBLIC_PRINC, canViewOriginals); + mockFactoryPrincipalRoles(collectionPid, AUTHENTICATED_PRINC, canViewOriginals); + + mockFactoryPrincipalRoles(pid, PUBLIC_PRINC, canViewReducedQuality); + mockFactoryPrincipalRoles(pid, AUTHENTICATED_PRINC, canViewOriginals); + + assertTrue(evaluator.hasPermission(pid, PATRON_PRINCIPLES, Permission.viewReducedResImages)); + } + @Test public void contentPatronGroupDowngraded() { addPidToAncestors(); diff --git a/model-api/src/main/java/edu/unc/lib/boxc/model/api/rdf/CdrAcl.java b/model-api/src/main/java/edu/unc/lib/boxc/model/api/rdf/CdrAcl.java index 32a15d56c2..41e4622f6c 100644 --- a/model-api/src/main/java/edu/unc/lib/boxc/model/api/rdf/CdrAcl.java +++ b/model-api/src/main/java/edu/unc/lib/boxc/model/api/rdf/CdrAcl.java @@ -64,6 +64,13 @@ public static String getURI() { public static final Property canViewAccessCopies = createProperty( "http://cdr.unc.edu/definitions/acl#canViewAccessCopies" ); + /** Grants the specified group or user permission to view metadata, access copies, and + * download reduced quality representations of the original file for this object and + * all of its children. Applies to cdr:Collection objects. Repeatable. + */ + public static final Property canViewReducedQuality = createProperty( + "http://cdr.unc.edu/definitions/acl#canViewReducedQuality" ); + /** Grants the specified group or user permission to view metadata, access copies and originals * for this object and all of its children. Applies to cdr:Collection objects. Repeatable. */ diff --git a/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/patronAccess/PatronAccessAssignmentRouterTest.java b/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/patronAccess/PatronAccessAssignmentRouterTest.java index b6a034ab99..da46b2b174 100644 --- a/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/patronAccess/PatronAccessAssignmentRouterTest.java +++ b/services-camel-app/src/test/java/edu/unc/lib/boxc/services/camel/patronAccess/PatronAccessAssignmentRouterTest.java @@ -1,7 +1,10 @@ package edu.unc.lib.boxc.services.camel.patronAccess; import static edu.unc.lib.boxc.auth.api.AccessPrincipalConstants.AUTHENTICATED_PRINC; +import static edu.unc.lib.boxc.auth.api.AccessPrincipalConstants.PUBLIC_PRINC; import static edu.unc.lib.boxc.auth.api.UserRole.canViewMetadata; +import static edu.unc.lib.boxc.auth.api.UserRole.canViewOriginals; +import static edu.unc.lib.boxc.auth.api.UserRole.canViewReducedQuality; import static java.util.Arrays.asList; import static org.mockito.Matchers.any; import static org.mockito.Mockito.timeout; @@ -83,6 +86,26 @@ public void validMessageTest() throws Exception { assertEquals(accessDetails.getRoles(), received.getAccessDetails().getRoles()); } + @Test + public void assignReducedQualityRoleTest() throws Exception { + AgentPrincipals agent = new AgentPrincipalsImpl(USER, new AccessGroupSetImpl(PRINCIPALS)); + PID pid = PIDs.get(UUID.randomUUID().toString()); + PatronAccessDetails accessDetails = new PatronAccessDetails(); + accessDetails.setRoles(asList( + new RoleAssignment(PUBLIC_PRINC, canViewReducedQuality), + new RoleAssignment(AUTHENTICATED_PRINC, canViewOriginals))); + + PatronAccessAssignmentRequest request = new PatronAccessAssignmentRequest(agent, pid, accessDetails); + patronAccessOperationSender.sendUpdateRequest(request); + + verify(patronAccessAssignmentService, timeout(1000)).updatePatronAccess(requestCaptor.capture()); + PatronAccessAssignmentRequest received = requestCaptor.getValue(); + + assertEquals(pid, received.getTargetPid()); + assertEquals(agent.getPrincipals(), received.getAgent().getPrincipals()); + assertEquals(accessDetails.getRoles(), received.getAccessDetails().getRoles()); + } + @Test public void insufficientPermissionsTest() throws Exception { AgentPrincipals agent = new AgentPrincipalsImpl(USER, new AccessGroupSetImpl(PRINCIPALS)); diff --git a/static/js/admin/vue-permissions-editor/src/components/patronRoles.vue b/static/js/admin/vue-permissions-editor/src/components/patronRoles.vue index c43859510a..d55b65d780 100644 --- a/static/js/admin/vue-permissions-editor/src/components/patronRoles.vue +++ b/static/js/admin/vue-permissions-editor/src/components/patronRoles.vue @@ -112,6 +112,7 @@ const STAFF_ONLY_ROLE = 'none'; const VIEW_METADATA_ROLE = 'canViewMetadata'; const VIEW_ACCESS_COPIES_ROLE = 'canViewAccessCopies'; + const VIEW_REDUCED_QUALITY_ROLE = 'canViewReducedQuality'; const VIEW_ORIGINAL_ROLE = 'canViewOriginals'; const DEFAULT_ROLE = VIEW_ORIGINAL_ROLE; diff --git a/static/js/admin/vue-permissions-editor/src/mixins/patronHelpers.js b/static/js/admin/vue-permissions-editor/src/mixins/patronHelpers.js index c4693d8738..0e36bcb6e8 100644 --- a/static/js/admin/vue-permissions-editor/src/mixins/patronHelpers.js +++ b/static/js/admin/vue-permissions-editor/src/mixins/patronHelpers.js @@ -29,6 +29,7 @@ export default { { text: 'No Access', role: 'none' }, { text: 'Metadata Only', role: 'canViewMetadata' }, { text: 'Access Copies', role: 'canViewAccessCopies' }, + { text: 'Access Copies + Low Res Downloads', role: 'canViewReducedQuality' }, { text: `All of this ${displayType}`, role: 'canViewOriginals' } ] } diff --git a/static/js/admin/vue-permissions-editor/tests/unit/patronRoles.spec.js b/static/js/admin/vue-permissions-editor/tests/unit/patronRoles.spec.js index 1151204081..1cb6f66cc1 100644 --- a/static/js/admin/vue-permissions-editor/tests/unit/patronRoles.spec.js +++ b/static/js/admin/vue-permissions-editor/tests/unit/patronRoles.spec.js @@ -120,7 +120,7 @@ describe('patronRoles.vue', () => { wrapper.find('#add-principal').trigger('click'); // Select the existing patron principal to add wrapper.findAll('#add-new-patron-principal-id option')[0].setSelected(); - wrapper.findAll('#add-new-patron-principal-role option')[4].setSelected(); + wrapper.findAll('#add-new-patron-principal-role option')[5].setSelected(); wrapper.find('#add-principal').trigger('click'); await wrapper.vm.$nextTick(); @@ -203,7 +203,7 @@ describe('patronRoles.vue', () => { wrapper.find('#add-principal').trigger('click'); // Select values for new patron role and then click the add button again wrapper.findAll('#add-new-patron-principal-id option')[0].setSelected(); - wrapper.findAll('#add-new-patron-principal-role option')[4].setSelected(); + wrapper.findAll('#add-new-patron-principal-role option')[5].setSelected(); await wrapper.find('#add-principal').trigger('click'); // Model should have updated by adding the new role to the list of assigned roles @@ -873,7 +873,7 @@ describe('patronRoles.vue', () => { { principal: 'authenticated', role: 'canViewMetadata', deleted: false, embargo: false, type: 'assigned', assignedTo: UUID } ]); - wrapper.findAll('.patron-assigned')[1].findAll('option')[3].setSelected(); + wrapper.findAll('.patron-assigned')[1].findAll('option')[4].setSelected(); let updated_authenticated_roles = [ { principal: 'everyone', role: 'canViewMetadata', assignedTo: UUID }, @@ -890,6 +890,33 @@ describe('patronRoles.vue', () => { }); }); + it("updates permissions for public and auth users with canViewReducedQuality", (done) => { + stubDataLoad(); + + moxios.wait(() => { + expect(wrapper.vm.assignedPatronRoles).toEqual([ + {principal: 'everyone', role: 'canViewAccessCopies', assignedTo: UUID }, + {principal: 'authenticated', role: 'canViewAccessCopies', assignedTo: UUID } + ]); + + wrapper.findAll('.patron-assigned option')[3].setSelected(); + wrapper.findAll('.patron-assigned')[1].findAll('option')[3].setSelected(); + + expect(wrapper.vm.assignedPatronRoles).toEqual([ + {principal: 'everyone', role: 'canViewReducedQuality', assignedTo: UUID }, + {principal: 'authenticated', role: 'canViewReducedQuality', assignedTo: UUID } + ]); + expect(wrapper.vm.displayAssignments).toEqual([ + { principal: 'patron', role: 'canViewReducedQuality', deleted: false, embargo: false, type: 'assigned', assignedTo: UUID } + ]); + expect(wrapper.vm.submissionAccessDetails().roles).toEqual([ + {principal: 'everyone', role: 'canViewReducedQuality', assignedTo: UUID }, + {principal: 'authenticated', role: 'canViewReducedQuality', assignedTo: UUID } + ]); + done(); + }); + }); + it("does not update display roles if an embargo is not set from server response", (done) => { stubDataLoad(); @@ -1196,7 +1223,7 @@ describe('patronRoles.vue', () => { await wrapper.find('#add-principal').trigger('click'); // Select values for new patron role and then click the add button again await wrapper.findAll('#add-new-patron-principal-id option')[0].setSelected(); - await wrapper.findAll('#add-new-patron-principal-role option')[4].setSelected(); + await wrapper.findAll('#add-new-patron-principal-role option')[5].setSelected(); await wrapper.find('#add-principal').trigger('click'); stubBulkDataSaveResponse(); diff --git a/static/js/vue-cdr-access/src/components/full_record/aggregateRecord.vue b/static/js/vue-cdr-access/src/components/full_record/aggregateRecord.vue index 7ca23daa76..7200912357 100644 --- a/static/js/vue-cdr-access/src/components/full_record/aggregateRecord.vue +++ b/static/js/vue-cdr-access/src/components/full_record/aggregateRecord.vue @@ -75,7 +75,7 @@ {{ $t('full_record.edit') }} - diff --git a/static/js/vue-cdr-access/src/mixins/fileDownloadUtils.js b/static/js/vue-cdr-access/src/mixins/fileDownloadUtils.js index 0773a0ab7f..4b0d175436 100644 --- a/static/js/vue-cdr-access/src/mixins/fileDownloadUtils.js +++ b/static/js/vue-cdr-access/src/mixins/fileDownloadUtils.js @@ -25,7 +25,7 @@ export default { }, showImageDownload(brief_object) { - return this.hasPermission(brief_object, 'viewOriginal') && + return this.hasPermission(brief_object, 'viewReducedResImages') && brief_object.format.includes('Image') && this.getOriginalFile(brief_object) !== undefined }, @@ -62,13 +62,15 @@ export default { html += `${this.$t('full_record.large') } JPG (2500px)`; } + if (this.hasPermission(brief_object, 'viewOriginal')) { + html += `${this.$t('full_record.full_size')} JPG`; + html += ''; + html += `${this.$t('full_record.original_file')}`; + } - html += `${this.$t('full_record.full_size')} JPG`; - html += ''; - html += `${this.$t('full_record.original_file')}`; - - html += '' - html += '' + html += ''; + html += ''; + html += ''; return html; } else { diff --git a/static/js/vue-cdr-access/src/mixins/fullRecordUtils.js b/static/js/vue-cdr-access/src/mixins/fullRecordUtils.js index ea1f246d4b..214d2548b2 100644 --- a/static/js/vue-cdr-access/src/mixins/fullRecordUtils.js +++ b/static/js/vue-cdr-access/src/mixins/fullRecordUtils.js @@ -50,7 +50,14 @@ export default { this.recordData.briefObject.groupRoleMap.everyone === undefined) { return false; } - return !this.recordData.briefObject.groupRoleMap.everyone.includes('canViewOriginals'); + if (this.recordData.briefObject.groupRoleMap.everyone.includes('canViewOriginals')) { + return false; + } + // For File objects, content is not restricted if the user can at least download low res files + if (this.recordData.resourceType == 'File' && this.hasDownloadAccess(this.recordData)) { + return false; + } + return true; }, loginUrl() { diff --git a/static/js/vue-cdr-access/src/mixins/permissionUtils.js b/static/js/vue-cdr-access/src/mixins/permissionUtils.js index 11825cadac..ec4d75c2c8 100644 --- a/static/js/vue-cdr-access/src/mixins/permissionUtils.js +++ b/static/js/vue-cdr-access/src/mixins/permissionUtils.js @@ -34,6 +34,18 @@ export default { return recordData.permissions.includes(permission); }, + // Determines if the user has access to download either the original or reduced quality forms of it + hasDownloadAccess(recordData) { + recordData = this.permissionData(recordData); + if (recordData.permissions === undefined) { + return false; + } + if (recordData.permissions.includes('viewOriginal')) { + return true; + } + return recordData.permissions.includes('viewReducedResImages') && recordData.format.includes('Image'); + }, + markedForDeletion(record) { if (record.status === undefined) return false; return /marked.*?deletion/i.test(this.restrictions(record)); diff --git a/static/js/vue-cdr-access/tests/unit/adminUnit.spec.js b/static/js/vue-cdr-access/tests/unit/adminUnit.spec.js index 0254eb9efb..66352a09ee 100644 --- a/static/js/vue-cdr-access/tests/unit/adminUnit.spec.js +++ b/static/js/vue-cdr-access/tests/unit/adminUnit.spec.js @@ -56,6 +56,7 @@ const recordData = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewHidden", "assignStaffRoles", diff --git a/static/js/vue-cdr-access/tests/unit/aggregateRecord.spec.js b/static/js/vue-cdr-access/tests/unit/aggregateRecord.spec.js index dc5bf3b615..eec5f57ca9 100644 --- a/static/js/vue-cdr-access/tests/unit/aggregateRecord.spec.js +++ b/static/js/vue-cdr-access/tests/unit/aggregateRecord.spec.js @@ -85,6 +85,7 @@ const record = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewMetadata", "viewHidden", @@ -194,6 +195,7 @@ const record = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewMetadata", "viewHidden", @@ -298,6 +300,7 @@ const record = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewMetadata", "viewHidden", @@ -398,6 +401,7 @@ const record = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewMetadata", "viewHidden", @@ -499,6 +503,7 @@ const record = { "ingest", "orderMembers", "viewOriginal", + 'viewReducedResImages', "viewAccessCopies", "viewMetadata", "viewHidden", diff --git a/static/js/vue-cdr-access/tests/unit/analyticsUtils.spec.js b/static/js/vue-cdr-access/tests/unit/analyticsUtils.spec.js index 8cbb12ad44..0509307200 100644 --- a/static/js/vue-cdr-access/tests/unit/analyticsUtils.spec.js +++ b/static/js/vue-cdr-access/tests/unit/analyticsUtils.spec.js @@ -114,6 +114,7 @@ describe('analyticsUtils', () => { permissions: [ "viewAccessCopies", "viewOriginal", + "viewReducedResImages", "viewMetadata" ], groupRoleMap: { diff --git a/static/js/vue-cdr-access/tests/unit/collectionFolder.spec.js b/static/js/vue-cdr-access/tests/unit/collectionFolder.spec.js index 8f8ad294f1..0c8ac61188 100644 --- a/static/js/vue-cdr-access/tests/unit/collectionFolder.spec.js +++ b/static/js/vue-cdr-access/tests/unit/collectionFolder.spec.js @@ -70,6 +70,7 @@ const recordData = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewMetadata", "viewHidden", diff --git a/static/js/vue-cdr-access/tests/unit/fileList.spec.js b/static/js/vue-cdr-access/tests/unit/fileList.spec.js index 905266c704..08c8cfa613 100644 --- a/static/js/vue-cdr-access/tests/unit/fileList.spec.js +++ b/static/js/vue-cdr-access/tests/unit/fileList.spec.js @@ -100,6 +100,7 @@ describe('fileList.vue', () => { updatedBriefObj.permissions = [ "viewAccessCopies", "viewMetadata", + "viewReducedResImages", "viewOriginal" ] updatedBriefObj.groupRoleMap = { @@ -128,6 +129,7 @@ describe('fileList.vue', () => { updatedBriefObj.permissions = [ "viewAccessCopies", "viewMetadata", + "viewReducedResImages", "viewOriginal" ] updatedBriefObj.datastream = ['original_file|application/pdf|pdf file||416330|urn:sha1:4945153c9f5ce152ef8eda495deba043f536f388||']; diff --git a/static/js/vue-cdr-access/tests/unit/fileRecord.spec.js b/static/js/vue-cdr-access/tests/unit/fileRecord.spec.js index 3fe8e7bba9..989b414bdc 100644 --- a/static/js/vue-cdr-access/tests/unit/fileRecord.spec.js +++ b/static/js/vue-cdr-access/tests/unit/fileRecord.spec.js @@ -93,6 +93,7 @@ const record = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewHidden", "assignStaffRoles", @@ -202,6 +203,7 @@ const record = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewHidden", "assignStaffRoles", @@ -308,6 +310,7 @@ const record = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewHidden", "assignStaffRoles", diff --git a/static/js/vue-cdr-access/tests/unit/permissionUtils.spec.js b/static/js/vue-cdr-access/tests/unit/permissionUtils.spec.js index 255563b164..5e8234ce04 100644 --- a/static/js/vue-cdr-access/tests/unit/permissionUtils.spec.js +++ b/static/js/vue-cdr-access/tests/unit/permissionUtils.spec.js @@ -149,4 +149,28 @@ describe('permissionUtils', () => { expect(wrapper.vm.hasPermission(updatedRecord, 'viewOriginal')).toBe(false); expect(wrapper.vm.hasPermission(updatedRecord, 'destroy')).toBe(false); }); + + it("checks for download access with viewOriginal permission", () => { + expect(wrapper.vm.hasDownloadAccess(recordData)).toBe(true); + }); + + it("checks for download access with images and viewReducedResImages permission", () => { + let updatedRecord = cloneDeep(recordData); + updatedRecord.briefObject.permissions = ['viewMetadata', 'viewAccessCopies', 'viewReducedResImages']; + updatedRecord.briefObject.format = ['Image']; + expect(wrapper.vm.hasDownloadAccess(updatedRecord)).toBe(true); + }); + + it("checks for download access with non-images and viewReducedResImages permission", () => { + let updatedRecord = cloneDeep(recordData); + updatedRecord.briefObject.permissions = ['viewMetadata', 'viewAccessCopies', 'viewReducedResImages']; + updatedRecord.briefObject.format = ['Text']; + expect(wrapper.vm.hasDownloadAccess(updatedRecord)).toBe(false); + }); + + it("checks for download access without permission", () => { + let updatedRecord = cloneDeep(recordData); + updatedRecord.briefObject.permissions = ['viewMetadata', 'viewAccessCopies']; + expect(wrapper.vm.hasDownloadAccess(updatedRecord)).toBe(false); + }); }); \ No newline at end of file diff --git a/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js b/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js index 56bbf9f4ac..e77d317110 100644 --- a/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js +++ b/static/js/vue-cdr-access/tests/unit/restrictedContent.spec.js @@ -81,26 +81,7 @@ const record = { } ], permissions: [ - "markForDeletionUnit", - "move", - "reindex", - "destroy", - "editResourceType", - "destroyUnit", - "bulkUpdateDescription", - "changePatronAccess", - "runEnhancements", - "createAdminUnit", - "ingest", - "orderMembers", - "viewOriginal", - "viewAccessCopies", - "viewHidden", - "assignStaffRoles", - "viewMetadata", - "markForDeletion", - "editDescription", - "createCollection" + "viewMetadata" ], groupRoleMap: { authenticated: 'canViewOriginals', @@ -119,221 +100,6 @@ const record = { timestamp: 1679922126871 }, viewerType: "uv", - neighborList: [ - { - filesizeTotal: 69481, - added: "2023-01-17T13:53:48.103Z", - format: [ - "Image" - ], - thumbnail_url: "https://localhost:8080/services/api/thumb/4053adf7-7bdc-4c9c-8769-8cc5da4ce81d/large", - title: "bee1.jpg", - type: "File", - fileDesc: [ - "JPEG Image" - ], - parentCollectionName: "deansCollection", - contentStatus: [ - "Not Described", - "Is Primary Object" - ], - rollup: "7d6c30fe-ca72-4362-931d-e9fe28a8ec83", - objectPath: [ - { - pid: "collections", - name: "Content Collections Root", - container: true - }, - { - pid: "353ee09f-a4ed-461e-a436-18a1bee77b01", - name: "deansAdminUnit", - container: true - }, - { - pid: "fc77a9be-b49d-4f4e-b656-1644c9e964fc", - name: "deansCollection", - container: true - }, - { - pid: "7d6c30fe-ca72-4362-931d-e9fe28a8ec83", - name: "Bees", - container: true - }, - { - pid: "4053adf7-7bdc-4c9c-8769-8cc5da4ce81d", - name: "bee1.jpg", - container: true - } - ], - datastream: [ - "original_file|image/jpeg|bee1.jpg|jpg|69481|urn:sha1:87d7bed6cb33c87c589cfcdc2a2ce6110712fabb||607x1024", - "techmd_fits|text/xml|techmd_fits.xml|xml|7013|urn:sha1:0c4a500c73146214d5fa08f278c0cdaadede79d0||", - "jp2|image/jp2|4053adf7-7bdc-4c9c-8769-8cc5da4ce81d.jp2|jp2|415163|||", - "thumbnail_small|image/png|4053adf7-7bdc-4c9c-8769-8cc5da4ce81d.png|png|4802|||", - "thumbnail_large|image/png|4053adf7-7bdc-4c9c-8769-8cc5da4ce81d.png|png|16336|||", - "event_log|application/n-triples|event_log.nt|nt|5852|urn:sha1:8d80f0de467fa650d4bc8568d4a958e5ced85f96||" - ], - parentCollectionId: "fc77a9be-b49d-4f4e-b656-1644c9e964fc", - ancestorPath: [ - { - id: "collections", - title: "collections" - }, - { - id: "353ee09f-a4ed-461e-a436-18a1bee77b01", - title: "353ee09f-a4ed-461e-a436-18a1bee77b01" - }, - { - id: "fc77a9be-b49d-4f4e-b656-1644c9e964fc", - title: "fc77a9be-b49d-4f4e-b656-1644c9e964fc" - }, - { - id: "7d6c30fe-ca72-4362-931d-e9fe28a8ec83", - title: "7d6c30fe-ca72-4362-931d-e9fe28a8ec83" - } - ], - permissions: [ - "markForDeletionUnit", - "move", - "reindex", - "destroy", - "editResourceType", - "destroyUnit", - "bulkUpdateDescription", - "changePatronAccess", - "runEnhancements", - "createAdminUnit", - "ingest", - "orderMembers", - "viewOriginal", - "viewAccessCopies", - "viewHidden", - "assignStaffRoles", - "viewMetadata", - "markForDeletion", - "editDescription", - "createCollection" - ], - groupRoleMap: {}, - id: "4053adf7-7bdc-4c9c-8769-8cc5da4ce81d", - updated: "2023-03-27T16:43:35.724Z", - fileType: [ - "image/jpeg" - ], - status: [ - "Marked For Deletion", - "Parent Is Embargoed", - "Parent Has Staff-Only Access" - ], - timestamp: 1679935418494 - }, - { - filesizeTotal: 694904, - added: "2023-03-27T13:01:58.067Z", - format: [ - "Image" - ], - thumbnail_url: "https://localhost:8080/services/api/thumb/4db695c0-5fd5-4abf-9248-2e115d43f57d/large", - title: "beez", - type: "File", - fileDesc: [ - "JPEG Image" - ], - parentCollectionName: "deansCollection", - contentStatus: [ - "Not Described" - ], - rollup: "7d6c30fe-ca72-4362-931d-e9fe28a8ec83", - objectPath: [ - { - pid: "collections", - name: "Content Collections Root", - container: true - }, - { - pid: "353ee09f-a4ed-461e-a436-18a1bee77b01", - name: "deansAdminUnit", - container: true - }, - { - pid: "fc77a9be-b49d-4f4e-b656-1644c9e964fc", - name: "deansCollection", - container: true - }, - { - pid: "7d6c30fe-ca72-4362-931d-e9fe28a8ec83", - name: "Bees", - container: true - }, - { - pid: "4db695c0-5fd5-4abf-9248-2e115d43f57d", - name: "beez", - container: true - } - ], - datastream: [ - "techmd_fits|text/xml|techmd_fits.xml|xml|4709|urn:sha1:5b0eabd749222a7c0bcdb92002be9fe3eff60128||", - "original_file|image/jpeg|beez||694904|urn:sha1:0d48dadb5d61ae0d41b4998280a3c39577a2f94a||2048x1536", - "jp2|image/jp2|4db695c0-5fd5-4abf-9248-2e115d43f57d.jp2|jp2|2189901|||", - "thumbnail_small|image/png|4db695c0-5fd5-4abf-9248-2e115d43f57d.png|png|6768|||", - "thumbnail_large|image/png|4db695c0-5fd5-4abf-9248-2e115d43f57d.png|png|23535|||", - "event_log|application/n-triples|event_log.nt|nt|4334|urn:sha1:aabf004766f954db4ac4ab9aa0a115bb10b708b4||" - ], - parentCollectionId: "fc77a9be-b49d-4f4e-b656-1644c9e964fc", - ancestorPath: [ - { - id: "collections", - title: "collections" - }, - { - id: "353ee09f-a4ed-461e-a436-18a1bee77b01", - title: "353ee09f-a4ed-461e-a436-18a1bee77b01" - }, - { - id: "fc77a9be-b49d-4f4e-b656-1644c9e964fc", - title: "fc77a9be-b49d-4f4e-b656-1644c9e964fc" - }, - { - id: "7d6c30fe-ca72-4362-931d-e9fe28a8ec83", - title: "7d6c30fe-ca72-4362-931d-e9fe28a8ec83" - } - ], - permissions: [ - "markForDeletionUnit", - "move", - "reindex", - "destroy", - "editResourceType", - "destroyUnit", - "bulkUpdateDescription", - "changePatronAccess", - "runEnhancements", - "createAdminUnit", - "ingest", - "orderMembers", - "viewOriginal", - "viewAccessCopies", - "viewHidden", - "assignStaffRoles", - "viewMetadata", - "markForDeletion", - "editDescription", - "createCollection" - ], - groupRoleMap: {}, - id: "4db695c0-5fd5-4abf-9248-2e115d43f57d", - updated: "2023-03-27T13:01:58.067Z", - fileType: [ - "image/jpeg" - ], - status: [ - "Parent Is Embargoed", - "Parent Has Staff-Only Access", - "Inherited Patron Settings" - ], - timestamp: 1679922126871 - } - ], dataFileUrl: "content/4db695c0-5fd5-4abf-9248-2e115d43f57d", markedForDeletion: false, resourceType: "File" @@ -383,36 +149,20 @@ describe('restrictedContent.vue', () => { }); it('does not show view options if a user is logged in', async () => { - wrapper = mount(restrictedContent, { - global: { - plugins: [i18n, router, createTestingPinia({ - initialState: { - access: { - isLoggedIn: true, - username: 'test_user' - } - }, - stubActions: false - })] - }, - props: { - recordData: record - } - }); - store = useAccessStore(); + logUserIn(); expect(wrapper.find('.restricted-access').exists()).toBe(false); }); - it('shows an edit option if user has edit permissions', () => { + it('shows an edit option if user has edit permissions', async () => { + logUserIn(); + await setRecordPermissions(record, ['viewMetadata', 'viewAccessCopies', 'viewReducedResImages', + 'viewOriginal', 'viewHidden', 'editDescription']); expect(wrapper.find('a.edit').exists()).toBe(true); }); it('does not show an edit option if user does not have edit permissions', async () => { - const updated_data = cloneDeep(record); - updated_data.briefObject.permissions = []; - await wrapper.setProps({ - recordData: updated_data - }); + await setRecordPermissions(record, []); + expect(wrapper.find('a.edit').exists()).toBe(false); }); @@ -425,7 +175,10 @@ describe('restrictedContent.vue', () => { expect(wrapper.find('.noaction').exists()).toBe(false); }); - it('shows a view option if user can view originals and resource is a file', () => { + it('shows a view option if user can view originals and resource is a file', async () => { + await setRecordPermissions(record, ['viewMetadata', 'viewAccessCopies', 'viewReducedResImages', + 'viewOriginal']); + expect(wrapper.find('a.view').exists()).toBe(true); }); @@ -442,10 +195,8 @@ describe('restrictedContent.vue', () => { const updated_data = cloneDeep(record); updated_data.dataFileUrl = 'content/4db695c0-5fd5-4abf-9248-2e115d43f57d'; updated_data.resourceType = 'Work'; - updated_data.briefObject.permissions = ['viewAccessCopies', 'viewOriginal']; - await wrapper.setProps({ - recordData: updated_data - }); + await setRecordPermissions(updated_data, ['viewAccessCopies', 'viewReducedResImages', 'viewOriginal']); + expect(wrapper.find('.download').exists()).toBe(false); }); @@ -460,25 +211,56 @@ describe('restrictedContent.vue', () => { expect(wrapper.find('.download').exists()).toBe(false); }); - it('displays a download button for files with the proper permissions', async () => { + it('displays a download button for non-image with the proper permissions', async () => { const updated_data = cloneDeep(record); updated_data.dataFileUrl = 'content/4db695c0-5fd5-4abf-9248-2e115d43f57d'; updated_data.resourceType = 'File'; - updated_data.briefObject.permissions = ['viewAccessCopies', 'viewOriginal']; - await wrapper.setProps({ - recordData: updated_data - }); - expect(wrapper.find('.download').exists()).toBe(true); + updated_data.briefObject.format = ['Text']; + await setRecordPermissions(updated_data, ['viewAccessCopies', 'viewReducedResImages', 'viewOriginal']); + + expect(wrapper.find('.download.action').exists()).toBe(true); + expect(wrapper.find('.download.action').attributes('href')).toEqual('/content/4db695c0-5fd5-4abf-9248-2e115d43f57d?dl=true'); + }); + + it('displays a download button with all download options for image with viewOriginal', async () => { + const updated_data = cloneDeep(record); + updated_data.dataFileUrl = 'content/4db695c0-5fd5-4abf-9248-2e115d43f57d'; + updated_data.resourceType = 'File'; + await setRecordPermissions(updated_data, ['viewAccessCopies', 'viewReducedResImages', 'viewOriginal']); + + expect(wrapper.find('.download.dropdown').exists()).toBe(true); + await wrapper.find('button').trigger('click'); // Open + const dropdown_items = wrapper.findAll('.dropdown-item'); + expect(dropdown_items[0].text()).toEqual('Small JPG (800px)'); + expect(dropdown_items[1].text()).toEqual('Medium JPG (1600px)'); + expect(dropdown_items[2].text()).toEqual('Large JPG (2500px)'); + expect(dropdown_items[3].text()).toEqual('Full Size JPG'); + expect(dropdown_items[4].text()).toEqual('Original File'); + expect(dropdown_items.length).toEqual(5); + }); + + it('displays a download button with reduced download options for image with only viewReducedResImages', async () => { + const updated_data = cloneDeep(record); + updated_data.dataFileUrl = 'content/4db695c0-5fd5-4abf-9248-2e115d43f57d'; + updated_data.resourceType = 'File'; + await setRecordPermissions(updated_data, ['viewAccessCopies', 'viewReducedResImages']); + + expect(wrapper.find('.download.dropdown').exists()).toBe(true); + + await wrapper.find('button').trigger('click'); // Open + const dropdown_items = wrapper.findAll('.dropdown-item'); + expect(dropdown_items[0].text()).toEqual('Small JPG (800px)'); + expect(dropdown_items[1].text()).toEqual('Medium JPG (1600px)'); + expect(dropdown_items[2].text()).toEqual('Large JPG (2500px)'); + expect(dropdown_items.length).toEqual(3); }); it('does not display a download button for non-works/files', async () => { const updated_data = cloneDeep(record); updated_data.dataFileUrl = 'content/4db695c0-5fd5-4abf-9248-2e115d43f57d'; updated_data.resourceType = 'Folder'; - updated_data.briefObject.permissions = ['viewAccessCopies', 'viewOriginal']; - await wrapper.setProps({ - recordData: updated_data - }); + await setRecordPermissions(updated_data, ['viewAccessCopies', 'viewReducedResImages', 'viewOriginal']); + expect(wrapper.find('.download').exists()).toBe(false); }); @@ -513,20 +295,54 @@ describe('restrictedContent.vue', () => { }); it('hides the list of visible options when the options button is clicked', async () => { + await setRecordPermissions(record, ['viewAccessCopies', 'viewReducedResImages', 'viewOriginal']); + await wrapper.find('button').trigger('click'); // Open await wrapper.find('button').trigger('click'); // Close expect(wrapper.find('.image-download-options').classes('is-active')).toBe(false); }); it('hides the list of visible options when any non dropdown page element is clicked', async () => { + await setRecordPermissions(record, ['viewAccessCopies', 'viewReducedResImages', 'viewOriginal']); + await wrapper.find('button').trigger('click'); // Open await wrapper.trigger('click'); // Close expect(wrapper.find('.image-download-options').classes('is-active')).toBe(false); }); it('hides the list of visible options when the "ESC" key is hit', async () => { + await setRecordPermissions(record, ['viewAccessCopies', 'viewReducedResImages', 'viewOriginal']); + await wrapper.find('button').trigger('click'); // Open await wrapper.trigger('keyup.esc'); // Close expect(wrapper.find('.image-download-options').classes('is-active')).toBe(false); }); + + async function setRecordPermissions(rec, permissions) { + const updated_data = cloneDeep(rec); + updated_data.briefObject.permissions = permissions; + wrapper.setProps({ + recordData: updated_data + }); + } + + function logUserIn() { + wrapper = mount(restrictedContent, { + global: { + plugins: [i18n, router, createTestingPinia({ + initialState: { + access: { + isLoggedIn: true, + username: 'test_user' + } + }, + stubActions: false + })] + }, + props: { + recordData: record + } + }); + store = useAccessStore(); + } }); \ No newline at end of file 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 313c46f57f..29851a5b5e 100644 --- a/static/js/vue-cdr-access/tests/unit/thumbnail.spec.js +++ b/static/js/vue-cdr-access/tests/unit/thumbnail.spec.js @@ -68,6 +68,7 @@ const recordData = { "ingest", "orderMembers", "viewOriginal", + "viewReducedResImages", "viewAccessCopies", "viewMetadata", "viewHidden", 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 bbfe17aea7..a38d866617 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 @@ -1,23 +1,14 @@ package edu.unc.lib.boxc.web.common.services; -import static edu.unc.lib.boxc.auth.api.AccessPrincipalConstants.AUTHENTICATED_PRINC; -import static edu.unc.lib.boxc.auth.api.AccessPrincipalConstants.PUBLIC_PRINC; -import static edu.unc.lib.boxc.auth.api.Permission.editDescription; -import static edu.unc.lib.boxc.auth.api.UserRole.canViewOriginals; -import static edu.unc.lib.boxc.auth.api.services.DatastreamPermissionUtil.getPermissionForDatastream; -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_SMALL; -import static org.springframework.util.Assert.notNull; - import edu.unc.lib.boxc.auth.api.Permission; import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; import edu.unc.lib.boxc.auth.api.services.AccessControlService; import edu.unc.lib.boxc.model.api.DatastreamType; import edu.unc.lib.boxc.search.api.models.ContentObjectRecord; -import java.util.List; +import static edu.unc.lib.boxc.auth.api.services.DatastreamPermissionUtil.getPermissionForDatastream; +import static edu.unc.lib.boxc.model.api.DatastreamType.ORIGINAL_FILE; +import static org.springframework.util.Assert.notNull; /** * Helper for determining permissions of view objects. @@ -26,11 +17,6 @@ * */ public class PermissionsHelper { - - // RoleGroup value used to identify patron full public access - private static final String PUBLIC_ROLE_VALUE = canViewOriginals.getPredicate() + "|" + PUBLIC_PRINC; - private static final String AUTHENTICATED_ROLE_VALUE = canViewOriginals.getPredicate() + "|" + AUTHENTICATED_PRINC; - private AccessControlService accessControlService; public PermissionsHelper() { @@ -48,29 +34,6 @@ public boolean hasOriginalAccess(AccessGroupSet principals, ContentObjectRecord return hasDatastreamAccess(principals, ORIGINAL_FILE, metadata); } - /** - * Returns true if the principals can access the image preview belonging to - * the requested object, if present. - * - * @param principals - * @param metadata - * @return - */ - public boolean hasImagePreviewAccess(AccessGroupSet principals, ContentObjectRecord metadata) { - return hasDatastreamAccess(principals, JP2_ACCESS_COPY, metadata); - } - - /** - * Returns true if the principals can access the MODS description belonging to - * the requested object, if present. - * - * @param metadata - * @return - */ - public boolean hasDescriptionAccess(AccessGroupSet principals, ContentObjectRecord metadata) { - return hasDatastreamAccess(principals, MD_DESCRIPTIVE, metadata); - } - /** * Returns true if the provided principals have rights to access the * requested datastream, and the datastream is present. @@ -100,56 +63,6 @@ private static boolean containsDatastream(ContentObjectRecord metadata, String d .anyMatch(d -> d.getName().equals(datastream)); } - /** - * Returns true if the provided principals have permission to edit the - * objects description. - * - * @param principals agent principals - * @param metadata object metadata - * @return - */ - public boolean hasEditAccess(AccessGroupSet principals, ContentObjectRecord metadata) { - notNull(principals, "Requires agent principals"); - notNull(metadata, "Requires metadata object"); - - return accessControlService.hasAccess(metadata.getPid(), principals, editDescription); - } - - /** - * True if authenticated permissions are greater than no access - * @param metadata - * @return - */ - public boolean allowsFullAuthenticatedAccess(ContentObjectRecord metadata) { - List groups = metadata.getRoleGroup(); - - if (groups == null) { - return false; - } - - return groups.contains(AUTHENTICATED_ROLE_VALUE); - } - - /** - * Determines if full public access is allowed for the provided object. - * - * @param metadata object - * @return true if full public access is allow for the object - */ - public boolean allowsPublicAccess(ContentObjectRecord metadata) { - if (metadata.getRoleGroup() == null) { - return false; - } - return metadata.getRoleGroup().contains(PUBLIC_ROLE_VALUE); - } - - /** - * @return the accessControlService - */ - public AccessControlService getAccessControlService() { - return accessControlService; - } - /** * @param accessControlService the accessControlService to set */ 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 fe7a2589f3..19854e0cba 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 @@ -1,8 +1,22 @@ package edu.unc.lib.boxc.web.common.services; -import static edu.unc.lib.boxc.auth.api.Permission.editDescription; +import edu.unc.lib.boxc.auth.api.Permission; +import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; +import edu.unc.lib.boxc.auth.api.services.AccessControlService; +import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; +import edu.unc.lib.boxc.model.api.DatastreamType; +import edu.unc.lib.boxc.model.api.ids.PID; +import edu.unc.lib.boxc.search.solr.models.ContentObjectSolrRecord; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + import static edu.unc.lib.boxc.auth.api.Permission.viewAccessCopies; -import static edu.unc.lib.boxc.auth.api.Permission.viewMetadata; 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; @@ -13,23 +27,6 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; - -import edu.unc.lib.boxc.auth.api.Permission; -import edu.unc.lib.boxc.auth.api.models.AccessGroupSet; -import edu.unc.lib.boxc.auth.api.services.AccessControlService; -import edu.unc.lib.boxc.auth.fcrepo.models.AccessGroupSetImpl; -import edu.unc.lib.boxc.model.api.ids.PID; -import edu.unc.lib.boxc.search.solr.models.ContentObjectSolrRecord; - /** * * @author bbpennel @@ -75,27 +72,6 @@ void closeService() throws Exception { closeable.close(); } - @Test - public void testAllowsPublicAccess() { - roleGroups.add("canViewOriginals|everyone"); - - assertTrue(helper.allowsPublicAccess(mdObject), "Failed to determine object has full patron access"); - } - - @Test - public void testDoesNotAllowPublicAccess() { - roleGroups.add("canViewMetadata|everyone"); - - assertFalse(helper.allowsPublicAccess(mdObject), "Object must not have full patron access"); - } - - @Test - public void testAllowsPublicAccessNoRoleGroups() { - mdObject.setRoleGroup(null); - - assertFalse(helper.allowsPublicAccess(mdObject), "Object must not have full patron access"); - } - @Test public void testPermitOriginalAccess() { assignPermission(viewOriginal, true); @@ -118,45 +94,24 @@ public void testDenyOriginalAccess() { } @Test - public void testHasEditAccess() { - assignPermission(editDescription, true); - - assertTrue(helper.hasEditAccess(principals, mdObject)); - } - - @Test - public void testDoesNotHaveEditAccess() { - assignPermission(editDescription, false); + public void testHasDatastreamAccessNotPresent() { + assignPermission(viewOriginal, false); - assertFalse(helper.hasEditAccess(principals, mdObject)); + assertFalse(helper.hasDatastreamAccess(principals, DatastreamType.FULLTEXT_EXTRACTION, mdObject)); } @Test - public void testHasEditAccessNoPrincipals() { - Assertions.assertThrows(IllegalArgumentException.class, () -> { - assignPermission(editDescription, true); - - assertTrue(helper.hasEditAccess(null, mdObject)); - }); - } + public void testHasOriginalAccessDeny() { + assignPermission(viewAccessCopies, true); - @Test - public void testHasEnhancedAccessNoGroups() { - assertFalse(helper.allowsFullAuthenticatedAccess(mdObject)); + assertFalse(helper.hasOriginalAccess(principals, mdObject)); } @Test - public void testHasEnhancedAccessIfLoggedIn() { - assignPermission(viewMetadata, true); - roleGroups.add("canViewOriginals|authenticated"); - assertTrue(helper.allowsFullAuthenticatedAccess(mdObject)); - } + public void testHasOriginalAccessPermit() { + assignPermission(viewOriginal, true); - @Test - public void testDoesNotHaveEnhancedIfLoggedIn() { - assignPermission(viewMetadata, true); - roleGroups.add("canViewMetadata|authenticated"); - assertFalse(helper.allowsFullAuthenticatedAccess(mdObject)); + assertTrue(helper.hasOriginalAccess(principals, mdObject)); } private void assignPermission(Permission permission, boolean value) { diff --git a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadImageController.java b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadImageController.java index e36d01274e..e220dff254 100644 --- a/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadImageController.java +++ b/web-services-app/src/main/java/edu/unc/lib/boxc/web/services/rest/DownloadImageController.java @@ -45,7 +45,7 @@ public ResponseEntity getImage(@PathVariable("pid") String AccessGroupSet principals = getAgentPrincipals().getPrincipals(); aclService.assertHasAccess("Insufficient permissions to download access copy for " + pidString, - pid, principals, Permission.viewAccessCopies); + pid, principals, Permission.viewReducedResImages); var contentObjectRecord = solrSearchService.getObjectById(new SimpleIdRequest(pid, principals)); String validatedSize = downloadImageService.getSize(contentObjectRecord, size); diff --git a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java index 923e0234f2..3eb0152ea6 100644 --- a/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java +++ b/web-services-app/src/test/java/edu/unc/lib/boxc/web/services/rest/DownloadImageControllerIT.java @@ -1,7 +1,7 @@ package edu.unc.lib.boxc.web.services.rest; -import static edu.unc.lib.boxc.auth.api.Permission.viewAccessCopies; +import static edu.unc.lib.boxc.auth.api.Permission.viewReducedResImages; import static edu.unc.lib.boxc.model.fcrepo.ids.RepositoryPaths.idToPath; import com.github.tomakehurst.wiremock.client.WireMock; import edu.unc.lib.boxc.auth.api.exceptions.AccessRestrictionException; @@ -197,10 +197,10 @@ public void testGetImageAtPixelSizeBiggerThanFullNoPermission() throws Exception } @Test - public void testGetImageNoViewAccessCopyPermission() throws Exception { + public void testGetImageNoViewReducedPermission() throws Exception { var pid = makePid(); doThrow(new AccessRestrictionException()).when(accessControlService) - .assertHasAccess(anyString(), eq(pid), any(AccessGroupSetImpl.class), eq(viewAccessCopies)); + .assertHasAccess(anyString(), eq(pid), any(AccessGroupSetImpl.class), eq(viewReducedResImages)); MvcResult result = mvc.perform(get("/downloadImage/" + pid.getId() + "/1200")) .andExpect(status().isForbidden())