diff --git a/pic-sure-api-data/pom.xml b/pic-sure-api-data/pom.xml index e29e6118..55b6eecd 100755 --- a/pic-sure-api-data/pom.xml +++ b/pic-sure-api-data/pom.xml @@ -37,5 +37,10 @@ org.hibernate hibernate-core + + io.swagger.core.v3 + swagger-annotations + 2.2.8 + diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/AuthUser.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/AuthUser.java new file mode 100755 index 00000000..629ad310 --- /dev/null +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/AuthUser.java @@ -0,0 +1,69 @@ +package edu.harvard.dbmi.avillach.data.entity; + +import java.security.Principal; + +import javax.json.Json; + +/* + * This class is used to mirror the User object from the auth DB to maintain schema separation. - nc + */ +public class AuthUser extends BaseEntity implements Principal { + private String userId; + + private String subject; + + private String roles; + + private String email; + + public String getUserId() { + return userId; + } + + public AuthUser setUserId(String userId) { + this.userId = userId; + return this; + } + + public String getSubject() { + return subject; + } + + public AuthUser setSubject(String subject) { + this.subject = subject; + return this; + } + + public String getRoles() { + return roles; + } + + public AuthUser setRoles(String roles) { + this.roles = roles; + return this; + } + + public String getEmail(){ + return email; + } + + public AuthUser setEmail(String email){ + this.email = email; + return this; + } + + @Override // Principal method + public String getName() { + return getEmail(); + } + + @Override + public String toString() { + return Json.createObjectBuilder() + .add("userId", userId) + .add("subject", subject) + .add("email", email) + .add("roles", roles) + .build().toString(); + } +} diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/NamedDataset.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/NamedDataset.java new file mode 100644 index 00000000..23c9f762 --- /dev/null +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/NamedDataset.java @@ -0,0 +1,101 @@ +package edu.harvard.dbmi.avillach.data.entity; + +import java.util.Map; + +import javax.json.Json; +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import io.swagger.v3.oas.annotations.media.Schema; + +import edu.harvard.dbmi.avillach.data.entity.convert.JsonConverter; + +@Schema(description = "A NamedDataset object containing query, name, user, and archived status.") +@Entity(name = "named_dataset") +@Table(uniqueConstraints = { + @UniqueConstraint(name = "unique_queryId_user", columnNames = { "queryId", "user" }) +}) +public class NamedDataset extends BaseEntity { + @Schema(description = "The associated Query") + @OneToOne + @JoinColumn(name = "queryId") + private Query query; + + @Schema(description = "The user identifier") + @Column(length = 255) + private String user; + + @Schema(description = "The name user has assigned to this dataset") + @Column(length = 255) + private String name; + + @Schema(description = "The archived state") + private Boolean archived = false; + + @Schema(description = "A json string object containing override specific values") + @Column(length = 8192) + @Convert(converter = JsonConverter.class) + private Map metadata; + + public NamedDataset setName(String name) { + this.name = name; + return this; + } + + public String getName() { + return name; + } + + public NamedDataset setArchived(Boolean archived) { + this.archived = archived; + return this; + } + + public Boolean getArchived() { + return archived; + } + + public NamedDataset setQuery(Query query) { + this.query = query; + return this; + } + + public Query getQuery(){ + return query; + } + + public NamedDataset setUser(String user) { + this.user = user; + return this; + } + + public String getUser(){ + return user; + } + + public Map getMetadata(){ + return metadata; + } + + public NamedDataset setMetadata(Map metadata){ + this.metadata = metadata; + return this; + } + + @Override + public String toString() { + return Json.createObjectBuilder() + .add("uuid", uuid.toString()) + .add("name", name) + .add("archived", archived) + .add("queryId", query.getUuid().toString()) + .add("user", user) + .add("metadata", metadata.toString()) + .build().toString(); + } +} diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/User.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/User.java deleted file mode 100755 index 3c07c542..00000000 --- a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/User.java +++ /dev/null @@ -1,43 +0,0 @@ -package edu.harvard.dbmi.avillach.data.entity; - - -/* - * This class gets created as part of the pic-sure DB schema, but no objects of this type are ever persisted. - * Its use is to mirror the User object from the auth DB to maintain schema separation. - nc - * - */ -public class User extends BaseEntity { - - private String userId; - - private String subject; - - private String roles; - - public String getUserId() { - return userId; - } - - public User setUserId(String userId) { - this.userId = userId; - return this; - } - - public String getSubject() { - return subject; - } - - public User setSubject(String subject) { - this.subject = subject; - return this; - } - - public String getRoles() { - return roles; - } - - public User setRoles(String roles) { - this.roles = roles; - return this; - } -} diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/convert/JsonConverter.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/convert/JsonConverter.java new file mode 100644 index 00000000..77d7decc --- /dev/null +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/convert/JsonConverter.java @@ -0,0 +1,50 @@ +package edu.harvard.dbmi.avillach.data.entity.convert; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import javax.persistence.AttributeConverter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class JsonConverter implements AttributeConverter, String> { + private final Logger logger = LoggerFactory.getLogger(JsonConverter.class); + + @Override + public String convertToDatabaseColumn(Map objectData) { + if (objectData == null) { + return "{}"; + } + + String jsonData = null; + try { + jsonData = new ObjectMapper().writeValueAsString(objectData); + } catch (final JsonProcessingException e) { + logger.error("JSON writing error", e); + } + + return jsonData; + } + + @Override + public Map convertToEntityAttribute(String jsonData) { + if (jsonData == null) { + return new HashMap(); + } + + Map objectData = null; + try { + objectData = new ObjectMapper().readValue(jsonData, new TypeReference>() {}); + } catch (final IOException e) { + logger.error("JSON reading error", e); + } + + return objectData; + } +} \ No newline at end of file diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/NamedDatasetRepository.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/NamedDatasetRepository.java new file mode 100644 index 00000000..5dd6ffa3 --- /dev/null +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/NamedDatasetRepository.java @@ -0,0 +1,13 @@ +package edu.harvard.dbmi.avillach.data.repository; + +import edu.harvard.dbmi.avillach.data.entity.NamedDataset; + +import javax.enterprise.context.ApplicationScoped; +import javax.transaction.Transactional; +import java.util.UUID; + +@Transactional +@ApplicationScoped +public class NamedDatasetRepository extends BaseRepository{ + protected NamedDatasetRepository() {super(NamedDataset.class);} +} diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/request/NamedDatasetRequest.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/request/NamedDatasetRequest.java new file mode 100644 index 00000000..1f35dd3f --- /dev/null +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/request/NamedDatasetRequest.java @@ -0,0 +1,64 @@ +package edu.harvard.dbmi.avillach.data.request; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema( + description = "Request to add or update a named dataset.", + example = "{\n" + // + " \"queryId\": \"ec780aeb-d981-432a-b72b-51d4ecb3fd53\",\n" + // + " \"name\": \"My first Query\",\n" + // + " \"archived\": false\n" + // + " \"metadata\": {}\n" + // + "}" +) +public class NamedDatasetRequest { + @NotNull + private UUID queryId; + + @NotNull + @Pattern(regexp = "^[\\w\\d \\-\\\\/?+=\\[\\].():\"']+$") + private String name; + + private Boolean archived = false; + + private Map metadata = new HashMap(); + + public UUID getQueryId() { + return queryId; + } + + public void setQueryId(UUID query) { + this.queryId = query; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Boolean getArchived(){ + return archived; + } + + public void setArchived(Boolean archived) { + this.archived = archived; + } + + public Map getMetadata(){ + return metadata; + } + + public void setMetadata(Map metadata){ + this.metadata = metadata; + } +} diff --git a/pic-sure-api-data/src/main/resources/db/sql/V5__ADD_NAMED_DATASET_TABLE.sql b/pic-sure-api-data/src/main/resources/db/sql/V5__ADD_NAMED_DATASET_TABLE.sql new file mode 100644 index 00000000..2ae56962 --- /dev/null +++ b/pic-sure-api-data/src/main/resources/db/sql/V5__ADD_NAMED_DATASET_TABLE.sql @@ -0,0 +1,13 @@ +USE `picsure`; + +CREATE TABLE `named_dataset` ( + `uuid` binary(16) NOT NULL, + `queryId` binary(16) NOT NULL, + `user` varchar(255) COLLATE utf8_bin DEFAULT NULL, + `name` varchar(255) COLLATE utf8_bin DEFAULT NULL, + `archived` bit(1) NOT NULL DEFAULT FALSE, + `metadata` TEXT, + PRIMARY KEY (`uuid`), + CONSTRAINT `foreign_queryId` FOREIGN KEY (`queryId`) REFERENCES `query` (`uuid`), + CONSTRAINT `unique_queryId_user` UNIQUE (`queryId`, `user`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin; diff --git a/pic-sure-api-data/src/test/java/edu/harvard/dbmi/avillach/DataTest.java b/pic-sure-api-data/src/test/java/edu/harvard/dbmi/avillach/DataTest.java index c880e737..32279163 100755 --- a/pic-sure-api-data/src/test/java/edu/harvard/dbmi/avillach/DataTest.java +++ b/pic-sure-api-data/src/test/java/edu/harvard/dbmi/avillach/DataTest.java @@ -1,26 +1,26 @@ -package edu.harvard.dbmi.avillach; - -import edu.harvard.dbmi.avillach.data.entity.BaseEntity; -import edu.harvard.dbmi.avillach.data.entity.User; -import org.junit.Test; - -import java.util.UUID; - -import static org.junit.Assert.*; - -/** - * Unit test for simple App. - */ -public class DataTest { - - @Test - public void BaseEntityBasicFunctionsTest() { - BaseEntity user = new User(); - user.setUuid(UUID.fromString("6ef9387a-4cde-4253-bd47-0bdc74ff76ab")); - - BaseEntity user2 = new User(); - user2.setUuid(UUID.fromString("6ef9387a-4cde-4253-bd47-0bdc74ff76ab")); - - assertEquals(user, user2); - } -} +package edu.harvard.dbmi.avillach; + +import edu.harvard.dbmi.avillach.data.entity.BaseEntity; +import edu.harvard.dbmi.avillach.data.entity.AuthUser; +import org.junit.Test; + +import java.util.UUID; + +import static org.junit.Assert.*; + +/** + * Unit test for simple App. + */ +public class DataTest { + + @Test + public void BaseEntityBasicFunctionsTest() { + BaseEntity user = new AuthUser(); + user.setUuid(UUID.fromString("6ef9387a-4cde-4253-bd47-0bdc74ff76ab")); + + BaseEntity user2 = new AuthUser(); + user2.setUuid(UUID.fromString("6ef9387a-4cde-4253-bd47-0bdc74ff76ab")); + + assertEquals(user, user2); + } +} diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/NamedDatasetRS.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/NamedDatasetRS.java new file mode 100644 index 00000000..363ce129 --- /dev/null +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/NamedDatasetRS.java @@ -0,0 +1,184 @@ +package edu.harvard.dbmi.avillach; + +import java.util.UUID; + +import javax.inject.Inject; + +import javax.validation.Valid; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +import edu.harvard.dbmi.avillach.data.entity.NamedDataset; +import edu.harvard.dbmi.avillach.data.request.NamedDatasetRequest; +import edu.harvard.dbmi.avillach.service.NamedDatasetService; +import edu.harvard.dbmi.avillach.util.response.PICSUREResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; + +@Path("/dataset/named") +@Produces("application/json") +@Consumes("application/json") +public class NamedDatasetRS { + @Inject + NamedDatasetService namedDatasetService; + + @GET + @Path("/") + @Operation( + summary = "Returns a list of named datasets saved by the authenticated user.", + tags = { "dataset" }, + operationId = "namedDataset", + responses = { + @ApiResponse( + responseCode = "200", + description = "A list of named datasets saved by the authenticated user.", + content = @Content( + schema = @Schema( + implementation = NamedDataset.class + ) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Error finding any named datasets for user.", + content = @Content( + examples = {@ExampleObject( + name = "namedDatasets", + value = "{\"errorType\":\"error\",\"message\":\"Could not retrieve named datasets\"}" + )} + ) + ) + } + ) + public Response namedDatasets( + @Context SecurityContext context + ) { + String user = context.getUserPrincipal().getName(); + return namedDatasetService.getNamedDatasets(user) + .map(PICSUREResponse::success) + .orElse(PICSUREResponse.error("Could not retrieve named datasets")); + } + + @POST + @Path("/") + @Operation( + summary = "Returns a named dataset saved by the authenticated user.", + tags = { "dataset" }, + operationId = "addNamedDataset", + responses = { + @ApiResponse( + responseCode = "200", + description = "The named dataset saved by the authenticated user.", + content = @Content( + schema = @Schema( + implementation = NamedDataset.class + ) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Error adding any named datasets.", + content = @Content( + examples = {@ExampleObject( + name = "error", + value = "{\"errorType\":\"error\",\"message\":\"Could not save named dataset\"}" + )} + ) + ) + } + ) + public Response addNamedDataset( + @Context SecurityContext context, + @Parameter @Valid NamedDatasetRequest request + ) { + String user = context.getUserPrincipal().getName(); + return namedDatasetService.addNamedDataset(user, request) + .map(PICSUREResponse::success) + .orElse(PICSUREResponse.error("Could not save named dataset")); + } + + @GET + @Path("/{namedDatasetId}/") + @Operation( + summary = "Returns a named dataset requested by the authenticated user.", + tags = { "dataset" }, + operationId = "getNamedDatasetById", + responses = { + @ApiResponse( + responseCode = "200", + description = "The named dataset requested by the authenticated user.", + content = @Content( + schema = @Schema( + implementation = NamedDataset.class + ) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Error finding the named dataset.", + content = @Content( + examples = {@ExampleObject( + name = "error", + value = "{\"errorType\":\"error\",\"message\":\"Could not retrieve named dataset\"}" + )} + ) + ) + } + ) + public Response getNamedDatasetById( + @Context SecurityContext context, + @PathParam("namedDatasetId") UUID datasetId + ){ + String user = context.getUserPrincipal().getName(); + return namedDatasetService.getNamedDatasetById(user, datasetId) + .map(PICSUREResponse::success) + .orElse(PICSUREResponse.error("Could not retrieve named dataset")); + } + + @PUT + @Path("/{namedDatasetId}/") + @Operation( + summary = "Updates a named dataset that the authenticated user perviously saved.", + tags = { "dataset" }, + operationId = "updateNamedDataset", + responses = { + @ApiResponse( + responseCode = "200", + description = "The named dataset updated by the authenticated user.", + content = @Content( + schema = @Schema( + implementation = NamedDataset.class + ) + ) + ), + @ApiResponse( + responseCode = "500", + description = "Error updating the named dataset.", + content = @Content( + examples = {@ExampleObject( + name = "error", + value = "{\"errorType\":\"error\",\"message\":\"Could not update named dataset\"}" + )} + ) + ) + } + ) + public Response updateNamedDataset( + @Context SecurityContext context, + @PathParam("namedDatasetId") UUID datasetId, + @Parameter @Valid NamedDatasetRequest request + ){ + String user = context.getUserPrincipal().getName(); + return namedDatasetService.updateNamedDataset(user, datasetId, request) + .map(PICSUREResponse::success) + .orElse(PICSUREResponse.error("Could not update named dataset")); + } +} diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/security/AuthSecurityContext.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/security/AuthSecurityContext.java new file mode 100644 index 00000000..2f692fa1 --- /dev/null +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/security/AuthSecurityContext.java @@ -0,0 +1,47 @@ +package edu.harvard.dbmi.avillach.security; + +import edu.harvard.dbmi.avillach.data.entity.AuthUser; + +import javax.json.Json; +import javax.ws.rs.core.SecurityContext; +import java.security.Principal; + +public class AuthSecurityContext implements SecurityContext { + private AuthUser user; + private String scheme; + + public AuthSecurityContext(AuthUser user, String scheme) { + this.user = user; + this.scheme = scheme; + } + + @Override + public Principal getUserPrincipal() { + return this.user; + } + + @Override + public boolean isUserInRole(String role) { + if (user.getRoles() != null) + return user.getRoles().contains(role); + return false; + } + + @Override + public boolean isSecure() { + return "https".equals(this.scheme); + } + + @Override + public String getAuthenticationScheme() { + return SecurityContext.DIGEST_AUTH; + } + + @Override + public String toString(){ + return Json.createObjectBuilder() + .add("scheme", scheme) + .add("user", user.getName()) + .build().toString(); + } +} \ No newline at end of file diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/security/JWTFilter.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/security/JWTFilter.java index b5d945f2..82709920 100755 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/security/JWTFilter.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/security/JWTFilter.java @@ -1,277 +1,281 @@ -package edu.harvard.dbmi.avillach.security; - -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import edu.harvard.dbmi.avillach.PicSureWarInit; -import edu.harvard.dbmi.avillach.data.entity.Query; -import edu.harvard.dbmi.avillach.data.entity.User; -import edu.harvard.dbmi.avillach.data.repository.QueryRepository; -import edu.harvard.dbmi.avillach.data.repository.ResourceRepository; -import edu.harvard.dbmi.avillach.domain.QueryRequest; -import edu.harvard.dbmi.avillach.service.ResourceWebClient; -import edu.harvard.dbmi.avillach.util.exception.ApplicationException; -import edu.harvard.dbmi.avillach.util.response.PICSUREResponse; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.util.EntityUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Resource; -import javax.inject.Inject; -import javax.ws.rs.HttpMethod; -import javax.ws.rs.NotAuthorizedException; -import javax.ws.rs.container.ContainerRequestContext; -import javax.ws.rs.container.ContainerRequestFilter; -import javax.ws.rs.container.ResourceInfo; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriInfo; -import javax.ws.rs.ext.Provider; - -import java.io.*; -import java.util.*; - -import static edu.harvard.dbmi.avillach.util.Utilities.buildHttpClientContext; - -@Provider -public class JWTFilter implements ContainerRequestFilter { - - private final Logger logger = LoggerFactory.getLogger(JWTFilter.class); - - @Context - UriInfo uriInfo; - - @Context - ResourceInfo resourceInfo; - - @Inject - ResourceRepository resourceRepo; - - @Inject - ResourceWebClient resourceWebClient; - - @Resource(mappedName = "java:global/user_id_claim") - private String userIdClaim; - - ObjectMapper mapper = new ObjectMapper(); - - @Inject - PicSureWarInit picSureWarInit; - - @Inject - QueryRepository queryRepo; - - @Override - public void filter(ContainerRequestContext requestContext) throws IOException { - logger.debug("Entered jwtfilter.filter()..."); - - if (uriInfo.getPath().endsWith("/openapi.json")) { - return; - } - - if(requestContext.getUriInfo().getPath().contentEquals("/system/status") - && requestContext.getRequest().getMethod().contentEquals(HttpMethod.GET)) { - // GET calls to /system/status do not require authentication or authorization - requestContext.setProperty("username", "SYSTEM_MONITOR"); - }else { - // Everything else goes through PSAMA token introspection - String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - if (authorizationHeader == null || authorizationHeader.isEmpty()) { - throw new NotAuthorizedException("No authorization header found."); - } - String token = authorizationHeader.substring(6).trim(); - - String userForLogging = null; - - try { - User authenticatedUser = null; - - authenticatedUser = callTokenIntroEndpoint(requestContext, token, userIdClaim); - - if (authenticatedUser == null) { - logger.error("Cannot extract a user from token: " + token); - throw new NotAuthorizedException("Cannot find or create a user"); - } - - userForLogging = authenticatedUser.getUserId(); - - //The request context wants to remember who the user is - requestContext.setProperty("username", userForLogging); - - logger.info("User - " + userForLogging + " - has just passed all the authentication and authorization layers."); - - } catch (NotAuthorizedException e) { - // the detail of this exception should be logged right before the exception thrown out - // logger.error("User - " + userForLogging + " - is not authorized. " + e.getChallenges()); - // we should show different response based on role - requestContext.abortWith(PICSUREResponse.unauthorizedError("User is not authorized. " + e.getChallenges())); - } catch (Exception e){ - // we should show different response based on role - e.printStackTrace(); - requestContext.abortWith(PICSUREResponse.applicationError("Inner application error, please contact system admin")); - } - } - } - - /** - * - * @param token - * @param userIdClaim - * @return - * @throws IOException - */ - - private User callTokenIntroEndpoint(ContainerRequestContext requestContext, String token, String userIdClaim) { - logger.debug("TokenIntrospection - extractUserFromTokenIntrospection() starting..."); - - String token_introspection_url = picSureWarInit.getToken_introspection_url(); - String token_introspection_token = picSureWarInit.getToken_introspection_token(); - - if (token_introspection_url.isEmpty()) - throw new ApplicationException("token_introspection_url is empty"); - - if (token_introspection_token.isEmpty()){ - throw new ApplicationException("token_introspection_token is empty"); - } - - ObjectMapper json = PicSureWarInit.objectMapper; - CloseableHttpClient client = PicSureWarInit.CLOSEABLE_HTTP_CLIENT; - - HttpPost post = new HttpPost(token_introspection_url); - - Map tokenMap = new HashMap<>(); - tokenMap.put("token", token); - - - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - HashMap requestMap = new HashMap(); - try { - String requestPath = requestContext.getUriInfo().getPath(); - requestMap.put("Target Service", requestPath); - - Query initialQuery = null; - //Read the query from the backing store if we are getting the results (full query may not be specified in request) - if(requestPath.startsWith("/query/") && (requestPath.endsWith("result") || requestPath.endsWith("result/"))) { - //Path: /query/{queryId}/result - String[] pathParts = requestPath.split("/"); - UUID uuid = UUID.fromString(pathParts[2]); - initialQuery = queryRepo.getById(uuid); - } - - if(initialQuery != null) { - IOUtils.copy(new ByteArrayInputStream(initialQuery.getQuery().getBytes()), buffer); - } else { - //This stream is only consumable once, so we need to save & reset it. - InputStream entityStream = requestContext.getEntityStream(); - IOUtils.copy(entityStream, buffer); - requestContext.setEntityStream(new ByteArrayInputStream(buffer.toByteArray())); - } - - if(buffer.size()>0) { - /* - * We remove the resourceCredentials from the token introspection copy of the query to prevent logging them as - * part of token introspection. These credentials are between the backing resource and the user, PIC-SURE should - * do its best to keep them confidential. - */ - Object queryObject = new ObjectMapper().readValue(new ByteArrayInputStream(buffer.toByteArray()), Object.class); - if (queryObject instanceof Collection) { - for (Object query: (Collection)queryObject) { - if (query instanceof Map) { - ((Map) query).remove("resourceCredentials"); - } - } - } else if (queryObject instanceof Map){ - ((Map) queryObject).remove("resourceCredentials"); - } - requestMap.put("query", queryObject); - - if(requestPath.startsWith("/query/")) { - - UUID resourceUUID = null; - String resourceUUIDStr = (String) ((Map)queryObject).get("resourceUUID"); - if(resourceUUIDStr != null) { - resourceUUID = UUID.fromString(resourceUUIDStr); - } - - if(resourceUUID != null) { - edu.harvard.dbmi.avillach.data.entity.Resource resource = resourceRepo.getById(resourceUUID); - //logger.info("resource obj: " + resource + " path: " + resource.getResourceRSPath()); - if (resource != null && resource.getResourceRSPath() != null){ - QueryRequest queryRequest = new QueryRequest(); - queryRequest.getResourceCredentials().put(ResourceWebClient.BEARER_TOKEN_KEY, resource.getToken()); - queryRequest.setResourceUUID(resourceUUID); - queryRequest.setQuery(((Map)queryObject).get("query")); - - Response formatResponse = resourceWebClient.queryFormat(resource.getResourceRSPath(), queryRequest); - if(formatResponse.getStatus() == 200) { - //add the formatted query if available - String formattedQuery = IOUtils.toString((InputStream)formatResponse.getEntity(), "UTF-8"); - logger.debug("Formatted response: " + formattedQuery); - requestMap.put("formattedQuery", formattedQuery); - } - } - } - } - } - tokenMap.put("request", requestMap); - } catch (JsonParseException ex) { - requestMap.put("query",buffer.toString()); - tokenMap.put("request", requestMap); - } catch (IOException e1) { - logger.error("IOException caught trying to build requestMap for auditing.", e1); - throw new NotAuthorizedException("The request could not be properly audited. If you recieve this error multiple times, please contact an administrator."); - } - StringEntity entity = null; - try { - entity = new StringEntity(json.writeValueAsString(tokenMap)); - } catch (IOException e) { - logger.error("callTokenIntroEndpoint() - " + e.getClass().getSimpleName() + " when composing post"); - return null; - } - post.setEntity(entity); - post.setHeader("Content-Type", "application/json"); - //Authorize into the token introspection endpoint - post.setHeader("Authorization", "Bearer " + token_introspection_token); - CloseableHttpResponse response = null; - try { - response = client.execute(post, buildHttpClientContext()); - if (response.getStatusLine().getStatusCode() != 200){ - logger.error("callTokenIntroEndpoint() error back from token intro host server [" - + token_introspection_url + "]: " + EntityUtils.toString(response.getEntity())); - throw new ApplicationException("Token Introspection host server return " + response.getStatusLine().getStatusCode() + - ". Please see the log"); - } - JsonNode responseContent = json.readTree(response.getEntity().getContent()); - if (!responseContent.get("active").asBoolean()){ - logger.error("callTokenIntroEndpoint() Token intro endpoint return invalid token, content: " + responseContent); - throw new NotAuthorizedException("Token invalid or expired"); - } - - String sub = responseContent.get(userIdClaim) != null ? responseContent.get(userIdClaim).asText() : null; - User user = new User().setSubject(sub).setUserId(sub); - return user; - } catch (IOException ex){ - logger.error("callTokenIntroEndpoint() IOException when hitting url: " + post - + " with exception msg: " + ex.getMessage()); - } finally { - try { - if (response != null) - response.close(); - } catch (IOException ex) { - logger.error("callTokenIntroEndpoint() IOExcpetion when closing http response: " + ex.getMessage()); - } - } - - return null; - } - - void setUserIdClaim(String userIdClaim) { - this.userIdClaim = userIdClaim; - } -} +package edu.harvard.dbmi.avillach.security; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.harvard.dbmi.avillach.PicSureWarInit; +import edu.harvard.dbmi.avillach.data.entity.Query; +import edu.harvard.dbmi.avillach.data.entity.AuthUser; +import edu.harvard.dbmi.avillach.data.repository.QueryRepository; +import edu.harvard.dbmi.avillach.data.repository.ResourceRepository; +import edu.harvard.dbmi.avillach.domain.QueryRequest; +import edu.harvard.dbmi.avillach.service.ResourceWebClient; +import edu.harvard.dbmi.avillach.util.exception.ApplicationException; +import edu.harvard.dbmi.avillach.util.response.PICSUREResponse; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.Provider; + +import java.io.*; +import java.util.*; + +import static edu.harvard.dbmi.avillach.util.Utilities.buildHttpClientContext; + +@Provider +public class JWTFilter implements ContainerRequestFilter { + + private final Logger logger = LoggerFactory.getLogger(JWTFilter.class); + + @Context + UriInfo uriInfo; + + @Context + ResourceInfo resourceInfo; + + @Inject + ResourceRepository resourceRepo; + + @Inject + ResourceWebClient resourceWebClient; + + @Resource(mappedName = "java:global/user_id_claim") + private String userIdClaim; + + ObjectMapper mapper = new ObjectMapper(); + + @Inject + PicSureWarInit picSureWarInit; + + @Inject + QueryRepository queryRepo; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + logger.debug("Entered jwtfilter.filter()..."); + + if (uriInfo.getPath().endsWith("/openapi.json")) { + return; + } + + if(requestContext.getUriInfo().getPath().contentEquals("/system/status") + && requestContext.getRequest().getMethod().contentEquals(HttpMethod.GET)) { + // GET calls to /system/status do not require authentication or authorization + requestContext.setProperty("username", "SYSTEM_MONITOR"); + }else { + // Everything else goes through PSAMA token introspection + String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); + if (authorizationHeader == null || authorizationHeader.isEmpty()) { + throw new NotAuthorizedException("No authorization header found."); + } + String token = authorizationHeader.substring(6).trim(); + + String userForLogging = null; + + try { + AuthUser authenticatedUser = null; + + authenticatedUser = callTokenIntroEndpoint(requestContext, token, userIdClaim); + + if (authenticatedUser == null) { + logger.error("Cannot extract a user from token: " + token); + throw new NotAuthorizedException("Cannot find or create a user"); + } + + userForLogging = authenticatedUser.getUserId(); + + //The request context wants to remember who the user is + requestContext.setProperty("username", userForLogging); + requestContext.setSecurityContext(new AuthSecurityContext(authenticatedUser, uriInfo.getRequestUri().getScheme())); + + logger.info("User - " + userForLogging + " - has just passed all the authentication and authorization layers."); + + } catch (NotAuthorizedException e) { + // the detail of this exception should be logged right before the exception thrown out + // logger.error("User - " + userForLogging + " - is not authorized. " + e.getChallenges()); + // we should show different response based on role + requestContext.abortWith(PICSUREResponse.unauthorizedError("User is not authorized. " + e.getChallenges())); + } catch (Exception e){ + // we should show different response based on role + e.printStackTrace(); + requestContext.abortWith(PICSUREResponse.applicationError("Inner application error, please contact system admin")); + } + } + } + + /** + * + * @param token + * @param userIdClaim + * @return + * @throws IOException + */ + + private AuthUser callTokenIntroEndpoint(ContainerRequestContext requestContext, String token, String userIdClaim) { + logger.debug("TokenIntrospection - extractUserFromTokenIntrospection() starting..."); + + String token_introspection_url = picSureWarInit.getToken_introspection_url(); + String token_introspection_token = picSureWarInit.getToken_introspection_token(); + + if (token_introspection_url.isEmpty()) + throw new ApplicationException("token_introspection_url is empty"); + + if (token_introspection_token.isEmpty()){ + throw new ApplicationException("token_introspection_token is empty"); + } + + ObjectMapper json = PicSureWarInit.objectMapper; + CloseableHttpClient client = PicSureWarInit.CLOSEABLE_HTTP_CLIENT; + + HttpPost post = new HttpPost(token_introspection_url); + + Map tokenMap = new HashMap<>(); + tokenMap.put("token", token); + + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + HashMap requestMap = new HashMap(); + try { + String requestPath = requestContext.getUriInfo().getPath(); + requestMap.put("Target Service", requestPath); + + Query initialQuery = null; + //Read the query from the backing store if we are getting the results (full query may not be specified in request) + if(requestPath.startsWith("/query/") && (requestPath.endsWith("result") || requestPath.endsWith("result/"))) { + //Path: /query/{queryId}/result + String[] pathParts = requestPath.split("/"); + UUID uuid = UUID.fromString(pathParts[2]); + initialQuery = queryRepo.getById(uuid); + } + + if(initialQuery != null) { + IOUtils.copy(new ByteArrayInputStream(initialQuery.getQuery().getBytes()), buffer); + } else { + //This stream is only consumable once, so we need to save & reset it. + InputStream entityStream = requestContext.getEntityStream(); + IOUtils.copy(entityStream, buffer); + requestContext.setEntityStream(new ByteArrayInputStream(buffer.toByteArray())); + } + + if(buffer.size()>0) { + /* + * We remove the resourceCredentials from the token introspection copy of the query to prevent logging them as + * part of token introspection. These credentials are between the backing resource and the user, PIC-SURE should + * do its best to keep them confidential. + */ + Object queryObject = new ObjectMapper().readValue(new ByteArrayInputStream(buffer.toByteArray()), Object.class); + if (queryObject instanceof Collection) { + for (Object query: (Collection)queryObject) { + if (query instanceof Map) { + ((Map) query).remove("resourceCredentials"); + } + } + } else if (queryObject instanceof Map){ + ((Map) queryObject).remove("resourceCredentials"); + } + requestMap.put("query", queryObject); + + if(requestPath.startsWith("/query/")) { + + UUID resourceUUID = null; + String resourceUUIDStr = (String) ((Map)queryObject).get("resourceUUID"); + if(resourceUUIDStr != null) { + resourceUUID = UUID.fromString(resourceUUIDStr); + } + + if(resourceUUID != null) { + edu.harvard.dbmi.avillach.data.entity.Resource resource = resourceRepo.getById(resourceUUID); + //logger.info("resource obj: " + resource + " path: " + resource.getResourceRSPath()); + if (resource != null && resource.getResourceRSPath() != null){ + QueryRequest queryRequest = new QueryRequest(); + queryRequest.getResourceCredentials().put(ResourceWebClient.BEARER_TOKEN_KEY, resource.getToken()); + queryRequest.setResourceUUID(resourceUUID); + queryRequest.setQuery(((Map)queryObject).get("query")); + + Response formatResponse = resourceWebClient.queryFormat(resource.getResourceRSPath(), queryRequest); + if(formatResponse.getStatus() == 200) { + //add the formatted query if available + String formattedQuery = IOUtils.toString((InputStream)formatResponse.getEntity(), "UTF-8"); + logger.debug("Formatted response: " + formattedQuery); + requestMap.put("formattedQuery", formattedQuery); + } + } + } + } + } + tokenMap.put("request", requestMap); + } catch (JsonParseException ex) { + requestMap.put("query",buffer.toString()); + tokenMap.put("request", requestMap); + } catch (IOException e1) { + logger.error("IOException caught trying to build requestMap for auditing.", e1); + throw new NotAuthorizedException("The request could not be properly audited. If you recieve this error multiple times, please contact an administrator."); + } + StringEntity entity = null; + try { + entity = new StringEntity(json.writeValueAsString(tokenMap)); + } catch (IOException e) { + logger.error("callTokenIntroEndpoint() - " + e.getClass().getSimpleName() + " when composing post"); + return null; + } + post.setEntity(entity); + post.setHeader("Content-Type", "application/json"); + //Authorize into the token introspection endpoint + post.setHeader("Authorization", "Bearer " + token_introspection_token); + CloseableHttpResponse response = null; + try { + response = client.execute(post, buildHttpClientContext()); + if (response.getStatusLine().getStatusCode() != 200){ + logger.error("callTokenIntroEndpoint() error back from token intro host server [" + + token_introspection_url + "]: " + EntityUtils.toString(response.getEntity())); + throw new ApplicationException("Token Introspection host server return " + response.getStatusLine().getStatusCode() + + ". Please see the log"); + } + JsonNode responseContent = json.readTree(response.getEntity().getContent()); + if (!responseContent.get("active").asBoolean()){ + logger.error("callTokenIntroEndpoint() Token intro endpoint return invalid token, content: " + responseContent); + throw new NotAuthorizedException("Token invalid or expired"); + } + + String userId = responseContent.get(userIdClaim) != null ? responseContent.get(userIdClaim).asText() : null; + String sub = responseContent.get("sub") != null ? responseContent.get("sub").asText() : null; + String email = responseContent.get("email") != null ? responseContent.get("email").asText() : null; + String roles = responseContent.get("roles") != null ? responseContent.get("roles").asText() : null; + AuthUser user = new AuthUser().setUserId(userId).setSubject(sub).setEmail(email).setRoles(roles); + return user; + } catch (IOException ex){ + logger.error("callTokenIntroEndpoint() IOException when hitting url: " + post + + " with exception msg: " + ex.getMessage()); + } finally { + try { + if (response != null) + response.close(); + } catch (IOException ex) { + logger.error("callTokenIntroEndpoint() IOExcpetion when closing http response: " + ex.getMessage()); + } + } + + return null; + } + + void setUserIdClaim(String userIdClaim) { + this.userIdClaim = userIdClaim; + } +} diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/NamedDatasetService.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/NamedDatasetService.java new file mode 100644 index 00000000..04b916b8 --- /dev/null +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/NamedDatasetService.java @@ -0,0 +1,110 @@ +package edu.harvard.dbmi.avillach.service; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import edu.harvard.dbmi.avillach.data.entity.NamedDataset; +import edu.harvard.dbmi.avillach.data.entity.Query; +import edu.harvard.dbmi.avillach.data.repository.NamedDatasetRepository; +import edu.harvard.dbmi.avillach.data.repository.QueryRepository; +import edu.harvard.dbmi.avillach.data.request.NamedDatasetRequest; +import edu.harvard.dbmi.avillach.util.exception.ProtocolException; + +public class NamedDatasetService { + private final Logger logger = LoggerFactory.getLogger(NamedDatasetService.class); + + @Inject + NamedDatasetRepository namedDatasetRepo; + + @Inject + QueryRepository queryRepo; + + public Optional> getNamedDatasets(String user){ + List queries = namedDatasetRepo.getByColumn("user", user); + return Optional.ofNullable(queries); + } + + public Optional getNamedDatasetById(String user, UUID datasetId){ + NamedDataset dataset = namedDatasetRepo.getById(datasetId); + if (dataset == null){ + logger.error("named dataset not found with id " + datasetId.toString()); + return Optional.empty(); + } + + if (!dataset.getUser().toString().equals(user)){ + logger.error("named dataset with id " + datasetId.toString() + " not able to be viewed by user " + user); + return Optional.empty(); + } + + return Optional.of(dataset); + } + + public Optional addNamedDataset(String user, NamedDatasetRequest request){ + UUID queryId = request.getQueryId(); + Query query = queryRepo.getById(queryId); + if (query == null){ + logger.error(ProtocolException.QUERY_NOT_FOUND + queryId.toString()); + return Optional.empty(); + } + + NamedDataset dataset = new NamedDataset() + .setName(request.getName()) + .setQuery(query) + .setUser(user) + .setArchived(request.getArchived()) + .setMetadata(request.getMetadata()); + + try { + namedDatasetRepo.persist(dataset); + logger.debug("persisted named dataset with query id " + queryId.toString()); + } catch (Exception exception){ + logger.error("Error persisting named dataset with query id " + queryId.toString(), exception); + return Optional.empty(); + } + + return Optional.of(dataset); + } + + public Optional updateNamedDataset(String user, UUID datasetId, NamedDatasetRequest request){ + NamedDataset dataset = namedDatasetRepo.getById(datasetId); + if (dataset == null){ + logger.error("named dataset not found with id " + datasetId.toString()); + return Optional.empty(); + } + + if (!dataset.getUser().equals(user)){ + logger.error("named dataset with id " + datasetId.toString() + " not able to be updated by user " + user); + return Optional.empty(); + } + + UUID queryId = request.getQueryId(); + if (!dataset.getQuery().getUuid().equals(queryId)){ + Query query = queryRepo.getById(queryId); + if (query == null){ + logger.error(ProtocolException.QUERY_NOT_FOUND + queryId.toString()); + return Optional.empty(); + } + dataset.setQuery(query); + } + + dataset.setName(request.getName()) + .setArchived(request.getArchived()) + .setMetadata(request.getMetadata()); + + try { + namedDatasetRepo.merge(dataset); + logger.debug("updated named dataset with id " + datasetId.toString() + " and query id " + queryId.toString()); + } catch (Exception exception){ + logger.error("Error updating named dataset with id " + datasetId.toString() + " and query id " + queryId.toString(), exception); + return Optional.empty(); + } + + return Optional.of(dataset); + } +} diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/SystemService.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/SystemService.java index 62fde1d8..4902448d 100755 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/SystemService.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/SystemService.java @@ -2,13 +2,11 @@ import static edu.harvard.dbmi.avillach.util.Utilities.buildHttpClientContext; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.util.List; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.ws.rs.GET; -import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -25,11 +23,9 @@ import edu.harvard.dbmi.avillach.PicSureWarInit; import edu.harvard.dbmi.avillach.data.entity.Resource; -import edu.harvard.dbmi.avillach.data.entity.User; import edu.harvard.dbmi.avillach.data.repository.ResourceRepository; import edu.harvard.dbmi.avillach.domain.QueryRequest; import edu.harvard.dbmi.avillach.domain.ResourceInfo; -import edu.harvard.dbmi.avillach.security.JWTFilter; import edu.harvard.dbmi.avillach.util.exception.ApplicationException; @Path("/system") diff --git a/pic-sure-api-war/src/main/resources/META-INF/persistence.xml b/pic-sure-api-war/src/main/resources/META-INF/persistence.xml index 12c75db5..72761fce 100644 --- a/pic-sure-api-war/src/main/resources/META-INF/persistence.xml +++ b/pic-sure-api-war/src/main/resources/META-INF/persistence.xml @@ -4,9 +4,9 @@ xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> java:jboss/datasources/PicsureDS - edu.harvard.dbmi.avillach.data.entity.User edu.harvard.dbmi.avillach.data.entity.Query edu.harvard.dbmi.avillach.data.entity.Resource + edu.harvard.dbmi.avillach.data.entity.NamedDataset diff --git a/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/security/JWTFilterTest.java b/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/security/JWTFilterTest.java index 71ee3639..eeef73b0 100644 --- a/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/security/JWTFilterTest.java +++ b/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/security/JWTFilterTest.java @@ -1,7 +1,6 @@ package edu.harvard.dbmi.avillach.security; import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*; @@ -40,8 +39,7 @@ public class JWTFilterTest { private static final UUID RESOURCE_UUID = UUID.fromString("30ef4941-9656-4b47-af80-528f2b98cf17"); @Rule - public WireMockRule wireMockRule = new WireMockRule( - wireMockConfig().dynamicPort().dynamicHttpsPort()); + public WireMockRule wireMockRule = new WireMockRule(0); private int port; @@ -98,12 +96,21 @@ private Resource basicResource() { return resource; } - private void tokenIntrospectionStub(String tokenIntrospectionResult) { + private void tokenIntrospectionStub() { + tokenIntrospectionStub(true); + } + + private void tokenIntrospectionStub(Boolean active) { stubFor(post(urlEqualTo("/introspection_endpoint")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"active\":" + tokenIntrospectionResult + ",\"sub\":\"TEST_USER\"}"))); + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{" + + "\"active\":" + Boolean.toString(active) + "," + + "\"sub\":\"TEST_USER\"," + + "\"email\":\"some@email.com\"," + + "\"roles\":\"PIC_SURE_ANY_QUERY\"" + + "}"))); } @Test @@ -118,7 +125,7 @@ public void testSystemPathDoesNotRequireAuthenticationHeader() throws IOExceptio @Test public void testFilterCallsTokenIntrospectionAppropriatelyForQuerySync() throws IOException { - tokenIntrospectionStub("true"); + tokenIntrospectionStub(); ContainerRequestContext ctx = createRequestContext(); when(ctx.getUriInfo().getPath()).thenReturn("/query/sync"); @@ -141,7 +148,7 @@ public void testFilterCallsTokenIntrospectionAppropriatelyForQuerySync() throws @Test public void testFilterCallsTokenIntrospectionAppropriatelyForQuery() throws IOException { - tokenIntrospectionStub("true"); + tokenIntrospectionStub(); ContainerRequestContext ctx = createRequestContext(); when(ctx.getUriInfo().getPath()).thenReturn("/query"); @@ -164,7 +171,7 @@ public void testFilterCallsTokenIntrospectionAppropriatelyForQuery() throws IOEx @Test public void testFilterCallsTokenIntrospectionAppropriatelyForResultWithoutTrailingSlash() throws IOException { - tokenIntrospectionStub("true"); + tokenIntrospectionStub(); queryFormatStub(); @@ -193,7 +200,7 @@ public void testFilterCallsTokenIntrospectionAppropriatelyForResultWithoutTraili @Test public void testFilterCallsTokenIntrospectionAppropriatelyForResultWithTrailingSlash() throws IOException { - tokenIntrospectionStub("true"); + tokenIntrospectionStub(); queryFormatStub(); @@ -229,8 +236,7 @@ private void queryFormatStub() { @Test public void testFilterAbortsRequestIfTokenIntrospectionReturnsFalse() throws IOException { - String tokenIntrospectionResult = "false"; - tokenIntrospectionStub(tokenIntrospectionResult); + tokenIntrospectionStub(false); ContainerRequestContext ctx = createRequestContext(); when(ctx.getUriInfo().getPath()).thenReturn("/query/sync"); @@ -248,7 +254,7 @@ public void testFilterAbortsRequestIfTokenIntrospectionReturnsFalse() throws IOE @Test public void testFilterSetsUsernameIfTokenIntrospectionReturnsTrue() throws IOException { - tokenIntrospectionStub("true"); + tokenIntrospectionStub(); ContainerRequestContext ctx = createRequestContext(); when(ctx.getUriInfo().getPath()).thenReturn("/query/sync"); @@ -264,7 +270,7 @@ public void testFilterSetsUsernameIfTokenIntrospectionReturnsTrue() throws IOExc @Test public void testFilterRemovesResourceCredentialsBeforeSendingToTokenIntrospectionOrFormatter() throws IOException { - tokenIntrospectionStub("true"); + tokenIntrospectionStub(); ContainerRequestContext ctx = createRequestContext(); when(ctx.getUriInfo().getPath()).thenReturn("/query"); diff --git a/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/service/NamedDatasetServiceTest.java b/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/service/NamedDatasetServiceTest.java new file mode 100644 index 00000000..87466e3c --- /dev/null +++ b/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/service/NamedDatasetServiceTest.java @@ -0,0 +1,367 @@ +package edu.harvard.dbmi.avillach.service; + +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doThrow; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.HashMap; +import java.util.Optional; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import edu.harvard.dbmi.avillach.data.entity.NamedDataset; +import edu.harvard.dbmi.avillach.data.entity.Query; +import edu.harvard.dbmi.avillach.data.repository.NamedDatasetRepository; +import edu.harvard.dbmi.avillach.data.repository.QueryRepository; +import edu.harvard.dbmi.avillach.data.request.NamedDatasetRequest; + +@RunWith(MockitoJUnitRunner.class) +public class NamedDatasetServiceTest { + private String user = "test.user@email.com"; + private String testName = "test name"; + + @InjectMocks + private NamedDatasetService namedDatasetService = new NamedDatasetService(); + + @Mock + private NamedDatasetRepository datasetRepo = mock(NamedDatasetRepository.class); + + @Mock + private QueryRepository queryRepo = mock(QueryRepository.class); + + private Query makeQuery(UUID id){ + Query query = new Query(); + query.setUuid(id); + query.setQuery("{}"); + return query; + } + + private NamedDataset makeNamedDataset(UUID id, Query query){ + NamedDataset dataset = new NamedDataset(); + dataset.setUuid(id); + dataset.setUser(user); + dataset.setName(testName); + dataset.setQuery(query); + dataset.setArchived(false); + return dataset; + } + + private NamedDatasetRequest makeNamedDatasetRequest(UUID queryId){ + NamedDatasetRequest request = new NamedDatasetRequest(); + request.setName(testName); + request.setQueryId(queryId); + request.setArchived(false); + return request; + } + + @Test + public void getNamedDataset_success() { + // Given there is a saved dataset in the database for this user + Query query = makeQuery(UUID.randomUUID()); + NamedDataset dataset = makeNamedDataset(UUID.randomUUID(), query); + ArrayList datasets = new ArrayList(); + datasets.add(dataset); + when(datasetRepo.getByColumn("user", user)).thenReturn(datasets); + + // When the request is recieved + Optional> response = namedDatasetService.getNamedDatasets(user); + + // Then return a non-empty optional + assertTrue(response.isPresent()); + } + + @Test + public void getNamedDataset_novalues() { + // Given there is no saved dataset in the database for this user + ArrayList datasets = new ArrayList(); + when(datasetRepo.getByColumn("user", user)).thenReturn(datasets); + + // When the request is recieved + Optional> response = namedDatasetService.getNamedDatasets(user); + + // Then return a non-empty optional with an empy list + assertTrue(response.isPresent()); + assertTrue(response.get().size() == 0); + } + + @Test + public void getNamedDatasetById_success() { + // Given there is a saved dataset in the database for this user + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(UUID.randomUUID()); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + + // When the request is recieved + Optional response = namedDatasetService.getNamedDatasetById(user, namedDatasetId); + + // Then return a non-empty optional + assertTrue(response.isPresent()); + } + + @Test + public void getNamedDatasetById_datasetNotFromUser() { + // Given there is a saved dataset in the database with this id but a different user + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(UUID.randomUUID()); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + dataset.setUser("other.user@email.com"); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + + // When the request is recieved + Optional response = namedDatasetService.getNamedDatasetById(user, namedDatasetId); + + // Then return an empty optional + assertTrue(response.isEmpty()); + } + + @Test + public void getNamedDatasetById_noNamedDatasetWithId() { + // Given there is no saved dataset in the database with this id + UUID namedDatasetId = UUID.randomUUID(); + when(datasetRepo.getById(namedDatasetId)).thenReturn(null); + + // When the request is recieved + Optional response = namedDatasetService.getNamedDatasetById(user, namedDatasetId); + + // Then return an empty optional + assertTrue(response.isEmpty()); + } + + @Test + public void addNamedDataset_success() { + // Given there is a query in the database + UUID queryId = UUID.randomUUID(); + Query query = makeQuery(queryId); + when(queryRepo.getById(queryId)).thenReturn(query); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + Optional response = namedDatasetService.addNamedDataset(user, request); + + // Then return a non-empty optional + assertTrue(response.isPresent()); + assertEquals("related user is saved", user, response.get().getUser()); + assertEquals("related name is saved", testName, response.get().getName()); + assertEquals("related query is saved", queryId, response.get().getQuery().getUuid()); + } + + @Test + public void addNamedDataset_metadataSet_success() { + // Given there is a query in the database + UUID queryId = UUID.randomUUID(); + Query query = makeQuery(queryId); + when(queryRepo.getById(queryId)).thenReturn(query); + String testKey = "test"; + String testValue = "value"; + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + HashMap metadata = new HashMap(); + metadata.put(testKey, testValue); + request.setMetadata(metadata); + Optional response = namedDatasetService.addNamedDataset(user, request); + + // Then return a non-empty optional + assertTrue(response.isPresent()); + assertEquals("related metadata is saved", testValue, response.get().getMetadata().get(testKey)); + } + + @Test + public void addNamedDataset_noQueryWithID() { + // Given there is no query in the database with this id + UUID queryId = UUID.randomUUID(); + when(queryRepo.getById(queryId)).thenReturn(null); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + Optional response = namedDatasetService.addNamedDataset(user, request); + + // Then return an empty optional + assertTrue(response.isEmpty()); + } + + @Test + public void addNamedDataset_cannotPersist() { + // Given there is an error saving to the named dataset table + UUID queryId = UUID.randomUUID(); + Query query = makeQuery(queryId); + when(queryRepo.getById(queryId)).thenReturn(query); + doThrow(new RuntimeException()).when(datasetRepo).persist(any(NamedDataset.class)); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + Optional response = namedDatasetService.addNamedDataset(user, request); + + // Then return an empty optional + assertTrue(response.isEmpty()); + } + + @Test + public void updateNamedDataset_changeName_success() { + // Given there is a named dataset saved in the database with this id + UUID queryId = UUID.randomUUID(); + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(queryId); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + + // When the request is recieved + String newName = "new name"; + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + request.setName(newName); + Optional response = namedDatasetService.updateNamedDataset(user, namedDatasetId, request); + + // Then return a non-empty optional + assertTrue(response.isPresent()); + assertEquals("new name id is saved", newName, response.get().getName()); + } + + @Test + public void updateNamedDataset_changeArchiveState_success() { + // Given there is a named dataset saved in the database with this id + UUID queryId = UUID.randomUUID(); + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(queryId); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + request.setArchived(true); + Optional response = namedDatasetService.updateNamedDataset(user, namedDatasetId, request); + + // Then return a non-empty optional + assertTrue(response.isPresent()); + assertEquals("new archive state is retained", true, response.get().getArchived()); + } + + @Test + public void updateNamedDataset_changeMetadata_success() { + // Given there is a named dataset saved in the database with this id + UUID queryId = UUID.randomUUID(); + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(queryId); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + HashMap oldMetadata = new HashMap(); + oldMetadata.put("whatever", "something"); + dataset.setMetadata(oldMetadata); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + String testKey = "test"; + String testValue = "value"; + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + HashMap newMetadata = new HashMap(); + newMetadata.put(testKey, testValue); + request.setMetadata(newMetadata); + Optional response = namedDatasetService.updateNamedDataset(user, namedDatasetId, request); + + // Then return a non-empty optional + assertTrue(response.isPresent()); + assertEquals("new metadata is retained", testValue, response.get().getMetadata().get(testKey)); + } + + @Test + public void updateNamedDataset_changeQueryId_success() { + // Given there is a named dataset saved in the database with this id and the new query id is in the database + UUID queryId = UUID.randomUUID(); + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(queryId); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + + UUID newQueryId = UUID.randomUUID(); + Query newQuery = makeQuery(newQueryId); + when(queryRepo.getById(newQueryId)).thenReturn(newQuery); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(newQueryId); + Optional response = namedDatasetService.updateNamedDataset(user, namedDatasetId, request); + + // Then return a non-empty optional + assertTrue(response.isPresent()); + assertEquals("new query id is saved", newQueryId, response.get().getQuery().getUuid()); + } + + @Test + public void updateNamedDataset_noNamedDatasetWithId() { + // Given there is no named dataset in the database with this id + UUID namedDatasetId = UUID.randomUUID(); + when(datasetRepo.getById(namedDatasetId)).thenReturn(null); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(UUID.randomUUID()); + Optional response = namedDatasetService.updateNamedDataset(user, namedDatasetId, request); + + // Then return an empty optional + assertTrue(response.isEmpty()); + } + + @Test + public void updateNamedDataset_datasetNotFromUser() { + // Given there is a saved dataset in the database with this id but a different user + UUID queryId = UUID.randomUUID(); + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(queryId); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + dataset.setUser("other.user@email.com"); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + Optional response = namedDatasetService.updateNamedDataset(user, namedDatasetId, request); + + // Then return an empty optional + assertTrue(response.isEmpty()); + } + + @Test + public void updateNamedDataset_changeQueryId_noQueryWithID() { + // Given there is a named dataset saved in the database with this id but no query id as passed in + UUID queryId = UUID.randomUUID(); + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(queryId); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + + UUID newQueryId = UUID.randomUUID(); + when(queryRepo.getById(newQueryId)).thenReturn(null); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(newQueryId); + Optional response = namedDatasetService.updateNamedDataset(user, namedDatasetId, request); + + // Then return an empty optional + assertTrue(response.isEmpty()); + } + + @Test + public void updateNamedDataset_cannotPersistChanges() { + // Given there is an error saving to the named dataset table + UUID queryId = UUID.randomUUID(); + UUID namedDatasetId = UUID.randomUUID(); + Query query = makeQuery(queryId); + NamedDataset dataset = makeNamedDataset(namedDatasetId, query); + when(datasetRepo.getById(namedDatasetId)).thenReturn(dataset); + doThrow(new RuntimeException()).when(datasetRepo).merge(any(NamedDataset.class)); + + // When the request is recieved + NamedDatasetRequest request = makeNamedDatasetRequest(queryId); + Optional response = namedDatasetService.updateNamedDataset(user, namedDatasetId, request); + + // Then return an empty optional + assertTrue(response.isEmpty()); + } +} diff --git a/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/service/SystemServiceTest.java b/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/service/SystemServiceTest.java index f06767f7..354ca31e 100644 --- a/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/service/SystemServiceTest.java +++ b/pic-sure-api-war/src/test/java/edu/harvard/dbmi/avillach/service/SystemServiceTest.java @@ -1,7 +1,6 @@ package edu.harvard.dbmi.avillach.service; import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.*; @@ -21,8 +20,7 @@ public class SystemServiceTest { @Rule - public WireMockRule wireMockRule = new WireMockRule( - wireMockConfig().dynamicPort().dynamicHttpsPort()); + public WireMockRule wireMockRule = new WireMockRule(0); private int port; diff --git a/pic-sure-api-wildfly/src/main/resources/META-INF/persistence.xml b/pic-sure-api-wildfly/src/main/resources/META-INF/persistence.xml index 2cfbdeaa..84874384 100644 --- a/pic-sure-api-wildfly/src/main/resources/META-INF/persistence.xml +++ b/pic-sure-api-wildfly/src/main/resources/META-INF/persistence.xml @@ -3,9 +3,10 @@ org.hibernate.ejb.HibernatePersistence java:/PicsureDS - edu.harvard.dbmi.avillach.data.entity.User + edu.harvard.dbmi.avillach.data.entity.AuthUser edu.harvard.dbmi.avillach.data.entity.Resource edu.harvard.dbmi.avillach.data.entity.Query + edu.harvard.dbmi.avillach.data.entity.NamedDataset