diff --git a/caching/pom.xml b/caching/pom.xml index 24c76dc116501467ebe55038cc4e40be50c6ecb1..fdf98454b376a68b8a85541de02f6ab917d1d9df 100644 --- a/caching/pom.xml +++ b/caching/pom.xml @@ -9,7 +9,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> <relativePath>../pom.xml</relativePath> </parent> <properties> @@ -45,7 +45,7 @@ <!-- Required for serialization --> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-jackson</artifactId> + <artifactId>quarkus-resteasy-reactive-jackson</artifactId> </dependency> <!-- Adds logging bindings for more natural logging --> <dependency> @@ -81,7 +81,7 @@ <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-qute</artifactId> + <artifactId>quarkus-resteasy-reactive-qute</artifactId> </dependency> <!-- Test dependencies --> <dependency> @@ -137,4 +137,4 @@ </plugin> </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/caching/src/main/java/org/eclipsefoundation/caching/model/ParameterizedCacheKey.java b/caching/src/main/java/org/eclipsefoundation/caching/model/ParameterizedCacheKey.java index a8e1dd51a1d401221bbda940dd2e17348efd3488..be9b8a09e6017e3ac7f054227ca2809e4c1e1714 100644 --- a/caching/src/main/java/org/eclipsefoundation/caching/model/ParameterizedCacheKey.java +++ b/caching/src/main/java/org/eclipsefoundation/caching/model/ParameterizedCacheKey.java @@ -12,13 +12,13 @@ package org.eclipsefoundation.caching.model; import org.eclipsefoundation.utils.helper.ParameterHelper; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.auto.value.AutoValue; import io.quarkus.arc.Arc; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -35,7 +35,7 @@ public abstract class ParameterizedCacheKey { public abstract MultivaluedMap<String, String> getParams(); public static Builder builder() { - return new AutoValue_ParameterizedCacheKey.Builder().setParams(new MultivaluedMapImpl<>()); + return new AutoValue_ParameterizedCacheKey.Builder().setParams(new MultivaluedHashMap<>()); } @Override diff --git a/caching/src/main/java/org/eclipsefoundation/caching/service/impl/QuarkusCachingService.java b/caching/src/main/java/org/eclipsefoundation/caching/service/impl/QuarkusCachingService.java index 881216b6d37cc341d479e93a6cc06c71e31b4e8d..f1d3d156e7cd0a645c42763607fb261a4ba2bd47 100644 --- a/caching/src/main/java/org/eclipsefoundation/caching/service/impl/QuarkusCachingService.java +++ b/caching/src/main/java/org/eclipsefoundation/caching/service/impl/QuarkusCachingService.java @@ -29,7 +29,6 @@ import org.eclipsefoundation.caching.namespaces.CachingPropertyNames; import org.eclipsefoundation.caching.service.CachingService; import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,6 +42,7 @@ import io.quarkus.cache.CacheName; import io.quarkus.cache.CacheResult; import io.quarkus.cache.CaffeineCache; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -84,7 +84,7 @@ public class QuarkusCachingService implements CachingService { throw new IllegalStateException("Cached paginated data can only be used in the context of a request"); } // copy the map to not mutate the passed reference when adding the pagination parameter - MultivaluedMap<String, String> paramMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> paramMap = new MultivaluedHashMap<>(); if (params == null) { paramMap .add(DefaultUrlParameterNames.PAGE_PARAMETER_NAME, @@ -202,7 +202,7 @@ public class QuarkusCachingService implements CachingService { * @return the copied map, or an empty map if null */ private MultivaluedMap<String, String> cloneMap(MultivaluedMap<String, String> paramMap) { - MultivaluedMap<String, String> out = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> out = new MultivaluedHashMap<>(); if (paramMap != null) { paramMap.entrySet().forEach(e -> out.addAll(e.getKey(), e.getValue())); } diff --git a/caching/src/test/java/org/eclipsefoundation/caching/service/impl/DefaultQuarkusCachingServiceTest.java b/caching/src/test/java/org/eclipsefoundation/caching/service/impl/DefaultQuarkusCachingServiceTest.java index 265dc08d1e1af162ab5f6c047b3ef8c34b4ac70d..c22b63bb0b9da9340881fd37993af26f112b823f 100644 --- a/caching/src/test/java/org/eclipsefoundation/caching/service/impl/DefaultQuarkusCachingServiceTest.java +++ b/caching/src/test/java/org/eclipsefoundation/caching/service/impl/DefaultQuarkusCachingServiceTest.java @@ -17,7 +17,6 @@ import java.util.UUID; import org.eclipsefoundation.caching.model.CacheWrapper; import org.eclipsefoundation.caching.model.ParameterizedCacheKey; import org.eclipsefoundation.caching.service.CachingService; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -26,6 +25,7 @@ import io.quarkus.cache.CacheName; import io.quarkus.cache.CaffeineCache; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -62,7 +62,7 @@ class DefaultQuarkusCachingServiceTest { // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); // do the lookup - CacheWrapper<String> value = svc.get(id, new MultivaluedMapImpl<>(), String.class, () -> "sample"); + CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> "sample"); Assertions .assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), @@ -83,7 +83,7 @@ class DefaultQuarkusCachingServiceTest { "Expected random UUID key to have no TTL key, but found value"); // do the lookup that should generate a TTL cache entry - svc.get(id, new MultivaluedMapImpl<>(), String.class, () -> cacheValue); + svc.get(id, new MultivaluedHashMap<>(), String.class, () -> cacheValue); Assertions .assertTrue(lookupCacheKeyUsingRawCache(id, Optional.empty(), ttlCache), @@ -96,7 +96,7 @@ class DefaultQuarkusCachingServiceTest { String id = UUID.randomUUID().toString(); String cacheValue = "sample"; // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); @@ -116,10 +116,10 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // create a param map to use in creating distinct keys - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(PARAMETER_NAME, "sample_value"); // check that we have no match for ID before proceeding @@ -144,7 +144,7 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); @@ -172,7 +172,7 @@ class DefaultQuarkusCachingServiceTest { Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); // value should be null as there was an error - CacheWrapper<String> value = svc.get(id, new MultivaluedMapImpl<>(), String.class, () -> { throw new RuntimeException("Kaboom"); }); + CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> { throw new RuntimeException("Kaboom"); }); Assertions.assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); Assertions.assertTrue(value.getData().isEmpty(), "Cache value should have been missing but was present"); Assertions.assertEquals(RuntimeException.class, value.getErrorType().get(), "The exception type caught in the wrapper should be the same as thrown"); @@ -186,7 +186,7 @@ class DefaultQuarkusCachingServiceTest { Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); // value should be null as there was an error - CacheWrapper<String> value = svc.get(id, new MultivaluedMapImpl<>(), String.class, () -> { throw new RuntimeException("Kaboom"); }); + CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> { throw new RuntimeException("Kaboom"); }); Assertions.assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); Assertions.assertTrue(value.getData().isEmpty(), "Cache value should have been missing but was present"); Assertions.assertEquals(RuntimeException.class, value.getErrorType().get(), "The exception type caught in the wrapper should be the same as thrown"); @@ -200,7 +200,7 @@ class DefaultQuarkusCachingServiceTest { Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); // value should be empty as null gets interpreted as an empty optional - CacheWrapper<String> value = svc.get(id, new MultivaluedMapImpl<>(), String.class, () -> null); + CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> null); Assertions.assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); Assertions.assertTrue(value.getData().isEmpty(), "Cache value should have been missing but was present"); @@ -215,7 +215,7 @@ class DefaultQuarkusCachingServiceTest { Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); // value should be null as there was an error - CacheWrapper<String> value = svc.get(id, new MultivaluedMapImpl<>(), String.class, () -> null); + CacheWrapper<String> value = svc.get(id, new MultivaluedHashMap<>(), String.class, () -> null); Assertions.assertTrue(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to have a value, but was empty"); Assertions.assertTrue(value.getData().isEmpty(), "Cache value should have been missing but was present"); Assertions.assertTrue(value.getErrorType().isEmpty(), "There should be no exception caught in this case"); @@ -234,7 +234,7 @@ class DefaultQuarkusCachingServiceTest { .assertFalse(svc.getCacheKeys().stream().anyMatch(k -> k.getId().equals(id)), "Expected random UUID key to be empty, but found key"); // run the op to populate the cache value - svc.get(id, new MultivaluedMapImpl<>(), String.class, () -> "sample"); + svc.get(id, new MultivaluedHashMap<>(), String.class, () -> "sample"); // check that the cache value can be seen in the cache key set Assertions @@ -265,7 +265,7 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map for initial post - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); // request + put the data in cache and check it's there @@ -275,7 +275,7 @@ class DefaultQuarkusCachingServiceTest { "Expected new cache value to have a key present in cache key set with empty map"); // update the map and create another entry - params = new MultivaluedMapImpl<>(); + params = new MultivaluedHashMap<>(); params.add(SECOND_PARAMETER_NAME, id); Assertions .assertFalse(lookupCacheKeyUsingMainCache(id, Optional.of(params)), @@ -303,7 +303,7 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map for initial post - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions .assertFalse(lookupCacheKeyUsingRawCache(id, Optional.of(params), Optional.of(String.class), cache), @@ -345,7 +345,7 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); @@ -369,7 +369,7 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions @@ -405,7 +405,7 @@ class DefaultQuarkusCachingServiceTest { .builder() .setId("other-key-not-present") .setClazz(Integer.class) - .setParams(new MultivaluedMapImpl<>()) + .setParams(new MultivaluedHashMap<>()) .build(); // attempt to remove the non-existent cache key @@ -417,7 +417,7 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); @@ -448,7 +448,7 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); @@ -474,10 +474,10 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // create a param map to use in creating distinct keys - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(PARAMETER_NAME, "sample_value"); // check that we have no match for ID before proceeding @@ -508,7 +508,7 @@ class DefaultQuarkusCachingServiceTest { // generate a random key String id = UUID.randomUUID().toString(); // create empty map to be used in comparisons - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); // check that we have no match for ID before proceeding Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); @@ -531,7 +531,7 @@ class DefaultQuarkusCachingServiceTest { Assertions.assertFalse(lookupCacheKeyUsingMainCache(id, Optional.empty()), "Expected random UUID key to be empty, but found value"); // keep map separate to reduce assertion to have 1 actual call - MultivaluedMap<String, String> emptyMap = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> emptyMap = new MultivaluedHashMap<>(); Assertions .assertThrows(RuntimeException.class, () -> svc.getExpiration(id, emptyMap, String.class), "Expected missing cache key to throw exception when there is no TTL present"); diff --git a/core/pom.xml b/core/pom.xml index 9031d2b2ce1a7ab6c9b08b0362487b62d32e261e..97621a5a8d20eba3e03f0f74cb0e4273a2bbd623 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -9,7 +9,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> <relativePath>../pom.xml</relativePath> </parent> <properties> diff --git a/core/src/main/java/org/eclipsefoundation/core/model/StandardPaginationResolver.java b/core/src/main/java/org/eclipsefoundation/core/model/StandardPaginationResolver.java index de3c523da0865aca9cecd25a515a10fc0b50c4aa..01427235815c167853308acf56e9305879b315a1 100644 --- a/core/src/main/java/org/eclipsefoundation/core/model/StandardPaginationResolver.java +++ b/core/src/main/java/org/eclipsefoundation/core/model/StandardPaginationResolver.java @@ -21,7 +21,7 @@ import org.eclipsefoundation.core.service.PaginationHeaderService.PaginationReso import org.eclipsefoundation.http.config.PaginationConfig; import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.http.response.PaginatedResultsFilter; -import org.jboss.resteasy.specimpl.MultivaluedTreeMap; +import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core/src/main/java/org/eclipsefoundation/core/response/CachedResponseFilter.java b/core/src/main/java/org/eclipsefoundation/core/response/CachedResponseFilter.java index aa82aab0bd0dc2ec95e67f893d88e7b32f5c69fb..be269255758f44126347996c270279930290b749 100644 --- a/core/src/main/java/org/eclipsefoundation/core/response/CachedResponseFilter.java +++ b/core/src/main/java/org/eclipsefoundation/core/response/CachedResponseFilter.java @@ -16,17 +16,16 @@ import java.io.IOException; import org.eclipsefoundation.core.service.PaginationHeaderService; import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.utils.helper.TransformationHelper; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.vertx.core.http.HttpMethod; -import jakarta.annotation.Priority; import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; import jakarta.ws.rs.container.ContainerResponseFilter; import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.ext.Provider; /** * Either applies recorded actions to the response or to record the response if nothing has been recorded yet. Priority @@ -35,8 +34,6 @@ import jakarta.ws.rs.ext.Provider; * * @author Martin Lowe */ -@Provider -@Priority(50) public class CachedResponseFilter implements ContainerResponseFilter { private static final Logger LOGGER = LoggerFactory.getLogger(CachedResponseFilter.class); @@ -45,7 +42,7 @@ public class CachedResponseFilter implements ContainerResponseFilter { @Inject RequestWrapper wrap; - @Override + @ServerResponseFilter(priority = 5050) public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { if (LOGGER.isTraceEnabled()) { LOGGER.trace("Checking request {}", TransformationHelper.formatLog(wrap.getEndpoint())); diff --git a/core/src/main/java/org/eclipsefoundation/core/service/APIMiddleware.java b/core/src/main/java/org/eclipsefoundation/core/service/APIMiddleware.java index 7501c3bea0c6ce6fd0bc94b8556510ed17aedf30..2918c4f3f8420d9cb0a672c2f5f7cb3f346c68e3 100644 --- a/core/src/main/java/org/eclipsefoundation/core/service/APIMiddleware.java +++ b/core/src/main/java/org/eclipsefoundation/core/service/APIMiddleware.java @@ -12,7 +12,6 @@ package org.eclipsefoundation.core.service; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.function.Function; @@ -29,9 +28,9 @@ public interface APIMiddleware { <T> List<T> paginationPassThrough(Function<BaseAPIParameters, Response> supplier, RequestWrapper request, Class<T> type); /** - * Returns the full list of data for the given API call, using a function that accepts the page number to iterate over - * the pages of data. The Link header is scraped off of the first request and is used to determine how many calls need - * to be made to get the full data set. + * Returns the full list of data for the given API call, using a function that accepts the page number to iterate over the pages of + * data. The Link header is scraped off of the first request and is used to determine how many calls need to be made to get the full + * data set. * * @param <T> the type of data that is retrieved * @param supplier function that accepts a page number and makes an API call for the given page. @@ -41,47 +40,18 @@ public interface APIMiddleware { <T> List<T> getAll(Function<BaseAPIParameters, Response> supplier, Class<T> type); public static class BaseAPIParameters { - private Integer page; - private Integer limit; - private RequestWrapper requestWrapper; - - public BaseAPIParameters() { - } - - public BaseAPIParameters(@Nullable Integer page, @Nullable Integer limit, @Nullable RequestWrapper requestWrapper) { - this.limit = limit; - this.page = page; - this.requestWrapper = requestWrapper; - } - - /** - * @return the page - */ @Nullable @QueryParam("page") - public Integer getPage() { - return page; - } - - /** - * @return the limit - */ + public Integer page; @Nullable @QueryParam("pagesize") - public Integer getLimit() { - return limit; - } - - /** - * @return the requestWrapper - */ - @Nullable - public RequestWrapper getRequestWrapper() { - return requestWrapper; - } + public Integer limit; + public final RequestWrapper requestWrapper; - public static Builder builder() { - return new Builder(); + public BaseAPIParameters(Integer page, Integer limit, RequestWrapper requestWrapper) { + this.page = page; + this.limit = limit; + this.requestWrapper = requestWrapper; } public static BaseAPIParameters buildFromWrapper(RequestWrapper request) { @@ -91,68 +61,12 @@ public interface APIMiddleware { if (page.isPresent() && StringUtils.isNumeric(page.get())) { pageActual = Integer.valueOf(page.get()); } - return BaseAPIParameters.builder().setPage(pageActual).setLimit(request.getPageSize()).setRequestWrapper(request).build(); + return new BaseAPIParameters(pageActual, request.getPageSize(), request); } @Override public String toString() { - return "BaseAPIParameters{" + "page=" + page + ", " + "limit=" + limit + "}"; - } - - @Override - public int hashCode() { - return Objects.hash(limit, page, requestWrapper); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - BaseAPIParameters other = (BaseAPIParameters) obj; - return Objects.equals(limit, other.limit) && Objects.equals(page, other.page) - && Objects.equals(requestWrapper, other.requestWrapper); - } - - public static class Builder { - private Integer page; - private Integer limit; - private RequestWrapper requestWrapper; - - /** - * @param page the page to set - */ - public Builder setPage(@Nullable Integer page) { - this.page = page; - return this; - } - - /** - * @param limit the limit to set - */ - @QueryParam("pagesize") - public Builder setLimit(@Nullable Integer limit) { - this.limit = limit; - return this; - } - - /** - * @param requestWrapper the requestWrapper to set - */ - public Builder setRequestWrapper(@Nullable RequestWrapper requestWrapper) { - this.requestWrapper = requestWrapper; - return this; - } - - public BaseAPIParameters build() { - return new BaseAPIParameters(page, limit, requestWrapper); - } + return "BaseAPIParameters [page=" + page + ", limit=" + limit + "]"; } } } \ No newline at end of file diff --git a/core/src/main/java/org/eclipsefoundation/core/service/impl/DefaultAPIMiddleware.java b/core/src/main/java/org/eclipsefoundation/core/service/impl/DefaultAPIMiddleware.java index 6332f7c09492b4ced98997c389cfd6b767a199e4..da3c6c1d507386cdee80ea44a39b21a84589eb9d 100644 --- a/core/src/main/java/org/eclipsefoundation/core/service/impl/DefaultAPIMiddleware.java +++ b/core/src/main/java/org/eclipsefoundation/core/service/impl/DefaultAPIMiddleware.java @@ -29,8 +29,7 @@ import org.eclipsefoundation.core.service.APIMiddleware; import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.http.response.PaginatedResultsFilter; import org.eclipsefoundation.utils.helper.ParameterHelper; -import org.jboss.resteasy.specimpl.MultivaluedTreeMap; -import org.jboss.resteasy.spi.LinkHeader; +import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +40,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.ServerErrorException; import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; @@ -141,9 +141,9 @@ public class DefaultAPIMiddleware implements APIMiddleware { */ private Response doCall(Function<BaseAPIParameters, Response> supplier, int count) { try { - return supplier.apply(BaseAPIParameters.builder().setPage(count).build()); + return supplier.apply(new BaseAPIParameters(count, null, null)); } catch (WebApplicationException e) { - LOGGER.error("Error retrieving a paginated request with params: {}", BaseAPIParameters.builder().setPage(count).build()); + LOGGER.error("Error retrieving a paginated request with params: {}", new BaseAPIParameters(count, null, null)); // continue throwing after logging details about the problematic request throw e; } @@ -174,16 +174,14 @@ public class DefaultAPIMiddleware implements APIMiddleware { } private int getLastPageIndex(Response r) { - List<Object> links = r.getHeaders().get("Link"); - // convert it to retrieve the index of the last page - if (links != null && !links.isEmpty()) { - LinkHeader h = LinkHeader.valueOf(links.get(0).toString()); - // check what the last page link has as its 'page' param val - URI lastLink = h.getLinkByRelationship("last").getUri(); - MultivaluedMap<String, String> params = ParameterHelper.parseQueryString(lastLink.getQuery()); - if (params.keySet().contains("page")) { - return Integer.parseInt(params.getFirst("page")); - } + Link lastPageLink = r.getLink("last"); + if (lastPageLink == null) { + return 0; + } + // check what the last page link has as its 'page' param val + MultivaluedMap<String, String> params = ParameterHelper.parseQueryString(lastPageLink.getUri().getQuery()); + if (params.keySet().contains("page")) { + return Integer.parseInt(params.getFirst("page")); } return 0; } diff --git a/core/src/test/java/org/eclipsefoundation/core/model/StandardPaginationResolverTest.java b/core/src/test/java/org/eclipsefoundation/core/model/StandardPaginationResolverTest.java index 3795bddf235fa2eb358d0863f5e006f08016ee3b..33466a1e89f2c84de97bc3e3a5801b6c854553e2 100644 --- a/core/src/test/java/org/eclipsefoundation/core/model/StandardPaginationResolverTest.java +++ b/core/src/test/java/org/eclipsefoundation/core/model/StandardPaginationResolverTest.java @@ -18,8 +18,8 @@ import java.util.UUID; import org.apache.commons.lang3.StringUtils; import org.eclipsefoundation.http.model.FlatRequestWrapper; import org.eclipsefoundation.http.response.PaginatedResultsFilter; -import org.jboss.resteasy.specimpl.MultivaluedTreeMap; -import org.jboss.resteasy.util.CaseInsensitiveMap; +import org.jboss.resteasy.reactive.common.util.CaseInsensitiveMap; +import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/org/eclipsefoundation/core/response/CachedResponseFilterTest.java b/core/src/test/java/org/eclipsefoundation/core/response/CachedResponseFilterTest.java index 74e74180ea1c134b35edacbb07d7f24f2815db95..c9f1200f95963823e31895735c8a904253732dbb 100644 --- a/core/src/test/java/org/eclipsefoundation/core/response/CachedResponseFilterTest.java +++ b/core/src/test/java/org/eclipsefoundation/core/response/CachedResponseFilterTest.java @@ -4,14 +4,15 @@ import static io.restassured.RestAssured.given; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.UUID; import org.eclipsefoundation.core.test.resources.TestResource; import org.eclipsefoundation.http.response.PaginatedResultsFilter; -import org.jboss.resteasy.spi.LinkHeader; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.logging.Log; import io.quarkus.test.junit.QuarkusTest; import io.restassured.response.Response; import jakarta.ws.rs.core.Link; @@ -23,21 +24,29 @@ public class CachedResponseFilterTest { void filter_success_headerCachingAppliesBeforeFilter() { String uniqueKey = UUID.randomUUID().toString(); Map<String, String> expectedHeaders = new HashMap<>(); - expectedHeaders.put(PaginatedResultsFilter.MAX_RESULTS_SIZE_HEADER, TestResource.PAGINATION_HEADER_RESULT_SIZE); - expectedHeaders.put(PaginatedResultsFilter.MAX_PAGE_SIZE_HEADER, TestResource.PAGINATION_HEADER_PAGE_SIZE_DEFAULT); + expectedHeaders.put(PaginatedResultsFilter.MAX_RESULTS_SIZE_HEADER, + TestResource.PAGINATION_HEADER_RESULT_SIZE); + expectedHeaders.put(PaginatedResultsFilter.MAX_PAGE_SIZE_HEADER, + TestResource.PAGINATION_HEADER_PAGE_SIZE_DEFAULT); // endpoint has stored pagination header sources Response vr = given().header("result-size", 120).get("test/pagination/cached/" + uniqueKey); - LinkHeader expectedValue = LinkHeader.valueOf(vr.header("Link")); - Link expectedLastRel = expectedValue.getLinkByRelationship("last"); + Log.error(vr.headers().getValues("Link")); + Optional<Link> expectedLastRel = vr.headers().getValues("Link").stream().map(l -> Link.valueOf(l)) + .filter(l -> "last".equals(l.getRel())).findFirst(); - // response that should have the cached results which would apply to the link header + // response that should have the cached results which would apply to the link + // header Response cachedResponse = given().get("test/pagination/cached/" + uniqueKey); - - LinkHeader value = LinkHeader.valueOf(cachedResponse.header("Link")); - Link lastRel = value.getLinkByRelationship("last"); - // check that our last link is the same as in the initial request which had the headers set - Assertions.assertEquals(expectedLastRel, lastRel); + Optional<Link> actualLastRel = cachedResponse.headers().getValues("Link").stream().map(l -> Link.valueOf(l)) + .filter(l -> "last".equals(l.getRel())).findFirst(); + + // check that our last link is the same as in the initial request which had the + // headers set + Assertions.assertTrue(expectedLastRel.isPresent()); + Assertions.assertTrue(actualLastRel.isPresent()); + Assertions.assertEquals(expectedLastRel.get(), actualLastRel.get()); // check that the intended value of 12 is used for the resultant query string - Assertions.assertEquals("page=12", lastRel.getUri().getQuery(), "Expected 12 pages (10 items per page)"); + Assertions.assertEquals("page=12", actualLastRel.get().getUri().getQuery(), + "Expected 12 pages (10 items per page)"); } } diff --git a/core/src/test/java/org/eclipsefoundation/core/service/impl/DefaultAPIMiddlewareTest.java b/core/src/test/java/org/eclipsefoundation/core/service/impl/DefaultAPIMiddlewareTest.java index 4082ca2848912a818225a63e754f58ac73726d3c..8c75f327746399cfe0c5091c4a958ff383fb899b 100644 --- a/core/src/test/java/org/eclipsefoundation/core/service/impl/DefaultAPIMiddlewareTest.java +++ b/core/src/test/java/org/eclipsefoundation/core/service/impl/DefaultAPIMiddlewareTest.java @@ -14,6 +14,7 @@ package org.eclipsefoundation.core.service.impl; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.net.URI; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -26,7 +27,6 @@ import org.eclipsefoundation.core.service.PaginationHeaderService; import org.eclipsefoundation.core.test.models.TestModel; import org.eclipsefoundation.http.model.FlatRequestWrapper; import org.eclipsefoundation.http.response.PaginatedResultsFilter; -import org.jboss.resteasy.spi.LinkHeader; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -39,6 +39,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.mockito.InjectSpy; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.Response; @QuarkusTest @@ -70,7 +71,8 @@ class DefaultAPIMiddlewareTest { @Test void getAll_success_GZIPContent() { List<TestModel> out = middleware - .getAll(params -> Response.ok(new ByteArrayInputStream(getListAsByteArray())).header("Content-Encoding", "GZIP").build(), + .getAll(params -> Response.ok(new ByteArrayInputStream(getListAsByteArray())) + .header("Content-Encoding", "GZIP").build(), TestModel.class); Assertions.assertEquals(2, out.size(), "Expected 2 objects in the return"); @@ -79,7 +81,8 @@ class DefaultAPIMiddlewareTest { @Test void getAll_success_GZIPContent_objectBase() { List<TestModel> out = middleware - .getAll(params -> Response.ok(new ByteArrayInputStream(getObjectAsByteArray())).header("Content-Encoding", "GZIP").build(), + .getAll(params -> Response.ok(new ByteArrayInputStream(getObjectAsByteArray())) + .header("Content-Encoding", "GZIP").build(), TestModel.class); Assertions.assertEquals(1, out.size(), "Expected a single object in the returned list"); @@ -93,7 +96,8 @@ class DefaultAPIMiddlewareTest { // test raw headers on response Assertions.assertEquals(10, Integer.parseInt(wrap.getHeader(PaginatedResultsFilter.MAX_PAGE_SIZE_HEADER))); - Assertions.assertEquals(getMaxResultsSize(pages), Integer.parseInt(wrap.getHeader(PaginatedResultsFilter.MAX_RESULTS_SIZE_HEADER))); + Assertions.assertEquals(getMaxResultsSize(pages), + Integer.parseInt(wrap.getHeader(PaginatedResultsFilter.MAX_RESULTS_SIZE_HEADER))); } @Test @@ -105,7 +109,8 @@ class DefaultAPIMiddlewareTest { // retrieve the pagination data for current request and test it Map<String, String> results = headerService.resolveHeadersForRequest(wrap); Assertions.assertEquals(10, Integer.parseInt(results.get(PaginatedResultsFilter.MAX_PAGE_SIZE_HEADER))); - Assertions.assertEquals(getMaxResultsSize(pages), Integer.parseInt(results.get(PaginatedResultsFilter.MAX_RESULTS_SIZE_HEADER))); + Assertions.assertEquals(getMaxResultsSize(pages), + Integer.parseInt(results.get(PaginatedResultsFilter.MAX_RESULTS_SIZE_HEADER))); } public static int getMaxResultsSize(int pageCount) { @@ -146,29 +151,32 @@ class DefaultAPIMiddlewareTest { // get current page safely Integer currentPage = 1; // null check the designated page in request - Integer designatedPage = base.getPage(); + Integer designatedPage = base.page; if (designatedPage != null) { currentPage = designatedPage; } // generate link headers - LinkHeader linkHeader = new LinkHeader(); - linkHeader - .addLink("this page of results", "self", String.format("https://sample.com/api?%spage=%d", leadingParams, currentPage), - ""); - linkHeader.addLink("first page of results", "first", String.format("https://sample.com/api?%spage=%d", leadingParams, 1), ""); - linkHeader - .addLink("last page of results", "last", String.format("https://sample.com/api?%spage=%d", leadingParams, pageCount), - ""); + + List<Link> links = new ArrayList<>(); + // add first + last page link headers + links.add(Link.fromUri(String.format("https://sample.com/api?%spage=%d", leadingParams, 1)).rel("first") + .title("first page of results").build()); + links.add(Link.fromUri(String.format("https://sample.com/api?%spage=%d", leadingParams, currentPage)) + .rel("self").title("this page of results").build()); + links.add(Link.fromUri(String.format("https://sample.com/api?%spage=%d", leadingParams, pageCount)) + .rel("last").title("last page of results").build()); + if (currentPage < pageCount) { - linkHeader - .addLink("next page of results", "next", - String.format("https://sample.com/api?%spage=%d", leadingParams, currentPage + 1), ""); + links.add( + Link.fromUri(String.format("https://sample.com/api?%spage=%d", leadingParams, currentPage + 1)) + .rel("next").title("next page of results").build()); } - // return basic return with link headers to test, w/ consistent fake data set size + // return basic return with link headers to test, w/ consistent fake data set + // size return Response .ok(Collections.emptyList()) - .header("Link", linkHeader) + .links(links.toArray(new Link[]{})) .header(PaginatedResultsFilter.MAX_RESULTS_SIZE_HEADER, getMaxResultsSize(pageCount)) .header(PaginatedResultsFilter.MAX_PAGE_SIZE_HEADER, 10) .build(); diff --git a/core/src/test/java/org/eclipsefoundation/core/service/impl/DefaultPaginationHeaderServiceTest.java b/core/src/test/java/org/eclipsefoundation/core/service/impl/DefaultPaginationHeaderServiceTest.java index f87d22f3e58488a25a5ebecbabb91284b4924e00..36750d6340122be7cd09b7ffea88f35604020dc7 100644 --- a/core/src/test/java/org/eclipsefoundation/core/service/impl/DefaultPaginationHeaderServiceTest.java +++ b/core/src/test/java/org/eclipsefoundation/core/service/impl/DefaultPaginationHeaderServiceTest.java @@ -21,7 +21,7 @@ import org.eclipsefoundation.core.test.models.TestModel; import org.eclipsefoundation.http.model.FlatRequestWrapper; import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; import org.eclipsefoundation.http.response.PaginatedResultsFilter; -import org.jboss.resteasy.specimpl.MultivaluedTreeMap; +import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; diff --git a/core/src/test/java/org/eclipsefoundation/core/test/models/SecondaryTestModel.java b/core/src/test/java/org/eclipsefoundation/core/test/models/SecondaryTestModel.java index afaf21fe5efbb9aeb9bc12ebb815494a4882fcd5..6d0fe20ce149e0f4aa62a2b10918c19258602788 100644 --- a/core/src/test/java/org/eclipsefoundation/core/test/models/SecondaryTestModel.java +++ b/core/src/test/java/org/eclipsefoundation/core/test/models/SecondaryTestModel.java @@ -22,6 +22,8 @@ import com.google.auto.value.AutoValue; @AutoValue @JsonDeserialize(builder = AutoValue_SecondaryTestModel.Builder.class) public abstract class SecondaryTestModel implements Serializable { + private static final long serialVersionUID = 1L; + @Nullable public abstract String getName(); diff --git a/core/src/test/java/org/eclipsefoundation/core/test/models/TestModel.java b/core/src/test/java/org/eclipsefoundation/core/test/models/TestModel.java index d567091b7ea397a481912baef4da95daac966c93..163519eb09599648dfd3b88cc7587dab2503cb0b 100644 --- a/core/src/test/java/org/eclipsefoundation/core/test/models/TestModel.java +++ b/core/src/test/java/org/eclipsefoundation/core/test/models/TestModel.java @@ -22,6 +22,8 @@ import com.google.auto.value.AutoValue; @AutoValue @JsonDeserialize(builder = AutoValue_TestModel.Builder.class) public abstract class TestModel implements Serializable { + private static final long serialVersionUID = 1L; + @Nullable public abstract String getName(); diff --git a/efservices/pom.xml b/efservices/pom.xml index 51e0c5f9b5a1ce4493e0cc0c6ad8c70d3e974f70..f0e7ee438815f144ff423b2845c7898baa77faa5 100644 --- a/efservices/pom.xml +++ b/efservices/pom.xml @@ -8,7 +8,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> <relativePath>../pom.xml</relativePath> </parent> <properties> @@ -22,8 +22,9 @@ </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-rest-client</artifactId> + <artifactId>quarkus-rest-client-reactive</artifactId> </dependency> + <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <dependency> <groupId>org.apache.commons</groupId> @@ -61,6 +62,12 @@ </dependency> <!-- Test dependencies --> + <dependency> + <groupId>org.eclipsefoundation</groupId> + <artifactId>quarkus-test-common</artifactId> + <version>${project.version}</version> + <scope>test</scope> + </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> @@ -99,4 +106,4 @@ </plugin> </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/api/DrupalOAuthAPI.java b/efservices/src/main/java/org/eclipsefoundation/efservices/api/DrupalOAuthAPI.java index c5da60d14d3f6fedeaa56a4db5ad50eac7a2dd63..d68e70d10df989f58a6dbe42295f10001e83adaa 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/api/DrupalOAuthAPI.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/api/DrupalOAuthAPI.java @@ -11,6 +11,10 @@ **********************************************************************/ package org.eclipsefoundation.efservices.api; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipsefoundation.efservices.api.models.DrupalOAuthData; +import org.eclipsefoundation.efservices.api.models.DrupalUserInfo; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; @@ -18,13 +22,9 @@ import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import org.eclipsefoundation.efservices.api.models.DrupalOAuthData; -import org.eclipsefoundation.efservices.api.models.DrupalUserInfo; -import org.jboss.resteasy.util.HttpHeaderNames; - /** * Drupal OAuth2 token validation API binding */ @@ -54,5 +54,5 @@ public interface DrupalOAuthAPI { */ @POST @Path("/userinfo") - DrupalUserInfo getUserInfoFromToken(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String token); + DrupalUserInfo getUserInfoFromToken(@HeaderParam(HttpHeaders.AUTHORIZATION) String token); } \ No newline at end of file diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/api/ProfileAPI.java b/efservices/src/main/java/org/eclipsefoundation/efservices/api/ProfileAPI.java index d7c98ab298b099ab74180c40e5d79ad809d51a17..5007694e4fbf4dbe503a452e486cb9854315be43 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/api/ProfileAPI.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/api/ProfileAPI.java @@ -13,6 +13,10 @@ package org.eclipsefoundation.efservices.api; import java.util.List; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipsefoundation.efservices.api.models.EfUser; +import org.eclipsefoundation.efservices.api.models.UserSearchParams; + import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.GET; @@ -20,11 +24,7 @@ import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; - -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import org.eclipsefoundation.efservices.api.models.EfUser; -import org.eclipsefoundation.efservices.api.models.UserSearchParams; -import org.jboss.resteasy.util.HttpHeaderNames; +import jakarta.ws.rs.core.HttpHeaders; /** * Profile-api binding. @@ -43,7 +43,7 @@ public interface ProfileAPI { */ @GET @Path("/account/profile") - List<EfUser> getUsers(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String token, @BeanParam UserSearchParams params); + List<EfUser> getUsers(@HeaderParam(HttpHeaders.AUTHORIZATION) String token, @BeanParam UserSearchParams params); /** * Fetches a profile that matches the given Ef username. An anauthenticated call @@ -56,7 +56,7 @@ public interface ProfileAPI { */ @GET @Path("/account/profile/{username}") - EfUser getUserByEfUsername(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String token, + EfUser getUserByEfUsername(@HeaderParam(HttpHeaders.AUTHORIZATION) String token, @PathParam("username") String username); /** @@ -70,6 +70,6 @@ public interface ProfileAPI { */ @GET @Path("/github/profile/{handle}") - EfUser getUserByGithubHandle(@HeaderParam(HttpHeaderNames.AUTHORIZATION) String token, + EfUser getUserByGithubHandle(@HeaderParam(HttpHeaders.AUTHORIZATION) String token, @PathParam("handle") String handle); } \ No newline at end of file diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/api/ProjectsAPI.java b/efservices/src/main/java/org/eclipsefoundation/efservices/api/ProjectsAPI.java index cecf905730322a8ad588a0f431a70850c5888f6d..20c77a1e84bd7723190806913ffd610731e9542e 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/api/ProjectsAPI.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/api/ProjectsAPI.java @@ -11,6 +11,10 @@ **********************************************************************/ package org.eclipsefoundation.efservices.api; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.eclipsefoundation.core.service.APIMiddleware.BaseAPIParameters; + +import io.quarkus.vertx.http.Compressed; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.BeanParam; import jakarta.ws.rs.GET; @@ -18,17 +22,12 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Response; -import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import org.eclipsefoundation.core.service.APIMiddleware.BaseAPIParameters; -import org.jboss.resteasy.annotations.GZIP; - /** * Project-API binding. Used to fetch Project and InterestGroup entities. */ @ApplicationScoped @Path("api") @RegisterRestClient(configKey = "projects-api") -@GZIP public interface ProjectsAPI { /** @@ -40,6 +39,7 @@ public interface ProjectsAPI { * @return A Response containing the project entities if they exist. */ @GET + @Compressed @Path("projects") Response getProjects(@BeanParam BaseAPIParameters params, @QueryParam("spec_project") int isSpecProject); diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/api/WorkingGroupsAPI.java b/efservices/src/main/java/org/eclipsefoundation/efservices/api/WorkingGroupsAPI.java index f104a05018e88ed8604ee3fe1d3a1052380c3624..f3cfd5bd61ed9a66df39e79b60cd301baae9f4bc 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/api/WorkingGroupsAPI.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/api/WorkingGroupsAPI.java @@ -36,15 +36,5 @@ public interface WorkingGroupsAPI { * @return response containing working group results. */ @GET - public Response get(@BeanParam BaseAPIParameters baseParams); - - /** - * Retrieve working groups by their status, such as incubating, active, or archived. - * - * @param baseParams base pagination parameters - * @param statuses list of statuses to retrieve working groups for - * @return list of working groups that match the passed status filters - */ - @GET - public Response getAllByStatuses(@BeanParam BaseAPIParameters baseParams, @QueryParam("status") List<String> statuses); + public Response get(@BeanParam BaseAPIParameters baseParams, @QueryParam("status") List<String> statuses); } diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/api/models/DrupalUserInfo.java b/efservices/src/main/java/org/eclipsefoundation/efservices/api/models/DrupalUserInfo.java index a7aec28845a359d0e1bfe0a291059a9848aa1e36..b78dfaf7ac0043084174957a7cc6e665e8e957e7 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/api/models/DrupalUserInfo.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/api/models/DrupalUserInfo.java @@ -11,38 +11,18 @@ **********************************************************************/ package org.eclipsefoundation.efservices.api.models; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import com.google.auto.value.AutoValue; - /** * Contains basic user data associated with a valid OAuth token. Used to access * basic information about the currently logged in user. */ -@AutoValue -@JsonDeserialize(builder = AutoValue_DrupalUserInfo.Builder.class) -public abstract class DrupalUserInfo { - - public abstract String getSub(); - - public abstract String getName(); - - public abstract String getGithubHandle(); - - public static Builder builder() { - return new AutoValue_DrupalUserInfo.Builder(); - } - - @AutoValue.Builder - @JsonPOJOBuilder(withPrefix = "set") - public abstract static class Builder { - - public abstract Builder setSub(String sub); - - public abstract Builder setName(String name); - - public abstract Builder setGithubHandle(String handle); - - public abstract DrupalUserInfo build(); +public record DrupalUserInfo(String sub, String name, String githubHandle) { + + /** + * Retrieves the uid of the user associated with the current token. + * + * @return The uid of the user associated with the current token. + */ + public String getCurrentUserUid() { + return this.sub(); } } \ No newline at end of file diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/api/models/UserSearchParams.java b/efservices/src/main/java/org/eclipsefoundation/efservices/api/models/UserSearchParams.java index 2f0b7348fe7facc4c7fdb240f5eadd773c79a53d..8a71b7b45060000f651fdf901b8ba34c3e28b7eb 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/api/models/UserSearchParams.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/api/models/UserSearchParams.java @@ -11,48 +11,33 @@ **********************************************************************/ package org.eclipsefoundation.efservices.api.models; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; -import com.google.auto.value.AutoValue; - import jakarta.annotation.Nullable; import jakarta.ws.rs.QueryParam; /** * Contains a set of query parameters used to perform authenticated user lookups with the ProfileService. */ -@AutoValue -@JsonDeserialize(builder = AutoValue_UserSearchParams.Builder.class) -public abstract class UserSearchParams { +public class UserSearchParams { @Nullable @QueryParam("uid") - public abstract String getUid(); + public String uid; @Nullable @QueryParam("name") - public abstract String getName(); + public String name; @Nullable @QueryParam("mail") - public abstract String getMail(); - - public abstract Builder toBuilder(); - - public static Builder builder() { - return new AutoValue_UserSearchParams.Builder(); + public String mail; + + public UserSearchParams() { + } - - @AutoValue.Builder - @JsonPOJOBuilder(withPrefix = "set") - public abstract static class Builder { - - public abstract Builder setUid(@Nullable String uid); - - public abstract Builder setName(@Nullable String name); - - public abstract Builder setMail(@Nullable String mail); - - public abstract UserSearchParams build(); + + public UserSearchParams(String uid, String name, String mail) { + this.uid = uid; + this.name = name; + this.mail = mail; } } \ No newline at end of file diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/config/AuthenticatedRequestWrapperProvider.java b/efservices/src/main/java/org/eclipsefoundation/efservices/config/AuthenticatedRequestWrapperProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..f7c12db8350e40f22920805f37c6abf5d2f42315 --- /dev/null +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/config/AuthenticatedRequestWrapperProvider.java @@ -0,0 +1,87 @@ +/********************************************************************* +* Copyright (c) 2024 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +/** + * + */ +package org.eclipsefoundation.efservices.config; + +import java.lang.reflect.Method; + +import org.eclipsefoundation.efservices.api.models.DrupalOAuthData; +import org.eclipsefoundation.efservices.helpers.DrupalAuthHelper; +import org.eclipsefoundation.efservices.models.AuthenticatedRequestWrapper; +import org.eclipsefoundation.efservices.namespace.RequestContextPropertyNames; +import org.eclipsefoundation.efservices.services.DrupalOAuthService; +import org.eclipsefoundation.efservices.services.ProfileService; +import org.eclipsefoundation.http.annotations.AuthenticatedAlternate; +import org.eclipsefoundation.http.config.OAuth2SecurityConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.HttpHeaders; + +/** + * + */ +public class AuthenticatedRequestWrapperProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticatedRequestWrapperProvider.class); + + /** + * Using the passed context + services, retrieve the current user for the request. This does not use standard OIDC flow as the usual alt + * server does not properly implement the OAuth2 spec required for OIDC binding. + * + * @param requestContext current HTTP request context + * @param info information about the endpoint called, used to check if alt authentication is enabled + * @param oauthService service binding to the OAuth2 service backing the alternate authentication + * @param profile instance for the EF user profile service, used in helper methods in the wrapper + * @param config values about the alternate authentication server to be used in the request + * @return the current authenticated request token and user, wrapped in a helper for ease of access + */ + @Produces + @RequestScoped + public AuthenticatedRequestWrapper requestTokenWrapper(ContainerRequestContext requestContext, ResourceInfo info, + DrupalOAuthService oauthService, ProfileService profile, OAuth2SecurityConfig config) { + Method m = info.getResourceMethod(); + AuthenticatedAlternate authenticated = m.getAnnotation(AuthenticatedAlternate.class); + + if (authenticated != null) { + try { + String token = DrupalAuthHelper.stripBearerToken(requestContext.getHeaderString(HttpHeaders.AUTHORIZATION)); + + DrupalOAuthData tokenStatus = oauthService + .validateTokenStatus(token, config.filter().validScopes(), config.filter().validClientIds()); + if (tokenStatus != null) { + + requestContext.setProperty(RequestContextPropertyNames.TOKEN_STATUS, tokenStatus); + + if (tokenStatus.getUserId() != null) { + // Fetch user data from token and set in context + LOGGER.debug("Fetching user info for token with uid: {}", tokenStatus.getUserId()); + return new AuthenticatedRequestWrapper(tokenStatus, oauthService.getTokenUserInfo(token), profile); + } + } + return new AuthenticatedRequestWrapper(tokenStatus, null, profile); + } catch (Exception e) { + // We want to prevent this from reaching user on profile queries. + LOGGER.debug("Invalid authentication", e); + + // If the endpoint is authenticated and doesn't have a valid token, we deny the request + if (!authenticated.allowPartialResponse()) { + throw e; + } + } + } + return new AuthenticatedRequestWrapper(null, null, profile); + } +} diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/models/RequestUserWrapper.java b/efservices/src/main/java/org/eclipsefoundation/efservices/models/AuthenticatedRequestWrapper.java similarity index 53% rename from efservices/src/main/java/org/eclipsefoundation/efservices/models/RequestUserWrapper.java rename to efservices/src/main/java/org/eclipsefoundation/efservices/models/AuthenticatedRequestWrapper.java index e19ed94754353a2dd1a0ba1ce9e883e1bb667ed6..a63937301ab5a6b977314af03c8499a99ee224e7 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/models/RequestUserWrapper.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/models/AuthenticatedRequestWrapper.java @@ -13,39 +13,38 @@ package org.eclipsefoundation.efservices.models; import java.util.Optional; +import org.eclipsefoundation.efservices.api.models.DrupalOAuthData; import org.eclipsefoundation.efservices.api.models.DrupalUserInfo; import org.eclipsefoundation.efservices.api.models.EfUser; import org.eclipsefoundation.efservices.api.models.UserSearchParams; -import org.eclipsefoundation.efservices.namespace.RequestContextPropertyNames; import org.eclipsefoundation.efservices.services.ProfileService; import org.eclipsefoundation.utils.exception.FinalForbiddenException; -import jakarta.enterprise.context.RequestScoped; -import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletRequest; - /** - * A RequestScoped bean that wraps the DrupalUserInfo value set in the request chain. This bean is used to retrieve various information - * about the user associated with the current token. To access this data, a DrupalUserInfo object must be set as the 'token_user' property - * in the HttpServletRequest bound to the current request chain. + * A RequestScoped bean that wraps the DrupalOAuthData value set in the request chain. This bean is used to retrieve various information + * about the current token. To access this data, a DrupalOAuthData object must be set as the 'token_status' property in the + * HttpServletRequest bound to the current request chain. */ -@RequestScoped -public class RequestUserWrapper { - +public class AuthenticatedRequestWrapper { private static final String NO_USER_ERR_MSG = "No user associated with this token"; + private final DrupalOAuthData tokenStatus; private final DrupalUserInfo currentUser; + private final ProfileService profile; - @Inject - ProfileService profileService; + public AuthenticatedRequestWrapper(DrupalOAuthData tokenStatus, DrupalUserInfo currentUser, ProfileService profile) { + this.tokenStatus = tokenStatus; + this.currentUser = currentUser; + this.profile = profile; + } /** - * Gets the token user from the request and sets it. Returns a RequestUserWrapper instance. + * Retrieves the DrupalOAuthData bound to the current request chain. * - * @param request The given HttpServletRequest + * @return The DrupalOAuthData bound to the current request. */ - public RequestUserWrapper(HttpServletRequest request) { - this.currentUser = (DrupalUserInfo) request.getAttribute(RequestContextPropertyNames.TOKEN_USER); + public DrupalOAuthData getTokenStatus() { + return this.tokenStatus; } /** @@ -61,39 +60,12 @@ public class RequestUserWrapper { } /** - * Retrieves the uid of the user associated with the current token. - * - * @return The uid of the user associated with the current token. - */ - public String getCurrentUserUid() { - if (currentUser == null) { - throw new FinalForbiddenException(NO_USER_ERR_MSG); - } - return this.currentUser.getSub(); - } - - /** - * Retrieves the ef username of the user associated with the current token. + * A simple check to determine if the current request has a valid token associated with it. * - * @return The username of the user associated with the current token. + * @return True if valid token, false if not. */ - public String getCurrentUserEfName() { - if (currentUser == null) { - throw new FinalForbiddenException(NO_USER_ERR_MSG); - } - return this.currentUser.getName(); - } - - /** - * Retrieves the GH handle of the user associated with the current token. - * - * @return The GH handle of the user associated with the current token. - */ - public String getCurrentUserGithubHandle() { - if (currentUser == null) { - throw new FinalForbiddenException(NO_USER_ERR_MSG); - } - return this.currentUser.getGithubHandle(); + public boolean isAuthenticated() { + return tokenStatus != null; } /** @@ -108,9 +80,9 @@ public class RequestUserWrapper { } // Fetch by username. Then fetch by gh handle if not found - Optional<EfUser> result = profileService - .fetchUserByUsername(getCurrentUserEfName(), false) - .or(() -> profileService.fetchUserByGhHandle(getCurrentUserGithubHandle(), false)); + Optional<EfUser> result = profile + .fetchUserByUsername(currentUser.name(), false) + .or(() -> profile.fetchUserByGhHandle(currentUser.githubHandle(), false)); if (result.isEmpty()) { throw new FinalForbiddenException(NO_USER_ERR_MSG); } @@ -129,13 +101,13 @@ public class RequestUserWrapper { } // Perform user search, then fetch by Gh handle if not found. - Optional<EfUser> result = profileService - .performUserSearch(UserSearchParams.builder().setUid(getCurrentUserUid()).setName(getCurrentUserEfName()).build()) - .or(() -> profileService.fetchUserByGhHandle(getCurrentUserGithubHandle(), true)); + Optional<EfUser> result = profile + .performUserSearch(new UserSearchParams(currentUser.getCurrentUserUid(), currentUser.name(), null)) + .or(() -> profile.fetchUserByGhHandle(currentUser.githubHandle(), true)); if (result.isEmpty()) { throw new FinalForbiddenException(NO_USER_ERR_MSG); } return result.get(); } -} \ No newline at end of file +} diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/models/RequestTokenWrapper.java b/efservices/src/main/java/org/eclipsefoundation/efservices/models/RequestTokenWrapper.java deleted file mode 100644 index 68d539a6c7c578de71c609bd533ea0c7f2db43d5..0000000000000000000000000000000000000000 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/models/RequestTokenWrapper.java +++ /dev/null @@ -1,60 +0,0 @@ -/********************************************************************* -* Copyright (c) 2023 Eclipse Foundation. -* -* This program and the accompanying materials are made -* available under the terms of the Eclipse Public License 2.0 -* which is available at https://www.eclipse.org/legal/epl-2.0/ -* -* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> -* -* SPDX-License-Identifier: EPL-2.0 -**********************************************************************/ -package org.eclipsefoundation.efservices.models; - -import jakarta.enterprise.context.RequestScoped; -import jakarta.servlet.http.HttpServletRequest; - -import org.eclipsefoundation.efservices.api.models.DrupalOAuthData; -import org.eclipsefoundation.efservices.namespace.RequestContextPropertyNames; - -/** - * A RequestScoped bean that wraps the DrupalOAuthData value set in the request - * chain. This bean is used to retrieve various information about the current - * token. To access this data, a DrupalOAuthData object must be set as the - * 'token_status' property in the HttpServletRequest bound to the current - * request chain. - */ -@RequestScoped -public class RequestTokenWrapper { - - private final DrupalOAuthData tokenStatus; - - /** - * Gets the token info from the request and sets it. Rreturns a - * RequestTokenWrapper instance. - * - * @param request The given HttpServletRequest - */ - public RequestTokenWrapper(HttpServletRequest request) { - this.tokenStatus = (DrupalOAuthData) request.getAttribute(RequestContextPropertyNames.TOKEN_STATUS); - } - - /** - * Retrieves the DrupalOAuthData bound to the current request chain. - * - * @return The DrupalOAuthData bound to the current request. - */ - public DrupalOAuthData getTokenStatus() { - return this.tokenStatus; - } - - /** - * A simple check to determine if the current request has a valid token - * associated with it. - * - * @return True if valid token, false if not. - */ - public boolean isAuthenticated() { - return tokenStatus != null; - } -} diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/precaches/WorkingGroupPrecacheProvider.java b/efservices/src/main/java/org/eclipsefoundation/efservices/precaches/WorkingGroupPrecacheProvider.java index 0149571ef4174f9a85b5c9e0b8d212f2e587c83a..633f33781e9fa1b1a4872dadbd1e8623b354717f 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/precaches/WorkingGroupPrecacheProvider.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/precaches/WorkingGroupPrecacheProvider.java @@ -46,7 +46,7 @@ public class WorkingGroupPrecacheProvider implements LoadingCacheProvider<Workin public List<WorkingGroup> fetchData( @MeterTag(resolver = CacheKeyClassTagResolver.class, key = METRICS_KEY_TAG_NAME) ParameterizedCacheKey k) { LOGGER.debug("LOADING PROJECTS WITH KEY: {}", k); - return middleware.getAll(params -> api.get(params), WorkingGroup.class); + return middleware.getAll(params -> api.get(params, null), WorkingGroup.class); } @Override diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/request/OAuthFilter.java b/efservices/src/main/java/org/eclipsefoundation/efservices/request/OAuthFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..69fc2bdc86b26dd579cebb8021e031d17a386c0b --- /dev/null +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/request/OAuthFilter.java @@ -0,0 +1,55 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.efservices.request; + +import org.eclipsefoundation.efservices.models.AuthenticatedRequestWrapper; +import org.eclipsefoundation.http.config.OAuth2SecurityConfig; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.UriInfo; + +/** + * This filter is used to validate the incoming Bearer tokens. Sets the current token user in the request context for use downstream. + */ +public class OAuthFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(OAuthFilter.class); + + private final OAuth2SecurityConfig config; + private final AuthenticatedRequestWrapper wrappedToken; + + /** + * Default constructor for oauth filter, accepts required configuration for checking if filter is enabled, as well as the current + * requests wrapped token instance. + * + * @param config oauth2 configuration for non-standard server binding + * @param wrappedToken oauth token wrapper associated with the current request + */ + public OAuthFilter(OAuth2SecurityConfig config, AuthenticatedRequestWrapper wrappedToken) { + this.config = config; + this.wrappedToken = wrappedToken; + } + + @ServerRequestFilter + public void filter(ContainerRequestContext requestContext, UriInfo uri, ResourceInfo info) { + if (Boolean.TRUE.equals(config.filter().enabled())) { + if (wrappedToken.isAuthenticated()) { + LOGGER.trace("User authenticated - {}", wrappedToken.getCurrentUser().name()); + } else { + LOGGER.trace("User not authenticated for current request to {}", uri.getPath()); + } + } + } +} diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultProfileService.java b/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultProfileService.java index 0116b742c06033bd359786f198eccd1bba7bc3d3..6794e49b7cdae8fec7b69827513e20afc73825d1 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultProfileService.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultProfileService.java @@ -24,12 +24,12 @@ import org.eclipsefoundation.efservices.api.models.UserSearchParams; import org.eclipsefoundation.efservices.namespace.EfServicesParameterNames; import org.eclipsefoundation.efservices.services.DrupalTokenService; import org.eclipsefoundation.efservices.services.ProfileService; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -101,15 +101,15 @@ public class DefaultProfileService implements ProfileService { * @return A MultivaluedMap containing the given cache key params */ private MultivaluedMap<String, String> buildSearchCacheParams(UserSearchParams params) { - MultivaluedMap<String, String> cacheParams = new MultivaluedMapImpl<>(); - if (params.getUid() != null) { - cacheParams.add(EfServicesParameterNames.UID_RAW, params.getUid().toString()); + MultivaluedMap<String, String> cacheParams = new MultivaluedHashMap<>(); + if (params.uid != null) { + cacheParams.add(EfServicesParameterNames.UID_RAW, params.uid); } - if (StringUtils.isNotBlank(params.getName())) { - cacheParams.add(EfServicesParameterNames.NAME_RAW, params.getName()); + if (StringUtils.isNotBlank(params.name)) { + cacheParams.add(EfServicesParameterNames.NAME_RAW, params.name); } - if (StringUtils.isNotBlank(params.getMail())) { - cacheParams.add(EfServicesParameterNames.MAIL_RAW, params.getMail()); + if (StringUtils.isNotBlank(params.mail)) { + cacheParams.add(EfServicesParameterNames.MAIL_RAW, params.mail); } return cacheParams; } @@ -125,7 +125,7 @@ public class DefaultProfileService implements ProfileService { * @return A MultivaluedMap containing the desired cache strategy. */ private MultivaluedMap<String, String> buildCacheStrategy(String strategy, String visibility) { - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(EfServicesParameterNames.STRATEGY.getName(), strategy); params.add(EfServicesParameterNames.VISIBILITY.getName(), visibility); return params; diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultProjectService.java b/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultProjectService.java index 76be9d2a54cec4c2da8a8f7a3339474e23a1b430..5f511ec0b1e6c18a4c4624a954165cb247fa8672 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultProjectService.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultProjectService.java @@ -20,13 +20,13 @@ import org.eclipsefoundation.efservices.api.models.InterestGroup; import org.eclipsefoundation.efservices.api.models.Project; import org.eclipsefoundation.efservices.namespace.EfServicesParameterNames; import org.eclipsefoundation.efservices.services.ProjectService; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import io.quarkus.runtime.Startup; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -60,12 +60,12 @@ public class DefaultProjectService implements ProjectService { return cacheManager.getList(ParameterizedCacheKey.builder() .setId(DEFAULT_CACHE_ID) .setClazz(Project.class) - .setParams(new MultivaluedMapImpl<>()).build()); + .setParams(new MultivaluedHashMap<>()).build()); } @Override public List<Project> getAllSpecProjects() { - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(EfServicesParameterNames.SPEC_PROJECT_RAW, "1"); return cacheManager.getList(ParameterizedCacheKey.builder() .setId(DEFAULT_CACHE_ID) @@ -78,6 +78,6 @@ public class DefaultProjectService implements ProjectService { return cacheManager.getList(ParameterizedCacheKey.builder() .setId(DEFAULT_CACHE_ID) .setClazz(InterestGroup.class) - .setParams(new MultivaluedMapImpl<>()).build()); + .setParams(new MultivaluedHashMap<>()).build()); } } diff --git a/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultWorkingGroupService.java b/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultWorkingGroupService.java index 3a3f1645632138cba46788c135194adfbfaae698..9e049f9a3cc10663617e185fd5069f55c588e873 100644 --- a/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultWorkingGroupService.java +++ b/efservices/src/main/java/org/eclipsefoundation/efservices/services/impl/DefaultWorkingGroupService.java @@ -28,69 +28,65 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; /** - * Retrieves working groups related to the Eclipse Foundation, as well as provides some basic filters built-in for ease of use. + * Retrieves working groups related to the Eclipse Foundation, as well as + * provides some basic filters built-in for ease of use. * * @author Martin Lowe */ @ApplicationScoped public class DefaultWorkingGroupService implements WorkingGroupService { - @Inject - LoadingCacheManager cache; + @Inject + LoadingCacheManager cache; - @Override - public List<WorkingGroup> get() { - return new ArrayList<>(cache.getList(ParameterizedCacheKey.builder().setId("all").setClazz(WorkingGroup.class).build())); - } + @Override + public List<WorkingGroup> get() { + return new ArrayList<>( + cache.getList(ParameterizedCacheKey.builder().setId("all").setClazz(WorkingGroup.class).build())); + } - @Override - public List<WorkingGroup> get(List<String> parentOrgs, List<String> statuses) { - return get() - .stream() - .filter(wg -> filterByParentOrganizations(parentOrgs, wg)) - .filter(wg -> filterByProjectStatuses(statuses, wg)) - .collect(Collectors.toList()); - } + @Override + public List<WorkingGroup> get(List<String> parentOrgs, List<String> statuses) { + return get().stream().filter(wg -> filterByParentOrganizations(parentOrgs, wg)) + .filter(wg -> filterByProjectStatuses(statuses, wg)).collect(Collectors.toList()); + } - @Override - public Optional<WorkingGroup> getByName(String name) { - return get().stream().filter(wg -> wg.getAlias().equalsIgnoreCase(name)).findFirst(); - } + @Override + public Optional<WorkingGroup> getByName(String name) { + return get().stream().filter(wg -> wg.getAlias().equalsIgnoreCase(name)).findFirst(); + } - @Override - public Optional<WorkingGroup> getByAgreementId(String docId) { - Optional<Entry<String, List<String>>> entry = getWGPADocumentIDs() - .entrySet() - .stream() - .filter(e -> e.getValue().contains(docId)) - .findFirst(); - return entry.isEmpty() ? Optional.empty() : getByName(entry.get().getKey()); - } + @Override + public Optional<WorkingGroup> getByAgreementId(String docId) { + Optional<Entry<String, List<String>>> entry = getWGPADocumentIDs().entrySet().stream() + .filter(e -> e.getValue().contains(docId)).findFirst(); + return entry.isEmpty() ? Optional.empty() : getByName(entry.get().getKey()); + } - @Override - public Map<String, List<String>> getWGPADocumentIDs() { - return get().stream().collect(Collectors.toMap(WorkingGroup::getAlias, this::extractWGPADocumentIDs)); - } + @Override + public Map<String, List<String>> getWGPADocumentIDs() { + return get().stream().collect(Collectors.toMap(WorkingGroup::getAlias, this::extractWGPADocumentIDs)); + } - private List<String> extractWGPADocumentIDs(WorkingGroup wg) { - List<String> ids = new ArrayList<>(); - WorkingGroupParticipationAgreement iwgpa = wg.getResources().getParticipationAgreements().getIndividual(); - if (iwgpa != null) { - ids.add(iwgpa.getDocumentId()); - } - WorkingGroupParticipationAgreement wgpa = wg.getResources().getParticipationAgreements().getOrganization(); - if (wgpa != null) { - ids.add(wgpa.getDocumentId()); - } - return ids; - } + private List<String> extractWGPADocumentIDs(WorkingGroup wg) { + List<String> ids = new ArrayList<>(); + WorkingGroupParticipationAgreement iwgpa = wg.getResources().getParticipationAgreements().getIndividual(); + if (iwgpa != null) { + ids.add(iwgpa.getDocumentId()); + } + WorkingGroupParticipationAgreement wgpa = wg.getResources().getParticipationAgreements().getOrganization(); + if (wgpa != null) { + ids.add(wgpa.getDocumentId()); + } + return ids; + } - private boolean filterByParentOrganizations(List<String> parentOrganizations, WorkingGroup wg) { - return parentOrganizations == null || parentOrganizations.isEmpty() ? true - : parentOrganizations.contains(wg.getParentOrganization()); - } + private boolean filterByParentOrganizations(List<String> parentOrganizations, WorkingGroup wg) { + return (parentOrganizations == null || parentOrganizations.isEmpty()) + || parentOrganizations.contains(wg.getParentOrganization()); + } - private boolean filterByProjectStatuses(List<String> statuses, WorkingGroup wg) { - return statuses == null || statuses.isEmpty() ? true : statuses.contains(wg.getStatus()); - } + private boolean filterByProjectStatuses(List<String> statuses, WorkingGroup wg) { + return (statuses == null || statuses.isEmpty()) || statuses.contains(wg.getStatus()); + } } diff --git a/efservices/src/main/resources/application.properties b/efservices/src/main/resources/application.properties index 166adb6fe8aa953c56af6f15657e0860c61f922b..b7b04728e43a71b42ef001027d94a1d344cfc6dc 100644 --- a/efservices/src/main/resources/application.properties +++ b/efservices/src/main/resources/application.properties @@ -1,3 +1,6 @@ +## Required for compression of REST client calls +quarkus.http.enable-compression=true + eclipse.cache.loading."projects".timeout=10 eclipse.cache.loading."projects".refresh-after=PT1H diff --git a/efservices/src/test/java/org/eclipsefoundation/efservices/request/OAuthFilterTest.java b/efservices/src/test/java/org/eclipsefoundation/efservices/request/OAuthFilterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..4f8dd5c6b5a41c58a2727875db6f46b2ebe27f94 --- /dev/null +++ b/efservices/src/test/java/org/eclipsefoundation/efservices/request/OAuthFilterTest.java @@ -0,0 +1,105 @@ +/********************************************************************* +* Copyright (c) 2024 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +/** + * + */ +package org.eclipsefoundation.efservices.request; + +import java.util.Map; +import java.util.Optional; + +import org.eclipsefoundation.efservices.test.AlternateAuthenticationFilterTestProfile; +import org.eclipsefoundation.testing.helpers.TestCaseHelper; +import org.eclipsefoundation.testing.models.EndpointTestBuilder; +import org.eclipsefoundation.testing.models.EndpointTestCase; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response.Status; + +/** + * + */ +@QuarkusTest +@TestInstance(Lifecycle.PER_CLASS) +@TestProfile(AlternateAuthenticationFilterTestProfile.class) +public class OAuthFilterTest { + public static final String STANDARD_AUTH_URL = "authenticated"; + public static final String PARTIAL_AUTH_URL = STANDARD_AUTH_URL + "/partial"; + + public static final Optional<Map<String, Object>> VALID_USER_AUTH_HEADER = Optional + .of(Map.of(HttpHeaders.AUTHORIZATION, "Bearer token5")); + public static final Optional<Map<String, Object>> INVALID_ANON_AUTH_HEADER = Optional + .of(Map.of(HttpHeaders.AUTHORIZATION, "Bearer othertoken")); + + public static final EndpointTestCase STANDARD_AUTH_WITH_USER_SUCCESS = TestCaseHelper + .prepareTestCase(STANDARD_AUTH_URL, new String[] {}, null) + .setHeaderParams(VALID_USER_AUTH_HEADER) + .build(); + public static final EndpointTestCase PARTIAL_AUTH_USER_SUCCESS = TestCaseHelper + .prepareTestCase(PARTIAL_AUTH_URL, new String[] {}, null) + .setHeaderParams(VALID_USER_AUTH_HEADER) + .build(); + + @Test + void oauthFilter_success() { + EndpointTestBuilder.from(STANDARD_AUTH_WITH_USER_SUCCESS).run(); + } + + @Test + void oauthFilter_failure_invalidAuth() { + // Invalid token, we want this to fail + EndpointTestBuilder + .from(TestCaseHelper + .prepareTestCase(STANDARD_AUTH_URL, new String[] {}, null) + .setHeaderParams(INVALID_ANON_AUTH_HEADER) + .setStatusCode(403) + .build()) + .run(); + } + + @Test + void oauthFilter_failure_noAuth() { + // No auth token, we want this to fail + EndpointTestBuilder.from(TestCaseHelper.prepareTestCase(STANDARD_AUTH_URL, new String[] {}, null).setStatusCode(403).build()).run(); + } + + @Test + void oauthFilterPartialAuth_success() { + EndpointTestBuilder.from(PARTIAL_AUTH_USER_SUCCESS).run(); + } + + @Test + void oauthFilterPartialAuth_failure_invalidAuth() { + // Invalid token, we want this to fail + EndpointTestBuilder + .from(TestCaseHelper + .prepareTestCase(PARTIAL_AUTH_URL, new String[] {}, null) + .setHeaderParams(INVALID_ANON_AUTH_HEADER) + .setStatusCode(Status.NO_CONTENT.getStatusCode()) + .build()) + .run(); + } + + @Test + void oauthFilterPartialAuth_failure_noAuth() { + // No auth token, we want this to fail + EndpointTestBuilder + .from(TestCaseHelper + .prepareTestCase(PARTIAL_AUTH_URL, new String[] {}, null) + .setStatusCode(Status.NO_CONTENT.getStatusCode()) + .build()) + .run(); + } +} diff --git a/efservices/src/test/java/org/eclipsefoundation/efservices/services/AccountServiceTest.java b/efservices/src/test/java/org/eclipsefoundation/efservices/services/AccountServiceTest.java index 99cae4e257dfa603c98e4d4cdd768ab058a59ed8..b136f965a653193ce5e7b1678ebc27ddd74c7f4a 100644 --- a/efservices/src/test/java/org/eclipsefoundation/efservices/services/AccountServiceTest.java +++ b/efservices/src/test/java/org/eclipsefoundation/efservices/services/AccountServiceTest.java @@ -101,20 +101,20 @@ class ProfileServiceTest { @Test void testSearchUser_success() { // Valid uid - UserSearchParams params = UserSearchParams.builder().setUid("666").build(); + UserSearchParams params = new UserSearchParams("666", null, null); Optional<EfUser> result = profileService.performUserSearch(params); Assertions.assertTrue(result.isPresent(), String.format(SEARCH_NOT_FOUND_MSG_FORMAT, params.toString())); Assertions.assertEquals("666", result.get().getUid()); // Invalid uid. Valid name - params = params.toBuilder().setUid("12").setName("firstlast").build(); + params = new UserSearchParams("12", "firstlast", null); result = profileService.performUserSearch(params); Assertions.assertTrue(result.isPresent(), String.format(SEARCH_NOT_FOUND_MSG_FORMAT, params.toString())); Assertions.assertEquals("666", result.get().getUid()); Assertions.assertEquals("firstlast", result.get().getName()); // Invalid uid and name. Valid email - params = params.toBuilder().setUid("12").setName("wrongname").setMail("firstlast@test.com").build(); + params = new UserSearchParams("12", "wrongname", "firstlast@test.com"); result = profileService.performUserSearch(params); Assertions.assertTrue(result.isPresent(), String.format(SEARCH_NOT_FOUND_MSG_FORMAT, params.toString())); Assertions.assertEquals("666", result.get().getUid()); @@ -125,17 +125,17 @@ class ProfileServiceTest { @Test void testSearchUser_failure_notfound() { // Invalid uid - UserSearchParams params = UserSearchParams.builder().setUid("12").build(); + UserSearchParams params = new UserSearchParams("12", null, null); Optional<EfUser> result = profileService.performUserSearch(params); Assertions.assertTrue(result.isEmpty()); // Invalid uid and name - params = params.toBuilder().setUid("12").setName("wrongname").build(); + params = new UserSearchParams("12", "wrongname", null); result = profileService.performUserSearch(params); Assertions.assertTrue(result.isEmpty()); // Invalid uid, name, and email - params = params.toBuilder().setUid("12").setName("wrongname").setMail("wrongemail@test.co").build(); + params = new UserSearchParams("12", "wrongname", "wrongemail@test.co"); result = profileService.performUserSearch(params); Assertions.assertTrue(result.isEmpty()); } diff --git a/efservices/src/test/java/org/eclipsefoundation/efservices/test/AlternateAuthenticationFilterTestProfile.java b/efservices/src/test/java/org/eclipsefoundation/efservices/test/AlternateAuthenticationFilterTestProfile.java new file mode 100644 index 0000000000000000000000000000000000000000..8f691b9524ccda0354c476b7f0e7c377bab17d35 --- /dev/null +++ b/efservices/src/test/java/org/eclipsefoundation/efservices/test/AlternateAuthenticationFilterTestProfile.java @@ -0,0 +1,34 @@ +/********************************************************************* +* Copyright (c) 2024 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.efservices.test; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class AlternateAuthenticationFilterTestProfile implements QuarkusTestProfile { + + // private immutable copy of the configs for auth state + private static Map<String, String> configOverrides; + + @Override + public Map<String, String> getConfigOverrides() { + if (AlternateAuthenticationFilterTestProfile.configOverrides == null) { + Map<String, String> tmp = new HashMap<>(); + tmp.put("eclipse.security.oauth2.filter.enabled", "true"); + tmp.put("eclipse.security.oauth2.filter.valid-scopes", "admin"); + tmp.put("eclipse.security.oauth2.filter.valid-client-ids", "test-id"); + AlternateAuthenticationFilterTestProfile.configOverrides = Collections.unmodifiableMap(tmp); + } + return configOverrides; + } +} diff --git a/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockDrupalOAuthAPI.java b/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockDrupalOAuthAPI.java index c2efef0d765cd2f59f41e39f84f47ad53662c6cf..efbf4413929645795e47671dee20cc11f03481d0 100644 --- a/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockDrupalOAuthAPI.java +++ b/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockDrupalOAuthAPI.java @@ -36,65 +36,63 @@ public class MockDrupalOAuthAPI implements DrupalOAuthAPI { public MockDrupalOAuthAPI() { tokens = new ArrayList<>(); - tokens.addAll(Arrays.asList( - DrupalOAuthData.builder() - .setAccessToken("token1") - .setClientId("client-id") - .setExpires(1674111182) - .setScope("read write") - .build(), - DrupalOAuthData.builder() - .setAccessToken("token2") - .setClientId("test-id") - .setUserId("42") - .setExpires(Instant.now().getEpochSecond() + 20000) - .setScope("read write admin") - .build(), - DrupalOAuthData.builder() - .setAccessToken("token3") - .setClientId("test-id") - .setExpires(1234567890) - .setScope("read admin") - .build(), - DrupalOAuthData.builder() - .setAccessToken("token4") - .setClientId("client-id") - .setExpires(Instant.now().getEpochSecond() + 20000) - .setScope("read write") - .build(), - DrupalOAuthData.builder() - .setAccessToken("token5") - .setClientId("test-id") - .setUserId("333") - .setExpires(Instant.now().getEpochSecond() + 20000) - .setScope("admin") - .build(), - DrupalOAuthData.builder() - .setAccessToken("token6") - .setClientId("test-id") - .setExpires(Instant.now().getEpochSecond() + 20000) - .setScope("read write admin") - .build(), - DrupalOAuthData.builder() - .setAccessToken("token7") - .setClientId("test-id") - .setUserId("444") - .setExpires(Instant.now().getEpochSecond() + 20000) - .setScope("read write admin") - .build())); + tokens + .addAll(Arrays + .asList(DrupalOAuthData + .builder() + .setAccessToken("token1") + .setClientId("client-id") + .setExpires(1674111182) + .setScope("read write") + .build(), + DrupalOAuthData + .builder() + .setAccessToken("token2") + .setClientId("test-id") + .setUserId("42") + .setExpires(Instant.now().getEpochSecond() + 20000) + .setScope("read write admin") + .build(), + DrupalOAuthData + .builder() + .setAccessToken("token3") + .setClientId("test-id") + .setExpires(1234567890) + .setScope("read admin") + .build(), + DrupalOAuthData + .builder() + .setAccessToken("token4") + .setClientId("client-id") + .setExpires(Instant.now().getEpochSecond() + 20000) + .setScope("read write") + .build(), + DrupalOAuthData + .builder() + .setAccessToken("token5") + .setClientId("test-id") + .setUserId("333") + .setExpires(Instant.now().getEpochSecond() + 20000) + .setScope("admin") + .build(), + DrupalOAuthData + .builder() + .setAccessToken("token6") + .setClientId("test-id") + .setExpires(Instant.now().getEpochSecond() + 20000) + .setScope("read write admin") + .build(), + DrupalOAuthData + .builder() + .setAccessToken("token7") + .setClientId("test-id") + .setUserId("444") + .setExpires(Instant.now().getEpochSecond() + 20000) + .setScope("read write admin") + .build())); users = new ArrayList<>(); - users.addAll(Arrays.asList( - DrupalUserInfo.builder() - .setSub("42") - .setName("fakeuser") - .setGithubHandle("fakeuser") - .build(), - DrupalUserInfo.builder() - .setSub("333") - .setName("otheruser") - .setGithubHandle("other") - .build())); + users.addAll(Arrays.asList(new DrupalUserInfo("42", "fakeuser", "fakeuser"), new DrupalUserInfo("333", "otheruser", "other"))); } @Override @@ -109,7 +107,6 @@ public class MockDrupalOAuthAPI implements DrupalOAuthAPI { throw new FinalForbiddenException("The access token provided is invalid"); } - return users.stream().filter(u -> u.getSub().equalsIgnoreCase(tokenInfo.getUserId())).findFirst() - .orElse(null); + return users.stream().filter(u -> u.sub().equalsIgnoreCase(tokenInfo.getUserId())).findFirst().orElse(null); } } \ No newline at end of file diff --git a/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockProfileAPI.java b/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockProfileAPI.java index 6cf480cfd25c11b720a37b67c4ddf9810f327acd..9aae6745d3c808b6634b13fe85d4b4e7cbd5fdb1 100644 --- a/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockProfileAPI.java +++ b/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockProfileAPI.java @@ -17,11 +17,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.NotFoundException; import org.apache.commons.lang3.StringUtils; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -34,6 +29,9 @@ import org.eclipsefoundation.efservices.helpers.DrupalAuthHelper; import org.eclipsefoundation.efservices.services.DrupalOAuthService; import io.quarkus.test.Mock; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.NotFoundException; @Mock @RestClient @@ -52,92 +50,98 @@ public class MockProfileAPI implements ProfileAPI { public MockProfileAPI() { this.users = new ArrayList<>(); - this.users.addAll(Arrays.asList( - EfUser.builder() - .setUid("666") - .setName("firstlast") - .setGithubHandle("handle") - .setMail("firstlast@test.com") - .setPicture("pic url") - .setFirstName("fake") - .setLastName("user") - .setFullName("fake user") - .setPublisherAgreements(new HashMap<>()) - .setTwitterHandle("") - .setOrg("null") - .setJobTitle("employee") - .setWebsite("site url") - .setCountry(Country.builder().setCode("CA").setName("Canada").build()) - .setInterests(Arrays.asList()) - .build(), - EfUser.builder() - .setUid("42") - .setName("fakeuser") - .setPicture("pic url") - .setFirstName("fake") - .setLastName("user") - .setFullName("fake user") - .setMail("fakeuser@test.com") - .setPublisherAgreements(new HashMap<>()) - .setGithubHandle("fakeuser") - .setTwitterHandle("") - .setOrg("null") - .setJobTitle("employee") - .setWebsite("site url") - .setCountry(Country.builder().setCode("CA").setName("Canada").build()) - .setInterests(Arrays.asList()) - .build(), - EfUser.builder() - .setUid("333") - .setName("name") - .setGithubHandle("name") - .setMail("name@test.com") - .setPicture("pic url") - .setFirstName("fake") - .setLastName("user") - .setFullName("fake user") - .setPublisherAgreements(new HashMap<>()) - .setTwitterHandle("") - .setOrg("null") - .setJobTitle("employee") - .setWebsite("site url") - .setCountry(Country.builder().setCode("CA").setName("Canada").build()) - .setInterests(Arrays.asList()) - .build(), - EfUser.builder() - .setUid("11") - .setName("testtesterson") - .setGithubHandle("mctesty") - .setMail("testtesterson@test.com") - .setPicture("pic url") - .setFirstName("fake") - .setLastName("user") - .setFullName("fake user") - .setPublisherAgreements(new HashMap<>()) - .setTwitterHandle("") - .setOrg("null") - .setJobTitle("employee") - .setWebsite("site url") - .setCountry(Country.builder().setCode("CA").setName("Canada").build()) - .setInterests(Arrays.asList()) - .build(), - EfUser.builder() - .setUid("444") - .setName("nodoc") - .setGithubHandle("nodoc") - .setMail("nodoc@test.com") - .setPicture("pic url") - .setFirstName("fake") - .setLastName("user") - .setFullName("fake user") - .setPublisherAgreements(new HashMap<>()) - .setTwitterHandle("") - .setOrg("null") - .setJobTitle("employee") - .setWebsite("site url") - .setCountry(Country.builder().setCode("CA").setName("Canada").build()) - .setInterests(Collections.emptyList()) - .build())); + this.users + .addAll(Arrays + .asList(EfUser + .builder() + .setUid("666") + .setName("firstlast") + .setGithubHandle("handle") + .setMail("firstlast@test.com") + .setPicture("pic url") + .setFirstName("fake") + .setLastName("user") + .setFullName("fake user") + .setPublisherAgreements(new HashMap<>()) + .setTwitterHandle("") + .setOrg("null") + .setJobTitle("employee") + .setWebsite("site url") + .setCountry(Country.builder().setCode("CA").setName("Canada").build()) + .setInterests(Arrays.asList()) + .build(), + EfUser + .builder() + .setUid("42") + .setName("fakeuser") + .setPicture("pic url") + .setFirstName("fake") + .setLastName("user") + .setFullName("fake user") + .setMail("fakeuser@test.com") + .setPublisherAgreements(new HashMap<>()) + .setGithubHandle("fakeuser") + .setTwitterHandle("") + .setOrg("null") + .setJobTitle("employee") + .setWebsite("site url") + .setCountry(Country.builder().setCode("CA").setName("Canada").build()) + .setInterests(Arrays.asList()) + .build(), + EfUser + .builder() + .setUid("333") + .setName("name") + .setGithubHandle("name") + .setMail("name@test.com") + .setPicture("pic url") + .setFirstName("fake") + .setLastName("user") + .setFullName("fake user") + .setPublisherAgreements(new HashMap<>()) + .setTwitterHandle("") + .setOrg("null") + .setJobTitle("employee") + .setWebsite("site url") + .setCountry(Country.builder().setCode("CA").setName("Canada").build()) + .setInterests(Arrays.asList()) + .build(), + EfUser + .builder() + .setUid("11") + .setName("testtesterson") + .setGithubHandle("mctesty") + .setMail("testtesterson@test.com") + .setPicture("pic url") + .setFirstName("fake") + .setLastName("user") + .setFullName("fake user") + .setPublisherAgreements(new HashMap<>()) + .setTwitterHandle("") + .setOrg("null") + .setJobTitle("employee") + .setWebsite("site url") + .setCountry(Country.builder().setCode("CA").setName("Canada").build()) + .setInterests(Arrays.asList()) + .build(), + EfUser + .builder() + .setUid("444") + .setName("nodoc") + .setGithubHandle("nodoc") + .setMail("nodoc@test.com") + .setPicture("pic url") + .setFirstName("fake") + .setLastName("user") + .setFullName("fake user") + .setPublisherAgreements(new HashMap<>()) + .setTwitterHandle("") + .setOrg("null") + .setJobTitle("employee") + .setWebsite("site url") + .setCountry(Country.builder().setCode("CA").setName("Canada").build()) + .setInterests(Collections.emptyList()) + .build())); } @Override @@ -145,24 +149,21 @@ public class MockProfileAPI implements ProfileAPI { // Ensure request is authenticated oauthService.validateTokenStatus(DrupalAuthHelper.stripBearerToken(token), validScopes, validClientIds); - if (params.getUid() == null && StringUtils.isBlank(params.getMail()) && StringUtils.isBlank(params.getName())) { + if (params.uid == null && StringUtils.isBlank(params.mail) && StringUtils.isBlank(params.name)) { return Collections.emptyList(); } List<EfUser> results = Collections.emptyList(); // Only filter via additional fields if it can't find with previous ones - if (params.getUid() != null) { - results = users.stream().filter(u -> u.getUid().equalsIgnoreCase(params.getUid())) - .collect(Collectors.toList()); + if (params.uid != null) { + results = users.stream().filter(u -> u.getUid().equalsIgnoreCase(params.uid)).toList(); } - if (StringUtils.isNotBlank(params.getName()) && results.isEmpty()) { - results = users.stream().filter(u -> u.getName().equalsIgnoreCase(params.getName())) - .collect(Collectors.toList()); + if (StringUtils.isNotBlank(params.name) && results.isEmpty()) { + results = users.stream().filter(u -> u.getName().equalsIgnoreCase(params.name)).toList(); } - if (StringUtils.isNotBlank(params.getMail()) && results.isEmpty()) { - results = users.stream().filter(u -> u.getMail().equalsIgnoreCase(params.getMail())) - .collect(Collectors.toList()); + if (StringUtils.isNotBlank(params.mail) && results.isEmpty()) { + results = users.stream().filter(u -> u.getMail().equalsIgnoreCase(params.mail)).toList(); } return results; @@ -189,8 +190,7 @@ public class MockProfileAPI implements ProfileAPI { } /* - * Strips the public fields from the incoming EfUser entity if the request if - * not properly authenticated. + * Strips the public fields from the incoming EfUser entity if the request if not properly authenticated. */ private EfUser privateProfileFilter(String token, EfUser user) { try { @@ -198,12 +198,7 @@ public class MockProfileAPI implements ProfileAPI { oauthService.validateTokenStatus(DrupalAuthHelper.stripBearerToken(token), validScopes, validClientIds); return user; } catch (Exception e) { - return user.toBuilder() - .setMail("") - .setCountry(Country.builder() - .setCode(null) - .setName(null).build()) - .build(); + return user.toBuilder().setMail("").setCountry(Country.builder().setCode(null).setName(null).build()).build(); } } } diff --git a/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockWorkingGroupAPI.java b/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockWorkingGroupAPI.java index 2fdf211184221fd65cd8f51bb228e5deedccbb28..92c0156a4399f9d8bd5b7b471c3af6953dd2bcaf 100644 --- a/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockWorkingGroupAPI.java +++ b/efservices/src/test/java/org/eclipsefoundation/efservices/test/api/MockWorkingGroupAPI.java @@ -61,13 +61,8 @@ public class MockWorkingGroupAPI implements WorkingGroupsAPI { } @Override - public Response get(BaseAPIParameters baseParams) { - return Response.ok(wgs).build(); - } - - @Override - public Response getAllByStatuses(BaseAPIParameters baseParams, List<String> statuses) { - return Response.ok(wgs.stream().filter(wg -> statuses.contains(wg.getStatus())).collect(Collectors.toList())).build(); + public Response get(BaseAPIParameters baseParams, List<String> statuses) { + return Response.ok(wgs.stream().filter(wg -> statuses == null || statuses.contains(wg.getStatus())).toList()).build(); } private WorkingGroup.Builder buildBasic(String alias) { diff --git a/efservices/src/test/java/org/eclipsefoundation/efservices/test/resources/TestResources.java b/efservices/src/test/java/org/eclipsefoundation/efservices/test/resources/TestResources.java new file mode 100644 index 0000000000000000000000000000000000000000..260361a030ef8b0d54d0ce5dc70169cfa3bf539a --- /dev/null +++ b/efservices/src/test/java/org/eclipsefoundation/efservices/test/resources/TestResources.java @@ -0,0 +1,50 @@ +/********************************************************************* +* Copyright (c) 2024 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +/** + * + */ +package org.eclipsefoundation.efservices.test.resources; + +import org.eclipsefoundation.efservices.models.AuthenticatedRequestWrapper; +import org.eclipsefoundation.http.annotations.AuthenticatedAlternate; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; + +/** + * Test resource for alternate authentication + */ +@Path("authenticated") +public class TestResources { + + @Inject + AuthenticatedRequestWrapper altAuthUser; + + @GET + @AuthenticatedAlternate + public Response getAuthenticated() { + return Response.ok().build(); + } + + @GET + @Path("partial") + @AuthenticatedAlternate(allowPartialResponse = true) + public Response getAuthenticatedOptional() { + if (altAuthUser.isAuthenticated()) { + return Response.ok().build(); + } + // use slightly different response for validation + return Response.status(Status.NO_CONTENT).build(); + } + +} diff --git a/http/pom.xml b/http/pom.xml index 621a4e2a06ff59a013f6691b75853534c6fd6058..94bc176472ff8751fcda132097097d6b47291682 100644 --- a/http/pom.xml +++ b/http/pom.xml @@ -9,7 +9,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> <relativePath>../pom.xml</relativePath> </parent> <properties> @@ -23,15 +23,11 @@ </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy</artifactId> + <artifactId>quarkus-resteasy-reactive</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-jackson</artifactId> - </dependency> - <dependency> - <groupId>io.quarkus</groupId> - <artifactId>quarkus-undertow</artifactId> + <artifactId>quarkus-resteasy-reactive-jackson</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> @@ -65,7 +61,7 @@ <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-qute</artifactId> + <artifactId>quarkus-resteasy-reactive-qute</artifactId> </dependency> <!-- Test dependencies --> <dependency> @@ -121,4 +117,4 @@ </plugin> </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/http/src/main/java/org/eclipsefoundation/http/annotations/AuthenticatedAlternate.java b/http/src/main/java/org/eclipsefoundation/http/annotations/AuthenticatedAlternate.java new file mode 100644 index 0000000000000000000000000000000000000000..52684f4f834ed58f7bae349b1f89bb3338df9659 --- /dev/null +++ b/http/src/main/java/org/eclipsefoundation/http/annotations/AuthenticatedAlternate.java @@ -0,0 +1,28 @@ +/********************************************************************* +* Copyright (c) 2023 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org> +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.http.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * A Runtime annotation used to allow full blocking on some endpoints, while allowing a partial profile responses. + */ +@Retention(RUNTIME) +@Target(METHOD) +public @interface AuthenticatedAlternate { + + boolean allowPartialResponse() default false; +} diff --git a/http/src/main/java/org/eclipsefoundation/http/config/AdditionalUserDataProvider.java b/http/src/main/java/org/eclipsefoundation/http/config/AdditionalUserDataProvider.java index 291a0fd9cbcc04c97661988bb4f6f147f2c6ed4e..3fab409212b3579f3c5ee470f5e6e2f1b98ebf26 100644 --- a/http/src/main/java/org/eclipsefoundation/http/config/AdditionalUserDataProvider.java +++ b/http/src/main/java/org/eclipsefoundation/http/config/AdditionalUserDataProvider.java @@ -16,7 +16,7 @@ import org.eclipsefoundation.utils.model.AdditionalUserData; import io.quarkus.arc.DefaultBean; import io.quarkus.arc.Unremovable; import jakarta.enterprise.context.Dependent; -import jakarta.enterprise.context.SessionScoped; +import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.inject.Produces; /** @@ -32,7 +32,7 @@ public class AdditionalUserDataProvider { @Produces @DefaultBean - @SessionScoped + @RequestScoped public AdditionalUserData generator() { return new AdditionalUserData(); } diff --git a/http/src/main/java/org/eclipsefoundation/http/model/DefaultRequestWrapper.java b/http/src/main/java/org/eclipsefoundation/http/model/DefaultRequestWrapper.java index 5e0e9cd69280bd91c7ed58f41f66f5c2284a70f5..d90a32303b644c60c23fa0e6f431d5b187496579 100644 --- a/http/src/main/java/org/eclipsefoundation/http/model/DefaultRequestWrapper.java +++ b/http/src/main/java/org/eclipsefoundation/http/model/DefaultRequestWrapper.java @@ -26,15 +26,15 @@ import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; import org.eclipsefoundation.http.namespace.RequestHeaderNames; import org.eclipsefoundation.http.request.CacheBypassFilter; import org.eclipsefoundation.utils.namespace.UrlParameterNamespace.UrlParameter; -import org.jboss.resteasy.core.ResteasyContext; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import io.quarkus.arc.Unremovable; import io.quarkus.security.identity.SecurityIdentity; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.UriInfo; @@ -50,7 +50,7 @@ public class DefaultRequestWrapper implements RequestWrapper { private static final String EMPTY_KEY_MESSAGE = "Key must not be null or blank"; private Map<String, List<String>> params; - private MultivaluedMap<String, String> headers; + private MultivaluedMap<String, String> headers = new MultivaluedHashMap<>(); @Inject SecurityIdentity ident; @@ -58,17 +58,14 @@ public class DefaultRequestWrapper implements RequestWrapper { @Inject PaginationConfig config; - private UriInfo uriInfo; - private HttpServletRequest request; - private HttpServletResponse response; - - /** Generates a wrapper around the context data available from the servlet container. */ - public DefaultRequestWrapper() { - this.headers = new MultivaluedMapImpl<>(); - this.uriInfo = ResteasyContext.getContextData(UriInfo.class); - this.request = ResteasyContext.getContextData(HttpServletRequest.class); - this.response = ResteasyContext.getContextData(HttpServletResponse.class); - } + @Inject + UriInfo uriInfo; + @Inject + HttpServerRequest request; + @Inject + ContainerRequestContext requestContext; + @Inject + HttpServerResponse response; /** * Retrieves the first value set in a list from the map for a given key. @@ -163,7 +160,7 @@ public class DefaultRequestWrapper implements RequestWrapper { */ @Override public MultivaluedMap<String, String> asMap() { - MultivaluedMap<String, String> out = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> out = new MultivaluedHashMap<>(); getParams().forEach((key, values) -> out.addAll(key, new ArrayList<>(values))); return out; } @@ -200,7 +197,7 @@ public class DefaultRequestWrapper implements RequestWrapper { */ @Override public Optional<Object> getAttribute(String key) { - return Optional.ofNullable(request.getAttribute(key)); + return Optional.ofNullable(requestContext.getProperty(key)); } /** @@ -210,7 +207,7 @@ public class DefaultRequestWrapper implements RequestWrapper { */ @Override public boolean isCacheBypass() { - Object attr = request.getAttribute(CacheBypassFilter.ATTRIBUTE_NAME); + Object attr = requestContext.getProperty(CacheBypassFilter.ATTRIBUTE_NAME); // if we have the attribute set on the request, return it. otherwise, false. return attr instanceof Boolean ? (boolean) attr : Boolean.FALSE; } @@ -229,7 +226,7 @@ public class DefaultRequestWrapper implements RequestWrapper { @Override public String getResponseHeader(String key) { if (getResponse() != null) { - return getResponse().getHeader(key); + return getResponse().headers().get(key); } return headers.getFirst(key); } @@ -254,15 +251,12 @@ public class DefaultRequestWrapper implements RequestWrapper { @Override public void setHeader(String name, String value) { if (getResponse() != null) { - getResponse().setHeader(name, value); + getResponse().headers().add(name, value); } headers.add(name, value); } - protected HttpServletResponse getResponse() { - if (response == null) { - response = ResteasyContext.getContextData(HttpServletResponse.class); - } + protected HttpServerResponse getResponse() { return response; } @@ -299,8 +293,8 @@ public class DefaultRequestWrapper implements RequestWrapper { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("RequestWrapper ["); - sb.append("ip=").append(request.getRemoteAddr()); - sb.append(", uri=").append(request.getRequestURI()); + sb.append("ip=").append(request.remoteAddress()); + sb.append(", uri=").append(request.uri()); sb.append(", params=").append(getParams()); return sb.toString(); } diff --git a/http/src/main/java/org/eclipsefoundation/http/model/FlatRequestWrapper.java b/http/src/main/java/org/eclipsefoundation/http/model/FlatRequestWrapper.java index 4c5ebe22f563452de0117586420f61073c4f1748..774106bbb3eeee54abf84dc116b819845b117979 100644 --- a/http/src/main/java/org/eclipsefoundation/http/model/FlatRequestWrapper.java +++ b/http/src/main/java/org/eclipsefoundation/http/model/FlatRequestWrapper.java @@ -22,10 +22,10 @@ import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; import org.eclipsefoundation.utils.namespace.UrlParameterNamespace.UrlParameter; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -35,8 +35,8 @@ import jakarta.ws.rs.core.MultivaluedMap; * */ public class FlatRequestWrapper implements RequestWrapper { - private MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); - private MultivaluedMap<String, String> headers = new MultivaluedMapImpl<>(); + private MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + private MultivaluedMap<String, String> headers = new MultivaluedHashMap<>(); private URI endpoint; private static final int DEFAULT_FLAT_PAGE_SIZE = 1000; @@ -84,7 +84,7 @@ public class FlatRequestWrapper implements RequestWrapper { @Override public MultivaluedMap<String, String> asMap() { - MultivaluedMap<String, String> out = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> out = new MultivaluedHashMap<>(); getParams().forEach(out::addAll); return out; } diff --git a/http/src/main/java/org/eclipsefoundation/http/request/CSRFSecurityFilter.java b/http/src/main/java/org/eclipsefoundation/http/request/CSRFSecurityFilter.java index 1290eaf3a7944f53cd95138c0bc8e1e473751110..0a8e07fcc225eb3abb988e7858f57250ece14618 100644 --- a/http/src/main/java/org/eclipsefoundation/http/request/CSRFSecurityFilter.java +++ b/http/src/main/java/org/eclipsefoundation/http/request/CSRFSecurityFilter.java @@ -22,65 +22,57 @@ import org.eclipsefoundation.utils.config.CSRFSecurityConfig; import org.eclipsefoundation.utils.exception.FinalForbiddenException; import org.eclipsefoundation.utils.helper.CSRFHelper; import org.eclipsefoundation.utils.model.AdditionalUserData; -import org.jboss.resteasy.core.interception.jaxrs.PostMatchContainerRequestContext; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.undertow.httpcore.HttpMethodNames; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletRequest; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.SecurityContext; /** - * Creates a security layer in front of mutation requests to require CSRF tokens - * (if enabled). This layer does not perform the check of the token in-case - * there are other conditions that would rebuff the request. + * Creates a security layer in front of mutation requests to require CSRF tokens (if enabled). This layer does not perform the check of the + * token in-case there are other conditions that would rebuff the request. * * @author Martin Lowe */ -@Provider -public class CSRFSecurityFilter implements ContainerRequestFilter { +public class CSRFSecurityFilter { public static final Logger LOGGER = LoggerFactory.getLogger(CSRFSecurityFilter.class); - @Inject - Instance<CSRFSecurityConfig> config; + private final CSRFSecurityConfig config; + private final CSRFHelper csrf; + private final AdditionalUserData aud; - @Context - HttpServletRequest httpServletRequest; - @Inject - Instance<CSRFHelper> csrf; - @Inject - AdditionalUserData aud; - - @Override - public void filter(ContainerRequestContext requestContext) throws IOException { - if (config.get().enabled()) { + public CSRFSecurityFilter(CSRFHelper csrf, AdditionalUserData aud, CSRFSecurityConfig config) { + this.config = config; + this.csrf = csrf; + this.aud = aud; + } + @ServerRequestFilter + public void filter(ContainerRequestContext requestContext, ResourceInfo info, HttpServerRequest request) throws IOException { + if (config.enabled()) { // Reflect method and get annotation - Method m = ((PostMatchContainerRequestContext) requestContext).getResourceMethod().getMethod(); + Method m = info.getResourceMethod(); Csrf annotation = m.getAnnotation(Csrf.class); - if ((annotation != null && annotation.enabled()) - || (annotation == null && isMutationAction(requestContext.getMethod()))) { - validateCsrfToken(requestContext.getHeaderString(RequestHeaderNames.CSRF_TOKEN)); + if ((annotation != null && annotation.enabled()) || (annotation == null && isMutationAction(requestContext.getMethod()))) { + validateCsrfToken(requestContext.getHeaderString(RequestHeaderNames.CSRF_TOKEN), request, + requestContext.getSecurityContext()); } } } /** - * Check if the HTTP method indicates a mutation action such as DELETE, POST, or - * PUT. + * Check if the HTTP method indicates a mutation action such as DELETE, POST, or PUT. * * @param method The given HTTP method * @return True if DELETE, POST, or PUT */ private boolean isMutationAction(String method) { - return HttpMethodNames.DELETE.equals(method) || HttpMethodNames.POST.equals(method) - || HttpMethodNames.PUT.equals(method); + return HttpMethod.DELETE.name().equals(method) || HttpMethod.POST.name().equals(method) || HttpMethod.PUT.name().equals(method); } /** @@ -88,16 +80,16 @@ public class CSRFSecurityFilter implements ContainerRequestFilter { * * @param token The request CSRF token */ - private void validateCsrfToken(String token) { + private void validateCsrfToken(String token, HttpServerRequest request, SecurityContext context) { // Validate presence if (StringUtils.isBlank(token)) { throw new FinalForbiddenException("No CSRF token passed for mutation call, refusing connection"); - } else if (config.get().distributedMode().enabled()) { + } else if (config.distributedMode().enabled()) { // distributed mode should return the stored token if it exists - csrf.get().compareCSRF(csrf.get().getNewCSRFToken(httpServletRequest), token); + csrf.compareCSRF(csrf.getNewCSRFToken(request, context), token); } else { // run comparison. If error, exception will be thrown - csrf.get().compareCSRF(aud.getCsrf(), token); + csrf.compareCSRF(aud.getCsrf(), token); } } } diff --git a/http/src/main/java/org/eclipsefoundation/http/request/CacheBypassFilter.java b/http/src/main/java/org/eclipsefoundation/http/request/CacheBypassFilter.java index e7cc3f081202434ac11ce1542662842307657794..fcd114ddd164245e840503d0eb52aebf74727a23 100644 --- a/http/src/main/java/org/eclipsefoundation/http/request/CacheBypassFilter.java +++ b/http/src/main/java/org/eclipsefoundation/http/request/CacheBypassFilter.java @@ -13,14 +13,14 @@ package org.eclipsefoundation.http.request; import java.io.IOException; +import org.jboss.resteasy.reactive.server.ServerRequestFilter; + +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.core.Response; /** * Checks passed parameters and if any match one of the criteria for bypassing @@ -30,35 +30,29 @@ import jakarta.ws.rs.ext.Provider; * @author Martin Lowe * */ -@Provider -public class CacheBypassFilter implements ContainerRequestFilter { +public class CacheBypassFilter { public static final String ATTRIBUTE_NAME = "bypass-cache"; @Inject Instance<BypassCondition> conditions; - @Context - HttpServletRequest request; - - @Context - HttpServletResponse response; - - @Override - public void filter(ContainerRequestContext requestContext) throws IOException { + @ServerRequestFilter + public Response filter(ContainerRequestContext requestContext, HttpServerRequest request, HttpServerResponse response) throws IOException { // check for random sort order, which always bypasses cache for (BypassCondition cond : conditions) { - if (cond.matches(requestContext, request)) { - setBypass(); - return; + if (cond.matches(requestContext)) { + setBypass(requestContext, response); + return null; } } - request.setAttribute(ATTRIBUTE_NAME, Boolean.FALSE); + requestContext.setProperty(ATTRIBUTE_NAME, Boolean.FALSE); + return null; } - private void setBypass() { - request.setAttribute(ATTRIBUTE_NAME, Boolean.TRUE); + private void setBypass(ContainerRequestContext requestContext, HttpServerResponse response) { + requestContext.setProperty(ATTRIBUTE_NAME, Boolean.TRUE); // no-store should be used as cache bypass should not return - response.setHeader("Cache-Control", "no-store"); + response.putHeader("Cache-Control", "no-store"); } /** @@ -74,10 +68,8 @@ public class CacheBypassFilter implements ContainerRequestFilter { * should bypass the cache layer. * * @param requestContext the current requests container context - * @param request raw servlet request containing more information about - * the request * @return true if the request should bypass the cache, false otherwise. */ - boolean matches(ContainerRequestContext requestContext, HttpServletRequest request); + boolean matches(ContainerRequestContext requestContext); } } diff --git a/http/src/main/java/org/eclipsefoundation/http/request/OptionalPathFilter.java b/http/src/main/java/org/eclipsefoundation/http/request/OptionalPathFilter.java index 739478092c7b5f9096373b0d3d8936147089e397..1ead0736a11b741888e3ae4d46dd286758419d20 100644 --- a/http/src/main/java/org/eclipsefoundation/http/request/OptionalPathFilter.java +++ b/http/src/main/java/org/eclipsefoundation/http/request/OptionalPathFilter.java @@ -4,21 +4,22 @@ import java.io.IOException; import java.lang.reflect.Method; import java.util.Optional; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipsefoundation.http.annotations.OptionalPath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import jakarta.annotation.Priority; import jakarta.enterprise.inject.Instance; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.ext.Provider; -import org.eclipse.microprofile.config.ConfigProvider; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.eclipsefoundation.http.annotations.OptionalPath; -import org.jboss.resteasy.core.interception.jaxrs.PostMatchContainerRequestContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * Does lookups on the current path to see if the resource is disabled. * @@ -33,10 +34,13 @@ public class OptionalPathFilter implements ContainerRequestFilter { @ConfigProperty(name = "eclipse.optional-resources.enabled", defaultValue = "false") Instance<Boolean> optionalResourcesEnabled; + @Context + ResourceInfo info; + @Override public void filter(ContainerRequestContext requestContext) throws IOException { // check annotation on target endpoint to be sure that endpoint is enabled - Method m = ((PostMatchContainerRequestContext) requestContext).getResourceMethod().getMethod(); + Method m = info.getResourceMethod(); OptionalPath opt = m.getAnnotation(OptionalPath.class); if (opt != null) { if (Boolean.FALSE.equals(optionalResourcesEnabled.get())) { diff --git a/http/src/main/java/org/eclipsefoundation/http/request/SecuredResourceFilter.java b/http/src/main/java/org/eclipsefoundation/http/request/SecuredResourceFilter.java index 51f84a840a56308b1b799147ee5c1eab0d10fbf7..72ec74a841eb92ff90d8a65b04e0d895e4c9f85f 100644 --- a/http/src/main/java/org/eclipsefoundation/http/request/SecuredResourceFilter.java +++ b/http/src/main/java/org/eclipsefoundation/http/request/SecuredResourceFilter.java @@ -14,24 +14,24 @@ package org.eclipsefoundation.http.request; import java.io.IOException; import java.lang.reflect.Method; -import jakarta.annotation.Priority; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.Response.Status; -import jakarta.ws.rs.ext.Provider; - import org.apache.commons.lang3.StringUtils; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipsefoundation.http.annotations.KeySecured; import org.eclipsefoundation.http.helper.SecureResourceKey; -import org.jboss.resteasy.core.interception.jaxrs.PostMatchContainerRequestContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.quarkus.runtime.configuration.ConfigUtils; +import jakarta.annotation.Priority; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.Response.Status; +import jakarta.ws.rs.ext.Provider; /** * Checks if the current resource is protected by the shared {@link SecureResourceKey} and evaluates the key to make @@ -50,10 +50,13 @@ public class SecuredResourceFilter implements ContainerRequestFilter { @Inject SecureResourceKey key; + @Context + ResourceInfo info; + @Override public void filter(ContainerRequestContext requestContext) throws IOException { // check annotation on target endpoint to be sure that endpoint is enabled - Method m = ((PostMatchContainerRequestContext) requestContext).getResourceMethod().getMethod(); + Method m = info.getResourceMethod(); KeySecured opt = m.getAnnotation(KeySecured.class); if (opt != null) { // retrieve the passed key from the parameters and abort request if it doesn't match diff --git a/http/src/main/java/org/eclipsefoundation/http/response/CSRFHeaderFilter.java b/http/src/main/java/org/eclipsefoundation/http/response/CSRFHeaderFilter.java index 2ff962b4b98bdc51060077a8ecad5d291f1bc89f..927015aa92e753a3b7a671dd8862790630fcfd3a 100644 --- a/http/src/main/java/org/eclipsefoundation/http/response/CSRFHeaderFilter.java +++ b/http/src/main/java/org/eclipsefoundation/http/response/CSRFHeaderFilter.java @@ -11,50 +11,41 @@ **********************************************************************/ package org.eclipsefoundation.http.response; -import java.io.IOException; - import org.eclipsefoundation.http.namespace.RequestHeaderNames; import org.eclipsefoundation.utils.config.CSRFSecurityConfig; import org.eclipsefoundation.utils.helper.CSRFHelper; import org.eclipsefoundation.utils.model.AdditionalUserData; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; -import jakarta.enterprise.inject.Instance; -import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletRequest; +import io.vertx.core.http.HttpServerRequest; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; -import jakarta.ws.rs.container.ContainerResponseFilter; -import jakarta.ws.rs.core.Context; -import jakarta.ws.rs.ext.Provider; /** * Injects the CSRF header token into the response when enabled for a server. * * @author Martin Lowe */ -@Provider -public class CSRFHeaderFilter implements ContainerResponseFilter { - - @Inject - Instance<CSRFSecurityConfig> config; +public class CSRFHeaderFilter { - @Context - HttpServletRequest httpServletRequest; + private final CSRFSecurityConfig config; + private final CSRFHelper csrf; + private final AdditionalUserData aud; - @Inject - CSRFHelper csrf; - @Inject - AdditionalUserData aud; + public CSRFHeaderFilter(CSRFHelper csrf, AdditionalUserData aud, CSRFSecurityConfig config) { + this.config = config; + this.csrf = csrf; + this.aud = aud; + } - @Override - public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) - throws IOException { + @ServerResponseFilter + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext, HttpServerRequest request) { // only attach if CSRF is enabled for the current runtime - if (config.get().enabled()) { + if (config.enabled()) { // generate the token - String token = csrf.getNewCSRFToken(httpServletRequest); + String token = csrf.getNewCSRFToken(request, requestContext.getSecurityContext()); // store token in session if not distributed mode - if (aud.getCsrf() == null && !config.get().distributedMode().enabled()) { + if (aud.getCsrf() == null && !config.distributedMode().enabled()) { aud.setCsrf(token); } // attach the current CSRF token as a header on the request diff --git a/http/src/main/java/org/eclipsefoundation/http/response/PaginatedResultsFilter.java b/http/src/main/java/org/eclipsefoundation/http/response/PaginatedResultsFilter.java index 8a12e91aae92428a893628ac29dab55eb4728129..633cd18f5d01973274a428d7782b7a54bafb5fb5 100644 --- a/http/src/main/java/org/eclipsefoundation/http/response/PaginatedResultsFilter.java +++ b/http/src/main/java/org/eclipsefoundation/http/response/PaginatedResultsFilter.java @@ -14,250 +14,227 @@ package org.eclipsefoundation.http.response; import java.io.IOException; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; -import java.util.stream.Collectors; -import jakarta.annotation.Priority; +import org.apache.commons.lang3.StringUtils; +import org.eclipsefoundation.http.annotations.Pagination; +import org.eclipsefoundation.http.config.PaginationConfig; +import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerResponseContext; -import jakarta.ws.rs.container.ContainerResponseFilter; import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.UriBuilder; import jakarta.ws.rs.core.UriInfo; -import jakarta.ws.rs.ext.Provider; - -import org.apache.commons.lang3.StringUtils; -import org.eclipsefoundation.http.annotations.Pagination; -import org.eclipsefoundation.http.config.PaginationConfig; -import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; -import org.jboss.resteasy.core.ResteasyContext; -import org.jboss.resteasy.spi.LinkHeader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Adds pagination and Link headers to the response by slicing the response - * entity if its a list entity. This will not - * dig into complex entities to avoid false positives. + * entity if its a list entity. This will not dig into complex entities to avoid + * false positives. * * @author Martin Lowe */ -@Provider -@Priority(5) -public class PaginatedResultsFilter implements ContainerResponseFilter { - private static final Logger LOGGER = LoggerFactory.getLogger(PaginatedResultsFilter.class); - // should be set whenever we pass a limited subset to the response to be - // paginated of a larger subset - public static final String MAX_RESULTS_SIZE_HEADER = "X-Max-Result-Size"; - // should be set whenever we request data that has a maximum page size that is - // different than the default here - public static final String MAX_PAGE_SIZE_HEADER = "X-Max-Page-Size"; - - @Inject - Instance<PaginationConfig> config; - - @Context - HttpServletResponse response; - @Context - ResourceInfo resourceInfo; - - @Override - public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) - throws IOException { - - if (checkPaginationAnnotation() && config.get().filter().enabled()) { - Object entity = responseContext.getEntity(); - - // only try and paginate if there are multiple entities - if (entity instanceof Set) { - paginateResults(responseContext, (new TreeSet<>((Set<?>) entity)).stream() - .collect(Collectors.toList())); - } else if (entity instanceof List) { - paginateResults(responseContext, (List<?>) entity); - } - } - } - - private void paginateResults(ContainerResponseContext responseContext, List<?> listEntity) { - int pageSize = getCurrentLimit(responseContext); - // if available, use max results header value - String rawMaxSize = getResponseHeader(responseContext, MAX_RESULTS_SIZE_HEADER); - int maxSize = listEntity.size(); - if (StringUtils.isNumeric(rawMaxSize)) { - maxSize = Integer.valueOf(rawMaxSize); - } - LOGGER.trace("Using max results of {} with page size {}", maxSize, pageSize); - int page = getRequestedPage(maxSize, pageSize); - - int lastPage = (int) Math.ceil((double) maxSize / pageSize); - // slice if the results set is larger than page size - if (listEntity.size() > pageSize) { - // set the sliced array as the entity - responseContext - .setEntity(listEntity - .subList(getArrayLimitedNumber(listEntity, Math.max(0, page - 1) * pageSize), - getArrayLimitedNumber(listEntity, pageSize * page))); - } - // set the link header to the response - responseContext.getHeaders().add("Link", createLinkHeader(page, lastPage)); - } - - /** - * Checks the current method for pagination annotations and if present, checks - * to make sure pagination is enabled. Defaults to using pagination if the - * pagination annotation is missing or cannot be read. - * - * @return true if pagination should be enabled, false otherwise - */ - private boolean checkPaginationAnnotation() { - - // method can be null sometimes for quarkus native endpoints - Method method = resourceInfo.getResourceMethod(); - if (method != null) { - Pagination annotation = method.getAnnotation(Pagination.class); - return annotation == null || annotation.value(); - } - return true; - } - - /** - * Gets a header value if available, checking first the servlet response then - * the mutable response wrapper. - * - * @param responseContext the mutable response wrapper - * @param headerName the header to retrieve - * @return the header value if set, null otherwise. - */ - private String getResponseHeader(ContainerResponseContext responseContext, String headerName) { - String rawHeaderVal = response.getHeader(headerName); - if (rawHeaderVal == null) { - rawHeaderVal = responseContext.getHeaderString(headerName); - } - return rawHeaderVal; - } - - private LinkHeader createLinkHeader(int page, int lastPage) { - // add link headers for paginated page hints - - UriBuilder builder = getUriInfo().getRequestUriBuilder(); - LinkHeader lh = new LinkHeader(); - // add first + last page link headers - lh.addLink("this page of results", "self", buildHref(builder, page), ""); - lh.addLink("first page of results", "first", buildHref(builder, 1), ""); - lh.addLink("last page of results", "last", buildHref(builder, lastPage), ""); - // add next/prev if needed - if (page > 1) { - lh.addLink("previous page of results", "prev", buildHref(builder, page - 1), ""); - } - if (page < lastPage) { - lh.addLink("next page of results", "next", buildHref(builder, page + 1), ""); - } - return lh; - } - - /** - * Gets the current requested page, rounding down to max if larger than the max - * page number, and up if below 1. - * - * @param listEntity list entity used to determine the number of pages present - * for current call. - * @return the current page number if set, the last page if greater, or 1 if not - * set or negative. - */ - private int getRequestedPage(int maxSize, int pageSize) { - MultivaluedMap<String, String> params = getUriInfo().getQueryParameters(); - if (params.containsKey(DefaultUrlParameterNames.PAGE.getName())) { - try { - int page = Integer.parseInt(params.getFirst(DefaultUrlParameterNames.PAGE.getName())); - // use double cast int to allow ceil call to round up for pages - int maxPage = (int) Math.ceil((double) maxSize / pageSize); - - // get page, with min of 1 and max of last page - return Math.min(Math.max(1, page), maxPage); - } catch (NumberFormatException e) { - // page isn't a number, just return - LOGGER.error("Passed bad page value: {}", params.getFirst("page")); - return 1; - } - } - return 1; - } - - /** - * Allows for external bindings to affect the current page size, defaulting to - * the internal set configuration. - * - * @param responseContext - * @return - */ - private int getCurrentLimit(ContainerResponseContext responseContext) { - // check the pagesize param - MultivaluedMap<String, String> params = getUriInfo().getQueryParameters(); - if (params.containsKey(DefaultUrlParameterNames.PAGESIZE.getName()) - && StringUtils.isNumeric(params.getFirst(DefaultUrlParameterNames.PAGESIZE.getName()))) { - int pageSize = Integer.parseInt(params.getFirst(DefaultUrlParameterNames.PAGESIZE.getName())); - int maxPageSize = getInternalMaxPageSize(responseContext); - return Math.min(pageSize, maxPageSize); - } - return getInternalMaxPageSize(responseContext); - } - - /** - * Returns the max page size as defined by internal metrics (ignoring pagesize - * params). - * - * @return the internal max page size. - */ - private int getInternalMaxPageSize(ContainerResponseContext responseContext) { - String rawPageSize = getResponseHeader(responseContext, MAX_PAGE_SIZE_HEADER); - if (StringUtils.isNotBlank(rawPageSize)) { - try { - return Integer.parseInt(rawPageSize); - } catch (NumberFormatException e) { - // page size isn't a number, allow to return default outside current scope - } - } - return config.get().filter().defaultPageSize(); - } - - /** - * Builds an href for a paginated link using the BaseUri UriBuilder from the - * UriInfo object, replacing just the page query parameter. - * - * @param builder base URI builder from the UriInfo object. - * @param page the page to link to in the returned link - * @return fully qualified HREF for the paginated results - */ - private String buildHref(UriBuilder builder, int page) { - // Force scheme of header links to be a given value, useful for proxied requests - if (config.get().filter().scheme().enforce()) { - return builder.scheme(config.get().filter().scheme().value()).replaceQueryParam("page", page).build() - .toString(); - } - return builder.replaceQueryParam("page", page).build().toString(); - } - - /** - * Gets an int bound by the size of a list. - * - * @param list the list to bind the number by - * @param num the number to check for exceeding bounds. - * @return the passed number if its within the size of the given array, 0 if the - * number is negative, and the array size if greater than the maximum - * bounds. - */ - private int getArrayLimitedNumber(List<?> list, int num) { - return Math.min(list.size(), Math.max(0, num)); - } - - private UriInfo getUriInfo() { - return ResteasyContext.getContextData(UriInfo.class); - } +public class PaginatedResultsFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(PaginatedResultsFilter.class); + // should be set whenever we pass a limited subset to the response to be + // paginated of a larger subset + public static final String MAX_RESULTS_SIZE_HEADER = "X-Max-Result-Size"; + // should be set whenever we request data that has a maximum page size that is + // different than the default here + public static final String MAX_PAGE_SIZE_HEADER = "X-Max-Page-Size"; + + @Inject + Instance<PaginationConfig> config; + + @ServerResponseFilter + public void filter(ResourceInfo resourceInfo, UriInfo uriInfo, ContainerResponseContext responseContext) + throws IOException { + + if (checkPaginationAnnotation(resourceInfo) && config.get().filter().enabled()) { + Object entity = responseContext.getEntity(); + + // only try and paginate if there are multiple entities + if (entity instanceof Set) { + paginateResults(responseContext, uriInfo, (new TreeSet<>((Set<?>) entity)).stream().toList()); + } else if (entity instanceof List) { + paginateResults(responseContext, uriInfo, (List<?>) entity); + } + } + } + + private void paginateResults(ContainerResponseContext responseContext, UriInfo uriInfo, List<?> listEntity) { + int pageSize = getCurrentLimit(uriInfo, responseContext); + // if available, use max results header value + String rawMaxSize = getResponseHeader(responseContext, MAX_RESULTS_SIZE_HEADER); + int maxSize = listEntity.size(); + if (StringUtils.isNumeric(rawMaxSize)) { + maxSize = Integer.valueOf(rawMaxSize); + } + LOGGER.trace("Using max results of {} with page size {}", maxSize, pageSize); + int page = getRequestedPage(uriInfo, maxSize, pageSize); + + int lastPage = (int) Math.ceil((double) maxSize / pageSize); + // slice if the results set is larger than page size + if (listEntity.size() > pageSize) { + // set the sliced array as the entity + responseContext + .setEntity(listEntity.subList(getArrayLimitedNumber(listEntity, Math.max(0, page - 1) * pageSize), + getArrayLimitedNumber(listEntity, pageSize * page))); + } + // set the link header to the response + responseContext.getHeaders().put("Link", + createLinkHeader(uriInfo, page, lastPage).stream().map(l -> (Object) l).toList()); + } + + /** + * Checks the current method for pagination annotations and if present, checks + * to make sure pagination is enabled. Defaults to using pagination if the + * pagination annotation is missing or cannot be read. + * + * @return true if pagination should be enabled, false otherwise + */ + private boolean checkPaginationAnnotation(ResourceInfo resourceInfo) { + // method can be null sometimes for quarkus native endpoints + Method method = resourceInfo.getResourceMethod(); + if (method != null) { + Pagination annotation = method.getAnnotation(Pagination.class); + return annotation == null || annotation.value(); + } + return true; + } + + /** + * Gets a header value if available, checking first the servlet response then + * the mutable response wrapper. + * + * @param responseContext the response context + * @param headerName the header to retrieve + * @return the header value if set, null otherwise. + */ + private String getResponseHeader(ContainerResponseContext responseContext, String headerName) { + return responseContext.getHeaderString(headerName); + } + + private List<Link> createLinkHeader(UriInfo uriInfo, int page, int lastPage) { + // add link headers for paginated page hints + + UriBuilder builder = uriInfo.getRequestUriBuilder(); + List<Link> links = new ArrayList<>(); + // add first + last page link headers + links.add(Link.fromUri(buildHref(builder, 1)).rel("first").title("first page of results").build()); + links.add(Link.fromUri(buildHref(builder, page)).rel("self").title("this page of results").build()); + links.add(Link.fromUri(buildHref(builder, lastPage)).rel("last").title("last page of results").build()); + // add next/prev if needed + if (page > 1) { + links.add(Link.fromUri(buildHref(builder, page - 1)).rel("prev").title("previous page of results").build()); + } + if (page < lastPage) { + links.add(Link.fromUri(buildHref(builder, page + 1)).rel("next").title("next page of results").build()); + } + return links; + } + + /** + * Gets the current requested page, rounding down to max if larger than the max + * page number, and up if below 1. + * + * @param listEntity list entity used to determine the number of pages present + * for current call. + * @return the current page number if set, the last page if greater, or 1 if not + * set or negative. + */ + private int getRequestedPage(UriInfo uriInfo, int maxSize, int pageSize) { + MultivaluedMap<String, String> params = uriInfo.getQueryParameters(); + if (params.containsKey(DefaultUrlParameterNames.PAGE.getName())) { + try { + int page = Integer.parseInt(params.getFirst(DefaultUrlParameterNames.PAGE.getName())); + // use double cast int to allow ceil call to round up for pages + int maxPage = (int) Math.ceil((double) maxSize / pageSize); + + // get page, with min of 1 and max of last page + return Math.min(Math.max(1, page), maxPage); + } catch (NumberFormatException e) { + // page isn't a number, just return + LOGGER.error("Passed bad page value: {}", params.getFirst("page")); + return 1; + } + } + return 1; + } + + /** + * Allows for external bindings to affect the current page size, defaulting to + * the internal set configuration. + * + * @param responseContext + * @return + */ + private int getCurrentLimit(UriInfo uriInfo, ContainerResponseContext responseContext) { + // check the pagesize param + MultivaluedMap<String, String> params = uriInfo.getQueryParameters(); + if (params.containsKey(DefaultUrlParameterNames.PAGESIZE.getName()) + && StringUtils.isNumeric(params.getFirst(DefaultUrlParameterNames.PAGESIZE.getName()))) { + int pageSize = Integer.parseInt(params.getFirst(DefaultUrlParameterNames.PAGESIZE.getName())); + int maxPageSize = getInternalMaxPageSize(responseContext); + return Math.min(pageSize, maxPageSize); + } + return getInternalMaxPageSize(responseContext); + } + + /** + * Returns the max page size as defined by internal metrics (ignoring pagesize + * params). + * + * @return the internal max page size. + */ + private int getInternalMaxPageSize(ContainerResponseContext responseContext) { + String rawPageSize = getResponseHeader(responseContext, MAX_PAGE_SIZE_HEADER); + if (StringUtils.isNotBlank(rawPageSize)) { + try { + return Integer.parseInt(rawPageSize); + } catch (NumberFormatException e) { + // page size isn't a number, allow to return default outside current scope + } + } + return config.get().filter().defaultPageSize(); + } + + /** + * Builds an href for a paginated link using the BaseUri UriBuilder from the + * UriInfo object, replacing just the page query parameter. + * + * @param builder base URI builder from the UriInfo object. + * @param page the page to link to in the returned link + * @return fully qualified HREF for the paginated results + */ + private String buildHref(UriBuilder builder, int page) { + // Force scheme of header links to be a given value, useful for proxied requests + if (config.get().filter().scheme().enforce()) { + return builder.scheme(config.get().filter().scheme().value()).replaceQueryParam("page", page).build() + .toString(); + } + return builder.replaceQueryParam("page", page).build().toString(); + } + + /** + * Gets an int bound by the size of a list. + * + * @param list the list to bind the number by + * @param num the number to check for exceeding bounds. + * @return the passed number if its within the size of the given array, 0 if the + * number is negative, and the array size if greater than the maximum + * bounds. + */ + private int getArrayLimitedNumber(List<?> list, int num) { + return Math.min(list.size(), Math.max(0, num)); + } } diff --git a/http/src/main/resources/application.properties b/http/src/main/resources/application.properties index 866efd1b42805c2211c102f2d457ce74797bcca0..3a0fcd8f49c3497ab4218261ee0156635623ca09 100644 --- a/http/src/main/resources/application.properties +++ b/http/src/main/resources/application.properties @@ -2,6 +2,10 @@ quarkus.oauth2.enabled=false quarkus.oauth2.introspection-url=http://accounts.eclipse.org/oauth2/introspect +## CSRF settings +quarkus.csrf-reactive.token-header-name=x-csrf-token +quarkus.csrf-reactive.cookie-http-only=false + # MISC quarkus.resteasy.gzip.enabled=true quarkus.http.port=8090 diff --git a/http/src/test/java/org/eclipsefoundation/http/authenticated/request/CSRFSecurityFilterTest.java b/http/src/test/java/org/eclipsefoundation/http/authenticated/request/CSRFSecurityFilterTest.java deleted file mode 100644 index ca7d26a3629e530cf338695dd3c1c32b08ef14a9..0000000000000000000000000000000000000000 --- a/http/src/test/java/org/eclipsefoundation/http/authenticated/request/CSRFSecurityFilterTest.java +++ /dev/null @@ -1,124 +0,0 @@ -/********************************************************************* -* Copyright (c) 2022, 2024 Eclipse Foundation. -* -* This program and the accompanying materials are made -* available under the terms of the Eclipse Public License 2.0 -* which is available at https://www.eclipse.org/legal/epl-2.0/ -* -* Author: Martin Lowe <martin.lowe@eclipse-foundation.org> -* -* SPDX-License-Identifier: EPL-2.0 -**********************************************************************/ -package org.eclipsefoundation.http.authenticated.request; - -import static io.restassured.RestAssured.given; - -import org.eclipsefoundation.http.namespace.RequestHeaderNames; -import org.eclipsefoundation.http.test.AuthenticatedTestProfile; -import org.junit.jupiter.api.Test; - -import io.quarkus.test.junit.QuarkusTest; -import io.quarkus.test.junit.TestProfile; -import io.restassured.filter.session.SessionFilter; -import io.restassured.response.Response; -import jakarta.ws.rs.core.Response.Status; - -/** - * Test the CSRF security filter which can block requests based on presence of CSRF token. This makes use of the - * authenticated test profile to reduce complexity of testing other facets of the core lib that have no interactions - * with security. - * - * @author Martin Lowe - * - */ -@QuarkusTest -@TestProfile(AuthenticatedTestProfile.class) -class CSRFSecurityFilterTest { - - @Test - void validateNoToken() { - - // CSRF enabled via annotation - given().when().get("/test").then().statusCode(Status.FORBIDDEN.getStatusCode()); - - // CSRF enabled by default for mutation calls via configs - given().when().post("/test").then().statusCode(Status.FORBIDDEN.getStatusCode()); - given().when().put("/test").then().statusCode(Status.FORBIDDEN.getStatusCode()); - given().when().delete("/test").then().statusCode(Status.FORBIDDEN.getStatusCode()); - - // No token required - given().when().get("/test/unguarded").then().statusCode(Status.OK.getStatusCode()); - - // CSRF requirement removed via annotation - given().when().post("/test/unguarded").then().statusCode(Status.OK.getStatusCode()); - } - - @Test - void validateWrongToken() { - // do a good request to trigger the build of the header internally - given().when().get("/test/unguarded").then().statusCode(Status.OK.getStatusCode()); - // expect rebuff as no CSRF token was passed - given() - .header(RequestHeaderNames.CSRF_TOKEN, "bad-header-value") - .when() - .get("/test") - .then() - .statusCode(Status.FORBIDDEN.getStatusCode()); - given() - .header(RequestHeaderNames.CSRF_TOKEN, "bad-header-value") - .when() - .post("/test") - .then() - .statusCode(Status.FORBIDDEN.getStatusCode()); - given() - .header(RequestHeaderNames.CSRF_TOKEN, "bad-header-value") - .when() - .put("/test") - .then() - .statusCode(Status.FORBIDDEN.getStatusCode()); - given() - .header(RequestHeaderNames.CSRF_TOKEN, "bad-header-value") - .when() - .delete("/test") - .then() - .statusCode(Status.FORBIDDEN.getStatusCode()); - } - - @Test - void validateRightCSRFToken() { - SessionFilter sessionFilter = new SessionFilter(); - // do a good request to trigger the build of the header internally - Response r = given().filter(sessionFilter).when().get("/test/unguarded"); - String expectedHeader = r.getHeader(RequestHeaderNames.CSRF_TOKEN); - - // expect rebuff as no CSRF token was passed - given() - .filter(sessionFilter) - .header(RequestHeaderNames.CSRF_TOKEN, expectedHeader) - .when() - .post("/test") - .then() - .statusCode(Status.OK.getStatusCode()); - given() - .filter(sessionFilter) - .header(RequestHeaderNames.CSRF_TOKEN, expectedHeader) - .when() - .delete("/test") - .then() - .statusCode(Status.OK.getStatusCode()); - given() - .filter(sessionFilter) - .header(RequestHeaderNames.CSRF_TOKEN, expectedHeader) - .when() - .put("/test") - .then() - .statusCode(Status.OK.getStatusCode()); - given() - .filter(sessionFilter) - .header(RequestHeaderNames.CSRF_TOKEN, expectedHeader) - .when() - .get("/test") - .then() - .statusCode(Status.OK.getStatusCode()); - } -} diff --git a/http/src/test/java/org/eclipsefoundation/http/request/CacheBypassFilterTest.java b/http/src/test/java/org/eclipsefoundation/http/request/CacheBypassFilterTest.java index 41d3c911e76a7954c23c8425724c2163eaa46d23..05f40b2168f340c3dad09255fc02baac5e14c873 100644 --- a/http/src/test/java/org/eclipsefoundation/http/request/CacheBypassFilterTest.java +++ b/http/src/test/java/org/eclipsefoundation/http/request/CacheBypassFilterTest.java @@ -13,15 +13,13 @@ package org.eclipsefoundation.http.request; import static io.restassured.RestAssured.given; -import jakarta.enterprise.context.Dependent; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.container.ContainerRequestContext; - import org.eclipsefoundation.http.request.CacheBypassFilter.BypassCondition; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; +import jakarta.enterprise.context.Dependent; +import jakarta.ws.rs.container.ContainerRequestContext; /** * Basic tests to ensure that the bypass condition filters can activate as intended on requests. @@ -49,8 +47,8 @@ class CacheBypassFilterTest { @Dependent public static class TestBypassCondition implements BypassCondition { @Override - public boolean matches(ContainerRequestContext requestContext, HttpServletRequest request) { - return "true".equalsIgnoreCase(request.getHeader("Use-Test-Bypass")); + public boolean matches(ContainerRequestContext requestContext) { + return "true".equalsIgnoreCase(requestContext.getHeaderString("Use-Test-Bypass")); } } } diff --git a/http/src/test/java/org/eclipsefoundation/http/resource/LoggerResourceTest.java b/http/src/test/java/org/eclipsefoundation/http/resource/LoggerResourceTest.java index 4878021fdd686076f1f18bf0f19a43052da41a9d..c9128cddfc37ba9da2a7644d91df57a14e310785 100644 --- a/http/src/test/java/org/eclipsefoundation/http/resource/LoggerResourceTest.java +++ b/http/src/test/java/org/eclipsefoundation/http/resource/LoggerResourceTest.java @@ -11,11 +11,8 @@ package org.eclipsefoundation.http.resource; import static io.restassured.RestAssured.given; -import java.util.List; import java.util.UUID; - -import jakarta.inject.Inject; -import jakarta.ws.rs.core.Response.Status; +import java.util.stream.Stream; import org.eclipsefoundation.http.helper.SecureResourceKey; import org.eclipsefoundation.http.resource.LoggerResource.LoggerWrapper; @@ -23,6 +20,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response.Status; @QuarkusTest class LoggerResourceTest { @@ -40,7 +39,8 @@ class LoggerResourceTest { @Test void updateLogger_usesInstanceKey() { - given().when().get("/loggers?key={key}", UUID.randomUUID().toString()).then().statusCode(Status.FORBIDDEN.getStatusCode()); + given().when().get("/loggers?key={key}", UUID.randomUUID().toString()).then() + .statusCode(Status.FORBIDDEN.getStatusCode()); given() .when() .get("/loggers?key={key}&clazz={clazz}&level={level}", key.getKey(), VALID_CLASS_NAME, VALID_LOG_LEVEL) @@ -66,7 +66,8 @@ class LoggerResourceTest { void updateLogger_error_failsInvalidClass() { given() .when() - .get("/loggers?key={key}&clazz={clazz}&level={level}", key.getKey(), "org.eclipsefoundation.core.resource.NotARealClass", + .get("/loggers?key={key}&clazz={clazz}&level={level}", key.getKey(), + "org.eclipsefoundation.core.resource.NotARealClass", VALID_LOG_LEVEL) .then() .statusCode(Status.BAD_REQUEST.getStatusCode()); @@ -83,23 +84,27 @@ class LoggerResourceTest { @Test void getCurrentLoggers_usesInstanceKey() { - given().when().get("/loggers/all?key={key}", UUID.randomUUID().toString()).then().statusCode(Status.FORBIDDEN.getStatusCode()); + given().when().get("/loggers/all?key={key}", UUID.randomUUID().toString()).then() + .statusCode(Status.FORBIDDEN.getStatusCode()); given().when().get("/loggers/all?key={key}", key.getKey()).then().statusCode(Status.OK.getStatusCode()); } @Test void getCurrentLoggers_containsEfAndBaseAppLoggers() { - // do the call to the loggers endpoint, and convert the response to a list of returned loggers - List<LoggerWrapper> loggers = given() + // do the call to the loggers endpoint, and convert the response to a list of + // returned loggers + LoggerWrapper[] loggers = given() .when() .get("/loggers/all?key={key}", key.getKey()) .then() .extract() - .jsonPath() - .getList("", LoggerWrapper.class); - // do 2 assertions to check we have both application and base app loggers present - Assertions.assertTrue(loggers.stream().anyMatch(loggerName -> loggerName.getName().startsWith("org.eclipsefoundation"))); - Assertions.assertTrue(loggers.stream().anyMatch(loggerName -> !loggerName.getName().startsWith("org.eclipsefoundation"))); + .as(LoggerWrapper[].class); + // do 2 assertions to check we have both application and base app loggers + // present + Assertions.assertTrue( + Stream.of(loggers).anyMatch(loggerName -> loggerName.getName().startsWith("org.eclipsefoundation"))); + Assertions.assertTrue( + Stream.of(loggers).anyMatch(loggerName -> !loggerName.getName().startsWith("org.eclipsefoundation"))); } @Test @@ -114,15 +119,16 @@ class LoggerResourceTest { @Test void getApplicationLoggers_onlyContainsEclipseNamespace() { - // do the call to the loggers endpoint, and convert the response to a list of returned loggers - List<LoggerWrapper> loggers = given() + // do the call to the loggers endpoint, and convert the response to a list of + // returned loggers + LoggerWrapper[] loggers = given() .when() .get("/loggers/application?key={key}", key.getKey()) .then() .extract() - .jsonPath() - .getList("", LoggerWrapper.class); + .as(LoggerWrapper[].class); // do assertion to check we have only application loggers present - Assertions.assertTrue(loggers.stream().anyMatch(loggerName -> loggerName.getName().startsWith("org.eclipsefoundation"))); + Assertions.assertTrue( + Stream.of(loggers).anyMatch(loggerName -> loggerName.getName().startsWith("org.eclipsefoundation"))); } } diff --git a/http/src/test/java/org/eclipsefoundation/http/test/resources/TestResource.java b/http/src/test/java/org/eclipsefoundation/http/test/resources/TestResource.java index 2cac673d617da302ddce727fa9aca55f254dae30..29f9d4c6270403ec7562b5c72cfb55f1e525dbc9 100644 --- a/http/src/test/java/org/eclipsefoundation/http/test/resources/TestResource.java +++ b/http/src/test/java/org/eclipsefoundation/http/test/resources/TestResource.java @@ -18,8 +18,6 @@ import org.eclipsefoundation.http.annotations.Csrf; import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; import org.eclipsefoundation.http.response.PaginatedResultsFilter; -import org.eclipsefoundation.utils.helper.CSRFHelper; -import org.eclipsefoundation.utils.model.AdditionalUserData; import jakarta.inject.Inject; import jakarta.ws.rs.DELETE; @@ -41,23 +39,8 @@ public class TestResource { public static final String PAGINATION_HEADER_RESULT_SIZE = "30"; public static final String PAGINATION_HEADER_PAGE_SIZE_DEFAULT = "10"; - @Inject - CSRFHelper csrf; - @Inject - AdditionalUserData aud; @Inject RequestWrapper wrap; - /** - * Basic sample GET call that can be gated through CSRF if enabled to protect the data further. - * - * @param passedCsrf the passed CSRF header value - * @return empty ok response if CSRF is disabled or properly passed, Status.FORBIDDEN.getStatusCode() response otherwise. - */ - @GET - @Csrf - public Response getGuarded() { - return Response.ok().build(); - } /** * Basic sample GET call that is not gated through CSRF. This could represent a user data endpoint, or a straight CSRF endpoint to diff --git a/http/src/test/resources/application.properties b/http/src/test/resources/application.properties index 09cf33f9d7b8a780b8ebcb5cc1a9fc33102691c3..a61947c43b33d79b9b79ad905bb45507b44b581b 100644 --- a/http/src/test/resources/application.properties +++ b/http/src/test/resources/application.properties @@ -1,4 +1,7 @@ -quarkus.jacoco.includes=**/request/**/* +quarkus.jacoco.includes=**/http/**/* + +## CSRF sample signature token - only used in testing +quarkus.csrf-reactive.token-signature-key=SUmkysx2ejh5&sghSah8z3khws!xvmQU ## OAUTH CONFIG quarkus.oauth2.enabled=true diff --git a/persistence/deployment/pom.xml b/persistence/deployment/pom.xml index 9773295a5625d170dbf69dadc286805cc92c6c23..a9dc1e183a37917a376fe08657cdc7dbd6b14893 100644 --- a/persistence/deployment/pom.xml +++ b/persistence/deployment/pom.xml @@ -11,7 +11,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-persistence-parent</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> </parent> <artifactId>quarkus-persistence-deployment</artifactId> <name>Persistence - Deployment</name> @@ -31,15 +31,11 @@ </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-deployment</artifactId> + <artifactId>quarkus-resteasy-reactive-deployment</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-jackson-deployment</artifactId> - </dependency> - <dependency> - <groupId>io.quarkus</groupId> - <artifactId>quarkus-undertow-deployment</artifactId> + <artifactId>quarkus-resteasy-reactive-jackson-deployment</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> @@ -67,7 +63,7 @@ </dependency> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy-qute-deployment</artifactId> + <artifactId>quarkus-resteasy-reactive-qute-deployment</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> @@ -90,4 +86,4 @@ </plugin> </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/persistence/pom.xml b/persistence/pom.xml index 8a1384197b693dfc1a95c15582ba0b6834673e88..c23ec1a9f62543aefdc46ecd119b0f12ccdd9987 100644 --- a/persistence/pom.xml +++ b/persistence/pom.xml @@ -10,7 +10,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> <relativePath>../pom.xml</relativePath> </parent> <modules> diff --git a/persistence/runtime/pom.xml b/persistence/runtime/pom.xml index dd9d861870449d247f6de7fdff16b669210fcf61..e7e7cdb072db49e76bad7879084f3eb69902efa0 100644 --- a/persistence/runtime/pom.xml +++ b/persistence/runtime/pom.xml @@ -7,7 +7,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-persistence-parent</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> </parent> <artifactId>quarkus-persistence</artifactId> <name>Persistence - Runtime</name> diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/DistributedCSRFGenerator.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/DistributedCSRFGenerator.java index 1c0b8474eeda37a52e9780ebac6b41d983e5a3fc..74f616154e006e699c4725f61477f0f72737ea2a 100644 --- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/DistributedCSRFGenerator.java +++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/DistributedCSRFGenerator.java @@ -23,18 +23,19 @@ import org.eclipsefoundation.persistence.dto.DistributedCSRFToken; import org.eclipsefoundation.persistence.dto.filter.DistributedCSRFTokenFilter; import org.eclipsefoundation.utils.helper.DateTimeHelper; import org.eclipsefoundation.utils.model.CSRFGenerator.DefaultCSRFGenerator; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; +import org.eclipsefoundation.utils.namespace.AdditionalHttpHeaders; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import io.undertow.httpcore.HttpHeaderNames; -import jakarta.servlet.http.HttpServletRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.vertx.core.http.HttpServerRequest; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.SecurityContext; /** - * Using IP address and useragent, create a fingerprint for the current user that will be used to access a stored - * distributed CSRF token. If one does not yet exist, one will be generated using the default method, stored for future - * reference, and returned. + * Using IP address and useragent, create a fingerprint for the current user that will be used to access a stored distributed CSRF token. If + * one does not yet exist, one will be generated using the default method, stored for future reference, and returned. * * @author Martin Lowe * @@ -56,17 +57,16 @@ public class DistributedCSRFGenerator extends DefaultCSRFGenerator { } @Override - public String getCSRFToken(HttpServletRequest httpServletRequest) { + public String getCSRFToken(HttpServerRequest request, SecurityContext context) { // generate a non-root query - MultivaluedMap<String, String> params = getQueryParameters(httpServletRequest); - RDBMSQuery<DistributedCSRFToken> q = new RDBMSQuery<>( - new FlatRequestWrapper(URI.create(httpServletRequest.getRequestURL().toString())), filter, params); + MultivaluedMap<String, String> params = getQueryParameters(request, context); + RDBMSQuery<DistributedCSRFToken> q = new RDBMSQuery<>(new FlatRequestWrapper(URI.create(request.uri())), filter, params); q.setRoot(false); // attempt to read existing tokens List<DistributedCSRFToken> tokens = manager.get(q); if (tokens.isEmpty()) { // generate a new token to be stored in the distributed persistence table - String token = super.getCSRFToken(httpServletRequest); + String token = super.getCSRFToken(request, context); DistributedCSRFToken t = new DistributedCSRFToken(); t.setIpAddress(params.getFirst(IP_PARAM)); t.setToken(token); @@ -97,34 +97,34 @@ public class DistributedCSRFGenerator extends DefaultCSRFGenerator { return null; } - public void destroyCurrentToken(HttpServletRequest httpServletRequest) { - MultivaluedMap<String, String> params = getQueryParameters(httpServletRequest); + public void destroyCurrentToken(HttpServerRequest request, SecurityContext context) { + MultivaluedMap<String, String> params = getQueryParameters(request, context); LOGGER .error("Destroying retrieving CSRF entry for current user; IP: {}, UA: {}", params.getFirst(IP_PARAM), params.getFirst(USERAGENT_PARAM)); - manager.delete(new RDBMSQuery<>(new FlatRequestWrapper(URI.create(httpServletRequest.getRequestURL().toString())), filter, params)); + manager.delete(new RDBMSQuery<>(new FlatRequestWrapper(URI.create(request.uri())), filter, params)); } - private MultivaluedMap<String, String> getQueryParameters(HttpServletRequest httpServletRequest) { + private MultivaluedMap<String, String> getQueryParameters(HttpServerRequest request, SecurityContext context) { // get the markers used to identify a user (outside of a unique session ID) - String ipAddr = getClientIpAddress(httpServletRequest); - String userAgent = httpServletRequest.getHeader(HttpHeaderNames.USER_AGENT); + String ipAddr = getClientIpAddress(request); + String userAgent = request.getHeader(HttpHeaderNames.USER_AGENT); // Iss #109 - Truncate the value if it's too long to keep table entries managable if (userAgent.length() > 255) { userAgent = userAgent.substring(0, 250); } - Principal user = httpServletRequest.getUserPrincipal(); - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + Principal user = context.getUserPrincipal(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(IP_PARAM, ipAddr); params.add(USERAGENT_PARAM, userAgent); params.add(USER_PARAM, user != null ? user.getName() : null); return params; } - private String getClientIpAddress(HttpServletRequest request) { - String forwardedFor = request.getHeader(HttpHeaderNames.X_FORWARDED_FOR); + private String getClientIpAddress(HttpServerRequest request) { + String forwardedFor = request.getHeader(AdditionalHttpHeaders.X_FORWARDED_FOR); if (forwardedFor == null) { - return request.getRemoteAddr(); + return request.remoteAddress().host(); } else { // get the first most address in forwarded chain return new StringTokenizer(forwardedFor, ",").nextToken().trim(); diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/RDBMSQuery.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/RDBMSQuery.java index bbd313519236ff30717e2a796b5ed3fe68257e5a..9aa072cae9731cff9947d7385bcbb54500596982 100644 --- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/RDBMSQuery.java +++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/RDBMSQuery.java @@ -15,19 +15,19 @@ import java.util.List; import java.util.Optional; import org.apache.commons.lang3.StringUtils; +import org.eclipsefoundation.http.model.RequestWrapper; +import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; import org.eclipsefoundation.persistence.dto.BareNode; import org.eclipsefoundation.persistence.dto.filter.DtoFilter; import org.eclipsefoundation.persistence.helper.SortableHelper; import org.eclipsefoundation.persistence.helper.SortableHelper.Sortable; import org.eclipsefoundation.persistence.namespace.PersistenceUrlParameterNames; import org.eclipsefoundation.persistence.namespace.SortOrder; -import org.eclipsefoundation.http.model.RequestWrapper; -import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; -import org.jboss.resteasy.specimpl.UnmodifiableMultivaluedMap; +import org.jboss.resteasy.reactive.common.util.UnmodifiableMultivaluedMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -55,7 +55,7 @@ public class RDBMSQuery<T extends BareNode> { public RDBMSQuery(RequestWrapper wrapper, DtoFilter<T> dtoFilter, MultivaluedMap<String, String> params) { this.wrapper = wrapper; this.dtoFilter = dtoFilter; - this.params = new MultivaluedMapImpl<>(); + this.params = new MultivaluedHashMap<>(); wrapper.asMap().forEach((key, valueList) -> this.params.addAll(key, valueList)); if (params != null) { // replace the values set in the param map diff --git a/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/dao/DefaultHibernateDaoTest.java b/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/dao/DefaultHibernateDaoTest.java index 88fb5d7f3e79c0737f3a8a60b8751dbfb625f618..3ce7f02756dc963aff034bbeab99ba0ff982d784 100644 --- a/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/dao/DefaultHibernateDaoTest.java +++ b/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/dao/DefaultHibernateDaoTest.java @@ -16,19 +16,19 @@ import java.util.Arrays; import java.util.List; import java.util.UUID; +import org.eclipsefoundation.http.model.FlatRequestWrapper; +import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; import org.eclipsefoundation.persistence.model.RDBMSQuery; import org.eclipsefoundation.persistence.test.dto.PersonAddress; import org.eclipsefoundation.persistence.test.dto.PersonAddress.PersonAddressFilter; import org.eclipsefoundation.persistence.test.dto.PersonDTO; import org.eclipsefoundation.persistence.test.dto.PersonDTO.PersonDTOFilter; -import org.eclipsefoundation.http.model.FlatRequestWrapper; -import org.eclipsefoundation.http.namespace.DefaultUrlParameterNames; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -176,7 +176,7 @@ class DefaultHibernateDaoTest { */ private RDBMSQuery<PersonDTO> getBasicQuery(String id) { // set up query for existing data - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(DefaultUrlParameterNames.ID.getName(), id); return new RDBMSQuery<>(new FlatRequestWrapper(URI.create("https://eclipse.org")), filter, params); } @@ -189,7 +189,7 @@ class DefaultHibernateDaoTest { */ private RDBMSQuery<PersonAddress> getPersonAddressQuery(String personId) { // set up query for existing data - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add("person_id", personId); return new RDBMSQuery<>(new FlatRequestWrapper(URI.create("https://eclipse.org")), paFilter, params); } diff --git a/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/model/DistributedCSRFGeneratorTest.java b/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/model/DistributedCSRFGeneratorTest.java index c38ca77d550d932e8e6621cc0685e29d70f38481..0d03e244e1a4acdeffdf4614b499704d4a2475a9 100644 --- a/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/model/DistributedCSRFGeneratorTest.java +++ b/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/model/DistributedCSRFGeneratorTest.java @@ -10,17 +10,19 @@ import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.persistence.dao.PersistenceDao; import org.eclipsefoundation.persistence.dto.DistributedCSRFToken; import org.eclipsefoundation.persistence.dto.filter.DistributedCSRFTokenFilter; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; +import org.eclipsefoundation.utils.namespace.AdditionalHttpHeaders; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import io.netty.handler.codec.http.HttpHeaderNames; import io.quarkus.test.junit.QuarkusTest; -import io.undertow.httpcore.HttpHeaderNames; +import io.vertx.core.http.HttpServerRequest; import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.SecurityContext; @QuarkusTest class DistributedCSRFGeneratorTest { @@ -46,21 +48,23 @@ class DistributedCSRFGeneratorTest { RequestWrapper wrap = new FlatRequestWrapper(URI.create(url)); // set up parameters to check for token before and after test - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(DistributedCSRFGenerator.IP_PARAM, ip); params.add(DistributedCSRFGenerator.USERAGENT_PARAM, userAgent); Assertions.assertTrue(dao.get(new RDBMSQuery<>(wrap, filter, params)).isEmpty(), "Expected no tokens for unique useragent in test"); // set up mocked request to forward to the generator - HttpServletRequest mockedRequest = Mockito.mock(HttpServletRequest.class); - Mockito.when(mockedRequest.getRequestURL()).thenReturn(new StringBuffer(url)); - Mockito.when(mockedRequest.getHeader(HttpHeaderNames.X_FORWARDED_FOR)).thenReturn(ip); + HttpServerRequest mockedRequest = Mockito.mock(HttpServerRequest.class); + Mockito.when(mockedRequest.uri()).thenReturn(url); + Mockito.when(mockedRequest.getHeader(AdditionalHttpHeaders.X_FORWARDED_FOR)).thenReturn(ip); Mockito.when(mockedRequest.getHeader(HttpHeaderNames.USER_AGENT)).thenReturn(userAgent); - Mockito.when(mockedRequest.getUserPrincipal()).thenReturn(p); + + SecurityContext mockedSecurity = Mockito.mock(SecurityContext.class); + Mockito.when(mockedSecurity.getUserPrincipal()).thenReturn(p); // generate the token, which should persist the token - String token = this.generator.getCSRFToken(mockedRequest); + String token = this.generator.getCSRFToken(mockedRequest, mockedSecurity); // get the tokens in the database w/ the same params List<DistributedCSRFToken> tokens = dao.get(new RDBMSQuery<>(wrap, filter, params)); // check that we have entries diff --git a/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/model/SqlBackedPaginationResolverTest.java b/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/model/SqlBackedPaginationResolverTest.java index 37114c68e1c0e8cb8f12f4bc01e52f55bbc37ff9..a91d9a6793341ff8f230144ccd0637929ac794d6 100644 --- a/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/model/SqlBackedPaginationResolverTest.java +++ b/persistence/runtime/src/test/java/org/eclipsefoundation/persistence/model/SqlBackedPaginationResolverTest.java @@ -17,13 +17,12 @@ import java.util.UUID; import org.eclipsefoundation.caching.model.ParameterizedCacheKey; import org.eclipsefoundation.core.service.PaginationHeaderService; +import org.eclipsefoundation.http.model.FlatRequestWrapper; +import org.eclipsefoundation.http.response.PaginatedResultsFilter; import org.eclipsefoundation.persistence.dao.PersistenceDao; import org.eclipsefoundation.persistence.test.dto.PersonDTO; import org.eclipsefoundation.persistence.test.dto.PersonDTO.PersonDTOFilter; import org.eclipsefoundation.persistence.test.namespace.PersistenceTestParameterNames; -import org.eclipsefoundation.http.model.FlatRequestWrapper; -import org.eclipsefoundation.http.response.PaginatedResultsFilter; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -32,6 +31,7 @@ import io.quarkus.cache.CacheName; import io.quarkus.cache.CaffeineCache; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** @@ -102,7 +102,7 @@ class SqlBackedPaginationResolverTest { void generateEntry_success_respectsFilters() { // set up the test with a filter FlatRequestWrapper wrap = new FlatRequestWrapper(URI.create("http://eclipse.org/sample/endpoint/" + UUID.randomUUID().toString())); - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(PersistenceTestParameterNames.AT_LEAST_OF_AGE.getName(), "40"); RDBMSQuery<PersonDTO> q = new RDBMSQuery<>(wrap, filter, params); // these fields should equal the headers in the result map @@ -151,7 +151,7 @@ class SqlBackedPaginationResolverTest { Assertions.assertEquals(Integer.toString(limit), results.get(PaginatedResultsFilter.MAX_PAGE_SIZE_HEADER)); // rerun with different params - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(PersistenceTestParameterNames.AT_LEAST_OF_AGE.getName(), "40"); q = new RDBMSQuery<>(wrap, filter, params); long newCount = dao.count(q); @@ -241,7 +241,7 @@ class SqlBackedPaginationResolverTest { // set up context source to be used in fallback scenario. No filters are available initially FlatRequestWrapper primaryWrap = new FlatRequestWrapper( URI.create("http://eclipse.org/sample/endpoint/" + UUID.randomUUID().toString())); - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add(PersistenceTestParameterNames.AT_LEAST_OF_AGE.getName(), "100"); RDBMSQuery<PersonDTO> q = new RDBMSQuery<>(primaryWrap, filter, params); QueryPlayback contextSource = new QueryPlayback(q, dao); diff --git a/pom.xml b/pom.xml index 84ad669e55f1c65b376e29b5e9981620f227312e..1db6d4e5f24f740fadf259fe385f2a9e6fcd5e40 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> <name>Java SDK Commons</name> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> <packaging>pom</packaging> <properties> <compiler-plugin.version>3.11.0</compiler-plugin.version> diff --git a/testing/pom.xml b/testing/pom.xml index 1e552045f9448991064ee24580a2075cf6ef63ff..10c4f54338d4b868fd76de0ac7653b0fccde06fb 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -9,7 +9,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> <relativePath>../pom.xml</relativePath> </parent> <properties> @@ -93,4 +93,4 @@ </plugin> </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/testing/src/test/java/org/eclipsefoundation/testing/models/EndpointTestBuilderTest.java b/testing/src/test/java/org/eclipsefoundation/testing/models/EndpointTestBuilderTest.java index 742f5ff88bd7eff0c8ff67017fd8717d63b3d128..72d567501343f6c3f561a91995eaecac0dc45179 100644 --- a/testing/src/test/java/org/eclipsefoundation/testing/models/EndpointTestBuilderTest.java +++ b/testing/src/test/java/org/eclipsefoundation/testing/models/EndpointTestBuilderTest.java @@ -52,6 +52,7 @@ class EndpointTestBuilderTest { private static final EndpointTestCase GET_NOT_FOUND = TestCaseHelper .prepareTestCase("nope", new String[] {}, SchemaNamespaceHelper.ERROR_SCHEMA_PATH) .setStatusCode(Status.NOT_FOUND.getStatusCode()) + .setHeaderParams(Optional.of(Map.of("Accept", "application/json"))) .setBodyValidationParams(Map.of("status_code", Status.NOT_FOUND.getStatusCode())) .build(); private static final EndpointTestCase GET_BAD_REQUEST = TestCaseHelper diff --git a/utils/pom.xml b/utils/pom.xml index 47b0ce328774e7d9d7163615f16b319ed628d8f9..a141969446f8f6d9558b040f3c1e0b115f352d7a 100644 --- a/utils/pom.xml +++ b/utils/pom.xml @@ -9,7 +9,7 @@ <parent> <groupId>org.eclipsefoundation</groupId> <artifactId>quarkus-commons</artifactId> - <version>1.0.3-SNAPSHOT</version> + <version>1.1.0</version> <relativePath>../pom.xml</relativePath> </parent> <dependencies> @@ -21,11 +21,7 @@ <!-- Required for session scoping + HTTP request classes --> <dependency> <groupId>io.quarkus</groupId> - <artifactId>quarkus-resteasy</artifactId> - </dependency> - <dependency> - <groupId>io.quarkus</groupId> - <artifactId>quarkus-undertow</artifactId> + <artifactId>quarkus-resteasy-reactive</artifactId> </dependency> <!-- Adds logging bindings for more natural logging --> <dependency> @@ -77,4 +73,4 @@ </plugin> </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/utils/src/main/java/org/eclipsefoundation/utils/helper/CSRFHelper.java b/utils/src/main/java/org/eclipsefoundation/utils/helper/CSRFHelper.java index 54ec005748c78e05b2118d68e0d5f56a839bba41..47629291ee6fe2c8d13d66788cd5f89fd64155ae 100644 --- a/utils/src/main/java/org/eclipsefoundation/utils/helper/CSRFHelper.java +++ b/utils/src/main/java/org/eclipsefoundation/utils/helper/CSRFHelper.java @@ -22,10 +22,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import io.quarkus.arc.Unremovable; +import io.vertx.core.http.HttpServerRequest; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import jakarta.inject.Singleton; -import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.core.SecurityContext; /** * Helper class for interacting with CSRF tokens within the server. Generates secure CSRF tokens and compares them to the copy that exists @@ -50,8 +51,8 @@ public final class CSRFHelper { * * @return a cryptographically-secure CSRF token to use in a session. */ - public String getNewCSRFToken(HttpServletRequest httpServletRequest) { - return generator.getCSRFToken(httpServletRequest); + public String getNewCSRFToken(HttpServerRequest request, SecurityContext context) { + return generator.getCSRFToken(request, context); } /** @@ -61,9 +62,9 @@ public final class CSRFHelper { * @param httpServletRequest the request context * @return the CSRF token for the current request. */ - public String getSessionCSRFToken(AdditionalUserData aud, HttpServletRequest httpServletRequest) { + public String getSessionCSRFToken(AdditionalUserData aud, HttpServerRequest request, SecurityContext context) { if (config.get().distributedMode().enabled()) { - return generator.getCSRFToken(httpServletRequest); + return generator.getCSRFToken(request, context); } else { return aud.getCsrf(); } diff --git a/utils/src/main/java/org/eclipsefoundation/utils/helper/ParameterHelper.java b/utils/src/main/java/org/eclipsefoundation/utils/helper/ParameterHelper.java index 2de456eb184b0556546236fa4fa16f9a354ce9e7..aa83f068dfb02bbcebaf19b497ceca76ba2c4d6b 100644 --- a/utils/src/main/java/org/eclipsefoundation/utils/helper/ParameterHelper.java +++ b/utils/src/main/java/org/eclipsefoundation/utils/helper/ParameterHelper.java @@ -17,6 +17,10 @@ import java.util.Map.Entry; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.eclipsefoundation.utils.namespace.UrlParameterNamespace; + +import io.quarkus.arc.Unremovable; +import io.quarkus.runtime.Startup; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; @@ -24,12 +28,6 @@ import jakarta.inject.Inject; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; -import org.eclipsefoundation.utils.namespace.UrlParameterNamespace; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; - -import io.quarkus.arc.Unremovable; -import io.quarkus.runtime.Startup; - /** * Helper for parameter lookups and filtering. * @@ -75,7 +73,7 @@ public class ParameterHelper { // merge the 2 lists and return prevVal.addAll(addVals); return prevVal; - }, MultivaluedMapImpl<String, String>::new)); + }, MultivaluedHashMap<String, String>::new)); } /** diff --git a/utils/src/main/java/org/eclipsefoundation/utils/model/CSRFGenerator.java b/utils/src/main/java/org/eclipsefoundation/utils/model/CSRFGenerator.java index 19cac22b05149594e9e2c422051f985fb663dea9..23eb2d7b3728ee4d2ccfa1fe08f2a6345623f534 100644 --- a/utils/src/main/java/org/eclipsefoundation/utils/model/CSRFGenerator.java +++ b/utils/src/main/java/org/eclipsefoundation/utils/model/CSRFGenerator.java @@ -13,15 +13,15 @@ package org.eclipsefoundation.utils.model; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; import java.util.Optional; import java.util.UUID; -import jakarta.servlet.http.HttpServletRequest; - import org.eclipse.microprofile.config.ConfigProvider; import org.eclipsefoundation.utils.namespace.MicroprofilePropertyNames; -import io.undertow.util.HexConverter; +import io.vertx.core.http.HttpServerRequest; +import jakarta.ws.rs.core.SecurityContext; /** * Interface used to generate random hardened tokens for use in requests. @@ -32,18 +32,17 @@ import io.undertow.util.HexConverter; public interface CSRFGenerator { /** - * Generates a random secure CSRF token to be used in requests. This token should be cryptographically secure and random - * to ensure that it cannot be generated from an external source. + * Generates a random secure CSRF token to be used in requests. This token should be cryptographically secure and random to ensure that + * it cannot be generated from an external source. * * @param requestContext current request context that can be used for fingerprinting the request if required. * @return a random encoded string token */ - public String getCSRFToken(HttpServletRequest httpServletRequest); + public String getCSRFToken(HttpServerRequest request, SecurityContext context); /** - * Default implementation of the CSRF generator, will use secure randoms generated on the fly at runtime to generate - * random seed values as base of token. These values will be hashed and salted to provide extra hardening of the value - * to make it harder to predict. + * Default implementation of the CSRF generator, will use secure randoms generated on the fly at runtime to generate random seed values + * as base of token. These values will be hashed and salted to provide extra hardening of the value to make it harder to predict. * * @author Martin Lowe * @@ -51,7 +50,7 @@ public interface CSRFGenerator { public static class DefaultCSRFGenerator implements CSRFGenerator { @Override - public String getCSRFToken(HttpServletRequest httpServletRequest) { + public String getCSRFToken(HttpServerRequest request, SecurityContext context) { Optional<String> salt = ConfigProvider.getConfig().getOptionalValue(MicroprofilePropertyNames.CSRF_TOKEN_SALT, String.class); // create new digest to hash the result @@ -70,7 +69,7 @@ public interface CSRFGenerator { // hash the results using the message digest byte[] array = md.digest(preHash.getBytes()); // convert back to a hex string to act as a token - return HexConverter.convertToHexString(array); + return HexFormat.of().formatHex(array); } } } diff --git a/utils/src/main/java/org/eclipsefoundation/utils/namespace/AdditionalHttpHeaders.java b/utils/src/main/java/org/eclipsefoundation/utils/namespace/AdditionalHttpHeaders.java new file mode 100644 index 0000000000000000000000000000000000000000..7355c587d6a64572ed5681f6dd431619429bc80e --- /dev/null +++ b/utils/src/main/java/org/eclipsefoundation/utils/namespace/AdditionalHttpHeaders.java @@ -0,0 +1,20 @@ +/********************************************************************* +* Copyright (c) 2024 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +/** + * + */ +package org.eclipsefoundation.utils.namespace; + +/** + * + */ +public class AdditionalHttpHeaders { + public final static String X_FORWARDED_FOR = "X-Forwarded-For"; +} diff --git a/utils/src/main/resources/application.properties b/utils/src/main/resources/application.properties new file mode 100644 index 0000000000000000000000000000000000000000..c664252918b6159df7c1fb4128c557d155e484d3 --- /dev/null +++ b/utils/src/main/resources/application.properties @@ -0,0 +1,2 @@ +## CSRF should be disabled by default +quarkus.csrf-reactive.enabled=false \ No newline at end of file diff --git a/utils/src/test/java/org/eclipsefoundation/utils/authenticated/helper/CSRFHelperTest.java b/utils/src/test/java/org/eclipsefoundation/utils/authenticated/helper/CSRFHelperTest.java index 443b93f69f4cc336c8cfb805aa22420af7905dbb..8a56355069ac501156ff80abdf4fc350a39a5994 100644 --- a/utils/src/test/java/org/eclipsefoundation/utils/authenticated/helper/CSRFHelperTest.java +++ b/utils/src/test/java/org/eclipsefoundation/utils/authenticated/helper/CSRFHelperTest.java @@ -11,9 +11,6 @@ **********************************************************************/ package org.eclipsefoundation.utils.authenticated.helper; -import jakarta.inject.Inject; -import jakarta.servlet.http.HttpServletRequest; - import org.eclipsefoundation.utils.exception.FinalForbiddenException; import org.eclipsefoundation.utils.helper.CSRFHelper; import org.eclipsefoundation.utils.test.AuthenticatedTestProfile; @@ -24,6 +21,9 @@ import org.mockito.Mockito; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; +import io.vertx.core.http.HttpServerRequest; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.SecurityContext; /** * Test CSRF functionality from the helper directly using the authentication secured test profile. @@ -36,18 +36,20 @@ class CSRFHelperTest { @Inject CSRFHelper csrf; - - private static HttpServletRequest mockRequest; + + private static HttpServerRequest mockRequest; + private static SecurityContext mockContext; @BeforeAll public static void setup() { - CSRFHelperTest.mockRequest = Mockito.mock(HttpServletRequest.class); + CSRFHelperTest.mockRequest = Mockito.mock(HttpServerRequest.class); + CSRFHelperTest.mockContext = Mockito.mock(SecurityContext.class); } - + @Test void compareCSRF_validToken() { // generate a token to use in test - String csrfToken = csrf.getNewCSRFToken(mockRequest); + String csrfToken = csrf.getNewCSRFToken(mockRequest, mockContext); // this should not throw as the tokens match Assertions.assertDoesNotThrow(() -> csrf.compareCSRF(csrfToken, csrfToken)); @@ -56,7 +58,7 @@ class CSRFHelperTest { @Test void compareCSRF_invalidToken() { // generate a token to use in test - String csrfToken = csrf.getNewCSRFToken(mockRequest); + String csrfToken = csrf.getNewCSRFToken(mockRequest, mockContext); // this should throw as the tokens are not the same Assertions.assertThrows(FinalForbiddenException.class, () -> csrf.compareCSRF(csrfToken, "some-other-value")); @@ -65,7 +67,7 @@ class CSRFHelperTest { @Test void compareCSRF_noSubmittedToken() { // generate a token to use in test - String csrfToken = csrf.getNewCSRFToken(mockRequest); + String csrfToken = csrf.getNewCSRFToken(mockRequest, mockContext); // this should throw as the tokens are not the same Assertions.assertThrows(FinalForbiddenException.class, () -> csrf.compareCSRF(csrfToken, null)); Assertions.assertThrows(FinalForbiddenException.class, () -> csrf.compareCSRF(csrfToken, "")); diff --git a/utils/src/test/java/org/eclipsefoundation/utils/helper/ParameterHelperTest.java b/utils/src/test/java/org/eclipsefoundation/utils/helper/ParameterHelperTest.java index ae43908e8c517313cefe3f9d3dacd85f1d38a249..3aab3544bb617344f553fef8210bc984bfa9e798 100644 --- a/utils/src/test/java/org/eclipsefoundation/utils/helper/ParameterHelperTest.java +++ b/utils/src/test/java/org/eclipsefoundation/utils/helper/ParameterHelperTest.java @@ -5,17 +5,18 @@ import java.util.Collections; import java.util.List; import org.eclipsefoundation.utils.namespace.UrlParameterNamespace; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.quarkus.test.junit.QuarkusTest; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; /** - * Contains base tests for confirming that parameter helper filters as expected based on registered parameters. + * Contains base tests for confirming that parameter helper filters as expected + * based on registered parameters. */ @QuarkusTest class ParameterHelperTest { @@ -26,7 +27,7 @@ class ParameterHelperTest { @Test void filterUnknownParameters_success_retainsKnownParameters() { // set up params with basic params that are tracked - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add("id", "sample"); MultivaluedMap<String, String> actualParams = helper.filterUnknownParameters(params); @@ -36,17 +37,19 @@ class ParameterHelperTest { @Test void filterUnknownParameters_success_removesUnknownParameters() { // set up params with basic params that are tracked - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); params.add("some-unknown-parameter", "sample"); MultivaluedMap<String, String> actualParams = helper.filterUnknownParameters(params); - Assertions.assertTrue(actualParams.isEmpty(), "Parameter map had an unexpected value when it expected value to be filtered"); + Assertions.assertTrue(actualParams.isEmpty(), + "Parameter map had an unexpected value when it expected value to be filtered"); } @Test void filterUnknownParameters_success_detectsCustomNamespaces() { // set up params with basic params that are tracked - MultivaluedMap<String, String> params = new MultivaluedMapImpl<>(); + MultivaluedMap<String, String> params = new MultivaluedHashMap<>(); + params.add(TestUrlParameterNamespace.SAMPLE_URL_PARAM_NAME, "sample"); MultivaluedMap<String, String> actualParams = helper.filterUnknownParameters(params); @@ -65,7 +68,8 @@ class ParameterHelperTest { @Test void parseQueryString_success() { - MultivaluedMap<String, String> out = ParameterHelper.parseQueryString("sample=1&fair_param=simple&sample=some string"); + MultivaluedMap<String, String> out = ParameterHelper + .parseQueryString("sample=1&fair_param=simple&sample=some string"); // check for multiple values on same key Assertions.assertNotNull(out.get("sample")); Assertions.assertEquals(2, out.get("sample").size());