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())