Skip to content

Commit

Permalink
DT-1143: Passthrough APIs for support requests and content uploads (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
rushtong authored Jan 22, 2025
1 parent aef01da commit 5ec63c2
Show file tree
Hide file tree
Showing 24 changed files with 402 additions and 143 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import org.broadinstitute.consent.http.resources.SchemaResource;
import org.broadinstitute.consent.http.resources.StatusResource;
import org.broadinstitute.consent.http.resources.StudyResource;
import org.broadinstitute.consent.http.resources.SupportResource;
import org.broadinstitute.consent.http.resources.SwaggerResource;
import org.broadinstitute.consent.http.resources.TDRResource;
import org.broadinstitute.consent.http.resources.TosResource;
Expand Down Expand Up @@ -232,6 +233,7 @@ public void run(ConsentConfiguration config, Environment env) {
env.jersey().register(new SchemaResource());
env.jersey().register(new SwaggerResource(config.getGoogleAuthentication()));
env.jersey().register(new StatusResource(env.healthChecks()));
env.jersey().register(injector.getInstance(SupportResource.class));
env.jersey().register(
new UserResource(samService, userService, datasetService, acknowledgementService));
env.jersey().register(new TosResource(samService));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@

public enum SupportRequestType {

QUESTION("question"), INCIDENT("incident"), PROBLEM("problem"), TASK("task");
QUESTION, BUG, FEATURE_REQUEST, TASK

private final String value;

SupportRequestType(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ public class ClaimsCache {

private static ClaimsCache INSTANCE;
public final Cache<String, Map<String, String>> cache;
public final static String OAUTH2_CLAIM_email = "OAUTH2_CLAIM_email";
public final static String OAUTH2_CLAIM_name = "OAUTH2_CLAIM_name";
public final static String OAUTH2_CLAIM_access_token = "OAUTH2_CLAIM_access_token";
public final static String OAUTH2_CLAIM_aud = "OAUTH2_CLAIM_aud";
public static final String OAUTH2_CLAIM_email = "OAUTH2_CLAIM_email";
public static final String OAUTH2_CLAIM_name = "OAUTH2_CLAIM_name";
public static final String OAUTH2_CLAIM_access_token = "OAUTH2_CLAIM_access_token";
public static final String OAUTH2_CLAIM_aud = "OAUTH2_CLAIM_aud";

private ClaimsCache() {
cache = CacheBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

public class DisabledDatasetMessage extends MailMessage {

private final static String MISSING_DATASET = "Datasets not available for Data Access Request Application id: %s.";
private static final String MISSING_DATASET = "Datasets not available for Data Access Request Application id: %s.";

public Mail disabledDatasetMessage(String toAddress, String fromAddress, Writer template,
String referenceId, String type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public enum AlternativeDataSharingPlanReason {
OTHER_INFORMED_CONSENT_LIMITATIONS_OR_CONCERNS("Other informed consent limitations or concerns"),
OTHER("Other");
private final String value;
private final static Map<String, AlternativeDataSharingPlanReason> CONSTANTS = new HashMap<String, AlternativeDataSharingPlanReason>();
private static final Map<String, AlternativeDataSharingPlanReason> CONSTANTS = new HashMap<String, AlternativeDataSharingPlanReason>();

static {
for (AlternativeDataSharingPlanReason c : values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ public enum DataLocation {
TDR_LOCATION("TDR Location"),
NOT_DETERMINED("Not Determined");
private final String value;
private final static Map<String, ConsentGroup.DataLocation> CONSTANTS = new HashMap<String, ConsentGroup.DataLocation>();
private static final Map<String, ConsentGroup.DataLocation> CONSTANTS = new HashMap<String, ConsentGroup.DataLocation>();

static {
for (ConsentGroup.DataLocation c : values()) {
Expand Down Expand Up @@ -817,7 +817,7 @@ public enum AccessManagement {
CONTROLLED("controlled"),
EXTERNAL("external");
private final String value;
private final static Map<String, ConsentGroup.AccessManagement> CONSTANTS = new HashMap<String, ConsentGroup.AccessManagement>();
private static final Map<String, ConsentGroup.AccessManagement> CONSTANTS = new HashMap<String, ConsentGroup.AccessManagement>();

static {
for (ConsentGroup.AccessManagement c : values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1273,7 +1273,7 @@ public enum AlternativeDataSharingPlanAccessManagement {
OPEN_ACCESS("Open Access"),
EXTERNAL_ACCESS("External Access");
private final String value;
private final static Map<String, DatasetRegistrationSchemaV1.AlternativeDataSharingPlanAccessManagement> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.AlternativeDataSharingPlanAccessManagement>();
private static final Map<String, DatasetRegistrationSchemaV1.AlternativeDataSharingPlanAccessManagement> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.AlternativeDataSharingPlanAccessManagement>();

static {
for (DatasetRegistrationSchemaV1.AlternativeDataSharingPlanAccessManagement c : values()) {
Expand Down Expand Up @@ -1320,7 +1320,7 @@ public enum AlternativeDataSharingPlanDataSubmitted {
BY_BATCHES_OVER_STUDY_TIMELINE_E_G_BASED_ON_CLINICAL_TRIAL_ENROLLMENT_BENCHMARKS(
"By batches over Study Timeline (e.g. based on clinical trial enrollment benchmarks)");
private final String value;
private final static Map<String, DatasetRegistrationSchemaV1.AlternativeDataSharingPlanDataSubmitted> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.AlternativeDataSharingPlanDataSubmitted>();
private static final Map<String, DatasetRegistrationSchemaV1.AlternativeDataSharingPlanDataSubmitted> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.AlternativeDataSharingPlanDataSubmitted>();

static {
for (DatasetRegistrationSchemaV1.AlternativeDataSharingPlanDataSubmitted c : values()) {
Expand Down Expand Up @@ -1371,7 +1371,7 @@ public enum NihAnvilUse {
I_AM_NOT_NHGRI_FUNDED_AND_DO_NOT_PLAN_TO_STORE_DATA_IN_AN_VIL(
"I am not NHGRI funded and do not plan to store data in AnVIL");
private final String value;
private final static Map<String, DatasetRegistrationSchemaV1.NihAnvilUse> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.NihAnvilUse>();
private static final Map<String, DatasetRegistrationSchemaV1.NihAnvilUse> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.NihAnvilUse>();

static {
for (DatasetRegistrationSchemaV1.NihAnvilUse c : values()) {
Expand Down Expand Up @@ -1439,7 +1439,7 @@ public enum NihInstitutionCenterSubmission {
NCATS("NCATS"),
NCCIH("NCCIH");
private final String value;
private final static Map<String, DatasetRegistrationSchemaV1.NihInstitutionCenterSubmission> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.NihInstitutionCenterSubmission>();
private static final Map<String, DatasetRegistrationSchemaV1.NihInstitutionCenterSubmission> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.NihInstitutionCenterSubmission>();

static {
for (DatasetRegistrationSchemaV1.NihInstitutionCenterSubmission c : values()) {
Expand Down Expand Up @@ -1491,7 +1491,7 @@ public enum StudyType {
CROSS_SECTIONAL("Cross-sectional"),
COHORT_STUDY("Cohort study");
private final String value;
private final static Map<String, DatasetRegistrationSchemaV1.StudyType> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.StudyType>();
private static final Map<String, DatasetRegistrationSchemaV1.StudyType> CONSTANTS = new HashMap<String, DatasetRegistrationSchemaV1.StudyType>();

static {
for (DatasetRegistrationSchemaV1.StudyType c : values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public enum FileType {
SURVEY("Survey"),
PHENOTYPE("Phenotype");
private final String value;
private final static Map<String, FileTypeObject.FileType> CONSTANTS = new HashMap<String, FileTypeObject.FileType>();
private static final Map<String, FileTypeObject.FileType> CONSTANTS = new HashMap<String, FileTypeObject.FileType>();

static {
for (FileTypeObject.FileType c : values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public enum NihICsSupportingStudy {
NCATS("NCATS"),
NCCIH("NCCIH");
private final String value;
private final static Map<String, NihICsSupportingStudy> CONSTANTS = new HashMap<String, NihICsSupportingStudy>();
private static final Map<String, NihICsSupportingStudy> CONSTANTS = new HashMap<String, NihICsSupportingStudy>();

static {
for (NihICsSupportingStudy c : values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@

import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.util.List;
import org.broadinstitute.consent.http.enumeration.SupportRequestType;
import org.broadinstitute.consent.http.util.gson.GsonUtil;
import org.zendesk.client.v2.model.Comment;
import org.zendesk.client.v2.model.CustomFieldValue;
import org.zendesk.client.v2.model.Request;
import org.zendesk.client.v2.model.Ticket;

Expand All @@ -18,7 +14,7 @@ public class TicketFactory {
* @param response The response content from the Zendesk Request API
* @return Parsed request.
*/
public Request parseRequestResponse(String response) {
public static Request parseRequestResponse(String response) {
Gson gson = GsonUtil.getInstance();
JsonObject obj = gson.fromJson(response, JsonObject.class);
JsonObject request = obj.get("request").getAsJsonObject();
Expand All @@ -28,68 +24,12 @@ public Request parseRequestResponse(String response) {
/**
* Constructs a DuosTicket with the proper structure to request support via Zendesk
*
* @param name The name of the user requesting support
* @param type The type of request ("question", "incident", "problem", "task")
* @param email The email of the user requesting support
* @param subject Subject line of the request
* @param description Description of the task or question
* @param url The origin url of this request
* @param uploads Optional list of attachment tokens
* @param ticketFields TicketFields
*/
public DuosTicket createTicket(String name, SupportRequestType type, String email, String subject,
String description, String url, List<String> uploads) {
if (name == null || email == null) {
throw new IllegalArgumentException("Name and email of user requesting support is required");
}
if (subject == null) {
throw new IllegalArgumentException("Support ticket subject is required");
}
if (description == null) {
throw new IllegalArgumentException("Support ticket description is required");
}
if (type == null) {
throw new IllegalArgumentException("Support ticket type is required");
}
if (url == null) {
throw new IllegalArgumentException("Support ticket url is required");
}

Ticket ticket = new Ticket(
new Ticket.Requester(name, email),
subject,
createComment(description, url, uploads));
ticket.setCustomFields(createCustomFields(name, type, email, description));
// This value specifies tickets as belonging to the DUOS group defined in Zendesk
ticket.setTicketFormId(360000669472L);
public static DuosTicket createTicket(TicketFields ticketFields) {
ticketFields.validate();
Ticket ticket = ticketFields.toTicket();
return new DuosTicket(ticket);
}

static private Comment createComment(String description, String url, List<String> uploads) {
Comment comment = new Comment();
comment.setBody(description + "\n\n------------------\nSubmitted from: " + url);
if (uploads != null && !uploads.isEmpty()) {
comment.setUploads(uploads);
}
return comment;
}

/**
* Custom fields represent a Zendesk ID that corresponds to a component of the created ticket.
* These fields have IDs that are defined in the Zendesk administrative interface.
*
* @param name Name of user
* @param type SupportRequestType Type of request, i.e. Question, Incident, etc.
* @param email User's email address
* @param description The contents of the support request
* @return List of Custom Fields that are required by Zendesk
*/
static private List<CustomFieldValue> createCustomFields(String name, SupportRequestType type,
String email, String description) {
return List.of(new CustomFieldValue(360012744452L, new String[]{type.getValue()}),
new CustomFieldValue(360007369412L, new String[]{description}),
new CustomFieldValue(360012744292L, new String[]{name}),
new CustomFieldValue(360012782111L, new String[]{email}),
new CustomFieldValue(360018545031L, new String[]{email}));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.broadinstitute.consent.http.models.support;

import java.util.List;
import org.broadinstitute.consent.http.enumeration.SupportRequestType;
import org.zendesk.client.v2.model.Comment;
import org.zendesk.client.v2.model.CustomFieldValue;
import org.zendesk.client.v2.model.Ticket;

public record TicketFields(String name, SupportRequestType type, String email, String subject,
String description, String url, List<String> uploads) {

private static void validate(boolean condition, String prefix) {
if (condition) {
throw new IllegalArgumentException(prefix + " is required");
}
}

public void validate() {
validate(name == null || email == null, "Name and email of user requesting support");
validate(subject == null, "Subject");
validate(description == null, "Description");
validate(type == null, "Type");
validate(url == null, "URL");
}

Ticket toTicket() {
Ticket ticket = new Ticket(new Ticket.Requester(name, email), subject, createComment());
ticket.setCustomFields(createCustomFields());
// This value specifies tickets as belonging to the DUOS group defined in Zendesk
ticket.setTicketFormId(360000669472L);
return ticket;
}

private Comment createComment() {
Comment comment = new Comment();
comment.setBody(description + "\n\n------------------\nSubmitted from: " + url);
if (uploads != null && !uploads.isEmpty()) {
comment.setUploads(uploads);
}
return comment;
}

/**
* Custom fields consist of predefined long values corresponding to field types in the Zendesk UI
*
* @return List<CustomFieldValue> List of custom fields
*/
private List<CustomFieldValue> createCustomFields() {
return List.of(new CustomFieldValue(360012744452L, new String[]{type.name()}),
new CustomFieldValue(360007369412L, new String[]{description}),
new CustomFieldValue(360012744292L, new String[]{name}),
new CustomFieldValue(360012782111L, new String[]{email}),
new CustomFieldValue(360018545031L, new String[]{email}));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,18 @@
abstract public class Resource implements ConsentLogger {

// Resource based role names
public final static String ADMIN = "Admin";
public final static String ALUMNI = "Alumni";
public final static String CHAIRPERSON = "Chairperson";
public final static String MEMBER = "Member";
public final static String RESEARCHER = "Researcher";
public final static String SIGNINGOFFICIAL = "SigningOfficial";
public final static String DATASUBMITTER = "DataSubmitter";
public final static String ITDIRECTOR = "ITDirector";
public static final String ADMIN = "Admin";
public static final String ALUMNI = "Alumni";
public static final String CHAIRPERSON = "Chairperson";
public static final String MEMBER = "Member";
public static final String RESEARCHER = "Researcher";
// nosemgrep
public static final String SIGNINGOFFICIAL = "SigningOfficial";
public static final String DATASUBMITTER = "DataSubmitter";
public static final String ITDIRECTOR = "ITDirector";

// NOTE: implement more Postgres vendor codes as we encounter them
private final static Map<String, Integer> vendorCodeStatusMap = Map.ofEntries(
private static final Map<String, Integer> vendorCodeStatusMap = Map.ofEntries(
new AbstractMap.SimpleEntry<>(PSQLState.UNIQUE_VIOLATION.getState(),
Response.Status.CONFLICT.getStatusCode())
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.broadinstitute.consent.http.resources;

import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.broadinstitute.consent.http.models.support.DuosTicket;
import org.broadinstitute.consent.http.models.support.TicketFactory;
import org.broadinstitute.consent.http.models.support.TicketFields;
import org.broadinstitute.consent.http.service.SupportRequestService;
import org.broadinstitute.consent.http.util.gson.GsonUtil;
import org.owasp.fileio.FileValidator;
import org.zendesk.client.v2.model.Request;

@Path("support")
public class SupportResource extends Resource {

private final SupportRequestService supportRequestService;
private static final Gson gson = GsonUtil.getInstance();
static final long MAX_FILE_UPLOAD_SIZE = new FileValidator().getMaxFileUploadSize();

@Inject
public SupportResource(SupportRequestService supportRequestService) {
this.supportRequestService = supportRequestService;
}

@POST
@Path("request")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response postRequest(String body) {
try {
TicketFields ticketFields = gson.fromJson(body, TicketFields.class);
DuosTicket ticket = TicketFactory.createTicket(ticketFields);
logInfo("Support Request Ticket: " + ticket);
Request request = supportRequestService.postTicketToSupport(ticket);
return Response.status(HttpStatusCodes.STATUS_CODE_CREATED).entity(request).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}

@POST
@Path("upload")
@Consumes("application/binary")
@Produces(MediaType.APPLICATION_JSON)
public Response postUpload(byte[] content) {
try {
if (content.length > MAX_FILE_UPLOAD_SIZE) {
return Response.status(Response.Status.BAD_REQUEST).build();
}
JsonObject token = supportRequestService.postAttachmentToSupport(content);
logInfo("Support Request Content Upload: " + content.length + " bytes");
return Response.status(HttpStatusCodes.STATUS_CODE_CREATED).entity(token).build();
} catch (Exception e) {
return createExceptionResponse(e);
}
}

}
Loading

0 comments on commit 5ec63c2

Please sign in to comment.