Skip to content

Commit

Permalink
BXC-4216 - Patron Role/Permission for accessing reduced quality images (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
bbpennel authored Jan 12, 2024
1 parent c63b224 commit 4b6d92d
Show file tree
Hide file tree
Showing 31 changed files with 460 additions and 546 deletions.
4 changes: 4 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 2 additions & 13 deletions auth-api/src/main/java/edu/unc/lib/boxc/auth/api/Permission.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
public enum Permission {
viewMetadata,
viewAccessCopies,
viewReducedResImages,
viewOriginal,
// TODO replaces viewAdminUI and viewEmbargoed
// Staff Permissions
viewHidden,
editDescription,
bulkUpdateDescription,
Expand All @@ -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;
}
}
99 changes: 25 additions & 74 deletions auth-api/src/main/java/edu/unc/lib/boxc/auth/api/UserRole.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,26 @@
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;
import static edu.unc.lib.boxc.auth.api.Permission.markForDeletion;
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;
Expand All @@ -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<String> 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;
Expand All @@ -94,56 +83,22 @@ public enum UserRole {

private static Map<Permission, Set<UserRole>> 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<Permission> 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<UserRole> getUserRoles(Collection<Permission> inPermissions) {
Expand Down Expand Up @@ -233,10 +188,6 @@ public Property getProperty() {
return property;
}

public URI getURI() {
return this.uri;
}

public Set<Permission> getPermissions() {
return permissions;
}
Expand Down
141 changes: 141 additions & 0 deletions auth-api/src/test/java/edu/unc/lib/boxc/auth/api/UserRoleTest.java
Original file line number Diff line number Diff line change
@@ -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 <T> void assertSetMatchesExactly(Set<T> expected, Set<T> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,6 +45,7 @@ public class ContentObjectAccessRestrictionValidator {
private static final Set<Property> collectionProperties = new HashSet<>(Arrays.asList(
canViewMetadata,
canViewAccessCopies,
canViewReducedQuality,
canViewOriginals,
canAccess,
canDescribe,
Expand All @@ -66,6 +68,7 @@ public class ContentObjectAccessRestrictionValidator {
private static final Set<Property> contentProperties = new HashSet<>(Arrays.asList(
canViewMetadata,
canViewAccessCopies,
canViewReducedQuality,
canViewOriginals,
none,
embargoUntil,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ private List<PID> 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);
}

Expand Down
Loading

0 comments on commit 4b6d92d

Please sign in to comment.