• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DataBiosphere / consent / #5834

02 May 2025 06:55PM UTC coverage: 78.772% (+0.04%) from 78.733%
#5834

push

web-flow
[DT-1601] Add a check that all datasets in a progress report must be approved (#2510)

5 of 5 new or added lines in 1 file covered. (100.0%)

54 existing lines in 6 files now uncovered.

10075 of 12790 relevant lines covered (78.77%)

0.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.61
/src/main/java/org/broadinstitute/consent/http/resources/Resource.java
1
package org.broadinstitute.consent.http.resources;
2

3
import com.google.api.client.http.HttpStatusCodes;
4
import com.google.gson.JsonSyntaxException;
5
import com.google.gson.stream.MalformedJsonException;
6
import io.sentry.Sentry;
7
import io.sentry.SentryEvent;
8
import jakarta.ws.rs.BadRequestException;
9
import jakarta.ws.rs.ForbiddenException;
10
import jakarta.ws.rs.NotAuthorizedException;
11
import jakarta.ws.rs.NotFoundException;
12
import jakarta.ws.rs.core.MediaType;
13
import jakarta.ws.rs.core.Response;
14
import jakarta.ws.rs.core.StreamingOutput;
15
import java.io.IOException;
16
import java.io.InputStream;
17
import java.sql.SQLException;
18
import java.sql.SQLSyntaxErrorException;
19
import java.util.HashMap;
20
import java.util.List;
21
import java.util.Map;
22
import java.util.Objects;
23
import org.apache.commons.io.IOUtils;
24
import org.apache.commons.lang3.tuple.ImmutablePair;
25
import org.broadinstitute.consent.http.enumeration.UserRoles;
26
import org.broadinstitute.consent.http.exceptions.ConsentConflictException;
27
import org.broadinstitute.consent.http.exceptions.LibraryCardRequiredException;
28
import org.broadinstitute.consent.http.exceptions.NIHComplianceRuleException;
29
import org.broadinstitute.consent.http.exceptions.SubmittedDARCannotBeEditedException;
30
import org.broadinstitute.consent.http.exceptions.UnknownIdentifierException;
31
import org.broadinstitute.consent.http.exceptions.UnprocessableEntityException;
32
import org.broadinstitute.consent.http.models.Error;
33
import org.broadinstitute.consent.http.models.User;
34
import org.broadinstitute.consent.http.util.ConsentLogger;
35
import org.broadinstitute.consent.http.util.gson.GsonUtil;
36
import org.glassfish.jersey.media.multipart.ContentDisposition;
37
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
38
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
39
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
40
import org.owasp.fileio.FileValidator;
41
import org.postgresql.util.PSQLException;
42
import org.postgresql.util.PSQLState;
43
import org.slf4j.LoggerFactory;
44

45

46
/**
47
 * Created by egolin on 9/17/14.
48
 * <p/>
49
 * Abstract superclass for all Resources.
50
 */
51
abstract public class Resource implements ConsentLogger {
1✔
52

53
  // Resource based role names
54
  public static final String ADMIN = "Admin";
55
  public static final String ALUMNI = "Alumni";
56
  public static final String CHAIRPERSON = "Chairperson";
57
  public static final String MEMBER = "Member";
58
  public static final String RESEARCHER = "Researcher";
59
  public static final String SERVICE_ACCOUNT = "ServiceAccount";
60
  // nosemgrep
61
  public static final String SIGNINGOFFICIAL = "SigningOfficial";
62
  public static final String DATASUBMITTER = "DataSubmitter";
63
  public static final String ITDIRECTOR = "ITDirector";
64

65
  // NOTE: implement more Postgres vendor codes as we encounter them
66
  private static final Map<String, ImmutablePair<Integer, String>> vendorCodeStatusMap = Map.ofEntries(
1✔
67
      Map.entry(PSQLState.UNKNOWN_STATE.getState(),
1✔
68
          ImmutablePair.of(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
1✔
69
              "Database error")),
70
      Map.entry(PSQLState.UNIQUE_VIOLATION.getState(),
1✔
71
          ImmutablePair.of(Response.Status.CONFLICT.getStatusCode(), "Database conflict")),
1✔
72
      Map.entry("22021",
1✔
73
          ImmutablePair.of(Response.Status.BAD_REQUEST.getStatusCode(), "Invalid byte sequence"))
1✔
74
  );
75
  private static final Map<Class<? extends Throwable>, ExceptionHandler> DISPATCH = new HashMap<>();
1✔
76

77
  static {
78
    DISPATCH.put(SubmittedDARCannotBeEditedException.class, e ->
1✔
79
        Response.status(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY)
1✔
80
            .type(MediaType.APPLICATION_JSON)
1✔
81
            .entity(new Error(e.getMessage(), HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY))
1✔
82
            .build());
1✔
83
    DISPATCH.put(LibraryCardRequiredException.class, e ->
1✔
84
        Response.status(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY)
×
85
            .type(MediaType.APPLICATION_JSON)
×
86
            .entity(new Error(e.getMessage(), HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY))
×
UNCOV
87
            .build());
×
88
    DISPATCH.put(NIHComplianceRuleException.class, e ->
1✔
89
        Response.status(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY)
×
90
            .type(MediaType.APPLICATION_JSON)
×
91
            .entity(new Error(e.getMessage(), HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY))
×
UNCOV
92
            .build());
×
93
    DISPATCH.put(ConsentConflictException.class, e ->
1✔
94
        Response.status(Response.Status.CONFLICT).type(MediaType.APPLICATION_JSON)
1✔
95
            .entity(new Error(e.getMessage(), Response.Status.CONFLICT.getStatusCode())).build());
1✔
96
    DISPATCH.put(UnprocessableEntityException.class, e ->
1✔
97
        Response.status(HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY)
1✔
98
            .type(MediaType.APPLICATION_JSON)
1✔
99
            .entity(new Error(e.getMessage(), HttpStatusCodes.STATUS_CODE_UNPROCESSABLE_ENTITY))
1✔
100
            .build());
1✔
101
    DISPATCH.put(UnsupportedOperationException.class, e ->
1✔
102
        Response.status(Response.Status.CONFLICT).type(MediaType.APPLICATION_JSON)
×
UNCOV
103
            .entity(new Error(e.getMessage(), Response.Status.CONFLICT.getStatusCode())).build());
×
104
    DISPATCH.put(IllegalArgumentException.class, e ->
1✔
105
        Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON)
1✔
106
            .entity(new Error(e.getMessage(), Response.Status.BAD_REQUEST.getStatusCode()))
1✔
107
            .build());
1✔
108
    DISPATCH.put(IOException.class, e ->
1✔
109
        Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON)
×
110
            .entity(new Error(e.getMessage(), Response.Status.BAD_REQUEST.getStatusCode()))
×
UNCOV
111
            .build());
×
112
    DISPATCH.put(BadRequestException.class, e ->
1✔
113
        Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON)
1✔
114
            .entity(new Error(e.getMessage(), Response.Status.BAD_REQUEST.getStatusCode()))
1✔
115
            .build());
1✔
116
    DISPATCH.put(MalformedJsonException.class, e ->
1✔
117
        Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON)
×
118
            .entity(new Error(e.getMessage(), Response.Status.BAD_REQUEST.getStatusCode()))
×
UNCOV
119
            .build());
×
120
    DISPATCH.put(JsonSyntaxException.class, e ->
1✔
121
        Response.status(Response.Status.BAD_REQUEST).type(MediaType.APPLICATION_JSON)
1✔
122
            .entity(new Error(e.getMessage(), Response.Status.BAD_REQUEST.getStatusCode()))
1✔
123
            .build());
1✔
124
    DISPATCH.put(NotAuthorizedException.class, e ->
1✔
125
        Response.status(Response.Status.UNAUTHORIZED).type(MediaType.APPLICATION_JSON)
1✔
126
            .entity(new Error(e.getMessage(), Response.Status.UNAUTHORIZED.getStatusCode()))
1✔
127
            .build());
1✔
128
    DISPATCH.put(ForbiddenException.class, e ->
1✔
129
        Response.status(Response.Status.FORBIDDEN).type(MediaType.APPLICATION_JSON)
1✔
130
            .entity(new Error(e.getMessage(), Response.Status.FORBIDDEN.getStatusCode())).build());
1✔
131
    DISPATCH.put(NotFoundException.class, e ->
1✔
132
        Response.status(Response.Status.NOT_FOUND).type(MediaType.APPLICATION_JSON)
1✔
133
            .entity(new Error(e.getMessage(), Response.Status.NOT_FOUND.getStatusCode())).build());
1✔
134
    DISPATCH.put(UnknownIdentifierException.class, e ->
1✔
135
        Response.status(Response.Status.NOT_FOUND).type(MediaType.APPLICATION_JSON)
×
UNCOV
136
            .entity(new Error(e.getMessage(), Response.Status.NOT_FOUND.getStatusCode())).build());
×
137
    DISPATCH.put(UnableToExecuteStatementException.class,
1✔
138
        Resource::unableToExecuteExceptionHandler);
139
    DISPATCH.put(PSQLException.class,
1✔
140
        Resource::unableToExecuteExceptionHandler);
141
    DISPATCH.put(SQLSyntaxErrorException.class, e ->
1✔
142
        errorLoggedExceptionHandler(e,
×
UNCOV
143
            new Error("Database Error", Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())));
×
144
    DISPATCH.put(SQLException.class, e ->
1✔
145
        errorLoggedExceptionHandler(e,
×
UNCOV
146
            new Error("Database Error", Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())));
×
147
    DISPATCH.put(Exception.class, e ->
1✔
148
        errorLoggedExceptionHandler(e,
1✔
149
            new Error(Response.Status.INTERNAL_SERVER_ERROR.getReasonPhrase(),
1✔
150
                Response.Status.INTERNAL_SERVER_ERROR.getStatusCode())));
1✔
151
  }
1✔
152

153
  private static Response errorLoggedExceptionHandler(Exception e, Error error) {
154
    LoggerFactory.getLogger(Resource.class.getName()).error(e.getMessage());
1✔
155
    // static makes using the interface less flexible
156
    Sentry.captureEvent(new SentryEvent(e));
1✔
157
    return Response.serverError().type(MediaType.APPLICATION_JSON).entity(error).build();
1✔
158
  }
159

160
  //Helper method to process generic JDBI Postgres exceptions for responses
161
  protected static Response unableToExecuteExceptionHandler(Exception e) {
162
    //default status definition
163
    LoggerFactory.getLogger(Resource.class.getName()).error(e.getMessage());
1✔
164
    // static makes using the interface less flexible
165
    Sentry.captureEvent(new SentryEvent(e));
1✔
166

167
    var status = vendorCodeStatusMap.get(PSQLState.UNKNOWN_STATE.getState());
1✔
168

169
    try {
170
      if (e.getCause() instanceof PSQLException) {
1✔
171
        String vendorCode = ((PSQLException) e.getCause()).getSQLState();
1✔
172
        if (vendorCodeStatusMap.containsKey(vendorCode)) {
1✔
173
          status = vendorCodeStatusMap.get(vendorCode);
1✔
174
        }
175
      }
UNCOV
176
    } catch (Exception error) {
×
177
      //no need to handle, default status already assigned
178
    }
1✔
179

180
    int statusCode = status.getLeft();
1✔
181
    String message = status.getRight();
1✔
182

183
    return Response.status(statusCode)
1✔
184
        .type(MediaType.APPLICATION_JSON)
1✔
185
        .entity(new Error(message, statusCode))
1✔
186
        .build();
1✔
187
  }
188

189
  protected Response createExceptionResponse(Exception e) {
190
    try {
191
      logWarn("Returning error response to client: " + e.getMessage());
1✔
192
      ExceptionHandler handler = DISPATCH.get(e.getClass());
1✔
193
      if (handler != null) {
1✔
194
        return handler.handle(e);
1✔
195
      } else {
196
        logException(e);
1✔
197
        return Response.serverError().type(MediaType.APPLICATION_JSON).entity(
1✔
198
                new Error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()))
1✔
199
            .build();
1✔
200
      }
201
    } catch (Throwable t) {
×
202
      logThrowable(t);
×
203
      return Response.serverError().type(MediaType.APPLICATION_JSON)
×
204
          .entity(new Error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()))
×
UNCOV
205
          .build();
×
206
    }
207
  }
208

209
  StreamingOutput createStreamingOutput(InputStream inputStream) {
210
    return output -> {
1✔
211
      try {
212
        output.write(IOUtils.toByteArray(inputStream));
1✔
213
      } catch (Exception e) {
×
214
        logException(e);
×
UNCOV
215
        throw e;
×
216
      }
1✔
217
    };
1✔
218
  }
219

220
  protected void validateFileDetails(ContentDisposition contentDisposition) {
221
    FileValidator validator = new FileValidator();
1✔
222
    boolean validName = validator.isValidFileName("validating uploaded file name",
1✔
223
        contentDisposition.getFileName(), true);
1✔
224
    if (!validName) {
1✔
225
      throw new IllegalArgumentException("File name is invalid");
1✔
226
    }
227
    boolean validSize = validator.getMaxFileUploadSize() >= contentDisposition.getSize();
1✔
228
    if (!validSize) {
1✔
229
      throw new IllegalArgumentException(
1✔
230
          "File size is invalid. Max size is: " + validator.getMaxFileUploadSize() / 1000000
1✔
231
              + " MB");
232
    }
233
  }
1✔
234

235
  /**
236
   * Validate that the current authenticated user can access this resource. If the user has one of
237
   * the provided roles, then access is allowed. If not, then the authenticated user must have the
238
   * same identity as the `userId` parameter they are requesting information for.
239
   * <p>
240
   * Typically, we use this to ensure that a non-privileged user is the creator of an entity. In
241
   * those cases, pass in an empty list of privileged roles.
242
   * <p>
243
   * Privileged users such as admins, chairpersons, and members, may be allowed access to some
244
   * resources even if they are not the creator/owner.
245
   *
246
   * @param privilegedRoles List of privileged UserRoles enums
247
   * @param authedUser      The authenticated User
248
   * @param userId          The user id that the authenticated user is requesting access for
249
   */
250
  void validateAuthedRoleUser(final List<UserRoles> privilegedRoles, final User authedUser,
251
      final Integer userId) {
252
    List<Integer> authedRoleIds = privilegedRoles.stream().
1✔
253
        map(UserRoles::getRoleId).toList();
1✔
254
    boolean authedUserHasRole = authedUser.getRoles().stream().
1✔
255
        anyMatch(userRole -> authedRoleIds.contains(userRole.getRoleId()));
1✔
256
    if (!authedUserHasRole && !authedUser.getUserId().equals(userId)) {
1✔
257
      throw new ForbiddenException("User does not have permission");
1✔
258
    }
259
  }
1✔
260

261
  /**
262
   * Validate that the user has the actual role name provided. This is useful for determining when a
263
   * user hits an endpoint that is permitted to multiple different roles and is requesting a
264
   * role-specific view of a data entity.
265
   * <p>
266
   * In these cases, we need to make sure that the role name provided is a real one and that the
267
   * user actually has that role to prevent escalated privilege violations.
268
   *
269
   * @param user     The User
270
   * @param roleName The UserRole name
271
   */
272
  void validateUserHasRoleName(User user, String roleName) {
273
    UserRoles thisRole = UserRoles.getUserRoleFromName(roleName);
1✔
274
    if (Objects.isNull(thisRole) || !user.hasUserRole(thisRole)) {
1✔
275
      throw new BadRequestException("Invalid role selection: " + roleName);
1✔
276
    }
277
  }
1✔
278

279
  /**
280
   * Unmarshal/serialize an object using `Gson`. In general, we should prefer Gson over Jackson for
281
   * ease of use and the need for far less boilerplate code.
282
   *
283
   * @param o The object to unmarshal
284
   * @return String version of the object
285
   */
286
  protected String unmarshal(Object o) {
287
    return GsonUtil.buildGson().toJson(o);
1✔
288
  }
289

290
  /**
291
   * Finds and validates all the files uploaded to the multipart.
292
   *
293
   * @param multipart Form data
294
   * @return Map of file body parts, where the key is the name of the field and the value is the
295
   * body part including the file(s).
296
   */
297
  protected Map<String, FormDataBodyPart> extractFilesFromMultiPart(FormDataMultiPart multipart) {
298
    if (Objects.isNull(multipart)) {
1✔
299
      return Map.of();
1✔
300
    }
301

302
    Map<String, FormDataBodyPart> files = new HashMap<>();
1✔
303
    for (List<FormDataBodyPart> parts : multipart.getFields().values()) {
1✔
304
      for (FormDataBodyPart part : parts) {
1✔
305
        if (Objects.nonNull(part.getContentDisposition().getFileName())) {
1✔
306
          validateFileDetails(part.getContentDisposition());
1✔
307
          files.put(part.getName(), part);
1✔
308
        }
309
      }
1✔
310
    }
1✔
311

312
    return files;
1✔
313
  }
314

315
  private interface ExceptionHandler {
316

317
    Response handle(Exception e);
318
  }
319
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc