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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
Expand Down Expand Up @@ -36,6 +37,9 @@
@Path("api/collections")
public class DarCollectionResource extends Resource {

private static final String CHAIR_ROLE_REQUIRED_ERROR =
"roleName=" + CHAIRPERSON + " is required to cancel the elections for a collection";

private final DarCollectionService darCollectionService;

@Inject
Expand Down Expand Up @@ -193,23 +197,33 @@ public Response getCollectionByReferenceId(
@PUT
@Path("{id}/cancel")
@Produces("application/json")
@RolesAllowed({ADMIN, CHAIRPERSON, RESEARCHER})
@RolesAllowed({CHAIRPERSON, RESEARCHER})
public Response cancelDarCollectionByCollectionId(
@Auth DuosUser duosUser,
@Context Request request,
@PathParam("id") Integer collectionId,
@QueryParam("roleName") String roleName) {
try {
User user = duosUser.getUser();
DarCollection collection = darCollectionService.getByCollectionId(user, collectionId);
isCollectionPresent(collection);

// Default to the least impactful role if none provided.
UserRoles actingRole = UserRoles.RESEARCHER;
if (roleName != null) {
UserRoles actingRole;
if (roleName == null) {
// Only the least impactful role is implied.
if (user.hasUserRole(UserRoles.RESEARCHER)) {
actingRole = UserRoles.RESEARCHER;
} else if (user.hasUserRole(UserRoles.CHAIRPERSON)) {
throw new BadRequestException(CHAIR_ROLE_REQUIRED_ERROR);
} else {
throw new ForbiddenException(DarCollectionService.CANCEL_ROLE_ERROR);
}
} else {
actingRole = validateUserHasRoleName(user, roleName);
if (actingRole != UserRoles.CHAIRPERSON && actingRole != UserRoles.RESEARCHER) {
throw new ForbiddenException(DarCollectionService.CANCEL_ROLE_ERROR);
}
}

DarCollection collection = darCollectionService.getByCollectionId(user, collectionId);
isCollectionPresent(collection);
DarCollection cancelledCollection =
darCollectionService.cancelDarCollectionByRole(user, collection, actingRole);
ComplianceLogger.logDARCancellation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@

public class DarCollectionService implements ConsentLogger {

public static final String CANCEL_ROLE_ERROR =
"Only chairpersons and researchers can cancel a collection";
private static final String CREATE_ELECTION_ROLE_ERROR = "Only chairpersons can create elections";
private static final String CREATE_ELECTION_DAC_ERROR =
"User is not a chairperson for any dataset in this collection";
private static final String CREATE_ELECTION_DATASET_ERROR = "At least one dataset is required";

private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
private final DacDAO dacDAO;
private final DaaDAO daaDAO;
Expand Down Expand Up @@ -139,30 +146,18 @@ private void determineCollectionStatus(
}

private void processDarCollectionSummariesForAdmin(List<DarCollectionSummary> summaries) {
// if at least one election is open, show cancel
// if at least one non-open/absent election, show open
summaries.forEach(
s -> {
// Admins are read-only.
s.getActions().clear();
Map<String, Integer> statusCount = new HashMap<>();
Map<Integer, Election> elections = s.getElections();
if (elections.isEmpty()) {
s.setStatus(DarCollectionStatus.SUBMITTED.getValue());
} else {
elections
.values()
.forEach(
e -> {
String status = e.getStatus();
updateStatusCount(statusCount, status);
if (status.equals(ElectionStatus.OPEN.getValue())) {
s.addAction(DarCollectionActions.CANCEL);
}
});
elections.values().forEach(e -> updateStatusCount(statusCount, e.getStatus()));
determineCollectionStatus(s, statusCount);
}
if (s.getCloseoutSupplement() != null) {
s.getActions().clear();
}
});
}

Expand Down Expand Up @@ -654,15 +649,18 @@ protected DarCollection addDatasetsToCollection(DarCollection collection) {

/**
* Cancel Elections or a dar for a DarCollection, given a user and a role. If the user is a chair,
* or admin, cancel elections. If the user is a researcher, cancel the dar.
* cancel elections. If the user is a researcher, cancel the dar.
*
* @param user The User initiating the cancel
* @param collection The DarCollection
* @param role The role of the user, must be one of ADMIN, CHAIRPERSON, or RESEARCHER
* @param role The role of the user, must be CHAIRPERSON or RESEARCHER
* @return The DarCollection that has been canceled
*/
public DarCollection cancelDarCollectionByRole(
User user, DarCollection collection, UserRoles role) {
if (role != UserRoles.CHAIRPERSON && role != UserRoles.RESEARCHER) {
throw new ForbiddenException(CANCEL_ROLE_ERROR);
}
Collection<DataAccessRequest> dars = collection.getDars().values();
if (dars.isEmpty()) {
logWarn(
Expand All @@ -672,20 +670,18 @@ public DarCollection cancelDarCollectionByRole(
}

DarCollection cancelledCollection =
switch (role) {
case ADMIN -> cancelDarCollectionElectionsAsAdmin(collection, user);
case CHAIRPERSON -> cancelDarCollectionElectionsAsChair(collection, user);
default -> cancelDarCollectionAsResearcher(collection, user);
};
role == UserRoles.CHAIRPERSON
? cancelDarCollectionElectionsAsChair(collection, user)
: cancelDarCollectionAsResearcher(collection, user);
return getByCollectionId(user, cancelledCollection.getDarCollectionId());
}

/**
* Cancel a DarCollection as a researcher.
*
* <p>If an election exists for a DAR within the collection, that DAR cannot be cancelled by the
* researcher. Since it's now under DAC review, it's up to the DAC Chair (or admin) to ultimately
* decline or cancel the elections for the collection.
* researcher. Since it's now under DAC review, it's up to the DAC Chair to ultimately decline or
* cancel the elections for the collection.
*
* @param collection The DarCollection
* @param user the researcher requesting the cancel
Expand Down Expand Up @@ -723,24 +719,6 @@ private DarCollection cancelDarCollectionAsResearcher(DarCollection collection,
return getByCollectionId(user, collection.getDarCollectionId());
}

/**
* Cancel Elections for a DarCollection as an admin.
*
* <p>Admins can cancel all elections in a DarCollection
*
* @param collection The DarCollection
* @return The DarCollection whose elections have been canceled
*/
private DarCollection cancelDarCollectionElectionsAsAdmin(DarCollection collection, User user) {
Collection<DataAccessRequest> dars = collection.getDars().values();
List<String> referenceIds = dars.stream().map(DataAccessRequest::getReferenceId).toList();

// Cancel all DAR elections
cancelElectionsForReferenceIds(referenceIds);

return getByCollectionId(user, collection.getDarCollectionId());
}

/**
* Cancel Elections for a DarCollection as a chairperson.
*
Expand Down Expand Up @@ -796,6 +774,36 @@ public DarCollection approveDarCollection(
throw new ConsentConflictException("This collection does not require approval.");
}

/**
* Validate that the user can create elections for the collection and return the DAR they will be
* created for.
*
* @param user The User initiating new elections
* @param collection The DarCollection
* @return The most recent DAR in the collection
*/
private DataAccessRequest validateElectionCreation(User user, DarCollection collection) {
if (!user.hasUserRole(UserRoles.CHAIRPERSON)) {
throw new ForbiddenException(CREATE_ELECTION_ROLE_ERROR);
}
DataAccessRequest dar = collection.getMostRecentDar();
if (Objects.isNull(dar)) {
throw new BadRequestException(
"DAR Collection ID: [%s] does not have any associated DAR ids"
.formatted(collection.getDarCollectionId()));
}
List<Integer> darDatasetIds = dar.getDatasetIds();
if (darDatasetIds.isEmpty()) {
throw new BadRequestException(CREATE_ELECTION_DATASET_ERROR);
}
Set<Integer> governedDatasetIds =
Set.copyOf(getDatasetIdsForUserAndRoleId(user, UserRoles.CHAIRPERSON.getRoleId()));
if (darDatasetIds.stream().noneMatch(governedDatasetIds::contains)) {
throw new ForbiddenException(CREATE_ELECTION_DAC_ERROR);
}
return dar;
}

/**
* DarCollections with no elections, or with previously canceled elections, are valid for
* initiating a new set of elections. Elections in open, closed, pending, or final states are not
Expand All @@ -807,7 +815,7 @@ public DarCollection approveDarCollection(
*/
public DarCollection createElectionsForDarCollection(User user, DarCollection collection)
throws BadRequestException, ForbiddenException, ConsentConflictException, SQLException {
DataAccessRequest dar = collection.getMostRecentDar();
DataAccessRequest dar = validateElectionCreation(user, collection);
if ((!dar.getRequiresSOApproval() || dar.getApprovingSigningOfficialUserId() != null)) {
try {
List<String> createdElectionReferenceIds =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
put:
summary: Cancel DAR Collection By ID
operationId: apiCollectionsCollectionIdCancelPut
description: Returns DAR Collection after canceling associated dars
description: Returns DAR Collection after a Researcher cancels their dars or a Chairperson cancels the elections they govern. Admins have no access and escalate to the governing DAC Chairperson.
tags:
- DAR Collection
parameters:
Expand All @@ -13,14 +13,11 @@ put:
type: integer
- name: roleName
in: query
description: |
String value that represents the role name to act as. If not provided,
the system will cancel the collection and associated elections as the
least permissioned role the user has (researcher) for safety.
description: Role name to act as. Defaults to Researcher; Chairperson must be requested explicitly.
required: false
schema:
type: string
enum: [Admin, Chairperson, Researcher]
enum: [Chairperson, Researcher]
responses:
200:
description: Returns target dar collection with DARs canceled
Expand All @@ -29,7 +26,9 @@ put:
schema:
$ref: '../schemas/DarCollection.yaml'
400:
description: Bad Request (result of elections present on DARs or the user not having the provided role name)
description: Bad Request (result of elections present on DARs, or a role name that is missing or invalid for this user)
403:
description: Forbidden. Not acting as a Chairperson or Researcher.
404:
description: Not Found
429:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
post:
summary: Create Elections for Collection By ID
operationId: apiCollectionsCollectionIdElectionPost
description: Returns created Elections for Dars in Collection
description: Returns created Elections for Dars in Collection, for the datasets the Chairperson governs. Limited to Chairpersons; Admins have no access and escalate to the governing DAC Chairperson.
tags:
- DAR Collection
- Election
Expand All @@ -20,9 +20,11 @@ post:
schema:
$ref: '../schemas/DarCollection.yaml'
400:
description: Bad Request. Check error details
description: Bad Request (collection has no DAR, or the DAR has no datasets)
401:
description: Not authorized to perform this action.
description: Authentication required.
403:
description: Forbidden. Not a Chairperson for any dataset in the collection.
404:
description: Not Found
409:
Expand Down
Loading
Loading