diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml
index 3dd605930da93..1cb948d149067 100644
--- a/.mvn/extensions.xml
+++ b/.mvn/extensions.xml
@@ -2,11 +2,11 @@
com.gradle
gradle-enterprise-maven-extension
- 1.18.1
+ 1.20
com.gradle
common-custom-user-data-maven-extension
- 1.12.2
+ 1.12.5
diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 0f12a2f243a89..1ee4b1581c351 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -103,7 +103,7 @@
6.4.2.Final
1.14.7
6.0.6.Final
- 2.2.1.Final
+ 2.2.2.Final
8.0.1.Final
7.0.0.Final
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingWriter.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingWriter.java
index f535eb9411946..b68be67a9f5bf 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingWriter.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/configuration/tracker/ConfigTrackingWriter.java
@@ -12,6 +12,7 @@
import java.util.Map;
import java.util.regex.Pattern;
+import io.quarkus.bootstrap.util.PropertyUtils;
import io.quarkus.deployment.configuration.BuildTimeConfigurationReader;
import io.quarkus.runtime.LaunchMode;
@@ -107,108 +108,6 @@ public static void write(Map readOptions, ConfigTrackingConfig c
* @throws IOException in case of a failure
*/
public static void write(Writer writer, String name, String value) throws IOException {
- if (value != null) {
- name = toWritableValue(name, true, true);
- value = toWritableValue(value, false, true);
- writer.write(name);
- writer.write("=");
- writer.write(value);
- writer.write(System.lineSeparator());
- }
- }
-
- /*
- * Converts unicodes to encoded \uxxxx and escapes
- * special characters with a preceding slash
- */
-
- /**
- * Escapes characters that are expected to be escaped when {@link java.util.Properties} load
- * files from disk.
- *
- * @param str property name or value
- * @param escapeSpace whether to escape a whitespace (should be true for property names)
- * @param escapeUnicode whether to converts unicodes to encoded \uxxxx
- * @return property name or value that can be written to a file
- */
- private static String toWritableValue(String str, boolean escapeSpace, boolean escapeUnicode) {
- int len = str.length();
- int bufLen = len * 2;
- if (bufLen < 0) {
- bufLen = Integer.MAX_VALUE;
- }
- StringBuilder outBuffer = new StringBuilder(bufLen);
-
- for (int x = 0; x < len; x++) {
- char aChar = str.charAt(x);
- // Handle common case first, selecting largest block that
- // avoids the specials below
- if ((aChar > 61) && (aChar < 127)) {
- if (aChar == '\\') {
- outBuffer.append('\\');
- outBuffer.append('\\');
- continue;
- }
- outBuffer.append(aChar);
- continue;
- }
- switch (aChar) {
- case ' ':
- if (x == 0 || escapeSpace) {
- outBuffer.append('\\');
- }
- outBuffer.append(' ');
- break;
- case '\t':
- outBuffer.append('\\');
- outBuffer.append('t');
- break;
- case '\n':
- outBuffer.append('\\');
- outBuffer.append('n');
- break;
- case '\r':
- outBuffer.append('\\');
- outBuffer.append('r');
- break;
- case '\f':
- outBuffer.append('\\');
- outBuffer.append('f');
- break;
- case '=': // Fall through
- case ':': // Fall through
- case '#': // Fall through
- case '!':
- outBuffer.append('\\');
- outBuffer.append(aChar);
- break;
- default:
- if (((aChar < 0x0020) || (aChar > 0x007e)) & escapeUnicode) {
- outBuffer.append('\\');
- outBuffer.append('u');
- outBuffer.append(toHex((aChar >> 12) & 0xF));
- outBuffer.append(toHex((aChar >> 8) & 0xF));
- outBuffer.append(toHex((aChar >> 4) & 0xF));
- outBuffer.append(toHex(aChar & 0xF));
- } else {
- outBuffer.append(aChar);
- }
- }
- }
- return outBuffer.toString();
- }
-
- /**
- * Convert a nibble to a hex character
- *
- * @param nibble the nibble to convert.
- */
- private static char toHex(int nibble) {
- return hexDigit[(nibble & 0xF)];
+ PropertyUtils.store(writer, name, value);
}
-
- /** A table of hex digits */
- private static final char[] hexDigit = {
- '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
- };
}
diff --git a/core/processor/pom.xml b/core/processor/pom.xml
index 924a0ec371883..8427a552eb841 100644
--- a/core/processor/pom.xml
+++ b/core/processor/pom.xml
@@ -32,6 +32,16 @@
com.fasterxml.jackson.core
jackson-databind
+
+ io.quarkus
+ quarkus-bootstrap-app-model
+
+
+ *
+ *
+
+
+
diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java
index 665fa57ad594c..3ac39611cfff3 100644
--- a/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java
+++ b/core/processor/src/main/java/io/quarkus/annotation/processor/ExtensionAnnotationProcessor.java
@@ -76,6 +76,7 @@
import io.quarkus.annotation.processor.generate_doc.ConfigDocItemScanner;
import io.quarkus.annotation.processor.generate_doc.ConfigDocWriter;
import io.quarkus.annotation.processor.generate_doc.DocGeneratorUtil;
+import io.quarkus.bootstrap.util.PropertyUtils;
public class ExtensionAnnotationProcessor extends AbstractProcessor {
@@ -239,23 +240,23 @@ public FileVisitResult postVisitDirectory(final Path dir, final IOException exc)
}
}
- try {
-
- final FileObject listResource = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
- "META-INF/quarkus-javadoc.properties");
- try (OutputStream os = listResource.openOutputStream()) {
- try (BufferedOutputStream bos = new BufferedOutputStream(os)) {
- try (OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) {
- try (BufferedWriter bw = new BufferedWriter(osw)) {
- javaDocProperties.store(bw, Constants.EMPTY);
+ if (!javaDocProperties.isEmpty()) {
+ try {
+ final FileObject listResource = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
+ "META-INF/quarkus-javadoc.properties");
+ try (OutputStream os = listResource.openOutputStream()) {
+ try (BufferedOutputStream bos = new BufferedOutputStream(os)) {
+ try (OutputStreamWriter osw = new OutputStreamWriter(bos, StandardCharsets.UTF_8)) {
+ try (BufferedWriter bw = new BufferedWriter(osw)) {
+ PropertyUtils.store(javaDocProperties, bw);
+ }
}
}
}
+ } catch (IOException e) {
+ processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write javadoc properties: " + e);
+ return;
}
-
- } catch (IOException e) {
- processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write javadoc properties: " + e);
- return;
}
try {
diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc
index e24923671bd91..61eba1e3bb8fb 100644
--- a/docs/src/main/asciidoc/getting-started-testing.adoc
+++ b/docs/src/main/asciidoc/getting-started-testing.adoc
@@ -640,14 +640,14 @@ public class Profiles {
public static class SingleTag implements QuarkusTestProfile {
@Override
public Set tags() {
- return Collections.singleton("test1");
+ return List.of("test1");
}
}
public static class MultipleTags implements QuarkusTestProfile {
@Override
public Set tags() {
- return new HashSet<>(Arrays.asList("test1", "test2"));
+ return Set.of("test1", "test2");
}
}
}
diff --git a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java
index 740b255164904..dd17a2b487338 100644
--- a/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java
+++ b/extensions/agroal/deployment/src/main/java/io/quarkus/agroal/deployment/AgroalProcessor.java
@@ -58,6 +58,7 @@
import io.quarkus.deployment.builditem.RemovedResourceBuildItem;
import io.quarkus.deployment.builditem.SslNativeConfigBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBundleBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
import io.quarkus.maven.dependency.ArtifactKey;
@@ -408,4 +409,16 @@ void adaptOpenTelemetryJdbcInstrumentationForNative(BuildProducer resourceBundleProducer,
+ BuildProducer nativeResourceProducer,
+ BuildProducer reflectiveClassProducer) {
+ resourceBundleProducer.produce(new NativeImageResourceBundleBuildItem("com.sun.rowset.RowSetResourceBundle"));
+ nativeResourceProducer.produce(new NativeImageResourceBuildItem("javax/sql/rowset/rowset.properties"));
+ reflectiveClassProducer.produce(ReflectiveClassBuildItem.builder(
+ "com.sun.rowset.providers.RIOptimisticProvider",
+ "com.sun.rowset.providers.RIXMLProvider").build());
+ }
}
diff --git a/extensions/csrf-reactive/deployment/pom.xml b/extensions/csrf-reactive/deployment/pom.xml
index a237a9890e57c..4e23e6777192c 100644
--- a/extensions/csrf-reactive/deployment/pom.xml
+++ b/extensions/csrf-reactive/deployment/pom.xml
@@ -38,6 +38,16 @@
io.quarkus
quarkus-vertx-http-deployment
+
+ io.quarkus
+ quarkus-junit5-internal
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
diff --git a/extensions/csrf-reactive/deployment/src/test/java/io/quarkus/csrf/reactive/CsrfTest.java b/extensions/csrf-reactive/deployment/src/test/java/io/quarkus/csrf/reactive/CsrfTest.java
new file mode 100644
index 0000000000000..4f527f65d5e1d
--- /dev/null
+++ b/extensions/csrf-reactive/deployment/src/test/java/io/quarkus/csrf/reactive/CsrfTest.java
@@ -0,0 +1,203 @@
+package io.quarkus.csrf.reactive;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.qute.Template;
+import io.quarkus.qute.TemplateInstance;
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import io.restassured.config.EncoderConfig;
+import io.restassured.config.RestAssuredConfig;
+import io.restassured.http.ContentType;
+import io.smallrye.mutiny.Uni;
+
+public class CsrfTest {
+
+ @RegisterExtension
+ static final QuarkusUnitTest config = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(TestResource.class)
+ .addAsResource("templates/csrfToken.html"));
+
+ private final static String COOKIE_NAME = "csrf-token";
+ private final static String HEADER_NAME = "X-CSRF-TOKEN";
+
+ @Test
+ public void testForm() {
+ String token = when()
+ .get("/csrfTokenForm")
+ .then()
+ .statusCode(200)
+ .cookie(COOKIE_NAME)
+ .extract()
+ .cookie(COOKIE_NAME);
+ EncoderConfig encoderConfig = EncoderConfig.encoderConfig().encodeContentTypeAs("multipart/form-data",
+ ContentType.TEXT);
+ RestAssuredConfig restAssuredConfig = RestAssured.config().encoderConfig(encoderConfig);
+
+ //no token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .config(restAssuredConfig)
+ .formParam("name", "testName")
+ .contentType(ContentType.URLENC)
+ .when()
+ .post("csrfTokenForm")
+ .then()
+ .statusCode(400);
+
+ //wrong token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .config(restAssuredConfig)
+ .formParam(COOKIE_NAME, "WRONG")
+ .formParam("name", "testName")
+ .contentType(ContentType.URLENC)
+ .when()
+ .post("csrfTokenForm")
+ .then()
+ .statusCode(400);
+
+ //valid token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .config(restAssuredConfig)
+ .formParam(COOKIE_NAME, token)
+ .formParam("name", "testName")
+ .contentType(ContentType.URLENC)
+ .when()
+ .post("csrfTokenForm")
+ .then()
+ .statusCode(200)
+ .body(Matchers.equalTo("testName"));
+ }
+
+ @Test
+ public void testNoBody() {
+ String token = when().get("/csrfTokenForm")
+ .then().statusCode(200).cookie(COOKIE_NAME)
+ .extract().cookie(COOKIE_NAME);
+
+ // no token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .when()
+ .post("csrfTokenPost")
+ .then()
+ .statusCode(400);
+
+ //wrong token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .header(HEADER_NAME, "WRONG")
+ .when()
+ .post("csrfTokenPost")
+ .then()
+ .statusCode(400);
+
+ //valid token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .header(HEADER_NAME, token)
+ .when()
+ .post("csrfTokenPost")
+ .then()
+ .statusCode(200)
+ .body(Matchers.equalTo("no user"));
+ }
+
+ @Test
+ public void testWithBody() {
+ String token = when()
+ .get("/csrfTokenForm")
+ .then()
+ .statusCode(200)
+ .cookie(COOKIE_NAME)
+ .extract()
+ .cookie(COOKIE_NAME);
+
+ // no token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .body("testName")
+ .contentType(ContentType.TEXT)
+ .when()
+ .post("csrfTokenPostBody")
+ .then()
+ .statusCode(400);
+
+ //wrong token
+ given()
+ .cookie(COOKIE_NAME, token)
+ .header(HEADER_NAME, "WRONG")
+ .body("testName")
+ .contentType(ContentType.TEXT)
+ .when()
+ .post("csrfTokenPostBody")
+ .then()
+ .statusCode(400);
+
+ //valid token => This test fails but should work
+ given()
+ .cookie(COOKIE_NAME, token)
+ .header(HEADER_NAME, token)
+ .body("testName")
+ .contentType(ContentType.TEXT)
+ .when()
+ .post("csrfTokenPostBody")
+ .then()
+ .statusCode(200)
+ .body(Matchers.equalTo("testName"));
+ }
+
+ @Path("")
+ public static class TestResource {
+
+ @Inject
+ Template csrfToken;
+
+ @GET
+ @Path("/csrfTokenForm")
+ @Produces(MediaType.TEXT_HTML)
+ public TemplateInstance getCsrfTokenForm() {
+ return csrfToken.instance();
+ }
+
+ @POST
+ @Path("/csrfTokenForm")
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ @Produces(MediaType.TEXT_PLAIN)
+ public Uni postCsrfTokenForm(@FormParam("name") String userName) {
+ return Uni.createFrom().item(userName);
+ }
+
+ @POST
+ @Path("/csrfTokenPost")
+ @Produces(MediaType.TEXT_PLAIN)
+ public Uni postJson() {
+ return Uni.createFrom().item("no user");
+ }
+
+ @POST
+ @Path("/csrfTokenPostBody")
+ @Consumes(MediaType.TEXT_PLAIN)
+ @Produces(MediaType.TEXT_PLAIN)
+ public Uni postJson(String body) {
+ return Uni.createFrom().item(body);
+ }
+ }
+}
diff --git a/extensions/csrf-reactive/deployment/src/test/resources/templates/csrfToken.html b/extensions/csrf-reactive/deployment/src/test/resources/templates/csrfToken.html
new file mode 100644
index 0000000000000..d9363624cbc40
--- /dev/null
+++ b/extensions/csrf-reactive/deployment/src/test/resources/templates/csrfToken.html
@@ -0,0 +1,16 @@
+
+
+
+
+ User Name Input
+
+
+User Name Input
+
+
+
+
\ No newline at end of file
diff --git a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java
index 556029fcbb48c..82d9091402c77 100644
--- a/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java
+++ b/extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/TenantConfigContext.java
@@ -9,6 +9,7 @@
import org.jboss.logging.Logger;
import io.quarkus.oidc.OIDCException;
+import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.common.runtime.OidcCommonUtils;
import io.quarkus.runtime.configuration.ConfigurationException;
@@ -158,6 +159,10 @@ public OidcTenantConfig getOidcTenantConfig() {
return oidcConfig;
}
+ public OidcConfigurationMetadata getOidcMetadata() {
+ return provider != null ? provider.getMetadata() : null;
+ }
+
public SecretKey getStateEncryptionKey() {
return stateSecretKey;
}
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java
index 5959d9e09c199..94b3a0993718a 100644
--- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java
+++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/RestPathAnnotationProcessor.java
@@ -182,7 +182,7 @@ static Optional searchPathAnnotationOnInterfaces(CombinedInd
* @param resultAcc accumulator for tail-recursion
* @return Collection of all interfaces und their parents. Never null.
*/
- private static Collection getAllClassInterfaces(
+ static Collection getAllClassInterfaces(
CombinedIndexBuildItem index,
Collection classInfos,
List resultAcc) {
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java
index df5f2f1a1ff3e..e525ee2caddfd 100644
--- a/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java
+++ b/extensions/resteasy-classic/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java
@@ -1,18 +1,21 @@
package io.quarkus.resteasy.deployment;
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
+import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.getAllClassInterfaces;
import static io.quarkus.resteasy.deployment.RestPathAnnotationProcessor.isRestEndpointMethod;
import static io.quarkus.security.spi.SecurityTransformerUtils.hasSecurityAnnotation;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
import java.util.Objects;
-import java.util.Optional;
+import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
+import org.jboss.logging.Logger;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.deployment.Capabilities;
@@ -34,6 +37,7 @@
import io.quarkus.resteasy.runtime.JaxRsSecurityConfig;
import io.quarkus.resteasy.runtime.NotFoundExceptionMapper;
import io.quarkus.resteasy.runtime.SecurityContextFilter;
+import io.quarkus.resteasy.runtime.StandardSecurityCheckInterceptor;
import io.quarkus.resteasy.runtime.UnauthorizedExceptionMapper;
import io.quarkus.resteasy.runtime.vertx.JsonArrayReader;
import io.quarkus.resteasy.runtime.vertx.JsonArrayWriter;
@@ -41,7 +45,6 @@
import io.quarkus.resteasy.runtime.vertx.JsonObjectWriter;
import io.quarkus.resteasy.server.common.deployment.ResteasyDeploymentBuildItem;
import io.quarkus.security.spi.AdditionalSecuredMethodsBuildItem;
-import io.quarkus.vertx.http.deployment.EagerSecurityInterceptorBuildItem;
import io.quarkus.vertx.http.deployment.HttpRootPathBuildItem;
import io.quarkus.vertx.http.deployment.devmode.NotFoundPageDisplayableEndpointBuildItem;
import io.quarkus.vertx.http.deployment.devmode.RouteDescriptionBuildItem;
@@ -51,6 +54,7 @@
public class ResteasyBuiltinsProcessor {
protected static final String META_INF_RESOURCES = "META-INF/resources";
+ private static final Logger LOG = Logger.getLogger(ResteasyBuiltinsProcessor.class);
@BuildStep
void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
@@ -66,10 +70,42 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
ClassInfo classInfo = index.getIndex().getClassByName(DotName.createSimple(className));
if (classInfo == null)
throw new IllegalStateException("Unable to find class info for " + className);
- if (!hasSecurityAnnotation(classInfo)) {
- for (MethodInfo methodInfo : classInfo.methods()) {
- if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
- methods.add(methodInfo);
+ // add unannotated class endpoints as well as parent class unannotated endpoints
+ addAllUnannotatedEndpoints(index, classInfo, methods);
+
+ // interface endpoints implemented on resources are already in, now we need to resolve default interface
+ // methods as there, CDI interceptors won't work, therefore neither will our additional secured methods
+ Collection interfaces = getAllClassInterfaces(index, List.of(classInfo), new ArrayList<>());
+ if (!interfaces.isEmpty()) {
+ final List interfaceEndpoints = new ArrayList<>();
+ for (ClassInfo anInterface : interfaces) {
+ addUnannotatedEndpoints(index, anInterface, interfaceEndpoints);
+ }
+ // look for implementors as implementors on resource classes are secured by CDI interceptors
+ if (!interfaceEndpoints.isEmpty()) {
+ interfaceBlock: for (MethodInfo interfaceEndpoint : interfaceEndpoints) {
+ if (interfaceEndpoint.isDefault()) {
+ for (MethodInfo endpoint : methods) {
+ boolean nameParamsMatch = endpoint.name().equals(interfaceEndpoint.name())
+ && (interfaceEndpoint.parameterTypes().equals(endpoint.parameterTypes()));
+ if (nameParamsMatch) {
+ // whether matched method is declared on class that implements interface endpoint
+ Predicate isEndpointInterface = interfaceEndpoint.declaringClass()
+ .name()::equals;
+ if (endpoint.declaringClass().interfaceNames().stream().anyMatch(isEndpointInterface)) {
+ continue interfaceBlock;
+ }
+ }
+ }
+ String configProperty = config.denyJaxRs ? "quarkus.security.jaxrs.deny-unannotated-endpoints"
+ : "quarkus.security.jaxrs.default-roles-allowed";
+ // this is logging only as I'm a bit worried about false positives and breaking things
+ // for what is very much edge case
+ LOG.warn("Default interface method '" + interfaceEndpoint
+ + "' cannot be secured with the '" + configProperty
+ + "' configuration property. Please implement this method for CDI "
+ + "interceptor binding to work");
+ }
}
}
}
@@ -86,13 +122,33 @@ void setUpDenyAllJaxRs(CombinedIndexBuildItem index,
}
}
+ private static void addAllUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo,
+ List methods) {
+ if (classInfo == null) {
+ return;
+ }
+ addUnannotatedEndpoints(index, classInfo, methods);
+ if (classInfo.superClassType() != null && !classInfo.superClassType().name().equals(DotName.OBJECT_NAME)) {
+ addAllUnannotatedEndpoints(index, index.getIndex().getClassByName(classInfo.superClassType().name()), methods);
+ }
+ }
+
+ private static void addUnannotatedEndpoints(CombinedIndexBuildItem index, ClassInfo classInfo, List methods) {
+ if (!hasSecurityAnnotation(classInfo)) {
+ for (MethodInfo methodInfo : classInfo.methods()) {
+ if (isRestEndpointMethod(index, methodInfo) && !hasSecurityAnnotation(methodInfo)) {
+ methods.add(methodInfo);
+ }
+ }
+ }
+ }
+
/**
* Install the JAX-RS security provider.
*/
@BuildStep
void setUpSecurity(BuildProducer providers,
- BuildProducer additionalBeanBuildItem, Capabilities capabilities,
- Optional eagerSecurityInterceptors) {
+ BuildProducer additionalBeanBuildItem, Capabilities capabilities) {
providers.produce(new ResteasyJaxrsProviderBuildItem(UnauthorizedExceptionMapper.class.getName()));
providers.produce(new ResteasyJaxrsProviderBuildItem(ForbiddenExceptionMapper.class.getName()));
providers.produce(new ResteasyJaxrsProviderBuildItem(AuthenticationFailedExceptionMapper.class.getName()));
@@ -102,10 +158,16 @@ void setUpSecurity(BuildProducer providers,
if (capabilities.isPresent(Capability.SECURITY)) {
providers.produce(new ResteasyJaxrsProviderBuildItem(SecurityContextFilter.class.getName()));
additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(SecurityContextFilter.class));
- if (eagerSecurityInterceptors.isPresent()) {
- providers.produce(new ResteasyJaxrsProviderBuildItem(EagerSecurityFilter.class.getName()));
- additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(EagerSecurityFilter.class));
- }
+ providers.produce(new ResteasyJaxrsProviderBuildItem(EagerSecurityFilter.class.getName()));
+ additionalBeanBuildItem.produce(AdditionalBeanBuildItem.unremovableOf(EagerSecurityFilter.class));
+ additionalBeanBuildItem.produce(
+ AdditionalBeanBuildItem.unremovableOf(StandardSecurityCheckInterceptor.RolesAllowedInterceptor.class));
+ additionalBeanBuildItem.produce(AdditionalBeanBuildItem
+ .unremovableOf(StandardSecurityCheckInterceptor.PermissionsAllowedInterceptor.class));
+ additionalBeanBuildItem.produce(
+ AdditionalBeanBuildItem.unremovableOf(StandardSecurityCheckInterceptor.PermitAllInterceptor.class));
+ additionalBeanBuildItem.produce(
+ AdditionalBeanBuildItem.unremovableOf(StandardSecurityCheckInterceptor.AuthenticatedInterceptor.class));
}
}
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java
index ee7c11733afc8..007fbb74b414e 100644
--- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java
+++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/AbstractSecurityEventTest.java
@@ -37,7 +37,8 @@ public abstract class AbstractSecurityEventTest {
protected static final Class>[] TEST_CLASSES = {
RolesAllowedResource.class, TestIdentityProvider.class, TestIdentityController.class,
- UnsecuredResource.class, UnsecuredSubResource.class, EventObserver.class
+ UnsecuredResource.class, UnsecuredSubResource.class, EventObserver.class, UnsecuredResourceInterface.class,
+ UnsecuredParentResource.class
};
@Inject
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java
index fb13a3d50cbf6..b9e18eed5475c 100644
--- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java
+++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedJaxRsTest.java
@@ -22,8 +22,8 @@ public class DefaultRolesAllowedJaxRsTest {
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(PermitAllResource.class, UnsecuredResource.class,
- TestIdentityProvider.class,
- TestIdentityController.class,
+ TestIdentityProvider.class, UnsecuredResourceInterface.class,
+ TestIdentityController.class, UnsecuredParentResource.class,
UnsecuredSubResource.class, HelloResource.class)
.addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = admin\n"),
"application.properties"));
@@ -41,6 +41,18 @@ public void shouldDenyUnannotated() {
assertStatus(path, 200, 403, 401);
}
+ @Test
+ public void shouldDenyUnannotatedOnParentClass() {
+ String path = "/unsecured/defaultSecurityParent";
+ assertStatus(path, 200, 403, 401);
+ }
+
+ @Test
+ public void shouldDenyUnannotatedOnInterface() {
+ String path = "/unsecured/defaultSecurityInterface";
+ assertStatus(path, 200, 403, 401);
+ }
+
@Test
public void shouldDenyDenyAllMethod() {
String path = "/unsecured/denyAll";
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java
index 4e0fd8c7dd808..ddcc31f5ae87e 100644
--- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java
+++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DefaultRolesAllowedStarJaxRsTest.java
@@ -17,8 +17,8 @@ public class DefaultRolesAllowedStarJaxRsTest {
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(PermitAllResource.class, UnsecuredResource.class,
- TestIdentityProvider.class,
- TestIdentityController.class,
+ TestIdentityProvider.class, UnsecuredParentResource.class,
+ TestIdentityController.class, UnsecuredResourceInterface.class,
UnsecuredSubResource.class)
.addAsResource(new StringAsset("quarkus.security.jaxrs.default-roles-allowed = **\n"),
"application.properties"));
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java
index 90cd2a9f77390..8ed4d8cbf445c 100644
--- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java
+++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/DenyAllJaxRsTest.java
@@ -26,8 +26,8 @@ public class DenyAllJaxRsTest {
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(PermitAllResource.class, UnsecuredResource.class,
- TestIdentityProvider.class,
- TestIdentityController.class,
+ TestIdentityProvider.class, UnsecuredParentResource.class,
+ TestIdentityController.class, UnsecuredResourceInterface.class,
UnsecuredSubResource.class, HelloResource.class)
.addAsResource(new StringAsset("quarkus.security.jaxrs.deny-unannotated-endpoints = true\n"),
"application.properties"));
@@ -58,6 +58,18 @@ public void shouldDenyUnannotated() {
assertStatus(path, 403, 401);
}
+ @Test
+ public void shouldDenyUnannotatedOnParentClass() {
+ String path = "/unsecured/defaultSecurityParent";
+ assertStatus(path, 403, 401);
+ }
+
+ @Test
+ public void shouldDenyUnannotatedOnInterface() {
+ String path = "/unsecured/defaultSecurityInterface";
+ assertStatus(path, 403, 401);
+ }
+
@Test
public void shouldDenyDenyAllMethod() {
String path = "/unsecured/denyAll";
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/EagerSecurityCheckTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/EagerSecurityCheckTest.java
new file mode 100644
index 0000000000000..d71665b4f1292
--- /dev/null
+++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/EagerSecurityCheckTest.java
@@ -0,0 +1,176 @@
+package io.quarkus.resteasy.test.security;
+
+import jakarta.annotation.security.DenyAll;
+import jakarta.annotation.security.PermitAll;
+import jakarta.annotation.security.RolesAllowed;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.security.Authenticated;
+import io.quarkus.security.test.utils.TestIdentityController;
+import io.quarkus.security.test.utils.TestIdentityProvider;
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+import io.restassured.http.ContentType;
+import io.restassured.response.Response;
+import io.vertx.core.json.JsonObject;
+
+/**
+ * Tests that {@link io.quarkus.security.spi.runtime.SecurityCheck}s are executed by Jakarta REST filters.
+ */
+public class EagerSecurityCheckTest {
+
+ @RegisterExtension
+ static QuarkusUnitTest runner = new QuarkusUnitTest()
+ .withApplicationRoot((jar) -> jar
+ .addClasses(TestIdentityProvider.class, TestIdentityController.class, JsonResource.class,
+ AbstractJsonResource.class, JsonSubResource.class));
+
+ @BeforeAll
+ public static void setupUsers() {
+ TestIdentityController.resetRoles()
+ .add("admin", "admin", "admin")
+ .add("user", "user", "user");
+ }
+
+ @Test
+ public void testAuthenticated() {
+ testPostJson("auth", "admin", true).then().statusCode(400);
+ testPostJson("auth", null, true).then().statusCode(401);
+ testPostJson("auth", "admin", false).then().statusCode(200);
+ testPostJson("auth", null, false).then().statusCode(401);
+ }
+
+ @Test
+ public void testRolesAllowed() {
+ testPostJson("roles", "admin", true).then().statusCode(400);
+ testPostJson("roles", "user", true).then().statusCode(403);
+ testPostJson("roles", "admin", false).then().statusCode(200);
+ testPostJson("roles", "user", false).then().statusCode(403);
+ }
+
+ @Test
+ public void testRolesAllowedOverriddenMethod() {
+ testPostJson("/roles-overridden", "admin", true).then().statusCode(400);
+ testPostJson("/roles-overridden", "user", true).then().statusCode(403);
+ testPostJson("/roles-overridden", "admin", false).then().statusCode(200);
+ testPostJson("/roles-overridden", "user", false).then().statusCode(403);
+ }
+
+ @Test
+ public void testDenyAll() {
+ testPostJson("deny", "admin", true).then().statusCode(403);
+ testPostJson("deny", null, true).then().statusCode(401);
+ testPostJson("deny", "admin", false).then().statusCode(403);
+ testPostJson("deny", null, false).then().statusCode(401);
+ }
+
+ @Test
+ public void testDenyAllClassLevel() {
+ testPostJson("/sub-resource/deny-class-level-annotation", "admin", true).then().statusCode(403);
+ testPostJson("/sub-resource/deny-class-level-annotation", null, true).then().statusCode(401);
+ testPostJson("/sub-resource/deny-class-level-annotation", "admin", false).then().statusCode(403);
+ testPostJson("/sub-resource/deny-class-level-annotation", null, false).then().statusCode(401);
+ }
+
+ @Test
+ public void testPermitAll() {
+ testPostJson("permit", "admin", true).then().statusCode(400);
+ testPostJson("permit", null, true).then().statusCode(400);
+ testPostJson("permit", "admin", false).then().statusCode(200);
+ testPostJson("permit", null, false).then().statusCode(200);
+ }
+
+ @Test
+ public void testSubResource() {
+ testPostJson("/sub-resource/roles", "admin", true).then().statusCode(400);
+ testPostJson("/sub-resource/roles", "user", true).then().statusCode(403);
+ testPostJson("/sub-resource/roles", "admin", false).then().statusCode(200);
+ testPostJson("/sub-resource/roles", "user", false).then().statusCode(403);
+ }
+
+ private static Response testPostJson(String path, String username, boolean invalid) {
+ var req = RestAssured.given();
+ if (username != null) {
+ req = req.auth().preemptive().basic(username, username);
+ }
+ return req
+ .contentType(ContentType.JSON)
+ .body((invalid ? "}" : "") + "{\"simple\": \"obj\"}").post(path);
+ }
+
+ @Path("/")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Consumes(MediaType.APPLICATION_JSON)
+ public static class JsonResource extends AbstractJsonResource {
+
+ @Authenticated
+ @Path("/auth")
+ @POST
+ public JsonObject auth(JsonObject array) {
+ return array.put("test", "testval");
+ }
+
+ @RolesAllowed("admin")
+ @Path("/roles")
+ @POST
+ public JsonObject roles(JsonObject array) {
+ return array.put("test", "testval");
+ }
+
+ @PermitAll
+ @Path("/permit")
+ @POST
+ public JsonObject permit(JsonObject array) {
+ return array.put("test", "testval");
+ }
+
+ @PermitAll
+ @Path("/sub-resource")
+ public JsonSubResource subResource() {
+ return new JsonSubResource();
+ }
+
+ @RolesAllowed("admin")
+ @Override
+ public JsonObject rolesOverridden(JsonObject array) {
+ return array.put("test", "testval");
+ }
+ }
+
+ @DenyAll
+ public static class JsonSubResource {
+ @RolesAllowed("admin")
+ @Path("/roles")
+ @POST
+ public JsonObject roles(JsonObject array) {
+ return array.put("test", "testval");
+ }
+
+ @Path("/deny-class-level-annotation")
+ @POST
+ public JsonObject denyClassLevelAnnotation(JsonObject array) {
+ return array.put("test", "testval");
+ }
+ }
+
+ public static abstract class AbstractJsonResource {
+ @DenyAll
+ @Path("/deny")
+ @POST
+ public JsonObject deny(JsonObject array) {
+ return array.put("test", "testval");
+ }
+
+ @Path("/roles-overridden")
+ @POST
+ public abstract JsonObject rolesOverridden(JsonObject array);
+ }
+}
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredParentResource.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredParentResource.java
new file mode 100644
index 0000000000000..abf5b385e9a0d
--- /dev/null
+++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredParentResource.java
@@ -0,0 +1,14 @@
+package io.quarkus.resteasy.test.security;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+public class UnsecuredParentResource {
+
+ @Path("/defaultSecurityParent")
+ @GET
+ public String defaultSecurityParent() {
+ return "defaultSecurityParent";
+ }
+
+}
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResource.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResource.java
index 116f041ba538d..7339c9c1eda07 100644
--- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResource.java
+++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResource.java
@@ -12,13 +12,18 @@
* @author Michal Szynkiewicz, michal.l.szynkiewicz@gmail.com
*/
@Path("/unsecured")
-public class UnsecuredResource {
+public class UnsecuredResource extends UnsecuredParentResource implements UnsecuredResourceInterface {
@Path("/defaultSecurity")
@GET
public String defaultSecurity() {
return "defaultSecurity";
}
+ @Override
+ public String defaultSecurityInterface() {
+ return UnsecuredResourceInterface.super.defaultSecurityInterface();
+ }
+
@Path("/permitAllPathParam/{index}")
@GET
@PermitAll
diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResourceInterface.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResourceInterface.java
new file mode 100644
index 0000000000000..d2498d46a8c63
--- /dev/null
+++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/UnsecuredResourceInterface.java
@@ -0,0 +1,14 @@
+package io.quarkus.resteasy.test.security;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+public interface UnsecuredResourceInterface {
+
+ @Path("/defaultSecurityInterface")
+ @GET
+ default String defaultSecurityInterface() {
+ return "defaultSecurityInterface";
+ }
+
+}
diff --git a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java
index 81a138aaa974e..e05df7c8dc751 100644
--- a/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java
+++ b/extensions/resteasy-classic/resteasy/runtime/src/main/java/io/quarkus/resteasy/runtime/EagerSecurityFilter.java
@@ -1,11 +1,15 @@
package io.quarkus.resteasy.runtime;
+import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_FAILURE;
+import static io.quarkus.security.spi.runtime.SecurityEventHelper.AUTHORIZATION_SUCCESS;
+
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import jakarta.annotation.Priority;
+import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
@@ -14,7 +18,19 @@
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.ext.Provider;
+import org.eclipse.microprofile.config.ConfigProvider;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.security.UnauthorizedException;
+import io.quarkus.security.identity.CurrentIdentityAssociation;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.security.spi.runtime.AuthorizationController;
+import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
+import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.MethodDescription;
+import io.quarkus.security.spi.runtime.SecurityCheck;
+import io.quarkus.security.spi.runtime.SecurityCheckStorage;
+import io.quarkus.security.spi.runtime.SecurityEventHelper;
import io.quarkus.vertx.http.runtime.security.EagerSecurityInterceptorStorage;
import io.vertx.ext.web.RoutingContext;
@@ -29,33 +45,106 @@ public void accept(RoutingContext routingContext) {
}
};
private final Map> cache = new HashMap<>();
+ private final EagerSecurityInterceptorStorage interceptorStorage;
+ private final SecurityEventHelper eventHelper;
@Context
ResourceInfo resourceInfo;
@Inject
- EagerSecurityInterceptorStorage interceptorStorage;
+ RoutingContext routingContext;
@Inject
- RoutingContext routingContext;
+ SecurityCheckStorage securityCheckStorage;
+
+ @Inject
+ CurrentIdentityAssociation identityAssociation;
+
+ @Inject
+ AuthorizationController authorizationController;
+
+ public EagerSecurityFilter() {
+ var interceptorStorageHandle = Arc.container().instance(EagerSecurityInterceptorStorage.class);
+ this.interceptorStorage = interceptorStorageHandle.isAvailable() ? interceptorStorageHandle.get() : null;
+ Event