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

DataBiosphere / consent / #5676

17 Apr 2025 12:57PM UTC coverage: 79.034% (-0.04%) from 79.075%
#5676

push

web-flow
[DT-1502] Enforce Library card rule on all DAR creation; return specific Error text when this rule is being enforced. (#2481)

82 of 92 new or added lines in 8 files covered. (89.13%)

1 existing line in 1 file now uncovered.

10261 of 12983 relevant lines covered (79.03%)

0.79 hits per line

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

84.35
/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.UnknownIdentifierException;
30
import org.broadinstitute.consent.http.exceptions.UnprocessableEntityException;
31
import org.broadinstitute.consent.http.models.Error;
32
import org.broadinstitute.consent.http.models.User;
33
import org.broadinstitute.consent.http.util.ConsentLogger;
34
import org.broadinstitute.consent.http.util.gson.GsonUtil;
35
import org.glassfish.jersey.media.multipart.ContentDisposition;
36
import org.glassfish.jersey.media.multipart.FormDataBodyPart;
37
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
38
import org.jdbi.v3.core.statement.UnableToExecuteStatementException;
39
import org.owasp.fileio.FileValidator;
40
import org.postgresql.util.PSQLException;
41
import org.postgresql.util.PSQLState;
42
import org.slf4j.LoggerFactory;
43

44

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

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

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

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

146
  private static Response errorLoggedExceptionHandler(Exception e, Error error) {
147
    LoggerFactory.getLogger(Resource.class.getName()).error(e.getMessage());
1✔
148
    // static makes using the interface less flexible
149
    Sentry.captureEvent(new SentryEvent(e));
1✔
150
    return Response.serverError().type(MediaType.APPLICATION_JSON).entity(error).build();
1✔
151
  }
152

153
  //Helper method to process generic JDBI Postgres exceptions for responses
154
  protected static Response unableToExecuteExceptionHandler(Exception e) {
155
    //default status definition
156
    LoggerFactory.getLogger(Resource.class.getName()).error(e.getMessage());
1✔
157
    // static makes using the interface less flexible
158
    Sentry.captureEvent(new SentryEvent(e));
1✔
159

160
    var status = vendorCodeStatusMap.get(PSQLState.UNKNOWN_STATE.getState());
1✔
161

162
    try {
163
      if (e.getCause() instanceof PSQLException) {
1✔
164
        String vendorCode = ((PSQLException) e.getCause()).getSQLState();
1✔
165
        if (vendorCodeStatusMap.containsKey(vendorCode)) {
1✔
166
          status = vendorCodeStatusMap.get(vendorCode);
1✔
167
        }
168
      }
169
    } catch (Exception error) {
×
170
      //no need to handle, default status already assigned
171
    }
1✔
172

173
    int statusCode = status.getLeft();
1✔
174
    String message = status.getRight();
1✔
175

176
    return Response.status(statusCode)
1✔
177
        .type(MediaType.APPLICATION_JSON)
1✔
178
        .entity(new Error(message, statusCode))
1✔
179
        .build();
1✔
180
  }
181

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

202
  StreamingOutput createStreamingOutput(InputStream inputStream) {
203
    return output -> {
1✔
204
      try {
205
        output.write(IOUtils.toByteArray(inputStream));
1✔
NEW
206
      } catch (Exception e) {
×
NEW
207
        logException(e);
×
NEW
208
        throw e;
×
209
      }
1✔
210
    };
1✔
211
  }
212

213
  protected void validateFileDetails(ContentDisposition contentDisposition) {
214
    FileValidator validator = new FileValidator();
1✔
215
    boolean validName = validator.isValidFileName("validating uploaded file name",
1✔
216
        contentDisposition.getFileName(), true);
1✔
217
    if (!validName) {
1✔
218
      throw new IllegalArgumentException("File name is invalid");
1✔
219
    }
220
    boolean validSize = validator.getMaxFileUploadSize() >= contentDisposition.getSize();
1✔
221
    if (!validSize) {
1✔
222
      throw new IllegalArgumentException(
1✔
223
          "File size is invalid. Max size is: " + validator.getMaxFileUploadSize() / 1000000
1✔
224
              + " MB");
225
    }
226
  }
1✔
227

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

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

272
  /**
273
   * Unmarshal/serialize an object using `Gson`. In general, we should prefer Gson over Jackson for
274
   * ease of use and the need for far less boilerplate code.
275
   *
276
   * @param o The object to unmarshal
277
   * @return String version of the object
278
   */
279
  protected String unmarshal(Object o) {
280
    return GsonUtil.buildGson().toJson(o);
1✔
281
  }
282

283
  /**
284
   * Finds and validates all the files uploaded to the multipart.
285
   *
286
   * @param multipart Form data
287
   * @return Map of file body parts, where the key is the name of the field and the value is the
288
   * body part including the file(s).
289
   */
290
  protected Map<String, FormDataBodyPart> extractFilesFromMultiPart(FormDataMultiPart multipart) {
291
    if (Objects.isNull(multipart)) {
1✔
292
      return Map.of();
1✔
293
    }
294

295
    Map<String, FormDataBodyPart> files = new HashMap<>();
1✔
296
    for (List<FormDataBodyPart> parts : multipart.getFields().values()) {
1✔
297
      for (FormDataBodyPart part : parts) {
1✔
298
        if (Objects.nonNull(part.getContentDisposition().getFileName())) {
1✔
299
          validateFileDetails(part.getContentDisposition());
1✔
300
          files.put(part.getName(), part);
1✔
301
        }
302
      }
1✔
303
    }
1✔
304

305
    return files;
1✔
306
  }
307

308
  private interface ExceptionHandler {
309

310
    Response handle(Exception e);
311
  }
312
}
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