From e710221334f246d9aaad30484df13d5a9a7462d5 Mon Sep 17 00:00:00 2001 From: Luke Sikina Date: Wed, 7 Feb 2024 14:39:23 -0500 Subject: [PATCH 01/10] [ALS-5827] Enable search by CA id - If a uuid does not match a query, instead search for common area UUID - If we have a collision, I'm buying a lottery ticket --- .../data/repository/QueryRepository.java | 17 +++++++++++++++-- .../avillach/service/PicsureQueryService.java | 1 + 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/QueryRepository.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/QueryRepository.java index 12c59bef..0c867ed0 100644 --- a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/QueryRepository.java +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/QueryRepository.java @@ -3,12 +3,25 @@ import edu.harvard.dbmi.avillach.data.entity.Query; import javax.enterprise.context.ApplicationScoped; +import javax.persistence.PersistenceException; import javax.transaction.Transactional; import java.util.UUID; @Transactional @ApplicationScoped -public class QueryRepository extends BaseRepository{ +public class QueryRepository extends BaseRepository { - protected QueryRepository() {super(Query.class);} + protected QueryRepository() { + super(Query.class); + } + + public Query getQueryUUIDFromCommonAreaUUID(UUID caID) { + String caIDRegex = "%commonAreaUUID\":\"" + caID + "\"%"; + String query = "SELECT * FROM query WHERE CONVERT(metadata USING utf8) LIKE ?"; + try { + return (Query) em().createNativeQuery(query, Query.class).setParameter(1, caIDRegex).getSingleResult(); + } catch (PersistenceException ignored) { + return null; + } + } } diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureQueryService.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureQueryService.java index b0c851bc..ca93d6d8 100644 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureQueryService.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureQueryService.java @@ -241,6 +241,7 @@ public Response querySync(QueryRequest queryRequest, HttpHeaders headers) { */ public QueryStatus queryMetadata(UUID queryId, HttpHeaders headers) { Query query = queryRepo.getById(queryId); + query = query == null ? queryRepo.getQueryUUIDFromCommonAreaUUID(queryId) : query; if (query == null) { throw new ProtocolException(ProtocolException.QUERY_NOT_FOUND + queryId.toString()); } From 315a78997e7ff2bc04727affec82ee8adb1abe3f Mon Sep 17 00:00:00 2001 From: Gcolon021 <34667267+Gcolon021@users.noreply.github.com> Date: Tue, 13 Feb 2024 09:10:34 -0500 Subject: [PATCH 02/10] [ALS-5787] Open Access StatViz is not showing "Other" (#180) * Update key length handling in limitKeySize The limitKeySize method in the VisualizationUtil class has been updated to ensure key uniqueness when keys exceed a certain length. A more robust method of shortening the keys has been implemented: if the key is longer than 45 characters, it is cut off and replaced with "..." and additional characters are appended to ensure uniqueness if needed. This helps limit key size while preserving uniqueness. * Add VisualizationUtilTests and refine key shortening logic Added a new test class, VisualizationUtilTests to validate the functionality of the VisualizationUtil class. Adjusted the 'limitKeySize' method to ensure key uniqueness when keys are shortened to a maximum length of 45 characters. If shortened keys are not unique, additional trailing characters are included until uniqueness is achieved. * Add check on length before substring * Update pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java * Update pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java * Update pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java * Update pic-sure-resources/pic-sure-visualization-resource/src/test/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationUtilTests.java Co-authored-by: Luke Sikina * Fix import for modified unit test * Refactor and add test cases for key size limiting function Refactors existing code by simplifying key size limiting logic in VisualizationUtil and added several new unit tests to ensure its correct behavior with different input scenarios including long keys, empty maps, null maps, and uniqueness near middle. * Update null handling in VisualizationUtil's limitKeySize Changed the handling of null input in VisualizationUtil's limitKeySize from returning a new HashMap to throwing an IllegalArgumentException. Also, updated the corresponding test to check for this exception instead of comparing with an empty map. --------- Co-authored-by: Luke Sikina --- .../service/VisualizationUtilTests.java | 94 +++++++++++++++++++ .../dbmi/avillach/util/VisualizationUtil.java | 57 +++++++---- 2 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 pic-sure-resources/pic-sure-visualization-resource/src/test/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationUtilTests.java diff --git a/pic-sure-resources/pic-sure-visualization-resource/src/test/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationUtilTests.java b/pic-sure-resources/pic-sure-visualization-resource/src/test/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationUtilTests.java new file mode 100644 index 00000000..78583922 --- /dev/null +++ b/pic-sure-resources/pic-sure-visualization-resource/src/test/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationUtilTests.java @@ -0,0 +1,94 @@ +package edu.harvard.hms.dbmi.avillach.resource.visualization.service; + +import edu.harvard.dbmi.avillach.util.VisualizationUtil; +import org.junit.Test; +import org.junit.jupiter.api.DisplayName; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class VisualizationUtilTests { + + @Test + @DisplayName("Test limitKeySize") + public void testLimitKeySizeUniqueness() { + Map axisMap = new HashMap<>(Map.of( + "Disease-Specific (Asthma, Allergy and Inflammation, PUB)", 1, + "Disease-Specific (Asthma, Allergy and Inflammation, PUB, NPU)", 1, + "Disease-Specific (Asthma, Allergy and Inflammation, NPU)", 1, + "Disease-Specific (Asthma, Allergy and Inflammation)", 1 + )); + + Map actual = VisualizationUtil.limitKeySize(axisMap); + + Map expected = Map.of( + "Disease-Specific (Asthma, Allergy an..., PUB)", 1, + "Disease-Specific (Asthma, Allergy an...ation)", 1, + "Disease-Specific (Asthma, Allergy an..., NPU)", 1, + "Disease-Specific (Asthma, Allergy a...B, NPU)", 1 + ); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test Empty Map limitKeySize") + public void testEmptyMapLimitKeySize() { + Map axisMap = new HashMap<>(); + Map actual = VisualizationUtil.limitKeySize(axisMap); + Map expected = new HashMap<>(); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test null Map limitKeySize") + public void testNullMapLimitKeySize() { + Map axisMap = null; + // this should throw a NullPointerException + try { + VisualizationUtil.limitKeySize(axisMap); + } catch (IllegalArgumentException e) { + assertEquals("axisMap cannot be null", e.getMessage()); + } + } + + @Test + @DisplayName("Test with no long keys limitKeySize") + public void testNoLongKeysLimitKeySize() { + // Test with no long keys + Map axisMap = new HashMap<>(); + for (int i = 0; i < 10; i++) { + axisMap.put("key" + i, 1); + } + Map actual = VisualizationUtil.limitKeySize(axisMap); + Map expected = new HashMap<>(axisMap); + assertEquals(expected, actual); + } + + @Test + @DisplayName("Test with keys of greater than 45 characters and uniqueness is near middle limitKeySize") + public void testKeysOfGreaterLengthAndUniquenessNearMiddleLimitKeySize() { + Map axisMap = new HashMap<>(); + axisMap.put("Hello, this is a long key that is STRING1 greater than 45 characters and is unique", 1); + axisMap.put("Hello, this is a long key that is STRING2 greater than 45 characters and is unique", 1); + axisMap.put("Hello, this is a long key that is STRING3 greater than 45 characters and is unique", 1); + + Map actual = VisualizationUtil.limitKeySize(axisMap); + + // loop through the keys and check if they are less than 45 characters + for (String key : actual.keySet()) { + assertEquals(45, key.length()); + } + + Map expected = Map.of( + "Hello, this is a long key that is ST...unique", 1, + "Hello, this is a long key that is S... unique", 1, + "Hello, this is a long key that is ...s unique", 1 + ); + + assertEquals(expected, actual); + } + + +} diff --git a/pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java b/pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java index 8a7d99a7..ed32a921 100644 --- a/pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java +++ b/pic-sure-util/src/main/java/edu/harvard/dbmi/avillach/util/VisualizationUtil.java @@ -69,25 +69,50 @@ public static Map doProcessResults(Map axisMap } /** - * Replaces long column names with shorter version. + * This method is used to limit the size of the keys in the axisMap to a maximum of 45 characters. If the key is longer + * than 45 characters, it will be shortened to 45 characters and the last 3 characters will be replaced with "...". + * If the shortened key is not unique, we will create a unique one + *

* - * @param axisMap - * @return + * @param axisMap - Map of the categories and their counts + * @return Map - Map of the categories and their counts with the keys limited to 45 characters */ - private static Map limitKeySize(Map axisMap) { - List toRemove = new ArrayList<>(); - Map toAdd = new HashMap<>(); - axisMap.keySet().forEach(key -> { - if (key.length() > MAX_X_LABEL_LINE_LENGTH) { - toRemove.add(key); - toAdd.put( - key.substring(0, MAX_X_LABEL_LINE_LENGTH - 3) + "...", - axisMap.get(key)); - } + public static Map limitKeySize(Map axisMap) { + if (axisMap == null) { + throw new IllegalArgumentException("axisMap cannot be null"); + } + + Map newAxisMap = new HashMap<>(); + HashSet keys = new HashSet<>(); + axisMap.forEach((key, value) -> { + String adjustedKey = key.length() < MAX_X_LABEL_LINE_LENGTH ? key : createAdjustedKey(axisMap, keys, key); + newAxisMap.put(adjustedKey, value); + keys.add(adjustedKey); }); - toRemove.forEach(key -> axisMap.remove(key)); - axisMap.putAll(toAdd); - return axisMap; + return newAxisMap; + } + + private static String createAdjustedKey(Map axisMap, HashSet keys, String key) { + String keyPrefix = key.substring(0, MAX_X_LABEL_LINE_LENGTH); + return isKeyPrefixInAxisMap(axisMap, keyPrefix) ? generateUniqueKey(keys, key) : appendEllipsis(keyPrefix); + } + + private static boolean isKeyPrefixInAxisMap(Map axisMap, String keyPrefix) { + return axisMap.keySet().stream().anyMatch(k -> k.startsWith(keyPrefix)); + } + + private static String generateUniqueKey(HashSet keys, String key) { + int countFromEnd = 6; + String proposedKey; + do { + proposedKey = String.format("%s...%s", key.substring(0, MAX_X_LABEL_LINE_LENGTH - 3 - countFromEnd), key.substring(key.length() - countFromEnd)); + countFromEnd++; + } while (keys.contains(proposedKey)); + return proposedKey; + } + + private static String appendEllipsis(String keyPrefixAdjusted) { + return String.format("%s...", keyPrefixAdjusted); } } From 68f7ac78468d31dfb8be89668ffecd6d38e1ddda Mon Sep 17 00:00:00 2001 From: Gcolon021 <34667267+Gcolon021@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:31:01 -0500 Subject: [PATCH 03/10] Refine the condition for empty return in DataProcessingService (#184) Adjusted the condition under which an empty HashMap is returned in the DataProcessingService. Now, an empty HashMap will only be returned when both the maximum and minimum values in the data are 0, and the number of bins is also 0. This change takes into account situations where the data contains values but the min and max are both 0. --- .../resource/visualization/service/DataProcessingService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/DataProcessingService.java b/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/DataProcessingService.java index 1988aab4..845909e4 100644 --- a/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/DataProcessingService.java +++ b/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/DataProcessingService.java @@ -158,8 +158,8 @@ private static Map bucketData(Map originalMap) int numBins = calcNumBins(data); double min = data.keySet().stream().min(Double::compareTo).orElse(0.0); double max = data.keySet().stream().max(Double::compareTo).orElse(0.0); - - if ((min == 0.0 && max == 0.0) || numBins == 0) return new HashMap<>(); + // The min and max can both be 0, but we could still have a numBins of 1 if there are values in the data. + if (min == 0.0 && max == 0.0 && numBins == 0) return new HashMap<>(); int binSize = (int) Math.ceil((max - min) / numBins); From 6c716b37a522eecb6f42edadbf05faf155066911 Mon Sep 17 00:00:00 2001 From: ramari16 Date: Tue, 12 Mar 2024 18:13:56 -0300 Subject: [PATCH 04/10] ALS-5387: Remove stack from resources table (#186) * [ALS-5422] Add persistence.xml to visualization resource (#161) (#162) The visualization resource is failing to start due to an error injecting persistence unit into CDI managed bean. It is unable to find a persistence unit named ''. * ALS-5387: Remove stack from resources table * ALS-5387: More robust implementation of feature --------- Co-authored-by: Gcolon021 <34667267+Gcolon021@users.noreply.github.com> Co-authored-by: GeorgeC --- .../dbmi/avillach/data/entity/Resource.java | 34 ++++++- .../data/repository/ResourceRepository.java | 2 +- .../avillach/service/PicsureInfoService.java | 88 +++++++++---------- 3 files changed, 77 insertions(+), 47 deletions(-) diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/Resource.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/Resource.java index 3faf9ddc..b2cb4367 100644 --- a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/Resource.java +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/entity/Resource.java @@ -1,12 +1,12 @@ package edu.harvard.dbmi.avillach.data.entity; import java.io.StringReader; +import java.util.Optional; import javax.json.Json; import javax.json.JsonObject; import javax.json.JsonReader; -import javax.persistence.Column; -import javax.persistence.Entity; +import javax.persistence.*; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -18,6 +18,9 @@ public class Resource extends BaseEntity{ @Column(length = 8192) private String description; private String targetURL; + + + @Convert(converter = ResourcePathConverter.class) private String resourceRSPath; @Column(length = 8192) @@ -102,4 +105,31 @@ public String toString() { .add("metadata", metadataObj) .build().toString(); } + + /** + * This resource path converter allows resource paths to contain a reference to a specific stack that is + * imputed at runtime based an an environment parameter. This allows multiple stacks to share the same database. + * + * The ___target_stack___ token in any resource path value will be replaced with the TARGET_STACK environment variable + */ + @Converter + public static class ResourcePathConverter implements AttributeConverter { + + public ResourcePathConverter() { + } + + private static final Optional targetStack = Optional.ofNullable(System.getProperty("TARGET_STACK", null)); + + @Override + public String convertToDatabaseColumn(String attribute) { + return attribute; + } + + @Override + public String convertToEntityAttribute(String dbData) { + return targetStack + .map(stack -> dbData.replace("___target_stack___", stack)) + .orElse(dbData); + } + } } diff --git a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/ResourceRepository.java b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/ResourceRepository.java index 8bb400f4..f377c9d6 100644 --- a/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/ResourceRepository.java +++ b/pic-sure-api-data/src/main/java/edu/harvard/dbmi/avillach/data/repository/ResourceRepository.java @@ -15,5 +15,5 @@ protected ResourceRepository() { super(Resource.class); } - + } diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureInfoService.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureInfoService.java index 922746f8..a64cf005 100644 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureInfoService.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/service/PicsureInfoService.java @@ -21,55 +21,55 @@ public class PicsureInfoService { - private final Logger logger = LoggerFactory.getLogger(PicsureQueryService.class); + private final Logger logger = LoggerFactory.getLogger(PicsureQueryService.class); - private final static ObjectMapper mapper = new ObjectMapper(); + private final static ObjectMapper mapper = new ObjectMapper(); - @Inject - ResourceRepository resourceRepo; + @Inject + ResourceRepository resourceRepo; - @Inject - ResourceWebClient resourceWebClient; + @Inject + ResourceWebClient resourceWebClient; - /** - * Retrieve resource info for a specific resource. - * - * @param resourceId - Resource UUID - * @param credentialsQueryRequest - Contains resource specific credentials map - * @return a {@link edu.harvard.dbmi.avillach.domain.ResourceInfo ResourceInfo} - */ - public ResourceInfo info(UUID resourceId, QueryRequest credentialsQueryRequest, HttpHeaders headers) { - Resource resource = resourceRepo.getById(resourceId); - if (resource == null){ - throw new ProtocolException(ProtocolException.RESOURCE_NOT_FOUND + resourceId.toString()); - } - if (resource.getResourceRSPath() == null){ - throw new ApplicationException(ApplicationException.MISSING_RESOURCE_PATH); - } - if (credentialsQueryRequest == null){ - credentialsQueryRequest = new GeneralQueryRequest(); - } - if (credentialsQueryRequest.getResourceCredentials() == null){ - credentialsQueryRequest.setResourceCredentials(new HashMap()); - } + /** + * Retrieve resource info for a specific resource. + * + * @param resourceId - Resource UUID + * @param credentialsQueryRequest - Contains resource specific credentials map + * @return a {@link edu.harvard.dbmi.avillach.domain.ResourceInfo ResourceInfo} + */ + public ResourceInfo info(UUID resourceId, QueryRequest credentialsQueryRequest, HttpHeaders headers) { + Resource resource = resourceRepo.getById(resourceId); + if (resource == null) { + throw new ProtocolException(ProtocolException.RESOURCE_NOT_FOUND + resourceId.toString()); + } + if (resource.getResourceRSPath() == null) { + throw new ApplicationException(ApplicationException.MISSING_RESOURCE_PATH); + } + if (credentialsQueryRequest == null) { + credentialsQueryRequest = new GeneralQueryRequest(); + } + if (credentialsQueryRequest.getResourceCredentials() == null) { + credentialsQueryRequest.setResourceCredentials(new HashMap()); + } - logger.info("path=/info/{resourceId}, resourceId={}, requestSource={}, credentialsQueryRequest={}", - resourceId, - Utilities.getRequestSourceFromHeader(headers), - Utilities.convertQueryRequestToString(mapper, credentialsQueryRequest) - ); + logger.info( + "path=/info/{resourceId}, resourceId={}, requestSource={}, credentialsQueryRequest={}", resourceId, + Utilities.getRequestSourceFromHeader(headers), Utilities.convertQueryRequestToString(mapper, credentialsQueryRequest) + ); - credentialsQueryRequest.getResourceCredentials().put(ResourceWebClient.BEARER_TOKEN_KEY, resource.getToken()); - return resourceWebClient.info(resource.getResourceRSPath(), credentialsQueryRequest); - } + credentialsQueryRequest.getResourceCredentials().put(ResourceWebClient.BEARER_TOKEN_KEY, resource.getToken()); + return resourceWebClient.info(resource.getResourceRSPath(), credentialsQueryRequest); + } - /** - * Retrieve a list of all available resources. - * - * @return List containing limited metadata about all available resources and ids. - */ - public Map resources(HttpHeaders headers) { - logger.info("path=/info/resources, requestSource={}", Utilities.getRequestSourceFromHeader(headers)); - return resourceRepo.list().stream().collect(Collectors.toMap(Resource::getUuid, Resource::getName)); - } + /** + * Retrieve a list of all available resources. + * + * @return List containing limited metadata about all available resources and ids. + */ + public Map resources(HttpHeaders headers) { + logger.info("path=/info/resources, requestSource={}", Utilities.getRequestSourceFromHeader(headers)); + return resourceRepo.list().stream().filter(resource -> !resource.getHidden()) + .collect(Collectors.toMap(Resource::getUuid, Resource::getName)); + } } From a4ddf4f1744f89509ab259d50bf0da6e5c50b06f Mon Sep 17 00:00:00 2001 From: Gcolon021 <34667267+Gcolon021@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:40:45 -0400 Subject: [PATCH 05/10] [ALS-6222] Status endpoints now filters resources (#189) The database contains both open and auth hpds even if it wasn't deployed. This means we attempt to check for a resource that doesn't exist. This results in our service always showing degraded. --- .../harvard/dbmi/avillach/PicSureWarInit.java | 21 +- .../dbmi/avillach/service/SystemService.java | 296 ++++++++++-------- 2 files changed, 173 insertions(+), 144 deletions(-) diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java index 95c07223..43356e1b 100644 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java @@ -25,7 +25,10 @@ public class PicSureWarInit { @Resource(mappedName = "java:global/token_introspection_token") private String token_introspection_token; - //to be able to pre modified + @Resource(mappedName = "java:global/defaultApplicationUUID") + private String default_application_uuid; + + // to be able to pre modified public static final ObjectMapper objectMapper = new ObjectMapper(); // check the example from Apache HttpClient official website: @@ -39,14 +42,9 @@ public class PicSureWarInit { static { HTTP_CLIENT_CONNECTION_MANAGER = new PoolingHttpClientConnectionManager(); HTTP_CLIENT_CONNECTION_MANAGER.setMaxTotal(100); - CLOSEABLE_HTTP_CLIENT = HttpClients - .custom() - .setConnectionManager(HTTP_CLIENT_CONNECTION_MANAGER) - .useSystemProperties() - .build(); + CLOSEABLE_HTTP_CLIENT = HttpClients.custom().setConnectionManager(HTTP_CLIENT_CONNECTION_MANAGER).useSystemProperties().build(); } - public String getToken_introspection_url() { return token_introspection_url; } @@ -54,4 +52,13 @@ public String getToken_introspection_url() { public String getToken_introspection_token() { return token_introspection_token; } + + /** + * This method is used to get the default application UUID. This value is either the open or auth hpds resource UUID. + * + * @return the default application UUID + */ + public String getDefaultApplicationUUID() { + return this.default_application_uuid; + } } 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 2d6328d9..a9c8fcb2 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 @@ -1,137 +1,159 @@ -package edu.harvard.dbmi.avillach.service; -import static edu.harvard.dbmi.avillach.util.Utilities.buildHttpClientContext; - -import java.io.IOException; -import java.util.List; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; - -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 com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import edu.harvard.dbmi.avillach.PicSureWarInit; -import edu.harvard.dbmi.avillach.data.entity.Resource; -import edu.harvard.dbmi.avillach.data.repository.ResourceRepository; -import edu.harvard.dbmi.avillach.domain.GeneralQueryRequest; -import edu.harvard.dbmi.avillach.domain.ResourceInfo; -import edu.harvard.dbmi.avillach.util.exception.ApplicationException; - -@Path("/system") -public class SystemService { - static int max_test_frequency = 60000; - - static final String RUNNING = "RUNNING"; - - static final String ONE_OR_MORE_COMPONENTS_DEGRADED = "ONE OR MORE COMPONENTS DEGRADED"; - - Logger logger = LoggerFactory.getLogger(SystemService.class); - - @Inject - PicSureWarInit picSureWarInit; - - String lastStatus = "UNTESTED"; - long lastStatusCheck = 0l; - - @Inject - ResourceRepository resourceRepo; - - String token_introspection_url; - String token_introspection_token; - - @PostConstruct - public void init() { - token_introspection_url = picSureWarInit.getToken_introspection_url(); - token_introspection_token = picSureWarInit.getToken_introspection_token(); - if(token_introspection_url == null || token_introspection_token == null) { - throw new RuntimeException( - "token_introspection_url and token_introspection_token not configured"); - } - } - - @GET - @Path("/status") - @Produces("text/plain") - public String status() { - // Because there is no auth on this service we limit actually performing the checking to 1 per minute to avoid DOS scenarios. - long timeOfRequest = System.currentTimeMillis(); - if(timeOfRequest-lastStatusCheck < max_test_frequency) { - return lastStatus; - }else { - lastStatusCheck = timeOfRequest; - try{ - List resourcesToTest = resourceRepo.list(); - if( resourcesToTest != null && // This proves the MySQL database is serving queries - !resourcesToTest.isEmpty() && // This proves at least one resources is configured - testPSAMAResponds() && // This proves we can perform token introspection - testResourcesRespond(resourcesToTest) ){ // This proves all resources are at least serving info requests. - lastStatus = RUNNING; - return lastStatus; - }else { - lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; - } - }catch(Exception e) { - e.printStackTrace(); - lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; - } - return lastStatus; - } - } - - private boolean testPSAMAResponds() throws UnsupportedOperationException, IOException { - CloseableHttpClient client = PicSureWarInit.CLOSEABLE_HTTP_CLIENT; - ObjectMapper json = PicSureWarInit.objectMapper; - - HttpPost post = new HttpPost(token_introspection_url); - post.setEntity(new StringEntity("{}")); - 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()){ - // This is actually the expected response as we did not send a token in the token_introspection_request. - return true; - } - - return true; - } finally { - try { - if (response != null) - response.close(); - } catch (IOException ex) { - logger.error("callTokenIntroEndpoint() IOExcpetion when closing http response: " + ex.getMessage()); - } - } - } - - private boolean testResourcesRespond(List resourcesToTest) { - for(Resource resource : resourcesToTest) { - ResourceInfo info = new ResourceWebClient().info(resource.getResourceRSPath(), new GeneralQueryRequest()); - if(info==null) { - return false; - } - } - return true; - } -} - +package edu.harvard.dbmi.avillach.service; + +import static edu.harvard.dbmi.avillach.util.Utilities.buildHttpClientContext; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +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 com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import edu.harvard.dbmi.avillach.PicSureWarInit; +import edu.harvard.dbmi.avillach.data.entity.Resource; +import edu.harvard.dbmi.avillach.data.repository.ResourceRepository; +import edu.harvard.dbmi.avillach.domain.GeneralQueryRequest; +import edu.harvard.dbmi.avillach.domain.ResourceInfo; +import edu.harvard.dbmi.avillach.util.exception.ApplicationException; + +@Path("/system") +public class SystemService { + static int max_test_frequency = 60000; + + static final String RUNNING = "RUNNING"; + + static final String ONE_OR_MORE_COMPONENTS_DEGRADED = "ONE OR MORE COMPONENTS DEGRADED"; + + Logger logger = LoggerFactory.getLogger(SystemService.class); + + @Inject + PicSureWarInit picSureWarInit; + + String lastStatus = "UNTESTED"; + long lastStatusCheck = 0l; + + @Inject + ResourceRepository resourceRepo; + + String token_introspection_url; + String token_introspection_token; + + String defaultApplicationUUID; + + @PostConstruct + public void init() { + token_introspection_url = picSureWarInit.getToken_introspection_url(); + token_introspection_token = picSureWarInit.getToken_introspection_token(); + defaultApplicationUUID = picSureWarInit.getDefaultApplicationUUID(); + if (token_introspection_url == null || token_introspection_token == null) { + throw new RuntimeException("token_introspection_url and token_introspection_token not configured"); + } + } + + @GET + @Path("/status") + @Produces("text/plain") + public String status() { + // Because there is no auth on this service we limit actually performing the checking to 1 per minute to avoid DOS scenarios. + long timeOfRequest = System.currentTimeMillis(); + if (timeOfRequest - lastStatusCheck < max_test_frequency) { + return lastStatus; + } else { + lastStatusCheck = timeOfRequest; + try { + List resourcesToTest = resourceRepo.list(); + if (resourcesToTest == null || resourcesToTest.isEmpty()) { + lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; + return lastStatus; + } + + // convert the default application uuid to an uuid object + UUID defaultApplicationUUID = UUID.fromString(this.defaultApplicationUUID); + + // We need to remove open or auth HPDS from the resource list depending on the environment deployed. + // This because both are included in the database, but only one is actually deployed. + // if the name contains hpds and is not the default application uuid, remove it. + resourcesToTest.removeIf( + resource -> resource.getName().toLowerCase().contains("hpds") && !resource.getUuid().equals(defaultApplicationUUID) + ); + + // This proves the MySQL database is serving queries + // This proves at least one resources is configured + // This proves we can perform token introspection + if (testPSAMAResponds() && testResourcesRespond(resourcesToTest)) { // This proves all resources are at least serving info + // requests. + lastStatus = RUNNING; + return lastStatus; + } else { + lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; + } + } catch (Exception e) { + e.printStackTrace(); + lastStatus = ONE_OR_MORE_COMPONENTS_DEGRADED; + } + return lastStatus; + } + } + + private boolean testPSAMAResponds() throws UnsupportedOperationException, IOException { + CloseableHttpClient client = PicSureWarInit.CLOSEABLE_HTTP_CLIENT; + ObjectMapper json = PicSureWarInit.objectMapper; + + HttpPost post = new HttpPost(token_introspection_url); + post.setEntity(new StringEntity("{}")); + 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()) { + // This is actually the expected response as we did not send a token in the token_introspection_request. + return true; + } + + return true; + } finally { + try { + if (response != null) response.close(); + } catch (IOException ex) { + logger.error("callTokenIntroEndpoint() IOExcpetion when closing http response: " + ex.getMessage()); + } + } + } + + private boolean testResourcesRespond(List resourcesToTest) { + for (Resource resource : resourcesToTest) { + ResourceInfo info = new ResourceWebClient().info(resource.getResourceRSPath(), new GeneralQueryRequest()); + if (info == null) { + return false; + } + } + return true; + } +} + From f7d0985608c5ed467ad5010bb89dbd138793faff Mon Sep 17 00:00:00 2001 From: GeorgeC Date: Wed, 11 Sep 2024 14:57:23 -0400 Subject: [PATCH 06/10] Add open access feature in JWTFilter for unauthenticated requests Implemented open access configuration parameters and methods in PicSureWarInit. JWTFilter now checks for open access settings and validates requests accordingly, allowing unauthenticated access when enabled. --- .../harvard/dbmi/avillach/PicSureWarInit.java | 15 + .../dbmi/avillach/security/JWTFilter.java | 579 +++++++++++------- 2 files changed, 356 insertions(+), 238 deletions(-) diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java index 43356e1b..9bd35b08 100644 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java @@ -28,6 +28,11 @@ public class PicSureWarInit { @Resource(mappedName = "java:global/defaultApplicationUUID") private String default_application_uuid; + @Resource(mappedName = "java:global/openAccessEnabled") + private boolean open_access_enabled; + @Resource(mappedName = "java:global/openAccessValidateUrl") + private String open_access_validate_url; + // to be able to pre modified public static final ObjectMapper objectMapper = new ObjectMapper(); @@ -45,6 +50,7 @@ public class PicSureWarInit { CLOSEABLE_HTTP_CLIENT = HttpClients.custom().setConnectionManager(HTTP_CLIENT_CONNECTION_MANAGER).useSystemProperties().build(); } + public String getToken_introspection_url() { return token_introspection_url; } @@ -61,4 +67,13 @@ public String getToken_introspection_token() { public String getDefaultApplicationUUID() { return this.default_application_uuid; } + + public boolean isOpenAccessEnabled() { + return open_access_enabled; + } + + public String getOpenAccessValidateUrl() { + return this.open_access_validate_url; + } + } 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 3f758f7c..ba07c02c 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 @@ -13,6 +13,7 @@ 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.commons.lang3.StringUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; @@ -42,242 +43,344 @@ @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){ - GeneralQueryRequest queryRequest = new GeneralQueryRequest(); - 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())); - logger.info("This callTokenIntroEndpoint error can happen when your introspection token has expired. " + - "You can fix this by running the Configure PIC-SURE Token Introspection Token job in Jenkins."); - 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; - } + 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; + } + + boolean isOpenAccessEnabled = picSureWarInit.isOpenAccessEnabled(); + 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 ( + (StringUtils.isBlank(authorizationHeader) && isOpenAccessEnabled) + || (StringUtils.isNotBlank(authorizationHeader) && authorizationHeader.length() <= 7 && isOpenAccessEnabled) + ) { + boolean isAuthorized = callOpenAccessValidationEndpoint(requestContext); + if (!isAuthorized) { + logger.error("User is not authorized."); + requestContext.abortWith(PICSUREResponse.unauthorizedError("User is not authorized.")); + } + + // There is no user associated with open access request. In order to provide traceability, + // we set the username to OPEN_ACCESS: + requestContext.setProperty("username", "OPEN_ACCESS:" + requestContext.getUriInfo().getRequestUri().getHost()); + } else { + if (authorizationHeader == null || authorizationHeader.isEmpty()) { + throw new NotAuthorizedException("No authorization header found."); + } + + String token = authorizationHeader.substring(6).trim(); + if (token.isEmpty()) { + throw new NotAuthorizedException("No token found in authorization header."); + } + + 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 - {} - has just passed all the authentication and authorization layers.", userForLogging); + } catch (NotAuthorizedException e) { + // the detail of this exception should be logged right before the exception thrown out + logger.error("User - {} - is not authorized. {}", userForLogging, e.getChallenges()); + requestContext.abortWith(PICSUREResponse.unauthorizedError("User is not authorized. " + e.getChallenges())); + } catch (Exception e) { + logger + .error("User - {} - is not authorized {} and an Inner application error occurred.", userForLogging, e.getMessage()); + 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); + + Map requestMap = prepareRequestMap(requestContext); + tokenMap.put("request", requestMap); + + 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()) + ); + logger.info( + "This callTokenIntroEndpoint error can happen when your introspection token has expired. " + + "You can fix this by running the Configure PIC-SURE Token Introspection Token job in Jenkins." + ); + 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"); + } + + if (responseContent.has("tokenRefreshed") && responseContent.get("tokenRefreshed").asBoolean()) { + requestContext.setProperty("refreshedToken", responseContent.get("token")); + } + + 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; + } + + private HashMap prepareRequestMap(ContainerRequestContext requestContext) { + 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) { + GeneralQueryRequest queryRequest = new GeneralQueryRequest(); + 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); + } + } + } + } + } + return requestMap; + } catch (JsonParseException ex) { + requestMap.put("query", buffer.toString()); + return 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." + ); + } + } + + private boolean callOpenAccessValidationEndpoint(ContainerRequestContext requestContext) { + String openAccessValidateUrl = picSureWarInit.getOpenAccessValidateUrl(); + String token_introspection_token = picSureWarInit.getToken_introspection_token(); + + if (openAccessValidateUrl.isEmpty()) { + throw new ApplicationException("callOpenAccessValidationEndpoint - openAccessValidateUrl is empty in application properties"); + } + + Map requestMap = new HashMap<>(); + Map queryMap = prepareRequestMap(requestContext); + requestMap.put("request", queryMap); + // There is no user associated with open access request. In order to provide traceability, + // we set the username to OPEN_ACCESS: + requestMap.put("ipAddress", "OPEN_ACCESS:" + requestContext.getUriInfo().getRequestUri().getHost()); + ObjectMapper json = PicSureWarInit.objectMapper; + + StringEntity entity = null; + try { + entity = new StringEntity(json.writeValueAsString(requestMap)); + } catch (IOException e) { + logger.error("callOpenAccessValidationEndpoint() - FAILED TO parse requestMap to json", e); + return false; + } + + CloseableHttpClient client = PicSureWarInit.CLOSEABLE_HTTP_CLIENT; + HttpPost post = new HttpPost(openAccessValidateUrl); + 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; + boolean isValid = false; + try { + response = client.execute(post, buildHttpClientContext()); + + if (response.getStatusLine().getStatusCode() == 200) { + + // A 200 is return as long as the request is successful, the actual validation result is in the response body + JsonNode responseContent = json.readTree(response.getEntity().getContent()); + if (!responseContent.isBoolean()) { + logger.error( + "callOpenAccessValidateEndpoint() Open access validate endpoint return invalid response, content: {}", + responseContent + ); + throw new ApplicationException("Open access validate endpoint return invalid response"); + } + + isValid = responseContent.asBoolean(); + } else { + logger.error( + "callOpenAccessValidateEndpoint() error back from open access validate host server [{}]: {}", openAccessValidateUrl, + EntityUtils.toString(response.getEntity()) + ); + throw new ApplicationException( + "Open access validate host server returned " + response.getStatusLine().getStatusCode() + ". Please see the log" + ); + } + + } catch (IOException ex) { + logger.error("callOpenAccessValidateEndpoint() IOException when hitting url: {} with exception msg: {}", post, ex.getMessage()); + } finally { + try { + if (response != null) response.close(); + } catch (IOException ex) { + logger.error("callOpenAccessValidateEndpoint() IOException when closing http response: {}", ex.getMessage()); + } + } + + return isValid; + } + + void setUserIdClaim(String userIdClaim) { + this.userIdClaim = userIdClaim; + } } From 72e5f73274f6785a713a83c1d0f3ac41128c822c Mon Sep 17 00:00:00 2001 From: GeorgeC Date: Thu, 12 Sep 2024 07:55:59 -0400 Subject: [PATCH 07/10] Refactor JWTFilter to check Open Access inside ELSE block --- .../main/java/edu/harvard/dbmi/avillach/security/JWTFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ba07c02c..10fda6d3 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 @@ -76,7 +76,6 @@ public void filter(ContainerRequestContext requestContext) throws IOException { return; } - boolean isOpenAccessEnabled = picSureWarInit.isOpenAccessEnabled(); if ( requestContext.getUriInfo().getPath().contentEquals("/system/status") && requestContext.getRequest().getMethod().contentEquals(HttpMethod.GET) @@ -86,6 +85,7 @@ public void filter(ContainerRequestContext requestContext) throws IOException { } else { // Everything else goes through PSAMA token introspection String authorizationHeader = requestContext.getHeaderString(HttpHeaders.AUTHORIZATION); + boolean isOpenAccessEnabled = picSureWarInit.isOpenAccessEnabled(); if ( (StringUtils.isBlank(authorizationHeader) && isOpenAccessEnabled) || (StringUtils.isNotBlank(authorizationHeader) && authorizationHeader.length() <= 7 && isOpenAccessEnabled) From b226bb8ed8eea7c9bb710eec480f3f05e28f73e2 Mon Sep 17 00:00:00 2001 From: GeorgeC Date: Mon, 16 Sep 2024 13:55:42 -0400 Subject: [PATCH 08/10] Refactor logging and condition checks --- .../service/VisualizationService.java | 73 +++++++++++-------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationService.java b/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationService.java index 0b5ef0c6..6a3c9043 100644 --- a/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationService.java +++ b/pic-sure-resources/pic-sure-visualization-resource/src/main/java/edu/harvard/hms/dbmi/avillach/resource/visualization/service/VisualizationService.java @@ -42,7 +42,7 @@ public class VisualizationService { logger.info("Initializing properties"); } properties.init("pic-sure-visualization-resource"); - logger.info("VisualizationResource initialized ->", properties.getOrigin()); + logger.info("VisualizationResource initialized -> {}", properties.getOrigin()); threshold = "< " + properties.getTargetPicsureObfuscationThreshold(); variance = "±" + properties.getTargetPicsureObfuscationVariance(); @@ -51,7 +51,7 @@ public class VisualizationService { /** * Handles a query request from the UI. This method is called from the VisualizationResource class. * - * @param query QueryRequest - the query request + * @param query QueryRequest - the query request * @param requestSource String - the request source, Authorized or Open * @return ProcessedCrossCountsResponse */ @@ -62,7 +62,7 @@ public Response handleQuerySync(QueryRequest query, String requestSource) { } catch (Exception e) { // The exception is caught here because I don't want to modify the method signature to throw the // exception. - logger.error("Error parsing query: \n" + query, e); + logger.error("Error parsing query: \n{}", query, e); return Response.status(Response.Status.BAD_REQUEST).entity("Could not parse query.").build(); } @@ -79,24 +79,29 @@ public Response handleQuerySync(QueryRequest query, String requestSource) { } } - private Response getProcessedCrossCountResponse(Map> categoryCrossCountsMap, Map> continuousCrossCountsMap) { - if ((categoryCrossCountsMap == null || categoryCrossCountsMap.isEmpty()) && (continuousCrossCountsMap == null || continuousCrossCountsMap.isEmpty())) - return Response.ok().build(); - ProcessedCrossCountsResponse response = buildProcessedCrossCountsResponse(categoryCrossCountsMap, continuousCrossCountsMap, false, false, false); + private Response getProcessedCrossCountResponse( + Map> categoryCrossCountsMap, Map> continuousCrossCountsMap + ) { + if ( + (categoryCrossCountsMap == null || categoryCrossCountsMap.isEmpty()) + && (continuousCrossCountsMap == null || continuousCrossCountsMap.isEmpty()) + ) return Response.ok().build(); + ProcessedCrossCountsResponse response = + buildProcessedCrossCountsResponse(categoryCrossCountsMap, continuousCrossCountsMap, false, false, false); return Response.ok(response).build(); } /** - * This method determines if the data is obfuscated and if so, converts the string values to integers by removing - * the obfuscation types. If the value was obfuscated the response will be marked as obfuscated so the UI can - * display the data accordingly. + * This method determines if the data is obfuscated and if so, converts the string values to integers by removing the obfuscation types. + * If the value was obfuscated the response will be marked as obfuscated so the UI can display the data accordingly. * - * @param categoryCrossCountsMap - the categorical cross counts + * @param categoryCrossCountsMap - the categorical cross counts * @param continuousCrossCountsMap - the continuous cross counts * @return Response - the processed cross counts response */ - private Response getOpenProcessedCrossCountResponse(Map> categoryCrossCountsMap, - Map> continuousCrossCountsMap) { + private Response getOpenProcessedCrossCountResponse( + Map> categoryCrossCountsMap, Map> continuousCrossCountsMap + ) { Map> cleanedCategoricalData = new HashMap<>(); boolean isCategoricalObfuscated = false; if (categoryCrossCountsMap != null && !categoryCrossCountsMap.isEmpty()) { @@ -111,13 +116,15 @@ private Response getOpenProcessedCrossCountResponse(Map> - the cleaned categorical data @@ -164,20 +171,25 @@ private boolean isObfuscated(Map> crossCounts) { return isObfuscated; } - private ProcessedCrossCountsResponse buildProcessedCrossCountsResponse(Map> categoryCrossCountsMap, - Map> continuousCrossCountsMap, - boolean isCategoricalObfuscated, boolean isContinuousObfuscated, boolean isOpenAccess) { + private ProcessedCrossCountsResponse buildProcessedCrossCountsResponse( + Map> categoryCrossCountsMap, Map> continuousCrossCountsMap, + boolean isCategoricalObfuscated, boolean isContinuousObfuscated, boolean isOpenAccess + ) { ProcessedCrossCountsResponse response = new ProcessedCrossCountsResponse(); - response.getCategoricalData().addAll(dataProcessingServices.getCategoricalData(categoryCrossCountsMap, isCategoricalObfuscated, isOpenAccess)); - response.getContinuousData().addAll(dataProcessingServices.getContinuousData(continuousCrossCountsMap, isContinuousObfuscated, isOpenAccess)); + response.getCategoricalData() + .addAll(dataProcessingServices.getCategoricalData(categoryCrossCountsMap, isCategoricalObfuscated, isOpenAccess)); + response.getContinuousData() + .addAll(dataProcessingServices.getContinuousData(continuousCrossCountsMap, isContinuousObfuscated, isOpenAccess)); return response; } private Map> getCategoryCrossCountsMap(QueryRequest query, Query queryJson) { Map> categoryCrossCountsMap; - if ((queryJson.categoryFilters != null && queryJson.categoryFilters.size() > 0) || - (queryJson.requiredFields != null && queryJson.requiredFields.size() > 0)) { + if ( + (queryJson.categoryFilters != null && !queryJson.categoryFilters.isEmpty()) + || (queryJson.requiredFields != null && !queryJson.requiredFields.isEmpty()) + ) { categoryCrossCountsMap = hpdsServices.getAuthCrossCountsMap(query, ResultType.CATEGORICAL_CROSS_COUNT); } else { categoryCrossCountsMap = new HashMap<>(); @@ -186,13 +198,13 @@ private Map> getCategoryCrossCountsMap(QueryRequest } /** - * @param query QueryRequest + * @param query QueryRequest * @param queryJson Query * @return Map> - the continuous cross counts */ private Map> getContinuousCrossCount(QueryRequest query, Query queryJson) { Map> continuousCrossCountsMap; - if ((queryJson.numericFilters != null && queryJson.numericFilters.size() > 0)) { + if ((queryJson.numericFilters != null && !queryJson.numericFilters.isEmpty())) { continuousCrossCountsMap = hpdsServices.getAuthCrossCountsMap(query, ResultType.CONTINUOUS_CROSS_COUNT); } else { continuousCrossCountsMap = new HashMap<>(); @@ -202,8 +214,10 @@ private Map> getContinuousCrossCount(QueryRequest q private Map> getOpenCategoricalCrossCounts(QueryRequest query, Query queryJson) { Map> crossCountsMap; - if ((queryJson.categoryFilters != null && queryJson.categoryFilters.size() > 0) || - (queryJson.requiredFields != null && queryJson.requiredFields.size() > 0)) { + if ( + (queryJson.categoryFilters != null && !queryJson.categoryFilters.isEmpty()) + || (queryJson.requiredFields != null && !queryJson.requiredFields.isEmpty()) + ) { crossCountsMap = hpdsServices.getOpenCrossCountsMap(query, ResultType.CATEGORICAL_CROSS_COUNT); } else { crossCountsMap = new HashMap<>(); @@ -214,7 +228,7 @@ private Map> getOpenCategoricalCrossCounts(QueryRequ private Map> getOpenContinuousCrossCounts(QueryRequest query, Query queryJson) { Map> crossCountsMap; - if ((queryJson.numericFilters != null && queryJson.numericFilters.size() > 0)) { + if ((queryJson.numericFilters != null && !queryJson.numericFilters.isEmpty())) { crossCountsMap = hpdsServices.getOpenCrossCountsMap(query, ResultType.CONTINUOUS_CROSS_COUNT); } else { crossCountsMap = new HashMap<>(); @@ -236,8 +250,7 @@ public Response generateContinuousBin(QueryRequest continuousData) { } logger.info("Continuous data: " + continuousData.getQuery()); - Map> continuousDataMap = mapper.convertValue(continuousData.getQuery(), new TypeReference<>() { - }); + Map> continuousDataMap = mapper.convertValue(continuousData.getQuery(), new TypeReference<>() {}); Map> continuousProcessedData = dataProcessingServices.binContinuousData(continuousDataMap); return Response.ok(continuousProcessedData).build(); } From 215736b7490b57acf8351ae9c13cc71a463b22ce Mon Sep 17 00:00:00 2001 From: GeorgeC Date: Mon, 16 Sep 2024 18:06:47 -0400 Subject: [PATCH 09/10] Add initialization for 'open_access_enabled' flag Implemented a @PostConstruct init method to parse and set the open_access_enabled flag. This ensures the flag reflects the correct boolean value based on the configured string. --- .../java/edu/harvard/dbmi/avillach/PicSureWarInit.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java index 9bd35b08..5706f1cf 100644 --- a/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java +++ b/pic-sure-api-war/src/main/java/edu/harvard/dbmi/avillach/PicSureWarInit.java @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.ejb.Singleton; import javax.enterprise.context.ApplicationScoped; @@ -29,10 +30,18 @@ public class PicSureWarInit { private String default_application_uuid; @Resource(mappedName = "java:global/openAccessEnabled") + private String open_access_enabled_str; + private boolean open_access_enabled; + @Resource(mappedName = "java:global/openAccessValidateUrl") private String open_access_validate_url; + @PostConstruct + public void init() { + this.open_access_enabled = Boolean.parseBoolean(open_access_enabled_str); + } + // to be able to pre modified public static final ObjectMapper objectMapper = new ObjectMapper(); From 0f2ab36c4c39f69f84353aca2784e5d424a67bed Mon Sep 17 00:00:00 2001 From: GeorgeC Date: Tue, 17 Sep 2024 14:48:18 -0400 Subject: [PATCH 10/10] Fix typos and improve error messages in JWTFilter Corrected typos in the JWTFilter error messages to enhance clarity. Updated the language for better understanding when exceptions are thrown. --- .../edu/harvard/dbmi/avillach/security/JWTFilter.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) 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 10fda6d3..feaed52e 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 @@ -304,7 +304,7 @@ private HashMap prepareRequestMap(ContainerRequestContext reques } 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." + "The request could not be properly audited. If you receive this error multiple times, please contact an administrator." ); } } @@ -353,18 +353,16 @@ private boolean callOpenAccessValidationEndpoint(ContainerRequestContext request "callOpenAccessValidateEndpoint() Open access validate endpoint return invalid response, content: {}", responseContent ); - throw new ApplicationException("Open access validate endpoint return invalid response"); + throw new ApplicationException("Open access validate endpoint returned an invalid response"); } isValid = responseContent.asBoolean(); } else { logger.error( - "callOpenAccessValidateEndpoint() error back from open access validate host server [{}]: {}", openAccessValidateUrl, + "callOpenAccessValidateEndpoint() error returned from psama [{}]: {}", openAccessValidateUrl, EntityUtils.toString(response.getEntity()) ); - throw new ApplicationException( - "Open access validate host server returned " + response.getStatusLine().getStatusCode() + ". Please see the log" - ); + throw new ApplicationException("Not able to validate open access request"); } } catch (IOException ex) {