diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbfebf19b..42b1d2cf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: echo "Running modules: ${MODULES_ARG}" echo "MODULES_MAVEN_PARAM=[\" -pl ${MODULES_ARG} -Dall-modules\"]" >> $GITHUB_OUTPUT else - echo "MODULES_MAVEN_PARAM=[' -P root-modules,spring-modules,http-modules,test-tooling-modules', ' -P security-modules,sql-db-modules,messaging-modules,websockets-modules,monitoring-modules']" >> $GITHUB_OUTPUT + echo "MODULES_MAVEN_PARAM=[' -P root-modules,cache-modules,spring-modules,http-modules,test-tooling-modules', ' -P security-modules,sql-db-modules,messaging-modules,websockets-modules,monitoring-modules']" >> $GITHUB_OUTPUT fi outputs: MODULES_MAVEN_PARAM: ${{ steps.prepare-modules-mvn-param.outputs.MODULES_MAVEN_PARAM }} @@ -73,7 +73,7 @@ jobs: strategy: matrix: java: [ 17 ] - cli: [ 3.2.8.Final ] + cli: [ 3.2.9.Final ] module-mvn-args: ${{ fromJSON(needs.prepare-jvm-latest-modules-mvn-param.outputs.MODULES_MAVEN_PARAM) }} steps: - uses: actions/checkout@v3 @@ -120,7 +120,7 @@ jobs: strategy: matrix: java: [ 11 ] - cli: [ 3.2.8.Final ] + cli: [ 3.2.9.Final ] steps: - uses: actions/checkout@v3 - name: Reclaim Disk Space diff --git a/README.md b/README.md index 878cd1d47..a4278f6bc 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ The following subsections will introduce how to deploy and run the test suite in If you have a look the main `pom.xml` you will notice that there are several profiles or in other words the test suite is a maven monorepo that you can compile and verify at once or by topics. Let's review the main profiles: * root-modules: talk about Quarkus "core stuff" as configuration or properties. Is a basic stuff that should work as a pre-requisite to other modules. +* cache-modules: cover Quarkus application data caching * http-modules: talk about HTTP extensions and no-application endpoints like `/q/health` * security-modules: cover all security stuff like OAuth, JWT, OpenId, Keycloak etc * messaging-modules: is focus on brokers as Kafka or Artemis-AMQP @@ -1084,6 +1085,15 @@ It covers different usages: 3. from a blocking endpoint 4. from a reactive endpoint +### `cache/redis` + +Verifies the `quarkus-redis-cache` extension using `@CacheResult`, `@CacheInvalidate`, `@CacheInvalidateAll` and `@CacheKey`. +It covers different usages: +1. from an application scoped service +2. from a request scoped service + +Also verify that Qute correctly indicate that does not work with remote cache. + ### `cache/spring` Verifies the `quarkus-spring-cache` extension using `@Cacheable`, `@CacheEvict` and `@CachePut`. diff --git a/cache/redis/pom.xml b/cache/redis/pom.xml new file mode 100644 index 000000000..51f1e140e --- /dev/null +++ b/cache/redis/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + io.quarkus.ts.qe + parent + 1.0.0-SNAPSHOT + ../.. + + cache-redis + jar + Quarkus QE TS: Cache: Redis + + + io.quarkus + quarkus-cache + + + io.quarkus + quarkus-redis-cache + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-qute + + Added dependency to check https://github.com/quarkusio/quarkus/issues/35680 <--> + + io.quarkus + quarkus-mailer + + + diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java new file mode 100644 index 000000000..baf9fbebb --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.caffeine; + +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class ApplicationScopeService extends BaseServiceWithCache { +} diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java new file mode 100644 index 000000000..c93a2e55d --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java @@ -0,0 +1,38 @@ +package io.quarkus.ts.cache.caffeine; + +import io.quarkus.cache.CacheInvalidate; +import io.quarkus.cache.CacheInvalidateAll; +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; + +public abstract class BaseServiceWithCache { + + private static final String CACHE_NAME = "service-cache"; + + private static int counter = 0; + + @CacheResult(cacheName = CACHE_NAME) + public String getValue() { + return "Value: " + counter++; + } + + @CacheInvalidate(cacheName = CACHE_NAME) + public void invalidate() { + // do nothing + } + + @CacheResult(cacheName = CACHE_NAME) + public String getValueWithPrefix(@CacheKey String prefix) { + return prefix + ": " + counter++; + } + + @CacheInvalidate(cacheName = CACHE_NAME) + public void invalidateWithPrefix(@CacheKey String prefix) { + // do nothing + } + + @CacheInvalidateAll(cacheName = CACHE_NAME) + public void invalidateAll() { + // do nothing + } +} diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java new file mode 100644 index 000000000..8e12392b5 --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.caffeine; + +import jakarta.enterprise.context.RequestScoped; + +@RequestScoped +public class RequestScopeService extends BaseServiceWithCache { +} diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java new file mode 100644 index 000000000..e71870276 --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java @@ -0,0 +1,65 @@ +package io.quarkus.ts.cache.caffeine; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +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.MediaType; + +@Path("/services") +public class ServiceWithCacheResource { + + public static final String APPLICATION_SCOPE_SERVICE_PATH = "application-scope"; + public static final String REQUEST_SCOPE_SERVICE_PATH = "request-scope"; + + @Inject + ApplicationScopeService applicationScopeService; + + @Inject + RequestScopeService requestScopeService; + + @GET + @Path("/{service}") + @Produces(MediaType.TEXT_PLAIN) + public String getValueFromService(@PathParam("service") String service) { + return lookupServiceByPathParam(service).getValue(); + } + + @POST + @Path("/{service}/invalidate-cache") + public void invalidateCacheFromService(@PathParam("service") String service) { + lookupServiceByPathParam(service).invalidate(); + } + + @POST + @Path("/{service}/invalidate-cache-all") + public void invalidateCacheAllFromService(@PathParam("service") String service) { + lookupServiceByPathParam(service).invalidateAll(); + } + + @GET + @Path("/{service}/using-prefix/{prefix}") + @Produces(MediaType.TEXT_PLAIN) + public String getValueUsingPrefixFromService(@PathParam("service") String service, @PathParam("prefix") String prefix) { + return lookupServiceByPathParam(service).getValueWithPrefix(prefix); + } + + @POST + @Path("/{service}/using-prefix/{prefix}/invalidate-cache") + public void invalidateCacheUsingPrefixFromService(@PathParam("service") String service, + @PathParam("prefix") String prefix) { + lookupServiceByPathParam(service).invalidateWithPrefix(prefix); + } + + private BaseServiceWithCache lookupServiceByPathParam(String service) { + if (APPLICATION_SCOPE_SERVICE_PATH.equals(service)) { + return applicationScopeService; + } else if (REQUEST_SCOPE_SERVICE_PATH.equals(service)) { + return requestScopeService; + } + + throw new IllegalArgumentException("Service " + service + " is not recognised"); + } +} diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java new file mode 100644 index 000000000..0e2bb2a76 --- /dev/null +++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java @@ -0,0 +1,31 @@ +package io.quarkus.ts.cache.caffeine; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.qute.Template; + +@Path("template") +public class TemplateCacheResource { + + @Inject + Template cached; + + /** + * Check for remote cache with Qute template. Qute should not use remote cache. + * See https://github.com/quarkusio/quarkus/issues/35680#issuecomment-1711153725 + * + * @return Should return error contains `not supported for remote caches` + */ + @GET + @Path("error") + public String getQuteTemplate() { + try { + return cached.render(); + } catch (IllegalStateException e) { + return e.getMessage(); + } + } + +} diff --git a/cache/redis/src/main/resources/application.properties b/cache/redis/src/main/resources/application.properties new file mode 100644 index 000000000..e69de29bb diff --git a/cache/redis/src/main/resources/templates/cached.html b/cache/redis/src/main/resources/templates/cached.html new file mode 100644 index 000000000..55584772d --- /dev/null +++ b/cache/redis/src/main/resources/templates/cached.html @@ -0,0 +1 @@ +{#cached}This cached template won't be working with remote cache like redis.{/cached} diff --git a/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java new file mode 100644 index 000000000..e96cf029d --- /dev/null +++ b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java @@ -0,0 +1,7 @@ +package io.quarkus.ts.cache.redis; + +import io.quarkus.test.scenarios.OpenShiftScenario; + +@OpenShiftScenario +public class OpenShiftRedisCacheIT extends RedisCacheIT { +} diff --git a/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java new file mode 100644 index 000000000..c5ef69b48 --- /dev/null +++ b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java @@ -0,0 +1,187 @@ +package io.quarkus.ts.cache.redis; + +import static io.quarkus.ts.cache.caffeine.ServiceWithCacheResource.APPLICATION_SCOPE_SERVICE_PATH; +import static io.quarkus.ts.cache.caffeine.ServiceWithCacheResource.REQUEST_SCOPE_SERVICE_PATH; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.test.bootstrap.DefaultService; +import io.quarkus.test.bootstrap.RestService; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.Container; +import io.quarkus.test.services.QuarkusApplication; + +@QuarkusScenario +public class RedisCacheIT { + + private static final String SERVICE_APPLICATION_SCOPE_PATH = "/services/" + APPLICATION_SCOPE_SERVICE_PATH; + private static final String SERVICE_REQUEST_SCOPE_PATH = "/services/" + REQUEST_SCOPE_SERVICE_PATH; + + private static final String PREFIX_ONE = "prefix1"; + private static final String PREFIX_TWO = "prefix2"; + + private static final int REDIS_PORT = 6379; + + @Container(image = "${redis.image}", port = REDIS_PORT, expectedLog = "Ready to accept connections") + static DefaultService redis = new DefaultService().withProperty("ALLOW_EMPTY_PASSWORD", "YES"); + + @QuarkusApplication + static RestService app = new RestService() + .withProperty("quarkus.redis.hosts", + () -> { + String redisHost = redis.getURI().withScheme("redis").getRestAssuredStyleUri(); + return String.format("%s:%d", redisHost, redis.getURI().getPort()); + }); + + /** + * Check whether the `@CacheResult` annotation works when used in a service. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetTheSameValueAlwaysWhenGettingValueFromPath(String path) { + // We call the service endpoint + String value = getFromPath(path); + + // At this point, the cache is populated and we should get the same value from the cache + assertEquals(value, getFromPath(path), "Value was different which means cache is not working"); + } + + /** + * Check whether the `@CacheInvalidate` annotation invalidates the cache when used in a service. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetDifferentValueWhenInvalidateCacheFromPath(String path) { + // We call the service endpoint + String value = getFromPath(path); + + // invalidate the cache + invalidateCacheFromPath(path); + + // Then the value should be different as we have invalidated the cache. + assertNotEquals(value, getFromPath(path), "Value was equal which means cache invalidate didn't work"); + } + + /** + * Check whether the `@CacheResult` annotation works when used in a service. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetTheSameValueForSamePrefixesWhenGettingValueFromPath(String path) { + // We call the service endpoint + String value = getValueFromPathUsingPrefix(path, PREFIX_ONE); + + // At this point, the cache is populated and we should get the same value from the cache + assertEquals(value, getValueFromPathUsingPrefix(path, PREFIX_ONE), + "Value was different which means cache is not working"); + // But different value using another prefix + assertNotEquals(value, getValueFromPathUsingPrefix(path, PREFIX_TWO), + "Value was equal which means @CacheKey didn't work"); + } + + /** + * Check whether the `@CacheInvalidate` annotation does not invalidate all the caches + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetTheSameValuesEvenAfterCallingToCacheInvalidateFromPath(String path) { + // We call the service endpoints + String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE); + String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO); + + // invalidate the cache: this should not invalidate all the keys + invalidateCacheFromPath(path); + + // At this point, the cache is populated and we should get the same value for both prefixes + assertEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE)); + assertEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO)); + } + + /** + * Check whether the `@CacheInvalidate` and `@CacheKey` annotations work as expected. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetDifferentValueWhenInvalidateCacheOnlyForOnePrefixFromPath(String path) { + // We call the service endpoints + String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE); + String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO); + + // invalidate the cache: this should not invalidate all the keys + invalidateCacheWithPrefixFromPath(path, PREFIX_ONE); + + // The cache was invalidated only for prefix1, so the value should be different + assertNotEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE)); + // The cache was not invalidated for prefix2, so the value should be the same + assertEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO)); + } + + /** + * Check whether the `@CacheInvalidateAll` annotation works as expected. + */ + @ParameterizedTest + @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH }) + public void shouldGetDifferentValueWhenInvalidateAllTheCacheFromPath(String path) { + // We call the service endpoints + String value = getFromPath(path); + String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE); + String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO); + + // invalidate all the cache + invalidateCacheAllFromPath(path); + + // Then, all the values should be different: + assertNotEquals(value, getFromPath(path)); + assertNotEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE)); + assertNotEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO)); + } + + /** + * Check if the usage of Qute and redis throw expected error + */ + @Test + @Tag("QUARKUS-3715") + public void quteShouldThrowError() { + assertThat(getFromPath("/template/error"), containsString("not supported for remote caches")); + } + + private void invalidateCacheAllFromPath(String path) { + postFromPath(path + "/invalidate-cache-all"); + } + + private void invalidateCacheWithPrefixFromPath(String path, String prefix) { + postFromPath(path + "/using-prefix/" + prefix + "/invalidate-cache"); + } + + private void invalidateCacheFromPath(String path) { + postFromPath(path + "/invalidate-cache"); + } + + private String getValueFromPathUsingPrefix(String path, String prefix) { + return getFromPath(path + "/using-prefix/" + prefix); + } + + private String getFromPath(String path) { + return app.given() + .when().get(path) + .then() + .statusCode(HttpStatus.SC_OK) + .extract().asString(); + } + + private void postFromPath(String path) { + app.given() + .when().post(path) + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + } + +} diff --git a/cache/redis/src/test/resources/test.properties b/cache/redis/src/test/resources/test.properties new file mode 100644 index 000000000..a72083e30 --- /dev/null +++ b/cache/redis/src/test/resources/test.properties @@ -0,0 +1 @@ +ts.redis.openshift.use-internal-service-as-url=true diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CookiesResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CookiesResource.java index 5d3d03644..b9a17e53c 100644 --- a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CookiesResource.java +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CookiesResource.java @@ -1,5 +1,7 @@ package io.quarkus.ts.http.advanced.reactive; +import java.util.Map; + import jakarta.ws.rs.Consumes; import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.FormParam; @@ -56,6 +58,19 @@ public Response getSameSiteAttributeFromFormParam(@FormParam(TEST_COOKIE) String return responseBuilder.cookie(newCookie).build(); } + @GET + @Path("newcookie-serialization") + public Map getRequestCookies(HttpHeaders httpHeaders) { + NewCookie cookie = new NewCookie.Builder(TEST_COOKIE).value("test-cookie-value").build(); + return Map.of(cookie.getName(), cookie); + } + + @GET + @Path("cookie-serialization") + public Map test(HttpHeaders httpHeaders) { + return httpHeaders.getCookies(); + } + public static String toRawCookie(String sameSite) { if (sameSite == null || sameSite.isEmpty()) { return String.format("%s=\"test-cookie-value\";Version=\"1\";", TEST_COOKIE); diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java new file mode 100644 index 000000000..28440613a --- /dev/null +++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java @@ -0,0 +1,123 @@ +package io.quarkus.ts.http.advanced.reactive; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import jakarta.ws.rs.ConstrainedTo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NameBinding; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.ext.WriterInterceptor; +import jakarta.ws.rs.ext.WriterInterceptorContext; + +@Path("/intercepted") +public class InterceptedResource { + + /** + * Interceptors write their message to this list, when they are invoked + * It is a bit dumb way, but it is the easier to get indicators if interceptors were invoked to the client + */ + public static List interceptorMessages = new ArrayList<>(); + + @WithWriterInterceptor + @GET + public InterceptedString getInterceptedString() { + return new InterceptedString("foo"); + } + + @GET() + @Path("/messages") + @Produces(MediaType.TEXT_PLAIN) + public String getMessages() { + StringBuilder outputMessage = new StringBuilder(); + for (String string : interceptorMessages) { + outputMessage.append(string); + } + return outputMessage.toString(); + } + + public static class InterceptedString { + public String name; + + public InterceptedString(String name) { + this.name = name; + } + } + + /** + * This annotation binds the providers to only intercept the method in this class. + * Otherwise, they would be global and intercept all endpoints across the entire application. + */ + @NameBinding + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface WithWriterInterceptor { + + } + + @Provider + public static class InterceptedStringHandler implements MessageBodyWriter { + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == InterceptedString.class; + } + + @Override + public void writeTo(InterceptedString interceptedString, Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + entityStream.write((interceptedString.name).getBytes(StandardCharsets.UTF_8)); + } + } + + @Provider + @WithWriterInterceptor + public static class UnconstrainedWriterInterceptor implements WriterInterceptor { + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Unconstrained interceptor "); + context.proceed(); + } + } + + @Provider + @ConstrainedTo(RuntimeType.CLIENT) + @WithWriterInterceptor + public static class ClientWriterInterceptor implements WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Client interceptor "); + context.proceed(); + } + } + + @Provider + @ConstrainedTo(RuntimeType.SERVER) + @WithWriterInterceptor + public static class ServerWriterInterceptor implements WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Server interceptor "); + context.proceed(); + } + } +} diff --git a/http/http-advanced-reactive/src/main/resources/application.properties b/http/http-advanced-reactive/src/main/resources/application.properties index bc40751d2..87a9528cc 100644 --- a/http/http-advanced-reactive/src/main/resources/application.properties +++ b/http/http-advanced-reactive/src/main/resources/application.properties @@ -60,6 +60,8 @@ quarkus.keycloak.policy-enforcer.paths.grpc.path=/api/grpc/* quarkus.keycloak.policy-enforcer.paths.grpc.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.client.path=/api/client/* quarkus.keycloak.policy-enforcer.paths.client.enforcement-mode=DISABLED +quarkus.keycloak.policy-enforcer.paths.intercepted.path=/api/intercepted* +quarkus.keycloak.policy-enforcer.paths.intercepted.enforcement-mode=DISABLED quarkus.oidc.client-id=test-application-client quarkus.oidc.credentials.secret=test-application-client-secret # tolerate 1 minute of clock skew between the Keycloak server and the application diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java index 5b01aa253..2452f8585 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java @@ -427,6 +427,24 @@ public void constraintsExist() throws JsonProcessingException { Assertions.assertEquals("^[A-Za-z]+$", validation.get("pattern").asText()); } + @Test + @Tag("https://github.com/quarkusio/quarkus/pull/36664") + public void interceptedTest() { + // make server to generate a response so interceptors might intercept it + // ignore response, we will read interceptors result later + getApp().given() + .get(ROOT_PATH + "/intercepted") + .thenReturn(); + + String response = getApp().given() + .get(ROOT_PATH + "/intercepted/messages") + .thenReturn().getBody().asString(); + + Assertions.assertTrue(response.contains("Unconstrained"), "Unconstrained interceptor should be invoked"); + Assertions.assertTrue(response.contains("Server"), "Server interceptor should be invoked"); + Assertions.assertFalse(response.contains("Client"), "Client interceptor should not be invoked"); + } + private void assertAcceptedMediaTypeEqualsResponseBody(String acceptedMediaType) { getApp() .given() diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/CookiesIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/CookiesIT.java index fadc9ea1c..de0094489 100644 --- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/CookiesIT.java +++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/CookiesIT.java @@ -1,11 +1,12 @@ package io.quarkus.ts.http.advanced.reactive; import static io.quarkus.ts.http.advanced.reactive.CookiesResource.TEST_COOKIE; -import static io.restassured.RestAssured.get; import static io.restassured.RestAssured.given; import static io.restassured.matcher.RestAssuredMatchers.detailedCookie; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import io.quarkus.test.bootstrap.RestService; @@ -52,6 +53,28 @@ void testSameSiteAttributeAddedByVertxHttpExt() { .cookie("vertx", detailedCookie().sameSite(sameSite).secured(true)); } + @Test + @Tag("QUARKUS-3736") + void testNewCookiesSerialization() { + given() + .get("/cookie/newcookie-serialization") + .then() + .statusCode(200) + .body(containsString(String.format("\"name\":\"%s\"", TEST_COOKIE)), + containsString("\"value\":\"test-cookie-value\"")); + } + + @Test + @Tag("QUARKUS-3736") + void testCookiesSerialization() { + given().cookie(String.format("%s=\"test-cookie-value\";", TEST_COOKIE)) + .get("/cookie/cookie-serialization") + .then() + .statusCode(200) + .body(containsString(String.format("\"name\":\"%s\"", TEST_COOKIE)), + containsString("\"value\":\"test-cookie-value\"")); + } + private static void assertSameSiteAttribute(String sameSite) { ValidatableResponse response; diff --git a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java new file mode 100644 index 000000000..008e04408 --- /dev/null +++ b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java @@ -0,0 +1,123 @@ +package io.quarkus.ts.http.advanced; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import jakarta.ws.rs.ConstrainedTo; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.NameBinding; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.RuntimeType; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; +import jakarta.ws.rs.ext.Provider; +import jakarta.ws.rs.ext.WriterInterceptor; +import jakarta.ws.rs.ext.WriterInterceptorContext; + +@Path("/intercepted") +public class InterceptedResource { + + /** + * Interceptors write their message to this list, when they are invoked + * It is a bit dumb way, but it is the easier to get indicators if interceptors were invoked to the client + */ + public static List interceptorMessages = new ArrayList<>(); + + @WithWriterInterceptor + @GET + public InterceptedString getInterceptedString() { + return new InterceptedString("foo"); + } + + @GET() + @Path("/messages") + @Produces(MediaType.TEXT_PLAIN) + public String getMessages() { + StringBuilder outputMessage = new StringBuilder(); + for (String string : interceptorMessages) { + outputMessage.append(string); + } + return outputMessage.toString(); + } + + public static class InterceptedString { + public String name; + + public InterceptedString(String name) { + this.name = name; + } + } + + /** + * This annotation binds the providers to only intercept the method in this class. + * Otherwise, they would be global and intercept all endpoints across the entire application. + */ + @NameBinding + @Target({ ElementType.METHOD, ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + public @interface WithWriterInterceptor { + + } + + @Provider + public static class InterceptedStringHandler implements MessageBodyWriter { + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return type == InterceptedString.class; + } + + @Override + public void writeTo(InterceptedString interceptedString, Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + entityStream.write((interceptedString.name).getBytes(StandardCharsets.UTF_8)); + } + } + + @Provider + @WithWriterInterceptor + public static class UnconstrainedWriterInterceptor implements WriterInterceptor { + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Unconstrained interceptor "); + context.proceed(); + } + } + + @Provider + @ConstrainedTo(RuntimeType.CLIENT) + @WithWriterInterceptor + public static class ClientWriterInterceptor implements WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Client interceptor "); + context.proceed(); + } + } + + @Provider + @ConstrainedTo(RuntimeType.SERVER) + @WithWriterInterceptor + public static class ServerWriterInterceptor implements WriterInterceptor { + + @Override + public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException { + InterceptedResource.interceptorMessages.add("Server interceptor "); + context.proceed(); + } + } +} diff --git a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/SseResource.java b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/SseResource.java new file mode 100644 index 000000000..79b327a45 --- /dev/null +++ b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/SseResource.java @@ -0,0 +1,77 @@ +package io.quarkus.ts.http.advanced; + +import java.util.Arrays; +import java.util.concurrent.locks.LockSupport; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.sse.OutboundSseEvent; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseEventSink; +import jakarta.ws.rs.sse.SseEventSource; + +import org.eclipse.microprofile.config.ConfigProvider; + +@Path("/sse") +public class SseResource { + @Context + Sse sse; + + @GET + @Path("/client") + public String sseClient() { + try { + return consumeSse(); + } + // in case that https://github.com/quarkusio/quarkus/issues/36402 throws java.lang.RuntimeException: java.lang.ClassNotFoundException: + // catch it and return the error message + catch (RuntimeException exception) { + return exception.getMessage(); + } + } + + private String consumeSse() { + StringBuilder response = new StringBuilder(); + int port = ConfigProvider.getConfig().getValue("quarkus.http.port", Integer.class); + + /* + * Client connects to itself (to server endpoint running on same app), + * because for https://github.com/quarkusio/quarkus/issues/36402 to reproduce client must run on native app. + * Which cannot be done in test code itself. + * This method acts just as a client + */ + WebTarget target = ClientBuilder.newClient().target("http://localhost:" + port + "/api/sse/server"); + SseEventSource updateSource = SseEventSource.target(target).build(); + updateSource.register(ev -> { + response.append("event: ").append(ev.getName()).append(" ").append(ev.readData()); + response.append("\n"); + + }, thr -> { + response.append("Error in SSE, message: ").append(thr.getMessage()).append("\n"); + response.append(Arrays.toString(thr.getStackTrace())); + }); + updateSource.open(); + + LockSupport.parkNanos(1_000_000_000L); + return response.toString(); + } + + @GET + @Path("/server") + @Produces(MediaType.SERVER_SENT_EVENTS) + public void sendSseEvents(@Context SseEventSink eventSink) { + eventSink.send(createEvent("test234", "test")); + } + + private OutboundSseEvent createEvent(String name, String data) { + return sse.newEventBuilder() + .name(name) + .data(data) + .build(); + } +} diff --git a/http/http-advanced/src/main/resources/application.properties b/http/http-advanced/src/main/resources/application.properties index 904607fef..da1dfa3e3 100644 --- a/http/http-advanced/src/main/resources/application.properties +++ b/http/http-advanced/src/main/resources/application.properties @@ -45,12 +45,16 @@ quarkus.keycloak.policy-enforcer.paths.version.enforcement-mode=DISABLED # Application endpoints quarkus.keycloak.policy-enforcer.paths.hello.path=/api/hello/* quarkus.keycloak.policy-enforcer.paths.hello.enforcement-mode=DISABLED +quarkus.keycloak.policy-enforcer.paths.sse.path=/api/sse/* +quarkus.keycloak.policy-enforcer.paths.sse.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.details.path=/api/details/* quarkus.keycloak.policy-enforcer.paths.details.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.grpc.path=/api/grpc/* quarkus.keycloak.policy-enforcer.paths.grpc.enforcement-mode=DISABLED quarkus.keycloak.policy-enforcer.paths.client.path=/api/client/* quarkus.keycloak.policy-enforcer.paths.client.enforcement-mode=DISABLED +quarkus.keycloak.policy-enforcer.paths.intercepted.path=/api/intercepted* +quarkus.keycloak.policy-enforcer.paths.intercepted.enforcement-mode=DISABLED quarkus.oidc.client-id=test-application-client quarkus.oidc.credentials.secret=test-application-client-secret # tolerate 1 minute of clock skew between the Keycloak server and the application diff --git a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java index 0f943a88d..d28e33aef 100644 --- a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java +++ b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java @@ -8,6 +8,8 @@ import static org.hamcrest.Matchers.in; import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.net.URISyntaxException; @@ -60,6 +62,7 @@ public abstract class BaseHttpAdvancedIT { private static final String PASSWORD = "password"; private static final String KEY_STORE_PATH = "META-INF/resources/server.keystore"; private static final int ASSERT_TIMEOUT_SECONDS = 10; + private static final String SSE_ERROR_MESSAGE = "java.lang.ClassNotFoundException: Provider for jakarta.ws.rs.sse.SseEventSource.Builder cannot be found"; protected abstract RestService getApp(); @@ -242,6 +245,34 @@ public void keepRequestScopeValuesAfterEventPropagation() { "Unexpected requestScope custom context value"); } + @Test + @Tag("https://github.com/quarkusio/quarkus/issues/36402") + public void sseConnectionTest() { + String response = getApp().given().get("/api/sse/client").thenReturn().body().asString(); + + assertFalse(response.contains(SSE_ERROR_MESSAGE), + "SSE failed, https://github.com/quarkusio/quarkus/issues/36402 not fixed"); + assertTrue(response.contains("event: test234 test"), "SSE failed, unknown bug. Response: " + response); + } + + @Test + @Tag("https://github.com/quarkusio/quarkus/pull/36664") + public void interceptedTest() { + // make server to generate a response so interceptors might intercept it + // ignore response, we will read interceptors result later + getApp().given() + .get(ROOT_PATH + "/intercepted") + .thenReturn(); + + String response = getApp().given() + .get(ROOT_PATH + "/intercepted/messages") + .thenReturn().getBody().asString(); + + Assertions.assertTrue(response.contains("Unconstrained"), "Unconstrained interceptor should be invoked"); + Assertions.assertTrue(response.contains("Server"), "Server interceptor should be invoked"); + Assertions.assertFalse(response.contains("Client"), "Client interceptor should not be invoked"); + } + protected Protocol getProtocol() { return Protocol.HTTPS; } diff --git a/nosql-db/mongodb-reactive/src/test/java/io/quarkus/ts/nosqldb/mongodb/reactive/OpenShiftMongoClientReactiveIT.java b/nosql-db/mongodb-reactive/src/test/java/io/quarkus/ts/nosqldb/mongodb/reactive/OpenShiftMongoClientReactiveIT.java index c56027d24..0c0d7ec4a 100644 --- a/nosql-db/mongodb-reactive/src/test/java/io/quarkus/ts/nosqldb/mongodb/reactive/OpenShiftMongoClientReactiveIT.java +++ b/nosql-db/mongodb-reactive/src/test/java/io/quarkus/ts/nosqldb/mongodb/reactive/OpenShiftMongoClientReactiveIT.java @@ -9,7 +9,6 @@ import io.quarkus.test.services.QuarkusApplication; @OpenShiftScenario -@DisabledIfSystemProperty(named = "ts.arm.missing.services.excludes", matches = "true", disabledReason = "https://github.com/quarkus-qe/quarkus-test-suite/issues/1146") @DisabledIfSystemProperty(named = "ts.s390x.missing.services.excludes", matches = "true", disabledReason = "bitnami/mongodb container not available on s390x.") public class OpenShiftMongoClientReactiveIT extends AbstractMongoClientReactiveIT { diff --git a/nosql-db/mongodb-reactive/src/test/resources/mongo-openshift-deployment-template.yml b/nosql-db/mongodb-reactive/src/test/resources/mongo-openshift-deployment-template.yml new file mode 100644 index 000000000..88a1e7f0b --- /dev/null +++ b/nosql-db/mongodb-reactive/src/test/resources/mongo-openshift-deployment-template.yml @@ -0,0 +1,46 @@ +--- +apiVersion: "v1" +kind: "List" +items: +- apiVersion: "v1" + kind: "Service" + metadata: + name: "${SERVICE_NAME}" + spec: + ports: + - name: "http" + port: ${INTERNAL_PORT} + targetPort: ${INTERNAL_PORT} + selector: + deploymentconfig: "${SERVICE_NAME}" + type: "ClusterIP" +- apiVersion: "apps.openshift.io/v1" + kind: "DeploymentConfig" + metadata: + name: "${SERVICE_NAME}" + spec: + replicas: 1 + selector: + deploymentconfig: "${SERVICE_NAME}" + template: + metadata: + labels: + deploymentconfig: "${SERVICE_NAME}" + spec: + volumes: + - name: mongo-db-data-volume + emptyDir: { } + containers: + - image: "${IMAGE}" + args: [${ARGS}] + imagePullPolicy: "IfNotPresent" + name: "${SERVICE_NAME}" + ports: + - containerPort: ${INTERNAL_PORT} + name: "http" + protocol: "TCP" + volumeMounts: + - name: mongo-db-data-volume + mountPath: /data/db + triggers: + - type: "ConfigChange" diff --git a/nosql-db/mongodb-reactive/src/test/resources/test.properties b/nosql-db/mongodb-reactive/src/test/resources/test.properties index 0b033fb40..e11c97950 100644 --- a/nosql-db/mongodb-reactive/src/test/resources/test.properties +++ b/nosql-db/mongodb-reactive/src/test/resources/test.properties @@ -1,3 +1,5 @@ ts.app.log.enable=true ts.database.log.enable=true ts.database.openshift.use-internal-service-as-url=true +# store MongoDB data in 'emptyDir' volume to work-around write permission issue +ts.database.openshift.template=/mongo-openshift-deployment-template.yml diff --git a/nosql-db/mongodb/src/test/java/io/quarkus/ts/nosqldb/mongodb/OpenShiftMongoClientIT.java b/nosql-db/mongodb/src/test/java/io/quarkus/ts/nosqldb/mongodb/OpenShiftMongoClientIT.java index b05bcb4ba..6b2dbaf8a 100644 --- a/nosql-db/mongodb/src/test/java/io/quarkus/ts/nosqldb/mongodb/OpenShiftMongoClientIT.java +++ b/nosql-db/mongodb/src/test/java/io/quarkus/ts/nosqldb/mongodb/OpenShiftMongoClientIT.java @@ -5,7 +5,6 @@ import io.quarkus.test.scenarios.OpenShiftScenario; @OpenShiftScenario -@DisabledIfSystemProperty(named = "ts.arm.missing.services.excludes", matches = "true", disabledReason = "https://github.com/quarkus-qe/quarkus-test-suite/issues/1146") @DisabledIfSystemProperty(named = "ts.s390x.missing.services.excludes", matches = "true", disabledReason = "bitnami/mongodb container not available on s390x.") public class OpenShiftMongoClientIT extends MongoClientIT { } diff --git a/nosql-db/mongodb/src/test/resources/mongo-openshift-deployment-template.yml b/nosql-db/mongodb/src/test/resources/mongo-openshift-deployment-template.yml new file mode 100644 index 000000000..88a1e7f0b --- /dev/null +++ b/nosql-db/mongodb/src/test/resources/mongo-openshift-deployment-template.yml @@ -0,0 +1,46 @@ +--- +apiVersion: "v1" +kind: "List" +items: +- apiVersion: "v1" + kind: "Service" + metadata: + name: "${SERVICE_NAME}" + spec: + ports: + - name: "http" + port: ${INTERNAL_PORT} + targetPort: ${INTERNAL_PORT} + selector: + deploymentconfig: "${SERVICE_NAME}" + type: "ClusterIP" +- apiVersion: "apps.openshift.io/v1" + kind: "DeploymentConfig" + metadata: + name: "${SERVICE_NAME}" + spec: + replicas: 1 + selector: + deploymentconfig: "${SERVICE_NAME}" + template: + metadata: + labels: + deploymentconfig: "${SERVICE_NAME}" + spec: + volumes: + - name: mongo-db-data-volume + emptyDir: { } + containers: + - image: "${IMAGE}" + args: [${ARGS}] + imagePullPolicy: "IfNotPresent" + name: "${SERVICE_NAME}" + ports: + - containerPort: ${INTERNAL_PORT} + name: "http" + protocol: "TCP" + volumeMounts: + - name: mongo-db-data-volume + mountPath: /data/db + triggers: + - type: "ConfigChange" diff --git a/nosql-db/mongodb/src/test/resources/test.properties b/nosql-db/mongodb/src/test/resources/test.properties index 0b033fb40..e11c97950 100644 --- a/nosql-db/mongodb/src/test/resources/test.properties +++ b/nosql-db/mongodb/src/test/resources/test.properties @@ -1,3 +1,5 @@ ts.app.log.enable=true ts.database.log.enable=true ts.database.openshift.use-internal-service-as-url=true +# store MongoDB data in 'emptyDir' volume to work-around write permission issue +ts.database.openshift.template=/mongo-openshift-deployment-template.yml diff --git a/pom.xml b/pom.xml index 8ccae1483..e9f5d68cd 100644 --- a/pom.xml +++ b/pom.xml @@ -18,8 +18,8 @@ 3.1.0 quarkus-bom io.quarkus - 3.2.8.Final - 3.2.8.Final + 3.2.9.Final + 3.2.9.Final 1.3.0.Beta28 2.4.0 4.5.14 @@ -256,8 +256,8 @@ docker.io/gvenzl/oracle-free:23-slim-faststart quay.io/quarkusqeteam/db2:11.5.7.0 - docker.io/bitnami/mongodb:5.0 - docker.io/library/redis:6.0 + docker.io/library/mongo:5.0 + docker.io/library/redis:7.2 quay.io/ocpmetal/wiremock docker.io/bitnami/consul:1.15.2 @@ -403,14 +403,12 @@ lifecycle-application external-applications scheduling/quartz - infinispan-client super-size/many-extensions quarkus-cli logging/jboss - cache/caffeine qute/multimodule qute/synchronous qute/reactive @@ -432,17 +430,30 @@ lifecycle-application external-applications scheduling/quartz - infinispan-client super-size/many-extensions quarkus-cli logging/jboss - cache/caffeine build-time-analytics + + cache-modules + + true + + all-modules + + + + env-info + cache/caffeine + cache/redis + infinispan-client + + http-modules @@ -787,6 +798,8 @@ registry.redhat.io/rhel8/mariadb-105 registry.redhat.io/amq7/amq-streams-kafka-27-rhel7 1.7.0 + + docker.io/library/mongo@sha256:6f851e31a1b317c6fa681b7dad5f94c622f1c3588477f3b769579dc5462ee659 diff --git a/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/AbstractSecurityCallbackTest.java b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/AbstractSecurityCallbackTest.java new file mode 100644 index 000000000..5ed4274b3 --- /dev/null +++ b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/AbstractSecurityCallbackTest.java @@ -0,0 +1,102 @@ +package io.quarkus.ts.openshift.security.basic.callback; + +import java.security.Permission; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import jakarta.enterprise.inject.spi.CDI; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; + +import io.quarkus.security.credential.Credential; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.test.scenarios.annotations.DisabledOnNative; +import io.quarkus.test.security.TestIdentityAssociation; +import io.smallrye.mutiny.Uni; + +@DisabledOnNative +@Tag("https://github.com/quarkusio/quarkus/issues/36601") +/* + * This set of tests are testing QuarkusSecurityTestExtension callbacks @BeforeEach and @AfterEach + * These should not interfere with CDI in when tests are not annotated with @TestSecurity + * Way to measure if these callbacks are doing something is by testing TestIdentity in + * CDI.current().select(TestIdentityAssociation.class) + * This should be set to value in BeforeEach and then set to null in AfterEach in case, @TestSecurity is present + * and not touched otherwise. + * To properly test handling in these methods, this test package uses a testClass for every testCase, + * to isolate calls of *Each methods + */ +abstract public class AbstractSecurityCallbackTest { + protected static final SecurityIdentity MOCK_SECURITY_IDENTITY = new SecurityIdentity() { + @Override + public Principal getPrincipal() { + return new Principal() { + @Override + public String getName() { + return ""; + } + }; + } + + @Override + public boolean isAnonymous() { + return true; + } + + @Override + public Set getRoles() { + return Collections.emptySet(); + } + + @Override + public boolean hasRole(String role) { + return false; + } + + @Override + public T getCredential(Class credentialType) { + return null; + } + + @Override + public Set getCredentials() { + return Collections.emptySet(); + } + + @Override + public T getAttribute(String name) { + return null; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + + @Override + public Uni checkPermission(Permission permission) { + return Uni.createFrom().item(false); + } + }; + + protected static void assertTestIdentityIsNull() { + Assertions.assertNull(CDI.current().select(TestIdentityAssociation.class).get().getTestIdentity(), + "TestIdentity should be null"); + } + + protected static void assertTestIdentityIsNotNull() { + Assertions.assertNotNull(CDI.current().select(TestIdentityAssociation.class).get().getTestIdentity(), + "TestIdentity should have value"); + } + + protected static void setTestIdentityToValue() { + CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(MOCK_SECURITY_IDENTITY); + } + + protected static void setTestIdentityToNull() { + CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(null); + } +} diff --git a/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityDisabledTest.java b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityDisabledTest.java new file mode 100644 index 000000000..d04dba2b3 --- /dev/null +++ b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityDisabledTest.java @@ -0,0 +1,49 @@ +package io.quarkus.ts.openshift.security.basic.callback; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class TestSecurityDisabledTest extends AbstractSecurityCallbackTest { + + @BeforeAll + public static void checkBeforeAll() { + // this is called before QuarkusSecurityTestExtension.beforeEach, so testIdentity should still be null + assertTestIdentityIsNull(); + } + + @BeforeEach + public void checkBeforeEach() { + // this is called after QuarkusSecurityTestExtension.beforeEach, with no @TestSecurity, this should be still null + assertTestIdentityIsNull(); + } + + @Test + public void checkTest() { + // QuarkusSecurityTestExtension.beforeEach should not set test identity + assertTestIdentityIsNull(); + + // set testIdentity, so we can later check, if it is set to null or not + setTestIdentityToValue(); + } + + @AfterEach + public void checkAfterEach() { + // testIdentity was set in test, it should be still set + assertTestIdentityIsNotNull(); + } + + @AfterAll + public static void checkAfterAll() { + // testIdentity was set in test, it should be still set + assertTestIdentityIsNotNull(); + + // reset testIdentity to null, so it won't break other tests + setTestIdentityToNull(); + } +} diff --git a/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityEnabledTest.java b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityEnabledTest.java new file mode 100644 index 000000000..61e969915 --- /dev/null +++ b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityEnabledTest.java @@ -0,0 +1,37 @@ +package io.quarkus.ts.openshift.security.basic.callback; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.security.TestSecurity; + +@QuarkusTest +public class TestSecurityEnabledTest extends AbstractSecurityCallbackTest { + @BeforeAll + public static void checkBeforeAll() { + // this is called before QuarkusSecurityTestExtension.beforeEach, so testIdentity should still be null + assertTestIdentityIsNull(); + } + + @BeforeEach + public void checkBeforeEach() { + // this is called after QuarkusSecurityTestExtension.beforeEach, so testIdentity should be set + assertTestIdentityIsNotNull(); + } + + @Test + @TestSecurity(user = "myUser") + public void checkTestItself() { + // QuarkusSecurityTestExtension.beforeEach should set test identity + assertTestIdentityIsNotNull(); + } + + @AfterAll + public static void checkAfterAll() { + // this is called after QuarkusSecurityTestExtension.afterEach, so testIdentity should be set to null again + assertTestIdentityIsNull(); + } +} diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/clients/TokenPropagationPongClient.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/clients/TokenPropagationPongClient.java index 37e6576b7..9e734e6fb 100644 --- a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/clients/TokenPropagationPongClient.java +++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/clients/TokenPropagationPongClient.java @@ -10,13 +10,14 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import io.quarkus.oidc.token.propagation.AccessToken; import io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.model.Score; +import io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.filters.DefaultTokenRequestFilter; @RegisterRestClient -@AccessToken +@RegisterProvider(DefaultTokenRequestFilter.class) @Path("/rest-pong") public interface TokenPropagationPongClient { diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/CustomTokenRequestFilter.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/CustomTokenRequestFilter.java new file mode 100644 index 000000000..5334997ca --- /dev/null +++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/CustomTokenRequestFilter.java @@ -0,0 +1,15 @@ +package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.filters; + +import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter; + +public class CustomTokenRequestFilter extends AccessTokenRequestFilter { + @Override + protected String getClientName() { + return "exchange-token"; + } + + @Override + protected boolean isExchangeToken() { + return true; + } +} diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/DefaultTokenRequestFilter.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/DefaultTokenRequestFilter.java new file mode 100644 index 000000000..d3ad622a8 --- /dev/null +++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/DefaultTokenRequestFilter.java @@ -0,0 +1,14 @@ +package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.filters; + +import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter; + +/** + * This class is required for + * {@link io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.TokenPropagationPongClient} + * It would not be required normally, but having {@link CustomTokenRequestFilter} causes AmbiguousResolutionException when + * getting a default filter. + * So this class is necessary to have unambiguous filter for TokenPropagatingPongClient. + * TODO: remove once issue is solved https://github.com/quarkusio/quarkus/issues/36994 + */ +public class DefaultTokenRequestFilter extends AccessTokenRequestFilter { +} diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/FilteredTokenResource.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/FilteredTokenResource.java new file mode 100644 index 000000000..b867bf85c --- /dev/null +++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/FilteredTokenResource.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal.clients.TokenPropagationFilteredClient; + +@Path("/token-propagation-filter") +public class FilteredTokenResource { + + @Inject + @RestClient + TokenPropagationFilteredClient tokenPropagationFilterClient; + + @GET + public String getUserName() { + return tokenPropagationFilterClient.getUserName(); + } +} diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/PrincipalResource.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/PrincipalResource.java new file mode 100644 index 000000000..196147e7d --- /dev/null +++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/PrincipalResource.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal; + +import java.security.Principal; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.security.Authenticated; + +@Path("/principal") +@Authenticated +public class PrincipalResource { + + @Inject + Principal principal; + + @GET + public String principalName() { + return principal.getName(); + } +} diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/clients/TokenPropagationFilteredClient.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/clients/TokenPropagationFilteredClient.java new file mode 100644 index 000000000..e95cecd55 --- /dev/null +++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/clients/TokenPropagationFilteredClient.java @@ -0,0 +1,20 @@ +package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal.clients; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.filters.CustomTokenRequestFilter; + +@RegisterRestClient +@RegisterClientHeaders +@Path("/principal") +@RegisterProvider(CustomTokenRequestFilter.class) +public interface TokenPropagationFilteredClient { + + @GET + String getUserName(); +} diff --git a/security/keycloak-oidc-client-extended/src/main/resources/application.properties b/security/keycloak-oidc-client-extended/src/main/resources/application.properties index cffc8f631..08a07d7d9 100644 --- a/security/keycloak-oidc-client-extended/src/main/resources/application.properties +++ b/security/keycloak-oidc-client-extended/src/main/resources/application.properties @@ -24,6 +24,13 @@ quarkus.oidc-client.test-user.grant.type=password quarkus.oidc-client.test-user.grant-options.password.username=test-user quarkus.oidc-client.test-user.grant-options.password.password=test-user + +## Exchange token client +quarkus.oidc-client.exchange-token.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.exchange-token.client-id=test-application-client +quarkus.oidc-client.exchange-token.credentials.secret=test-application-client-secret +quarkus.oidc-client.exchange-token.grant.type=exchange + # RestClient io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.PongClient/mp-rest/url=http://localhost:${quarkus.http.port} io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.PongClient/mp-rest/scope=jakarta.inject.Singleton @@ -38,5 +45,7 @@ io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.Auto io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.TokenPropagationPongClient/mp-rest/url=http://localhost:${quarkus.http.port} +io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal.clients.TokenPropagationFilteredClient/mp-rest/url=http://localhost:${quarkus.http.port} + #OpenAPI quarkus.smallrye-openapi.store-schema-directory=target/generated/jakarta-rest/ diff --git a/security/keycloak-oidc-client-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/TokenPropagationFilterIT.java b/security/keycloak-oidc-client-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/TokenPropagationFilterIT.java new file mode 100644 index 000000000..d3a751d7a --- /dev/null +++ b/security/keycloak-oidc-client-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/TokenPropagationFilterIT.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.scenarios.QuarkusScenario; + +@QuarkusScenario +public class TokenPropagationFilterIT extends BaseOidcIT { + + @Test + public void usernameTest() { + given() + .auth().oauth2(createToken()) + .when().get("/token-propagation-filter") + .then().statusCode(HttpStatus.SC_OK) + .body(containsString(USER)); + } +} diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/clients/TokenPropagationPongClient.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/clients/TokenPropagationPongClient.java index 7856d0bd0..aac3cad1f 100644 --- a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/clients/TokenPropagationPongClient.java +++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/clients/TokenPropagationPongClient.java @@ -13,11 +13,11 @@ import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; -import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter; import io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.model.Score; +import io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.filters.DefaultTokenRequestFilter; @RegisterRestClient -@RegisterProvider(AccessTokenRequestReactiveFilter.class) +@RegisterProvider(DefaultTokenRequestFilter.class) @Path("/rest-pong") public interface TokenPropagationPongClient { diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/CustomTokenRequestFilter.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/CustomTokenRequestFilter.java new file mode 100644 index 000000000..10386027b --- /dev/null +++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/CustomTokenRequestFilter.java @@ -0,0 +1,15 @@ +package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.filters; + +import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter; + +public class CustomTokenRequestFilter extends AccessTokenRequestReactiveFilter { + @Override + protected String getClientName() { + return "exchange-token"; + } + + @Override + protected boolean isExchangeToken() { + return true; + } +} diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/DefaultTokenRequestFilter.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/DefaultTokenRequestFilter.java new file mode 100644 index 000000000..b12d38b62 --- /dev/null +++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/DefaultTokenRequestFilter.java @@ -0,0 +1,9 @@ +package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.filters; + +import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter; + +/** + * TODO: remove once issue is solved https://github.com/quarkusio/quarkus/issues/36994 + */ +public class DefaultTokenRequestFilter extends AccessTokenRequestReactiveFilter { +} diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/FilteredTokenResource.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/FilteredTokenResource.java new file mode 100644 index 000000000..ea1d9526d --- /dev/null +++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/FilteredTokenResource.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal.clients.TokenPropagationFilteredClient; + +@Path("/token-propagation-filter") +public class FilteredTokenResource { + + @Inject + @RestClient + TokenPropagationFilteredClient tokenPropagationFilterClient; + + @GET + public String getUserName() { + return tokenPropagationFilterClient.getUserName(); + } +} diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/PrincipalResource.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/PrincipalResource.java new file mode 100644 index 000000000..0734a9676 --- /dev/null +++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/PrincipalResource.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal; + +import java.security.Principal; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import io.quarkus.security.Authenticated; + +@Path("/principal") +@Authenticated +public class PrincipalResource { + + @Inject + Principal principal; + + @GET + public String principalName() { + return principal.getName(); + } +} diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/clients/TokenPropagationFilteredClient.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/clients/TokenPropagationFilteredClient.java new file mode 100644 index 000000000..e7e1ddc4d --- /dev/null +++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/clients/TokenPropagationFilteredClient.java @@ -0,0 +1,20 @@ +package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal.clients; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.filters.CustomTokenRequestFilter; + +@RegisterRestClient +@RegisterClientHeaders +@Path("/principal") +@RegisterProvider(CustomTokenRequestFilter.class) +public interface TokenPropagationFilteredClient { + + @GET + String getUserName(); +} diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/resources/application.properties b/security/keycloak-oidc-client-reactive-extended/src/main/resources/application.properties index 9066bef98..42fc152a0 100644 --- a/security/keycloak-oidc-client-reactive-extended/src/main/resources/application.properties +++ b/security/keycloak-oidc-client-reactive-extended/src/main/resources/application.properties @@ -24,6 +24,12 @@ quarkus.oidc-client.test-user.grant.type=password quarkus.oidc-client.test-user.grant-options.password.username=test-user quarkus.oidc-client.test-user.grant-options.password.password=test-user +## Exchange token client +quarkus.oidc-client.exchange-token.auth-server-url=${quarkus.oidc.auth-server-url} +quarkus.oidc-client.exchange-token.client-id=test-application-client +quarkus.oidc-client.exchange-token.credentials.secret=test-application-client-secret +quarkus.oidc-client.exchange-token.grant.type=exchange + # RestClient io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.clients.PongClient/mp-rest/url=http://localhost:${quarkus.http.port} io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.clients.PongClient/mp-rest/scope=jakarta.inject.Singleton @@ -38,5 +44,7 @@ io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.clients.AutoAc io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.clients.TokenPropagationPongClient/mp-rest/url=http://localhost:${quarkus.http.port} +io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal.clients.TokenPropagationFilteredClient/mp-rest/url=http://localhost:${quarkus.http.port} + #OpenAPI quarkus.smallrye-openapi.store-schema-directory=target/generated/jakarta-rest/ diff --git a/security/keycloak-oidc-client-reactive-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/TokenPropagationFilterIT.java b/security/keycloak-oidc-client-reactive-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/TokenPropagationFilterIT.java new file mode 100644 index 000000000..6c7a2b980 --- /dev/null +++ b/security/keycloak-oidc-client-reactive-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/TokenPropagationFilterIT.java @@ -0,0 +1,22 @@ +package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.containsString; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.scenarios.QuarkusScenario; + +@QuarkusScenario +public class TokenPropagationFilterIT extends BaseOidcIT { + + @Test + public void usernameTest() { + given() + .auth().oauth2(createToken()) + .when().get("/token-propagation-filter") + .then().statusCode(HttpStatus.SC_OK) + .body(containsString(USER)); + } +}