diff --git a/bom/application/pom.xml b/bom/application/pom.xml index ca2a2ff9010b3..aa126ef05cb0d 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -51,7 +51,7 @@ 3.10.2 4.1.1 4.0.0 - 4.0.5 + 4.0.6 2.11.0 6.6.3 4.6.1 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java index 23ff1bda6ba40..a18cd6a8e7cf7 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/BytecodeRecorderImpl.java @@ -123,7 +123,8 @@ public class BytecodeRecorderImpl implements RecorderContext { private final Map, NewRecorder> existingRecorderValues = new ConcurrentHashMap<>(); private final List storedMethodCalls = new ArrayList<>(); - private final IdentityHashMap, String> classProxies = new IdentityHashMap<>(); + private final Map classProxyNamesToOriginalClassNames = new HashMap<>(); + private final Map> originalClassNamesToClassProxyClasses = new HashMap<>(); private final Map, SubstitutionHolder> substitutions = new HashMap<>(); private final Map, NonDefaultConstructorHolder> nonDefaultConstructors = new HashMap<>(); private final String className; @@ -273,14 +274,20 @@ public Class> classProxy(String name) { return void.class; } + Class> proxyClass = originalClassNamesToClassProxyClasses.get(name); + if (proxyClass != null) { + return proxyClass; + } + ProxyFactory factory = new ProxyFactory<>(new ProxyConfiguration() .setSuperClass(Object.class) .setClassLoader(classLoader) .setAnchorClass(getClass()) .setProxyNameSuffix("$$ClassProxy" + COUNT.incrementAndGet())); - Class theClass = factory.defineClass(); - classProxies.put(theClass, name); - return theClass; + proxyClass = factory.defineClass(); + classProxyNamesToOriginalClassNames.put(proxyClass.getName(), name); + originalClassNamesToClassProxyClasses.put(name, proxyClass); + return proxyClass; } @Override @@ -743,14 +750,10 @@ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle ar method.load(param.toString())); } }; - } else if (param instanceof Class>) { - if (!((Class) param).isPrimitive()) { + } else if (param instanceof Class> clazz) { + if (!clazz.isPrimitive()) { // Only try to load the class by name if it is not a primitive class - String name = classProxies.get(param); - if (name == null) { - name = ((Class) param).getName(); - } - String finalName = name; + String finalName = classProxyNamesToOriginalClassNames.getOrDefault(clazz.getName(), clazz.getName()); return new DeferredParameter() { @Override ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) { @@ -770,7 +773,7 @@ ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle ar return new DeferredParameter() { @Override ResultHandle doLoad(MethodContext context, MethodCreator method, ResultHandle array) { - return method.loadClassFromTCCL((Class) param); + return method.loadClassFromTCCL(clazz); } }; } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogMetricsHandler.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogMetricsHandler.java index f2cec8f96c236..d9fd436f064d8 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogMetricsHandler.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogMetricsHandler.java @@ -3,9 +3,9 @@ import java.util.Map.Entry; import java.util.NavigableMap; import java.util.concurrent.atomic.LongAdder; -import java.util.logging.Handler; -import java.util.logging.LogRecord; +import org.jboss.logmanager.ExtHandler; +import org.jboss.logmanager.ExtLogRecord; import org.jboss.logmanager.Level; /** @@ -16,7 +16,7 @@ * * Non-standard levels are counted with the lower standard level. */ -public class LogMetricsHandler extends Handler { +public class LogMetricsHandler extends ExtHandler { final NavigableMap logCounters; @@ -25,15 +25,13 @@ public LogMetricsHandler(NavigableMap logCounters) { } @Override - public void publish(LogRecord record) { - if (isLoggable(record)) { - Entry counter = logCounters.floorEntry(record.getLevel().intValue()); - if (counter != null) { - counter.getValue().increment(); - } else { - // Default to TRACE for anything lower - logCounters.get(Level.TRACE.intValue()).increment(); - } + protected void doPublish(ExtLogRecord record) { + Entry counter = logCounters.floorEntry(record.getLevel().intValue()); + if (counter != null) { + counter.getValue().increment(); + } else { + // Default to TRACE for anything lower + logCounters.get(Level.TRACE.intValue()).increment(); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 2d390ab877db0..3d8fe47a07940 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -33,6 +33,8 @@ import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.spi.ConfigSource; import org.jboss.logmanager.ExtFormatter; +import org.jboss.logmanager.ExtHandler; +import org.jboss.logmanager.ExtLogRecord; import org.jboss.logmanager.LogContext; import org.jboss.logmanager.LogContextInitializer; import org.jboss.logmanager.Logger; @@ -183,9 +185,9 @@ public void accept(String loggerName, CleanupFilterConfig config) { handlers.add(consoleHandler); } if (launchMode.isDevOrTest()) { - handlers.add(new Handler() { + handlers.add(new ExtHandler() { @Override - public void publish(LogRecord record) { + protected void doPublish(ExtLogRecord record) { if (record.getThrown() != null) { ExceptionReporting.notifyException(record.getThrown()); } @@ -613,9 +615,9 @@ private static Handler configureConsoleHandler( if (color && launchMode.isDevOrTest() && !config.async().enable()) { final Handler delegate = handler; - handler = new Handler() { + handler = new ExtHandler() { @Override - public void publish(LogRecord record) { + protected void doPublish(ExtLogRecord record) { BiConsumer> formatter = CurrentAppExceptionHighlighter.THROWABLE_FORMATTER; if (formatter != null) { formatter.accept(record, delegate::publish); diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 8ab61ce549e68..db6df8d52e591 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -301,7 +301,7 @@ of secret that contains the required credentials. Quarkus can automatically gene quarkus.kubernetes.generate-image-pull-secret=true ---- -More specifically a `Secret`like the one bellow is genrated: +More specifically a `Secret` like the one below is generated: [source,yaml] ---- diff --git a/docs/src/main/asciidoc/native-reference.adoc b/docs/src/main/asciidoc/native-reference.adoc index a9e8baa63417d..fd6982ce273ff 100644 --- a/docs/src/main/asciidoc/native-reference.adoc +++ b/docs/src/main/asciidoc/native-reference.adoc @@ -2293,14 +2293,29 @@ One can see which Mandrel version was used to generate a binary by inspecting th [source,bash] ---- -$ strings target/debugging-native-1.0.0-SNAPSHOT-runner | grep GraalVM -com.oracle.svm.core.VM=GraalVM 22.0.0.2-Final Java 11 Mandrel Distribution +$ strings target/debugging-native-1.0.0-SNAPSHOT-runner | \ + grep -e 'com.oracle.svm.core.VM=' -e 'com.oracle.svm.core.VM.Java.Version' -F +com.oracle.svm.core.VM.Java.Version=21.0.5 +com.oracle.svm.core.VM=Mandrel-23.1.5.0-Final ---- === How do I enable GC logging in native executables? See <> for details. +=== Can I get a thread dump of a native executable? + +Yes, Quarkus sets up a signal handler for the `SIGQUIT` signal (or `SIGBREAK` on windows) that will result in a thread +dump being printed when receiving the `SIGQUIT/SIGBREAK` signal. +You may use `kill -SIGQUIT ` to trigger a thread dump. + +[NOTE] +==== +Quarkus uses its own signal handler, to use GraalVM's default signal handler instead you will need to: +1. Add `-H:+DumpThreadStacksOnSignal` to `quarkus.native.additional-build-args` and rebuild the application. +2. Set the environment variable `DISABLE_SIGNAL_HANDLERS` before running the app. +==== + [[heap-dumps]] === Can I get a heap dump of a native executable? e.g. if it runs out of memory diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc index f72c020ab44c1..bc15e9117e74f 100644 --- a/docs/src/main/asciidoc/scheduler-reference.adoc +++ b/docs/src/main/asciidoc/scheduler-reference.adoc @@ -156,7 +156,7 @@ Property Expressions. .Time Zone Configuration Property Example [source,java] ---- -@Scheduled(cron = "0 15 10 * * ?", timeZone = "{myMethod.timeZone}") +@Scheduled(cron = "0 15 10 * * ?", timeZone = "${myMethod.timeZone}") void myMethod() { } ---- diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index 151064e376e9a..30fe4e03adb6a 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -1122,6 +1122,49 @@ public class ProjectPermissionChecker { TIP: Permission checks run by default on event loops. Annotate a permission checker method with the `io.smallrye.common.annotation.Blocking` annotation if you want to run the check on a worker thread. +Matching between the `@PermissionsAllowed` values and the `@PermissionChecker` value is based on string equality as shown in the example below: + +[source,java] +---- +package org.acme.security; + +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class FileService { + + @PermissionsAllowed({ "delete:all", "delete:dir" }) <1> + void deleteDirectory(Path directoryPath) { + // delete directory + } + + @PermissionsAllowed(value = { "delete:service", "delete:file" }, inclusive = true) <2> + void deleteServiceFile(Path serviceFilePath) { + // delete service file + } + + @PermissionChecker("delete:all") + boolean canDeleteAllDirectories(SecurityIdentity identity) { + String filePermissions = identity.getAttribute("user-group-file-permissions"); + return filePermissions != null && filePermissions.contains("w"); + } + + @PermissionChecker("delete:service") + boolean canDeleteService(SecurityIdentity identity) { + return identity.hasRole("admin"); + } + + @PermissionChecker("delete:file") + boolean canDeleteFile(Path serviceFilePath) { + return serviceFilePath != null && !serviceFilePath.endsWith("critical"); + } +} +---- +<1> The permission checker method `canDeleteAllDirectories` grants access to the `deleteDirectory` because the `delete:all` values are equal. +<2> There must be exactly two permission checker methods, one for the `delete:service` permission and other for the `delete:file` permission. + [[permission-meta-annotation]] ==== Create permission meta-annotations diff --git a/docs/src/main/asciidoc/security-ldap.adoc b/docs/src/main/asciidoc/security-ldap.adoc index 83b1695367a83..977ac6abacd07 100644 --- a/docs/src/main/asciidoc/security-ldap.adoc +++ b/docs/src/main/asciidoc/security-ldap.adoc @@ -177,6 +177,18 @@ quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou= The `elytron-security-ldap` extension requires a dir-context and an identity-mapping with at least one attribute-mapping to authenticate the user and its identity. +=== Map LDAP groups to `SecurityIdentity` roles + +Previously described application configuration showed how to map `CN` attribute of the LDAP Distinguished Name group to a Quarkus `SecurityIdentity` role. +More specifically, the `standardRole` CN was mapped to a `SecurityIdentity` role and thus allowed access to the `UserResource#me` endpoint. +However, required `SecurityIdentity` roles may differ between applications and you may need to map LDAP groups to local `SecurityIdentity` roles like in the example below: + +[source,properties] +---- +quarkus.http.auth.roles-mapping."standardRole"=user <1> +---- +<1> Map the `standardRole` role to the application-specific `SecurityIdentity` role `user`. + == Testing the Application The application is now protected and the identities are provided by our LDAP server. diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalEventLoggingListener.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalEventLoggingListener.java index c0959df54864f..fb6181b5140e7 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalEventLoggingListener.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalEventLoggingListener.java @@ -67,7 +67,12 @@ public void onConnectionDestroy(Connection connection) { @Override public void onWarning(String warning) { - log.warnv("{0}: {1}", datasourceName, warning); + // See https://github.com/quarkusio/quarkus/issues/44047 + if (warning != null && warning.contains("JDBC resources leaked")) { + log.debugv("{0}: {1}", datasourceName, warning); + } else { + log.warnv("{0}: {1}", datasourceName, warning); + } } @Override diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/ExtendedCharactersSupport.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/ExtendedCharactersSupport.java index e01f0ee3f278a..875f8e1db2349 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/ExtendedCharactersSupport.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/ExtendedCharactersSupport.java @@ -5,12 +5,13 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.jdbc.oracle.runtime.OracleInitRecorder; public final class ExtendedCharactersSupport { @Record(STATIC_INIT) - @BuildStep + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) public void preinitializeCharacterSets(NativeConfig config, OracleInitRecorder recorder) { recorder.setupCharSets(config.addAllCharsets()); } diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java index 7184b831e6173..fb18910d4c80d 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java @@ -12,6 +12,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.maven.dependency.ArtifactKey; /** @@ -35,7 +36,7 @@ * require it, so this would facilitate the option to revert to the older version in * case of problems. */ -@BuildSteps +@BuildSteps(onlyIf = NativeOrNativeSourcesBuild.class) public final class OracleMetadataOverrides { static final String DRIVER_JAR_MATCH_REGEX = "com\\.oracle\\.database\\.jdbc"; diff --git a/extensions/mongodb-client/deployment/pom.xml b/extensions/mongodb-client/deployment/pom.xml index 8bd99d1840900..ddd07240c5390 100644 --- a/extensions/mongodb-client/deployment/pom.xml +++ b/extensions/mongodb-client/deployment/pom.xml @@ -91,6 +91,11 @@ assertj-core test + + io.rest-assured + rest-assured + test + io.quarkus quarkus-resteasy-deployment diff --git a/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/MongoClientConfigTest.java b/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/MongoClientConfigTest.java index 3708be970d06e..24c6be8458d7b 100644 --- a/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/MongoClientConfigTest.java +++ b/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/MongoClientConfigTest.java @@ -1,11 +1,13 @@ package io.quarkus.mongodb; +import static io.restassured.RestAssured.when; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.TimeUnit; import jakarta.inject.Inject; +import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -94,4 +96,11 @@ public void testReactiveClientConfiuration() { assertThat(clientImpl.getSettings().getReadConcern()).isEqualTo(new ReadConcern(ReadConcernLevel.SNAPSHOT)); assertThat(clientImpl.getSettings().getReadPreference()).isEqualTo(ReadPreference.primary()); } + + @Test + public void healthCheck() { + when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } } diff --git a/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/NoConnectionHealthCheckTest.java b/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/NoConnectionHealthCheckTest.java new file mode 100644 index 0000000000000..c68bba88da4d9 --- /dev/null +++ b/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/NoConnectionHealthCheckTest.java @@ -0,0 +1,52 @@ +package io.quarkus.mongodb; + +import static io.restassured.RestAssured.when; + +import jakarta.inject.Inject; + +import org.bson.Document; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.mongodb.MongoClientException; +import com.mongodb.client.MongoClient; + +import io.quarkus.test.QuarkusUnitTest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class NoConnectionHealthCheckTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.devservices.enabled", "false") + .overrideConfigKey("quarkus.mongodb.connection-string", "mongodb://localhost:9999") + // timeouts set to the test doesn't take too long to run + .overrideConfigKey("quarkus.mongodb.connect-timeout", "2s") + .overrideConfigKey("quarkus.mongodb.server-selection-timeout", "2s") + .overrideConfigKey("quarkus.mongodb.read-timeout", "2s"); + + @Inject + MongoClient mongo; + + @Order(1) // done to ensure the health check runs before any application code touches the database + @Test + public void healthCheck() { + when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("DOWN")); + } + + @Order(2) + @Test + public void tryConnection() { + Assertions.assertThrows(MongoClientException.class, () -> { + mongo.getDatabase("admin").runCommand(new Document("ping", 1)); + }); + } + +} diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java index 953971bcf3455..382e3fb699238 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java @@ -42,7 +42,7 @@ public class MongoHealthCheck implements HealthCheck { private static final Document COMMAND = new Document("ping", 1); - public void configure(MongodbConfig config) { + public MongoHealthCheck(MongodbConfig config) { Iterable> handle = Arc.container().select(MongoClient.class, Any.Literal.INSTANCE) .handles(); Iterable> reactiveHandlers = Arc.container() @@ -61,7 +61,7 @@ public void configure(MongodbConfig config) { } } - config.mongoClientConfigs.forEach(new BiConsumer() { + config.mongoClientConfigs.forEach(new BiConsumer<>() { @Override public void accept(String name, MongoClientConfig cfg) { MongoClient client = getClient(handle, name); @@ -76,7 +76,6 @@ public void accept(String name, MongoClientConfig cfg) { } } }); - } private MongoClient getClient(Iterable> handle, String name) { diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java index e538026fe1f3a..446fda0de4351 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java @@ -58,12 +58,9 @@ import com.mongodb.event.ConnectionPoolListener; import com.mongodb.reactivestreams.client.ReactiveContextProvider; -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.mongodb.MongoClientName; -import io.quarkus.mongodb.health.MongoHealthCheck; import io.quarkus.mongodb.impl.ReactiveMongoClientImpl; import io.quarkus.mongodb.reactive.ReactiveMongoClient; @@ -111,17 +108,6 @@ public MongoClients(MongodbConfig mongodbConfig, MongoClientSupport mongoClientS Class.forName("sun.net.ext.ExtendedSocketOptions", true, ClassLoader.getSystemClassLoader()); } catch (ClassNotFoundException ignored) { } - - try { - Class.forName("org.eclipse.microprofile.health.HealthCheck"); - InstanceHandle instance = Arc.container() - .instance(MongoHealthCheck.class, Any.Literal.INSTANCE); - if (instance.isAvailable()) { - instance.get().configure(mongodbConfig); - } - } catch (ClassNotFoundException e) { - // Ignored - no health check - } } public MongoClient createMongoClient(String clientName) throws MongoException { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java index 9c4606ea68e99..65e2de1270e70 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java @@ -13,12 +13,11 @@ import java.time.Instant; import java.util.Map; import java.util.Optional; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogRecord; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logmanager.ExtHandler; import org.jboss.logmanager.ExtLogRecord; import io.opentelemetry.api.OpenTelemetry; @@ -28,7 +27,7 @@ import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.api.logs.Severity; -public class OpenTelemetryLogHandler extends Handler { +public class OpenTelemetryLogHandler extends ExtHandler { private final OpenTelemetry openTelemetry; @@ -37,7 +36,7 @@ public OpenTelemetryLogHandler(final OpenTelemetry openTelemetry) { } @Override - public void publish(LogRecord record) { + protected void doPublish(ExtLogRecord record) { if (openTelemetry == null) { return; // might happen at shutdown } @@ -60,24 +59,22 @@ public void publish(LogRecord record) { attributes.put(CODE_NAMESPACE, record.getSourceClassName()); attributes.put(CODE_FUNCTION, record.getSourceMethodName()); - if (record instanceof ExtLogRecord) { - attributes.put(CODE_LINENO, ((ExtLogRecord) record).getSourceLineNumber()); - attributes.put(THREAD_NAME, ((ExtLogRecord) record).getThreadName()); - attributes.put(THREAD_ID, ((ExtLogRecord) record).getLongThreadID()); - attributes.put(AttributeKey.stringKey("log.logger.namespace"), - ((ExtLogRecord) record).getLoggerClassName()); - - final Map mdcCopy = ((ExtLogRecord) record).getMdcCopy(); - if (mdcCopy != null) { - mdcCopy.forEach((k, v) -> { - // ignore duplicated span data already in the MDC - if (!k.toLowerCase().equals("spanid") && - !k.toLowerCase().equals("traceid") && - !k.toLowerCase().equals("sampled")) { - attributes.put(AttributeKey.stringKey(k), v); - } - }); - } + attributes.put(CODE_LINENO, record.getSourceLineNumber()); + attributes.put(THREAD_NAME, record.getThreadName()); + attributes.put(THREAD_ID, record.getLongThreadID()); + attributes.put(AttributeKey.stringKey("log.logger.namespace"), + record.getLoggerClassName()); + + final Map mdcCopy = record.getMdcCopy(); + if (mdcCopy != null) { + mdcCopy.forEach((k, v) -> { + // ignore duplicated span data already in the MDC + if (!k.equalsIgnoreCase("spanid") && + !k.equalsIgnoreCase("traceid") && + !k.equalsIgnoreCase("sampled")) { + attributes.put(AttributeKey.stringKey(k), v); + } + }); } if (record.getThrown() != null) { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java index c48bb6a630b65..80f130f9b4d81 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java @@ -124,8 +124,9 @@ public String peerAddress() { return attributes.get(PEER_ADDRESS); } - public long dbIndex() { - return Long.parseLong(attributes.get(DB_INSTANCE)); + public Long dbIndex() { + String dbInstance = attributes.get(DB_INSTANCE); + return dbInstance != null ? Long.valueOf(dbInstance) : null; } } diff --git a/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java b/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java index fd4169f64e0be..be4aa3b5071ad 100644 --- a/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java +++ b/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java @@ -19,34 +19,40 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.ClientWebApplicationException; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyReader; +import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader; import org.jboss.resteasy.reactive.common.util.EmptyInputStream; -import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; -public class ClientJacksonMessageBodyReader extends JacksonBasicMessageBodyReader implements ClientRestHandler { +public class ClientJacksonMessageBodyReader extends AbstractJsonMessageBodyReader implements ClientMessageBodyReader { private static final Logger log = Logger.getLogger(ClientJacksonMessageBodyReader.class); private final ConcurrentMap objectReaderMap = new ConcurrentHashMap<>(); - private RestClientRequestContext context; + private final ObjectReader defaultReader; @Inject public ClientJacksonMessageBodyReader(ObjectMapper mapper) { - super(mapper); + this.defaultReader = mapper.reader(); } @Override public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + return doRead(type, genericType, mediaType, entityStream, null); + } + + private Object doRead(Class type, Type genericType, MediaType mediaType, InputStream entityStream, + RestClientRequestContext context) + throws IOException { try { if (entityStream instanceof EmptyInputStream) { return null; } - ObjectReader reader = getEffectiveReader(mediaType); + ObjectReader reader = getEffectiveReader(mediaType, context); return reader.forType(reader.getTypeFactory().constructType(genericType != null ? genericType : type)) .readValue(entityStream); @@ -57,14 +63,18 @@ public Object readFrom(Class type, Type genericType, Annotation[] annota } @Override - public void handle(RestClientRequestContext requestContext) { - this.context = requestContext; + public Object readFrom(Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream, + RestClientRequestContext context) throws java.io.IOException, jakarta.ws.rs.WebApplicationException { + return doRead(type, genericType, mediaType, entityStream, context); } - private ObjectReader getEffectiveReader(MediaType responseMediaType) { + private ObjectReader getEffectiveReader(MediaType responseMediaType, RestClientRequestContext context) { ObjectMapper effectiveMapper = getObjectMapperFromContext(responseMediaType, context); if (effectiveMapper == null) { - return getEffectiveReader(); + return defaultReader; } return objectReaderMap.computeIfAbsent(effectiveMapper, new Function<>() { diff --git a/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java index bb91ba363f32b..cb8b9a9c6e961 100644 --- a/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java @@ -16,24 +16,20 @@ import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.ext.MessageBodyWriter; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyWriter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; -public class ClientJacksonMessageBodyWriter implements MessageBodyWriter, ClientRestHandler { +public class ClientJacksonMessageBodyWriter implements ClientMessageBodyWriter { - protected final ObjectMapper originalMapper; - protected final ObjectWriter defaultWriter; + private final ObjectWriter defaultWriter; private final ConcurrentMap objectWriterMap = new ConcurrentHashMap<>(); - private RestClientRequestContext context; @Inject public ClientJacksonMessageBodyWriter(ObjectMapper mapper) { - this.originalMapper = mapper; this.defaultWriter = createDefaultWriter(mapper); } @@ -45,15 +41,17 @@ public boolean isWriteable(Class type, Type genericType, Annotation[] annotation @Override public void writeTo(Object o, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { - doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType)); + doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType, null)); } @Override - public void handle(RestClientRequestContext requestContext) throws Exception { - this.context = requestContext; + public void writeTo(Object o, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream, + RestClientRequestContext context) throws IOException, WebApplicationException { + doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType, context)); } - protected ObjectWriter getEffectiveWriter(MediaType responseMediaType) { + protected ObjectWriter getEffectiveWriter(MediaType responseMediaType, RestClientRequestContext context) { ObjectMapper objectMapper = getObjectMapperFromContext(responseMediaType, context); if (objectMapper == null) { return defaultWriter; @@ -66,5 +64,4 @@ public ObjectWriter apply(ObjectMapper objectMapper) { } }); } - } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java index b48b744f588ea..a8d3629b167d5 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java @@ -134,22 +134,13 @@ private static Map getPermissionCheckers(Inde "supported return types are 'boolean' and 'Uni'. ") .formatted(toString(checkerMethod), checkerMethod.returnType().name())); } - var permissionToActions = parsePermissionToActions(annotationInstance.value().asString(), new HashMap<>()) - .entrySet().iterator().next(); - var permissionName = permissionToActions.getKey(); + var permissionName = annotationInstance.value().asString(); if (permissionName.isBlank()) { throw new IllegalArgumentException( "@PermissionChecker annotation placed on the '%s' attribute 'value' must not be blank" .formatted(toString(checkerMethod))); } - var permissionActions = permissionToActions.getValue(); - if (permissionActions != null && !permissionActions.isEmpty()) { - throw new IllegalArgumentException(""" - @PermissionChecker annotation instance placed on the '%s' has attribute 'value' with - permission name '%s' and actions '%s', however actions are currently not supported - """.formatted(toString(checkerMethod), permissionName, permissionActions)); - } boolean isBlocking = checkerMethod.hasDeclaredAnnotation(BLOCKING); if (isBlocking && isReactive) { throw new IllegalArgumentException(""" @@ -588,9 +579,47 @@ private void gatherPermissionKeys(AnnotationInstanc List cache, Map>> targetToPermissionKeys) { // @PermissionsAllowed value is in format permission:action, permission2:action, permission:action2, permission3 // here we transform it to permission -> actions - final var permissionToActions = new HashMap>(); - for (String permissionToAction : instance.value().asStringArray()) { - parsePermissionToActions(permissionToAction, permissionToActions); + record PermissionNameAndChecker(String permissionName, PermissionCheckerMetadata checker) { + } + boolean foundPermissionChecker = false; + final var permissionToActions = new HashMap>(); + for (String permissionValExpression : instance.value().asStringArray()) { + final PermissionCheckerMetadata checker = permissionNameToChecker.get(permissionValExpression); + if (checker != null) { + // matched @PermissionAllowed("value") with @PermissionChecker("value") + foundPermissionChecker = true; + final var permissionNameKey = new PermissionNameAndChecker(permissionValExpression, checker); + if (!permissionToActions.containsKey(permissionNameKey)) { + permissionToActions.put(permissionNameKey, Collections.emptySet()); + } + } else if (permissionValExpression.contains(PERMISSION_TO_ACTION_SEPARATOR)) { + + // expected format: permission:action + final String[] permissionToActionArr = permissionValExpression.split(PERMISSION_TO_ACTION_SEPARATOR); + if (permissionToActionArr.length != 2) { + throw new RuntimeException(String.format( + "PermissionsAllowed value '%s' contains more than one separator '%2$s', expected format is 'permissionName%2$saction'", + permissionValExpression, PERMISSION_TO_ACTION_SEPARATOR)); + } + final PermissionNameAndChecker permissionNameKey = new PermissionNameAndChecker(permissionToActionArr[0], + null); + final String action = permissionToActionArr[1]; + if (permissionToActions.containsKey(permissionNameKey)) { + permissionToActions.get(permissionNameKey).add(action); + } else { + final Set actions = new HashSet<>(); + actions.add(action); + permissionToActions.put(permissionNameKey, actions); + } + } else { + + // expected format: permission + final PermissionNameAndChecker permissionNameKey = new PermissionNameAndChecker(permissionValExpression, + null); + if (!permissionToActions.containsKey(permissionNameKey)) { + permissionToActions.put(permissionNameKey, new HashSet<>()); + } + } } if (permissionToActions.isEmpty()) { @@ -611,12 +640,54 @@ private void gatherPermissionKeys(AnnotationInstanc : instance.value("params").asStringArray(); final Type classType = getPermissionClass(instance); final boolean inclusive = instance.value("inclusive") != null && instance.value("inclusive").asBoolean(); + + if (inclusive && foundPermissionChecker) { + // @PermissionsAllowed({ "read", "read:all", "read:it", "write" } && @PermissionChecker("read") + // require @PermissionChecker for all 'read:action' because determining expected behavior would be too + // complex; similarly for @PermissionChecker("read:all") require 'read' and 'read:it' have checker as well + List checkerPermissions = permissionToActions.keySet().stream() + .filter(k -> k.checker != null).toList(); + for (PermissionNameAndChecker checkerPermission : checkerPermissions) { + // read -> read + // read:all -> read + String permissionName = checkerPermission.permissionName.contains(PERMISSION_TO_ACTION_SEPARATOR) + ? checkerPermission.permissionName.split(PERMISSION_TO_ACTION_SEPARATOR)[0] + : checkerPermission.permissionName; + for (var e : permissionToActions.entrySet()) { + PermissionNameAndChecker permissionNameKey = e.getKey(); + // look for permission names that match our permission checker value (before action-to-perm separator) + // for example: read:it + if (permissionNameKey.checker == null && permissionNameKey.permissionName.equals(permissionName)) { + boolean hasActions = e.getValue() != null && !e.getValue().isEmpty(); + final String permissionsJoinedWithActions; + if (hasActions) { + permissionsJoinedWithActions = e.getValue() + .stream() + .map(action -> permissionNameKey.permissionName + PERMISSION_TO_ACTION_SEPARATOR + + action) + .collect(Collectors.joining(", ")); + } else { + permissionsJoinedWithActions = permissionNameKey.permissionName; + } + throw new RuntimeException( + """ + @PermissionsAllowed annotation placed on the '%s' has inclusive relation between its permissions. + The '%s' permission has been matched with @PermissionChecker '%s', therefore you must also define + a @PermissionChecker for '%s' permissions. + """ + .formatted(toString(annotationTarget), permissionName, + toString(checkerPermission.checker.checkerMethod), + permissionsJoinedWithActions)); + } + } + } + } + for (var permissionToAction : permissionToActions.entrySet()) { - final var permissionName = permissionToAction.getKey(); + final var permissionNameKey = permissionToAction.getKey(); final var permissionActions = permissionToAction.getValue(); - final var permissionChecker = findPermissionChecker(permissionName, permissionActions); - final var key = new PermissionKey(permissionName, permissionActions, params, classType, inclusive, - permissionChecker, annotationTarget); + final var key = new PermissionKey(permissionNameKey.permissionName, permissionActions, params, classType, + inclusive, permissionNameKey.checker, annotationTarget); final int i = cache.indexOf(key); if (i == -1) { orPermissions.add(key); @@ -632,44 +703,6 @@ private void gatherPermissionKeys(AnnotationInstanc .add(List.copyOf(orPermissions)); } - private static HashMap> parsePermissionToActions(String permissionToAction, - HashMap> permissionToActions) { - if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { - - // expected format: permission:action - final String[] permissionToActionArr = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); - if (permissionToActionArr.length != 2) { - throw new RuntimeException(String.format( - "PermissionsAllowed value '%s' contains more than one separator '%2$s', expected format is 'permissionName%2$saction'", - permissionToAction, PERMISSION_TO_ACTION_SEPARATOR)); - } - final String permissionName = permissionToActionArr[0]; - final String action = permissionToActionArr[1]; - if (permissionToActions.containsKey(permissionName)) { - permissionToActions.get(permissionName).add(action); - } else { - final Set actions = new HashSet<>(); - actions.add(action); - permissionToActions.put(permissionName, actions); - } - } else { - - // expected format: permission - if (!permissionToActions.containsKey(permissionToAction)) { - permissionToActions.put(permissionToAction, new HashSet<>()); - } - } - return permissionToActions; - } - - private PermissionCheckerMetadata findPermissionChecker(String permissionName, Set permissionActions) { - if (permissionActions != null && !permissionActions.isEmpty()) { - // only permission name is supported for now - return null; - } - return permissionNameToChecker.get(permissionName); - } - private static Type getPermissionClass(AnnotationInstance instance) { return instance.value(PERMISSION_ATTR) == null ? Type.create(STRING_PERMISSION, Type.Kind.CLASS) : instance.value(PERMISSION_ATTR).asClass(); @@ -1361,8 +1394,8 @@ private static SecMethodAndPermCtorIdx[] matchPermCtorParamIdxBasedOnNameMatch(M : constructor.declaringClass().name().toString(); throw new RuntimeException(String.format( "No '%s' formal parameter name matches '%s' Permission %s parameter name '%s'", - securedMethod.name(), matchTarget, isQuarkusPermission ? "checker" : "constructor", - constructorParamName)); + PermissionSecurityChecksBuilder.toString(securedMethod), matchTarget, + isQuarkusPermission ? "checker" : "constructor", constructorParamName)); } } return matches; diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/MissingCheckerForInclusivePermsValidationFailureTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/MissingCheckerForInclusivePermsValidationFailureTest.java new file mode 100644 index 0000000000000..7371d84e4909c --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/MissingCheckerForInclusivePermsValidationFailureTest.java @@ -0,0 +1,45 @@ +package io.quarkus.security.test.permissionsallowed.checker; + +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.test.QuarkusUnitTest; + +public class MissingCheckerForInclusivePermsValidationFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .assertException(t -> { + Assertions.assertEquals(RuntimeException.class, t.getClass(), t.getMessage()); + Assertions.assertTrue(t.getMessage().contains("@PermissionsAllowed annotation placed on")); + Assertions.assertTrue( + t.getMessage().contains("SecuredBean#securedBean' has inclusive relation between its permissions")); + Assertions.assertTrue(t.getMessage().contains("you must also define")); + Assertions.assertTrue(t.getMessage().contains("@PermissionChecker for 'checker:missing' permissions")); + }); + + @Test + public void test() { + Assertions.fail(); + } + + @Singleton + public static class SecuredBean { + + @PermissionsAllowed(value = { "checker", "checker:missing" }, inclusive = true) + public void securedBean() { + // EMPTY + } + + @PermissionChecker("checker") + public boolean check(SecurityIdentity identity) { + return false; + } + } +} diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/PermissionCheckerNameWithColonsTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/PermissionCheckerNameWithColonsTest.java new file mode 100644 index 0000000000000..928b7369ea774 --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/PermissionCheckerNameWithColonsTest.java @@ -0,0 +1,333 @@ +package io.quarkus.security.test.permissionsallowed.checker; + +import static io.quarkus.security.test.utils.IdentityMock.ADMIN; +import static io.quarkus.security.test.utils.IdentityMock.USER; +import static io.quarkus.security.test.utils.SecurityTestUtils.assertFailureFor; +import static io.quarkus.security.test.utils.SecurityTestUtils.assertSuccess; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; +import io.quarkus.security.StringPermission; +import io.quarkus.security.UnauthorizedException; +import io.quarkus.security.test.utils.AuthData; +import io.quarkus.security.test.utils.IdentityMock; +import io.quarkus.security.test.utils.SecurityTestUtils; +import io.quarkus.test.QuarkusUnitTest; + +public class PermissionCheckerNameWithColonsTest { + + private static final AuthData USER_WITH_AUGMENTORS = new AuthData(USER, true); + private static final AuthData ADMIN_WITH_AUGMENTORS = new AuthData(ADMIN, true); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(IdentityMock.class, AuthData.class, SecurityTestUtils.class)); + + @Inject + SecuredBean bean; + + @Test + public void testIdentityPermissionWithActionGrantAccess() { + // @PermissionsAllowed({ "read", "read:all" }) says one of: either 'read' is granted by permission checker + // or 'read:all' is granted by identity permissions + var userWithReadAll = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("read", "all")), true); + assertSuccess(() -> bean.readAndReadAll(false), "readAndReadAll", userWithReadAll); + var userWithReadNothing = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("read", "nothing")), + true); + assertFailureFor(() -> bean.readAndReadAll(false), ForbiddenException.class, userWithReadNothing); + // check 'read' is granted by the checker method + assertSuccess(() -> bean.readAndReadAll(true), "readAndReadAll", userWithReadNothing); + // check 'read' can only be granted by the checker method + var userWithRead = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("read")), true); + assertFailureFor(() -> bean.readAndReadAll(false), ForbiddenException.class, userWithRead); + } + + @Test + public void testIdentityPermissionWithMultipleActionsGrantsAccess() { + // @PermissionsAllowed({ "write:all", "write", "write:essay" }) says one of: either 'write' is granted + // by permission checker or 'write:all' or 'write:essay' is granted by identity permissions + var userWithWriteAll = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("write", "all")), true); + assertSuccess(() -> bean.writeAndWriteAllAndEssay(false), "writeAndWriteAllAndEssay", userWithWriteAll); + var userWithWriteEssay = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("write", "essay")), + true); + assertSuccess(() -> bean.writeAndWriteAllAndEssay(false), "writeAndWriteAllAndEssay", userWithWriteEssay); + var userWithWriteEssayAndAll = new AuthData(Set.of("user"), false, "user", + Set.of(new StringPermission("write", "essay", "all")), true); + assertSuccess(() -> bean.writeAndWriteAllAndEssay(false), "writeAndWriteAllAndEssay", userWithWriteEssayAndAll); + var userWithWriteNothing = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("write", "nothing")), + true); + assertFailureFor(() -> bean.writeAndWriteAllAndEssay(false), ForbiddenException.class, userWithWriteNothing); + // check 'write' is granted by the checker method + assertSuccess(() -> bean.writeAndWriteAllAndEssay(true), "writeAndWriteAllAndEssay", userWithWriteNothing); + // check 'write' can only be granted by the checker method + var userWithWrite = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("write")), true); + assertFailureFor(() -> bean.writeAndWriteAllAndEssay(false), ForbiddenException.class, userWithWrite); + } + + @Test + public void testInclusivePermsAreOnlyGrantedByChecker() { + // @PermissionsAllowed(value = { "execute", "execute:all", "execute:dir" }, inclusive = true) + // check all "execute", "execute:all", "execute:dir" are granted by the checker, + // and they cannot be granted by the identity permissions at all + + // access is denied because permission checkers require 'true' + var userWithExecuteDirAndAll = new AuthData(Set.of("user"), false, "user", + Set.of(new StringPermission("execute", "all", "dir")), true); + assertFailureFor(() -> bean.executeAndAllAndDir(true, true, false), ForbiddenException.class, userWithExecuteDirAndAll); + assertFailureFor(() -> bean.executeAndAllAndDir(true, false, true), ForbiddenException.class, userWithExecuteDirAndAll); + assertFailureFor(() -> bean.executeAndAllAndDir(false, true, true), ForbiddenException.class, userWithExecuteDirAndAll); + + // access is granted because all arguments are true + assertSuccess(() -> bean.executeAndAllAndDir(true, true, true), "executeAndAllAndDir", userWithExecuteDirAndAll); + + // access is denied as anonymous user is not allow access any resources annotated with the @PermissionsAllowed + // because anonymous users can't have permissions + assertFailureFor(() -> bean.executeAndAllAndDir(true, true, true), UnauthorizedException.class, + new AuthData(IdentityMock.ANONYMOUS, true)); + } + + @Test + public void testInclusivePermsAreOnlyGrantedByCheckerAndExtraPermission() { + // @PermissionsAllowed(value = { "delete", "delete:all", "delete:dir", "purge" }, inclusive = true) + // check all "delete", "delete:all", "delete:dir" are granted by the checker, + // and they cannot be granted by the identity permissions at all + // but for the 'purge', identity permission can be used and the checker doesn't need to exist + + // access is denied because permission checkers require 'true' even though user has all the identity permissions + var userWithDeleteDirAndAllAndPurge = new AuthData(Set.of("user"), false, "user", + Set.of(new StringPermission("delete", "all", "dir"), new StringPermission("purge")), true); + assertFailureFor(() -> bean.deleteAndAllAndDir(true, true, false), ForbiddenException.class, + userWithDeleteDirAndAllAndPurge); + assertFailureFor(() -> bean.deleteAndAllAndDir(true, false, true), ForbiddenException.class, + userWithDeleteDirAndAllAndPurge); + assertFailureFor(() -> bean.deleteAndAllAndDir(false, true, true), ForbiddenException.class, + userWithDeleteDirAndAllAndPurge); + var userWithPurge = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("purge")), true); + assertFailureFor(() -> bean.deleteAndAllAndDir(true, true, false), ForbiddenException.class, userWithPurge); + assertFailureFor(() -> bean.deleteAndAllAndDir(true, false, true), ForbiddenException.class, userWithPurge); + assertFailureFor(() -> bean.deleteAndAllAndDir(false, true, true), ForbiddenException.class, userWithPurge); + + // access is granted because all arguments are true + assertSuccess(() -> bean.deleteAndAllAndDir(true, true, true), "deleteAndAllAndDir", userWithDeleteDirAndAllAndPurge); + assertSuccess(() -> bean.deleteAndAllAndDir(true, true, true), "deleteAndAllAndDir", userWithPurge); + + // access is not granted because identity doesn't have 'purge' permission + assertFailureFor(() -> bean.deleteAndAllAndDir(true, true, true), ForbiddenException.class, USER_WITH_AUGMENTORS); + } + + @Test + public void testPermissionCheckersForNamesWithActionSeparatorsOnly() { + // @PermissionsAllowed({ "edit:all", "edit", "edit:essay" }) + // permission checker is defined for: edit:all, edit:essay + + // assert that edit:all and edit:essay permission checkers grant access + assertSuccess(() -> bean.editAndEditAllAndEssay(true, false), "editAndEditAllAndEssay", USER_WITH_AUGMENTORS); + assertSuccess(() -> bean.editAndEditAllAndEssay(false, true), "editAndEditAllAndEssay", USER_WITH_AUGMENTORS); + assertSuccess(() -> bean.editAndEditAllAndEssay(true, true), "editAndEditAllAndEssay", USER_WITH_AUGMENTORS); + + // assert that 'edit' can be granted by identity permission + var userWithEdit = new AuthData(USER, true, new StringPermission("edit")); + assertSuccess(() -> bean.editAndEditAllAndEssay(false, false), "editAndEditAllAndEssay", userWithEdit); + + // assert user without either the identity permission or granted access by the checker method cannot access + assertFailureFor(() -> bean.editAndEditAllAndEssay(false, false), ForbiddenException.class, USER_WITH_AUGMENTORS); + + // @PermissionsAllowed({ "list:files", "list:dir" }) + // permission checker is defined for 'list:files' + + // is allowed because the checker grants access + assertSuccess(() -> bean.listFilesAndDir(true), "listFilesAndDir", USER_WITH_AUGMENTORS); + + // is not allowed because the checker does not grant access + assertFailureFor(() -> bean.listFilesAndDir(false), ForbiddenException.class, USER_WITH_AUGMENTORS); + + // is not allowed because identity permission cannot grant access with the 'list:files' when such checker exists + var userListFiles = new AuthData(USER, true, new StringPermission("list", "files")); + assertFailureFor(() -> bean.listFilesAndDir(false), ForbiddenException.class, userListFiles); + + // is allowed because identity permission can grant access with the 'list:dir' as no such checker exists + var userListDir = new AuthData(USER, true, new StringPermission("list", "dir")); + assertSuccess(() -> bean.listFilesAndDir(false), "listFilesAndDir", userListDir); + + // @PermissionsAllowed({ "list:files", "list:links" }) + // there is a permission checker for both permissions + assertSuccess(() -> bean.listFilesAndLinks(true, true), "listFilesAndLinks", USER_WITH_AUGMENTORS); + assertSuccess(() -> bean.listFilesAndLinks(true, false), "listFilesAndLinks", USER_WITH_AUGMENTORS); + assertSuccess(() -> bean.listFilesAndLinks(false, true), "listFilesAndLinks", USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.listFilesAndLinks(false, false), ForbiddenException.class, USER_WITH_AUGMENTORS); + var userWithListFilesAndLinks = new AuthData(USER, true, new StringPermission("list", "files", "links")); + assertFailureFor(() -> bean.listFilesAndLinks(false, false), ForbiddenException.class, userWithListFilesAndLinks); + } + + @Test + public void testInclusivePermissionCheckersAndRepeatedAnnotations() { + // @PermissionsAllowed("cut:array") + // @PermissionsAllowed(value = { "cut:blob", "cut:chars" }, inclusive = true) + // @PermissionsAllowed(value = { "cut:text", "cut:binary" }, inclusive = true) + // @PermissionsAllowed({ "cut:text", "cut:binary" }) - SHOULD be irrelevant + assertSuccess(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, true, true, true, true), + "cutTextAndBinaryAndBlobAndArrayAndChars", USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(false, true, true, true, true), + ForbiddenException.class, USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, false, true, true, true), + ForbiddenException.class, USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, true, false, true, true), + ForbiddenException.class, USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, true, true, false, true), + ForbiddenException.class, USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, true, true, true, false), + ForbiddenException.class, USER_WITH_AUGMENTORS); + var userWithAllCutActions = new AuthData(USER, true, + new StringPermission("cut", "array", "blob", "chars", "text", "binary")); + // assert failures as only checkers can grant these permissions + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(false, false, false, false, false), + ForbiddenException.class, userWithAllCutActions); + } + + @ApplicationScoped + public static class SecuredBean { + + @PermissionsAllowed({ "read", "read:all" }) + String readAndReadAll(boolean read) { + return "readAndReadAll"; + } + + @PermissionsAllowed({ "write:all", "write", "write:essay" }) + String writeAndWriteAllAndEssay(boolean write) { + return "writeAndWriteAllAndEssay"; + } + + @PermissionsAllowed(value = { "execute", "execute:all", "execute:dir" }, inclusive = true) + String executeAndAllAndDir(boolean execute, boolean executeAll, boolean executeDir) { + return "executeAndAllAndDir"; + } + + @PermissionsAllowed(value = { "delete", "delete:all", "delete:dir", "purge" }, inclusive = true) + String deleteAndAllAndDir(boolean delete, boolean deleteAll, boolean deleteDir) { + return "deleteAndAllAndDir"; + } + + @PermissionsAllowed({ "edit:all", "edit", "edit:essay" }) + String editAndEditAllAndEssay(boolean editAll, boolean editEssay) { + return "editAndEditAllAndEssay"; + } + + @PermissionsAllowed({ "list:files", "list:dir" }) + String listFilesAndDir(boolean listFiles) { + return "listFilesAndDir"; + } + + @PermissionsAllowed({ "list:files", "list:links" }) + String listFilesAndLinks(boolean listFiles, boolean listLinks) { + return "listFilesAndLinks"; + } + + @PermissionsAllowed("cut:array") + @PermissionsAllowed(value = { "cut:blob", "cut:chars" }, inclusive = true) + @PermissionsAllowed(value = { "cut:text", "cut:binary" }, inclusive = true) + @PermissionsAllowed({ "cut:text", "cut:binary" }) // this one SHOULD not have effect due to the instance above + String cutTextAndBinaryAndBlobAndArrayAndChars(boolean cutText, boolean cutBinary, boolean cutArray, boolean cutChars, + boolean cutBlob) { + return "cutTextAndBinaryAndBlobAndArrayAndChars"; + } + } + + @ApplicationScoped + public static class PermissionCheckers { + + @PermissionChecker("read") + boolean canRead(boolean read) { + return read; + } + + @PermissionChecker("write") + boolean canWrite(boolean write) { + return write; + } + + @PermissionChecker("execute") + boolean canExecute(boolean execute) { + return execute; + } + + @PermissionChecker("execute:all") + boolean canExecuteAll(boolean executeAll) { + return executeAll; + } + + @PermissionChecker("execute:dir") + boolean canExecuteDir(boolean executeDir) { + return executeDir; + } + + @PermissionChecker("delete") + boolean canDelete(boolean delete) { + return delete; + } + + @PermissionChecker("delete:all") + boolean canDeleteAll(boolean deleteAll) { + return deleteAll; + } + + @PermissionChecker("delete:dir") + boolean canDeleteDir(boolean deleteDir) { + return deleteDir; + } + + @PermissionChecker("edit:all") + boolean canEditAll(boolean editAll) { + return editAll; + } + + @PermissionChecker("edit:essay") + boolean canEditEssay(boolean editEssay) { + return editEssay; + } + + @PermissionChecker("list:files") + boolean canListFiles(boolean listFiles) { + return listFiles; + } + + @PermissionChecker("list:links") + boolean canListLinks(boolean listLinks) { + return listLinks; + } + + @PermissionChecker("cut:text") + boolean canCutText(boolean cutText) { + return cutText; + } + + @PermissionChecker("cut:binary") + boolean canCutBinary(boolean cutBinary) { + return cutBinary; + } + + @PermissionChecker("cut:blob") + boolean canCutBlob(boolean cutBlob) { + return cutBlob; + } + + @PermissionChecker("cut:array") + boolean canCutArray(boolean cutArray) { + return cutArray; + } + + @PermissionChecker("cut:chars") + boolean canCutChars(boolean cutChars) { + return cutChars; + } + + } +} diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/UnknownCheckerParamValidationFailureTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/UnknownCheckerParamValidationFailureTest.java index 55fa1603bd4c7..42b81fc04ff4f 100644 --- a/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/UnknownCheckerParamValidationFailureTest.java +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/UnknownCheckerParamValidationFailureTest.java @@ -18,7 +18,8 @@ public class UnknownCheckerParamValidationFailureTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .assertException(t -> { Assertions.assertEquals(RuntimeException.class, t.getClass(), t.getMessage()); - Assertions.assertTrue(t.getMessage().contains("No 'securedBean' formal parameter name matches")); + Assertions.assertTrue(t.getMessage().contains("No '")); + Assertions.assertTrue(t.getMessage().contains("SecuredBean#securedBean' formal parameter name matches")); Assertions.assertTrue(t.getMessage().contains("SecuredBean#check")); Assertions.assertTrue(t.getMessage().contains("parameter name 'unknownParameter'")); }); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java index d17b2ddcf9e8b..1cb02e5e4e614 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java @@ -12,6 +12,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -39,9 +40,9 @@ public class ResourceNotFoundData { private String baseUrl; private String httpRoot; - private List endpointRoutes; - private Set staticRoots; - private List additionalEndpoints; + private List endpointRoutes = Collections.emptyList(); + private Set staticRoots = Collections.emptySet(); + private List additionalEndpoints = Collections.emptyList(); public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java index b332eb3e72bff..cfcd32c452637 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java @@ -67,7 +67,7 @@ void addPlatformRelease(String propertyName, String propertyValue) { final String platformKey = propertyName.substring(PROPERTY_PREFIX.length(), platformKeyStreamSep); final String streamId = propertyName.substring(platformKeyStreamSep + 1, streamVersionSep); final String version = propertyName.substring(streamVersionSep + 1); - allPlatformInfo.computeIfAbsent(platformKey, k -> new PlatformInfo(k)).getOrCreateStream(streamId).addIfNotPresent( + allPlatformInfo.computeIfAbsent(platformKey, PlatformInfo::new).getOrCreateStream(streamId).addIfNotPresent( version, () -> { final PlatformReleaseInfo ri = new PlatformReleaseInfo(platformKey, streamId, version, propertyValue); @@ -81,10 +81,7 @@ public void addPlatformDescriptor(String groupId, String artifactId, String clas artifactId.substring(0, artifactId.length() - BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX.length()), version); - platformImports.computeIfAbsent(bomCoords, c -> { - platformBoms.add(bomCoords); - return new PlatformImport(); - }).descriptorFound = true; + platformImports.computeIfAbsent(bomCoords, this::newPlatformImport).descriptorFound = true; } public void addPlatformProperties(String groupId, String artifactId, String classifier, String type, String version, @@ -93,7 +90,7 @@ public void addPlatformProperties(String groupId, String artifactId, String clas artifactId.substring(0, artifactId.length() - BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX.length()), version); - platformImports.computeIfAbsent(bomCoords, c -> new PlatformImport()); + platformImports.computeIfAbsent(bomCoords, this::newPlatformImport); importedPlatformBoms.computeIfAbsent(groupId, g -> new ArrayList<>()); if (!importedPlatformBoms.get(groupId).contains(bomCoords)) { importedPlatformBoms.get(groupId).add(bomCoords); @@ -117,6 +114,17 @@ public void addPlatformProperties(String groupId, String artifactId, String clas } } + /** + * This method is meant to be called when a new platform BOM import was detected. + * + * @param bom platform BOM coordinates + * @return new platform import instance + */ + private PlatformImport newPlatformImport(ArtifactCoords bom) { + platformBoms.add(bom); + return new PlatformImport(); + } + public void setPlatformProperties(Map platformProps) { this.collectedProps.putAll(platformProps); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java index ea066ae352e12..5ba6fd50d5d77 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java @@ -1,6 +1,5 @@ package org.jboss.resteasy.reactive.client.handlers; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -41,6 +40,7 @@ import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; +import io.netty.buffer.ByteBufInputStream; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.LastHttpContent; @@ -361,7 +361,7 @@ public void handle(AsyncResult ar) { try { if (buffer.length() > 0) { requestContext.setResponseEntityStream( - new ByteArrayInputStream(buffer.getBytes())); + new ByteBufInputStream(buffer.getByteBuf())); } else { requestContext.setResponseEntityStream(null); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java index 99fa3837fd647..3234442eb35b2 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java @@ -19,7 +19,7 @@ import jakarta.ws.rs.ext.ReaderInterceptor; import jakarta.ws.rs.ext.ReaderInterceptorContext; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyReader; import org.jboss.resteasy.reactive.client.spi.MissingMessageBodyReaderErrorMessageContextualizer; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; @@ -76,15 +76,16 @@ public Object proceed() throws IOException, WebApplicationException { for (MessageBodyReader> reader : readers) { if (reader.isReadable(entityClass, entityType, annotations, mediaType)) { try { - if (reader instanceof ClientRestHandler) { - try { - ((ClientRestHandler) reader).handle(clientRequestContext); - } catch (Exception e) { - throw new WebApplicationException("Can't inject the client request context", e); - } + if (reader instanceof ClientMessageBodyReader) { + return ((ClientMessageBodyReader) reader).readFrom(entityClass, entityType, annotations, mediaType, + headers, + inputStream, clientRequestContext); + } else { + return ((MessageBodyReader) reader).readFrom(entityClass, entityType, annotations, mediaType, + headers, + inputStream); } - return ((MessageBodyReader) reader).readFrom(entityClass, entityType, annotations, mediaType, headers, - inputStream); + } catch (IOException e) { throw new ProcessingException(e); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java index fc837e422aae4..298cb8ebf7f5d 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java @@ -26,7 +26,7 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.client.providers.serialisers.ClientDefaultTextPlainBodyHandler; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyWriter; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.core.UnmanagedBeanFactory; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; @@ -115,16 +115,13 @@ public static Buffer invokeClientWriter(Entity> entity, Object entityObject, C if (writer.isWriteable(entityClass, entityType, entity.getAnnotations(), entity.getMediaType())) { if ((writerInterceptors == null) || writerInterceptors.length == 0) { VertxBufferOutputStream out = new VertxBufferOutputStream(); - if (writer instanceof ClientRestHandler) { - try { - ((ClientRestHandler) writer).handle(clientRequestContext); - } catch (Exception e) { - throw new WebApplicationException("Can't inject the client request context", e); - } + if (writer instanceof ClientMessageBodyWriter cw) { + cw.writeTo(entityObject, entityClass, entityType, entity.getAnnotations(), + entity.getMediaType(), headerMap, out, clientRequestContext); + } else { + writer.writeTo(entityObject, entityClass, entityType, entity.getAnnotations(), + entity.getMediaType(), headerMap, out); } - - writer.writeTo(entityObject, entityClass, entityType, entity.getAnnotations(), - entity.getMediaType(), headerMap, out); return out.getBuffer(); } else { return runClientWriterInterceptors(entityObject, entityClass, entityType, entity.getAnnotations(), diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientWriterInterceptorContextImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientWriterInterceptorContextImpl.java index 72ca6b69d29a0..ca517339db61e 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientWriterInterceptorContextImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientWriterInterceptorContextImpl.java @@ -16,7 +16,7 @@ import jakarta.ws.rs.ext.WriterInterceptor; import jakarta.ws.rs.ext.WriterInterceptorContext; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyWriter; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; @@ -70,16 +70,14 @@ public void proceed() throws IOException, WebApplicationException { effectiveWriter = newWriters.get(0); } - if (effectiveWriter instanceof ClientRestHandler) { - try { - ((ClientRestHandler) effectiveWriter).handle(clientRequestContext); - } catch (Exception e) { - throw new WebApplicationException("Can't inject the client request context", e); - } + if (effectiveWriter instanceof ClientMessageBodyWriter cw) { + cw.writeTo(entity, entityClass, entityType, + annotations, mediaType, headers, outputStream, clientRequestContext); + } else { + effectiveWriter.writeTo(entity, entityClass, entityType, + annotations, mediaType, headers, outputStream); } - effectiveWriter.writeTo(entity, entityClass, entityType, - annotations, mediaType, headers, outputStream); outputStream.close(); result = Buffer.buffer(baos.toByteArray()); done = true; diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyReader.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyReader.java new file mode 100644 index 0000000000000..563c6e9e83114 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyReader.java @@ -0,0 +1,20 @@ +package org.jboss.resteasy.reactive.client.spi; + +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; + +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; + +public interface ClientMessageBodyReader extends MessageBodyReader { + + T readFrom(Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream, + RestClientRequestContext context) throws java.io.IOException, jakarta.ws.rs.WebApplicationException; +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyWriter.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyWriter.java new file mode 100644 index 0000000000000..0a4c396ba5e03 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyWriter.java @@ -0,0 +1,22 @@ +package org.jboss.resteasy.reactive.client.spi; + +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; + +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; + +public interface ClientMessageBodyWriter extends MessageBodyWriter { + + void writeTo(T t, Class> type, Type genericType, Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream, + RestClientRequestContext context) + throws java.io.IOException, jakarta.ws.rs.WebApplicationException; + +} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml index 4fbe9a5db08b6..113cbbe4106e0 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml @@ -32,9 +32,6 @@ jobs: runs-on: ubuntu-latest steps: - - name: Install yq - uses: dcarbone/install-yq-action@v1.0.1 - - name: Set up Java uses: actions/setup-java@v4 with: diff --git a/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java b/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java index 17ca851fdd315..6c193564584ec 100644 --- a/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java +++ b/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java @@ -20,6 +20,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; @Path("/api") @ApplicationScoped @@ -45,4 +47,11 @@ public String forbidden() { return "authorized"; } + @GET + @Path("/requiresRootRole") + @RolesAllowed("root") + public String getPrincipalName(@Context SecurityContext securityContext) { + return securityContext.getUserPrincipal().getName(); + } + } diff --git a/integration-tests/elytron-security-ldap/src/main/resources/application.properties b/integration-tests/elytron-security-ldap/src/main/resources/application.properties index 4454fc4de0f62..4546c1d3776c1 100644 --- a/integration-tests/elytron-security-ldap/src/main/resources/application.properties +++ b/integration-tests/elytron-security-ldap/src/main/resources/application.properties @@ -14,3 +14,5 @@ quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou= quarkus.security.ldap.cache.enabled=true quarkus.security.ldap.cache.max-age=60s quarkus.security.ldap.cache.size=10 + +quarkus.http.auth.roles-mapping."adminRole"=root diff --git a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java deleted file mode 100644 index c0144886d8b05..0000000000000 --- a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.quarkus.elytron.security.ldap.it; - -import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.ldap.LdapServerTestResource; - -@QuarkusTestResource(LdapServerTestResource.class) -public class ElytronLdapExtensionTestResources { -} diff --git a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java index e2e107bdb26e6..5d4f2b0518b00 100644 --- a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java +++ b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java @@ -6,11 +6,13 @@ import org.junit.jupiter.api.Test; import com.unboundid.ldap.listener.InMemoryDirectoryServer; -import com.unboundid.ldap.sdk.LDAPException; +import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.ldap.LdapServerTestResource; import io.restassured.RestAssured; +@WithTestResource(LdapServerTestResource.class) @QuarkusTest class ElytronSecurityLdapTest { @@ -96,9 +98,31 @@ void admin_role_not_authorized() { .statusCode(403); } - @Test() + @Test @Order(8) - void standard_role_authenticated_cached() throws LDAPException { + void testMappingOfLdapGroupsToIdentityRoles() { + // LDAP groups are added as SecurityIdentity roles + // according to the quarkus.security.ldap.identity-mapping.attribute-mappings + // this test verifies that LDAP groups can be remapped to application-specific SecurityIdentity roles + // role 'adminRole' comes from 'cn' and we remapped it to 'root' + RestAssured.given() + .auth().preemptive().basic("standardUser", "standardUserPassword") + .when() + .get("/api/requiresRootRole") + .then() + .statusCode(403); + RestAssured.given() + .auth().preemptive().basic("adminUser", "adminUserPassword") + .when() + .get("/api/requiresRootRole") + .then() + .statusCode(200) + .body(containsString("adminUser")); // that is uid + } + + @Test + @Order(9) + void standard_role_authenticated_cached() { RestAssured.given() .redirects().follow(false) .when() diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml index 90b8196c640d0..2b31dd449d072 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml @@ -32,9 +32,6 @@ jobs: runs-on: ubuntu-latest steps: - - name: Install yq - uses: dcarbone/install-yq-action@v1.0.1 - - name: Set up Java uses: actions/setup-java@v4 with: diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java index 6d54d94df2376..5dd767961d316 100644 --- a/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java +++ b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java @@ -68,4 +68,14 @@ public Uni getReactiveInvalidOperation() { .replaceWithVoid(); } + // tainted + @GET + @Path("/tainted") + public String getTainted() { + ds.withConnection(conn -> { + conn.select(7); // taints the connection + conn.value(String.class).get("foobar"); + }); + return "OK"; + } } diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java index b0ed21891c967..40cc3f7566473 100644 --- a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java +++ b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java @@ -13,6 +13,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; 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 java.time.Duration; import java.util.List; @@ -195,6 +197,24 @@ public void reactiveInvalidOperation() { checkForException(exception); } + @Test + public void taintedConnection() { + RestAssured.get("redis/tainted") + .then() + .statusCode(200) + .body(CoreMatchers.is("OK")); + + Awaitility.await().atMost(Duration.ofSeconds(10)).until(() -> getSpans().size() == 3); + + Map span = findSpan(getSpans(), m -> SpanKind.CLIENT.name().equals(m.get("kind")) + && "get".equals(m.get("name"))); + Map attributes = (Map) span.get("attributes"); + + assertEquals("redis", attributes.get("db.system")); + assertEquals("get", attributes.get("db.operation")); + assertFalse(span.containsKey("db.redis.database_index")); + } + private List> getSpans() { return get("/opentelemetry/export").body().as(new TypeRef<>() { });
* Non-standard levels are counted with the lower standard level. */ -public class LogMetricsHandler extends Handler { +public class LogMetricsHandler extends ExtHandler { final NavigableMap logCounters; @@ -25,15 +25,13 @@ public LogMetricsHandler(NavigableMap logCounters) { } @Override - public void publish(LogRecord record) { - if (isLoggable(record)) { - Entry counter = logCounters.floorEntry(record.getLevel().intValue()); - if (counter != null) { - counter.getValue().increment(); - } else { - // Default to TRACE for anything lower - logCounters.get(Level.TRACE.intValue()).increment(); - } + protected void doPublish(ExtLogRecord record) { + Entry counter = logCounters.floorEntry(record.getLevel().intValue()); + if (counter != null) { + counter.getValue().increment(); + } else { + // Default to TRACE for anything lower + logCounters.get(Level.TRACE.intValue()).increment(); } } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 2d390ab877db0..3d8fe47a07940 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -33,6 +33,8 @@ import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.spi.ConfigSource; import org.jboss.logmanager.ExtFormatter; +import org.jboss.logmanager.ExtHandler; +import org.jboss.logmanager.ExtLogRecord; import org.jboss.logmanager.LogContext; import org.jboss.logmanager.LogContextInitializer; import org.jboss.logmanager.Logger; @@ -183,9 +185,9 @@ public void accept(String loggerName, CleanupFilterConfig config) { handlers.add(consoleHandler); } if (launchMode.isDevOrTest()) { - handlers.add(new Handler() { + handlers.add(new ExtHandler() { @Override - public void publish(LogRecord record) { + protected void doPublish(ExtLogRecord record) { if (record.getThrown() != null) { ExceptionReporting.notifyException(record.getThrown()); } @@ -613,9 +615,9 @@ private static Handler configureConsoleHandler( if (color && launchMode.isDevOrTest() && !config.async().enable()) { final Handler delegate = handler; - handler = new Handler() { + handler = new ExtHandler() { @Override - public void publish(LogRecord record) { + protected void doPublish(ExtLogRecord record) { BiConsumer> formatter = CurrentAppExceptionHighlighter.THROWABLE_FORMATTER; if (formatter != null) { formatter.accept(record, delegate::publish); diff --git a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc index 8ab61ce549e68..db6df8d52e591 100644 --- a/docs/src/main/asciidoc/deploying-to-kubernetes.adoc +++ b/docs/src/main/asciidoc/deploying-to-kubernetes.adoc @@ -301,7 +301,7 @@ of secret that contains the required credentials. Quarkus can automatically gene quarkus.kubernetes.generate-image-pull-secret=true ---- -More specifically a `Secret`like the one bellow is genrated: +More specifically a `Secret` like the one below is generated: [source,yaml] ---- diff --git a/docs/src/main/asciidoc/native-reference.adoc b/docs/src/main/asciidoc/native-reference.adoc index a9e8baa63417d..fd6982ce273ff 100644 --- a/docs/src/main/asciidoc/native-reference.adoc +++ b/docs/src/main/asciidoc/native-reference.adoc @@ -2293,14 +2293,29 @@ One can see which Mandrel version was used to generate a binary by inspecting th [source,bash] ---- -$ strings target/debugging-native-1.0.0-SNAPSHOT-runner | grep GraalVM -com.oracle.svm.core.VM=GraalVM 22.0.0.2-Final Java 11 Mandrel Distribution +$ strings target/debugging-native-1.0.0-SNAPSHOT-runner | \ + grep -e 'com.oracle.svm.core.VM=' -e 'com.oracle.svm.core.VM.Java.Version' -F +com.oracle.svm.core.VM.Java.Version=21.0.5 +com.oracle.svm.core.VM=Mandrel-23.1.5.0-Final ---- === How do I enable GC logging in native executables? See <> for details. +=== Can I get a thread dump of a native executable? + +Yes, Quarkus sets up a signal handler for the `SIGQUIT` signal (or `SIGBREAK` on windows) that will result in a thread +dump being printed when receiving the `SIGQUIT/SIGBREAK` signal. +You may use `kill -SIGQUIT ` to trigger a thread dump. + +[NOTE] +==== +Quarkus uses its own signal handler, to use GraalVM's default signal handler instead you will need to: +1. Add `-H:+DumpThreadStacksOnSignal` to `quarkus.native.additional-build-args` and rebuild the application. +2. Set the environment variable `DISABLE_SIGNAL_HANDLERS` before running the app. +==== + [[heap-dumps]] === Can I get a heap dump of a native executable? e.g. if it runs out of memory diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc index f72c020ab44c1..bc15e9117e74f 100644 --- a/docs/src/main/asciidoc/scheduler-reference.adoc +++ b/docs/src/main/asciidoc/scheduler-reference.adoc @@ -156,7 +156,7 @@ Property Expressions. .Time Zone Configuration Property Example [source,java] ---- -@Scheduled(cron = "0 15 10 * * ?", timeZone = "{myMethod.timeZone}") +@Scheduled(cron = "0 15 10 * * ?", timeZone = "${myMethod.timeZone}") void myMethod() { } ---- diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index 151064e376e9a..30fe4e03adb6a 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -1122,6 +1122,49 @@ public class ProjectPermissionChecker { TIP: Permission checks run by default on event loops. Annotate a permission checker method with the `io.smallrye.common.annotation.Blocking` annotation if you want to run the check on a worker thread. +Matching between the `@PermissionsAllowed` values and the `@PermissionChecker` value is based on string equality as shown in the example below: + +[source,java] +---- +package org.acme.security; + +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class FileService { + + @PermissionsAllowed({ "delete:all", "delete:dir" }) <1> + void deleteDirectory(Path directoryPath) { + // delete directory + } + + @PermissionsAllowed(value = { "delete:service", "delete:file" }, inclusive = true) <2> + void deleteServiceFile(Path serviceFilePath) { + // delete service file + } + + @PermissionChecker("delete:all") + boolean canDeleteAllDirectories(SecurityIdentity identity) { + String filePermissions = identity.getAttribute("user-group-file-permissions"); + return filePermissions != null && filePermissions.contains("w"); + } + + @PermissionChecker("delete:service") + boolean canDeleteService(SecurityIdentity identity) { + return identity.hasRole("admin"); + } + + @PermissionChecker("delete:file") + boolean canDeleteFile(Path serviceFilePath) { + return serviceFilePath != null && !serviceFilePath.endsWith("critical"); + } +} +---- +<1> The permission checker method `canDeleteAllDirectories` grants access to the `deleteDirectory` because the `delete:all` values are equal. +<2> There must be exactly two permission checker methods, one for the `delete:service` permission and other for the `delete:file` permission. + [[permission-meta-annotation]] ==== Create permission meta-annotations diff --git a/docs/src/main/asciidoc/security-ldap.adoc b/docs/src/main/asciidoc/security-ldap.adoc index 83b1695367a83..977ac6abacd07 100644 --- a/docs/src/main/asciidoc/security-ldap.adoc +++ b/docs/src/main/asciidoc/security-ldap.adoc @@ -177,6 +177,18 @@ quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou= The `elytron-security-ldap` extension requires a dir-context and an identity-mapping with at least one attribute-mapping to authenticate the user and its identity. +=== Map LDAP groups to `SecurityIdentity` roles + +Previously described application configuration showed how to map `CN` attribute of the LDAP Distinguished Name group to a Quarkus `SecurityIdentity` role. +More specifically, the `standardRole` CN was mapped to a `SecurityIdentity` role and thus allowed access to the `UserResource#me` endpoint. +However, required `SecurityIdentity` roles may differ between applications and you may need to map LDAP groups to local `SecurityIdentity` roles like in the example below: + +[source,properties] +---- +quarkus.http.auth.roles-mapping."standardRole"=user <1> +---- +<1> Map the `standardRole` role to the application-specific `SecurityIdentity` role `user`. + == Testing the Application The application is now protected and the identities are provided by our LDAP server. diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalEventLoggingListener.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalEventLoggingListener.java index c0959df54864f..fb6181b5140e7 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalEventLoggingListener.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/AgroalEventLoggingListener.java @@ -67,7 +67,12 @@ public void onConnectionDestroy(Connection connection) { @Override public void onWarning(String warning) { - log.warnv("{0}: {1}", datasourceName, warning); + // See https://github.com/quarkusio/quarkus/issues/44047 + if (warning != null && warning.contains("JDBC resources leaked")) { + log.debugv("{0}: {1}", datasourceName, warning); + } else { + log.warnv("{0}: {1}", datasourceName, warning); + } } @Override diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/ExtendedCharactersSupport.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/ExtendedCharactersSupport.java index e01f0ee3f278a..875f8e1db2349 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/ExtendedCharactersSupport.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/ExtendedCharactersSupport.java @@ -5,12 +5,13 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.pkg.NativeConfig; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.jdbc.oracle.runtime.OracleInitRecorder; public final class ExtendedCharactersSupport { @Record(STATIC_INIT) - @BuildStep + @BuildStep(onlyIf = NativeOrNativeSourcesBuild.class) public void preinitializeCharacterSets(NativeConfig config, OracleInitRecorder recorder) { recorder.setupCharSets(config.addAllCharsets()); } diff --git a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java index 7184b831e6173..fb18910d4c80d 100644 --- a/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java +++ b/extensions/jdbc/jdbc-oracle/deployment/src/main/java/io/quarkus/jdbc/oracle/deployment/OracleMetadataOverrides.java @@ -12,6 +12,7 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem; +import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild; import io.quarkus.maven.dependency.ArtifactKey; /** @@ -35,7 +36,7 @@ * require it, so this would facilitate the option to revert to the older version in * case of problems. */ -@BuildSteps +@BuildSteps(onlyIf = NativeOrNativeSourcesBuild.class) public final class OracleMetadataOverrides { static final String DRIVER_JAR_MATCH_REGEX = "com\\.oracle\\.database\\.jdbc"; diff --git a/extensions/mongodb-client/deployment/pom.xml b/extensions/mongodb-client/deployment/pom.xml index 8bd99d1840900..ddd07240c5390 100644 --- a/extensions/mongodb-client/deployment/pom.xml +++ b/extensions/mongodb-client/deployment/pom.xml @@ -91,6 +91,11 @@ assertj-core test + + io.rest-assured + rest-assured + test + io.quarkus quarkus-resteasy-deployment diff --git a/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/MongoClientConfigTest.java b/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/MongoClientConfigTest.java index 3708be970d06e..24c6be8458d7b 100644 --- a/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/MongoClientConfigTest.java +++ b/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/MongoClientConfigTest.java @@ -1,11 +1,13 @@ package io.quarkus.mongodb; +import static io.restassured.RestAssured.when; import static org.assertj.core.api.Assertions.assertThat; import java.util.concurrent.TimeUnit; import jakarta.inject.Inject; +import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -94,4 +96,11 @@ public void testReactiveClientConfiuration() { assertThat(clientImpl.getSettings().getReadConcern()).isEqualTo(new ReadConcern(ReadConcernLevel.SNAPSHOT)); assertThat(clientImpl.getSettings().getReadPreference()).isEqualTo(ReadPreference.primary()); } + + @Test + public void healthCheck() { + when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("UP")); + } } diff --git a/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/NoConnectionHealthCheckTest.java b/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/NoConnectionHealthCheckTest.java new file mode 100644 index 0000000000000..c68bba88da4d9 --- /dev/null +++ b/extensions/mongodb-client/deployment/src/test/java/io/quarkus/mongodb/NoConnectionHealthCheckTest.java @@ -0,0 +1,52 @@ +package io.quarkus.mongodb; + +import static io.restassured.RestAssured.when; + +import jakarta.inject.Inject; + +import org.bson.Document; +import org.hamcrest.CoreMatchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.mongodb.MongoClientException; +import com.mongodb.client.MongoClient; + +import io.quarkus.test.QuarkusUnitTest; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class NoConnectionHealthCheckTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .overrideConfigKey("quarkus.devservices.enabled", "false") + .overrideConfigKey("quarkus.mongodb.connection-string", "mongodb://localhost:9999") + // timeouts set to the test doesn't take too long to run + .overrideConfigKey("quarkus.mongodb.connect-timeout", "2s") + .overrideConfigKey("quarkus.mongodb.server-selection-timeout", "2s") + .overrideConfigKey("quarkus.mongodb.read-timeout", "2s"); + + @Inject + MongoClient mongo; + + @Order(1) // done to ensure the health check runs before any application code touches the database + @Test + public void healthCheck() { + when().get("/q/health/ready") + .then() + .body("status", CoreMatchers.equalTo("DOWN")); + } + + @Order(2) + @Test + public void tryConnection() { + Assertions.assertThrows(MongoClientException.class, () -> { + mongo.getDatabase("admin").runCommand(new Document("ping", 1)); + }); + } + +} diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java index 953971bcf3455..382e3fb699238 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/health/MongoHealthCheck.java @@ -42,7 +42,7 @@ public class MongoHealthCheck implements HealthCheck { private static final Document COMMAND = new Document("ping", 1); - public void configure(MongodbConfig config) { + public MongoHealthCheck(MongodbConfig config) { Iterable> handle = Arc.container().select(MongoClient.class, Any.Literal.INSTANCE) .handles(); Iterable> reactiveHandlers = Arc.container() @@ -61,7 +61,7 @@ public void configure(MongodbConfig config) { } } - config.mongoClientConfigs.forEach(new BiConsumer() { + config.mongoClientConfigs.forEach(new BiConsumer<>() { @Override public void accept(String name, MongoClientConfig cfg) { MongoClient client = getClient(handle, name); @@ -76,7 +76,6 @@ public void accept(String name, MongoClientConfig cfg) { } } }); - } private MongoClient getClient(Iterable> handle, String name) { diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java index e538026fe1f3a..446fda0de4351 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClients.java @@ -58,12 +58,9 @@ import com.mongodb.event.ConnectionPoolListener; import com.mongodb.reactivestreams.client.ReactiveContextProvider; -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; import io.quarkus.credentials.CredentialsProvider; import io.quarkus.credentials.runtime.CredentialsProviderFinder; import io.quarkus.mongodb.MongoClientName; -import io.quarkus.mongodb.health.MongoHealthCheck; import io.quarkus.mongodb.impl.ReactiveMongoClientImpl; import io.quarkus.mongodb.reactive.ReactiveMongoClient; @@ -111,17 +108,6 @@ public MongoClients(MongodbConfig mongodbConfig, MongoClientSupport mongoClientS Class.forName("sun.net.ext.ExtendedSocketOptions", true, ClassLoader.getSystemClassLoader()); } catch (ClassNotFoundException ignored) { } - - try { - Class.forName("org.eclipse.microprofile.health.HealthCheck"); - InstanceHandle instance = Arc.container() - .instance(MongoHealthCheck.class, Any.Literal.INSTANCE); - if (instance.isAvailable()) { - instance.get().configure(mongodbConfig); - } - } catch (ClassNotFoundException e) { - // Ignored - no health check - } } public MongoClient createMongoClient(String clientName) throws MongoException { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java index 9c4606ea68e99..65e2de1270e70 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/logs/OpenTelemetryLogHandler.java @@ -13,12 +13,11 @@ import java.time.Instant; import java.util.Map; import java.util.Optional; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogRecord; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logmanager.ExtHandler; import org.jboss.logmanager.ExtLogRecord; import io.opentelemetry.api.OpenTelemetry; @@ -28,7 +27,7 @@ import io.opentelemetry.api.logs.LogRecordBuilder; import io.opentelemetry.api.logs.Severity; -public class OpenTelemetryLogHandler extends Handler { +public class OpenTelemetryLogHandler extends ExtHandler { private final OpenTelemetry openTelemetry; @@ -37,7 +36,7 @@ public OpenTelemetryLogHandler(final OpenTelemetry openTelemetry) { } @Override - public void publish(LogRecord record) { + protected void doPublish(ExtLogRecord record) { if (openTelemetry == null) { return; // might happen at shutdown } @@ -60,24 +59,22 @@ public void publish(LogRecord record) { attributes.put(CODE_NAMESPACE, record.getSourceClassName()); attributes.put(CODE_FUNCTION, record.getSourceMethodName()); - if (record instanceof ExtLogRecord) { - attributes.put(CODE_LINENO, ((ExtLogRecord) record).getSourceLineNumber()); - attributes.put(THREAD_NAME, ((ExtLogRecord) record).getThreadName()); - attributes.put(THREAD_ID, ((ExtLogRecord) record).getLongThreadID()); - attributes.put(AttributeKey.stringKey("log.logger.namespace"), - ((ExtLogRecord) record).getLoggerClassName()); - - final Map mdcCopy = ((ExtLogRecord) record).getMdcCopy(); - if (mdcCopy != null) { - mdcCopy.forEach((k, v) -> { - // ignore duplicated span data already in the MDC - if (!k.toLowerCase().equals("spanid") && - !k.toLowerCase().equals("traceid") && - !k.toLowerCase().equals("sampled")) { - attributes.put(AttributeKey.stringKey(k), v); - } - }); - } + attributes.put(CODE_LINENO, record.getSourceLineNumber()); + attributes.put(THREAD_NAME, record.getThreadName()); + attributes.put(THREAD_ID, record.getLongThreadID()); + attributes.put(AttributeKey.stringKey("log.logger.namespace"), + record.getLoggerClassName()); + + final Map mdcCopy = record.getMdcCopy(); + if (mdcCopy != null) { + mdcCopy.forEach((k, v) -> { + // ignore duplicated span data already in the MDC + if (!k.equalsIgnoreCase("spanid") && + !k.equalsIgnoreCase("traceid") && + !k.equalsIgnoreCase("sampled")) { + attributes.put(AttributeKey.stringKey(k), v); + } + }); } if (record.getThrown() != null) { diff --git a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java index c48bb6a630b65..80f130f9b4d81 100644 --- a/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java +++ b/extensions/opentelemetry/runtime/src/main/java/io/quarkus/opentelemetry/runtime/tracing/intrumentation/vertx/RedisClientInstrumenterVertxTracer.java @@ -124,8 +124,9 @@ public String peerAddress() { return attributes.get(PEER_ADDRESS); } - public long dbIndex() { - return Long.parseLong(attributes.get(DB_INSTANCE)); + public Long dbIndex() { + String dbInstance = attributes.get(DB_INSTANCE); + return dbInstance != null ? Long.valueOf(dbInstance) : null; } } diff --git a/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java b/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java index fd4169f64e0be..be4aa3b5071ad 100644 --- a/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java +++ b/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyReader.java @@ -19,34 +19,40 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.ClientWebApplicationException; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyReader; +import org.jboss.resteasy.reactive.common.providers.serialisers.AbstractJsonMessageBodyReader; import org.jboss.resteasy.reactive.common.util.EmptyInputStream; -import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; -public class ClientJacksonMessageBodyReader extends JacksonBasicMessageBodyReader implements ClientRestHandler { +public class ClientJacksonMessageBodyReader extends AbstractJsonMessageBodyReader implements ClientMessageBodyReader { private static final Logger log = Logger.getLogger(ClientJacksonMessageBodyReader.class); private final ConcurrentMap objectReaderMap = new ConcurrentHashMap<>(); - private RestClientRequestContext context; + private final ObjectReader defaultReader; @Inject public ClientJacksonMessageBodyReader(ObjectMapper mapper) { - super(mapper); + this.defaultReader = mapper.reader(); } @Override public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + return doRead(type, genericType, mediaType, entityStream, null); + } + + private Object doRead(Class type, Type genericType, MediaType mediaType, InputStream entityStream, + RestClientRequestContext context) + throws IOException { try { if (entityStream instanceof EmptyInputStream) { return null; } - ObjectReader reader = getEffectiveReader(mediaType); + ObjectReader reader = getEffectiveReader(mediaType, context); return reader.forType(reader.getTypeFactory().constructType(genericType != null ? genericType : type)) .readValue(entityStream); @@ -57,14 +63,18 @@ public Object readFrom(Class type, Type genericType, Annotation[] annota } @Override - public void handle(RestClientRequestContext requestContext) { - this.context = requestContext; + public Object readFrom(Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream, + RestClientRequestContext context) throws java.io.IOException, jakarta.ws.rs.WebApplicationException { + return doRead(type, genericType, mediaType, entityStream, context); } - private ObjectReader getEffectiveReader(MediaType responseMediaType) { + private ObjectReader getEffectiveReader(MediaType responseMediaType, RestClientRequestContext context) { ObjectMapper effectiveMapper = getObjectMapperFromContext(responseMediaType, context); if (effectiveMapper == null) { - return getEffectiveReader(); + return defaultReader; } return objectReaderMap.computeIfAbsent(effectiveMapper, new Function<>() { diff --git a/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java index bb91ba363f32b..cb8b9a9c6e961 100644 --- a/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/rest-client-jackson/runtime/src/main/java/io/quarkus/rest/client/reactive/jackson/runtime/serialisers/ClientJacksonMessageBodyWriter.java @@ -16,24 +16,20 @@ import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.ext.MessageBodyWriter; import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyWriter; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; -public class ClientJacksonMessageBodyWriter implements MessageBodyWriter, ClientRestHandler { +public class ClientJacksonMessageBodyWriter implements ClientMessageBodyWriter { - protected final ObjectMapper originalMapper; - protected final ObjectWriter defaultWriter; + private final ObjectWriter defaultWriter; private final ConcurrentMap objectWriterMap = new ConcurrentHashMap<>(); - private RestClientRequestContext context; @Inject public ClientJacksonMessageBodyWriter(ObjectMapper mapper) { - this.originalMapper = mapper; this.defaultWriter = createDefaultWriter(mapper); } @@ -45,15 +41,17 @@ public boolean isWriteable(Class type, Type genericType, Annotation[] annotation @Override public void writeTo(Object o, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { - doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType)); + doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType, null)); } @Override - public void handle(RestClientRequestContext requestContext) throws Exception { - this.context = requestContext; + public void writeTo(Object o, Class> type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream, + RestClientRequestContext context) throws IOException, WebApplicationException { + doLegacyWrite(o, annotations, httpHeaders, entityStream, getEffectiveWriter(mediaType, context)); } - protected ObjectWriter getEffectiveWriter(MediaType responseMediaType) { + protected ObjectWriter getEffectiveWriter(MediaType responseMediaType, RestClientRequestContext context) { ObjectMapper objectMapper = getObjectMapperFromContext(responseMediaType, context); if (objectMapper == null) { return defaultWriter; @@ -66,5 +64,4 @@ public ObjectWriter apply(ObjectMapper objectMapper) { } }); } - } diff --git a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java index b48b744f588ea..a8d3629b167d5 100644 --- a/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java +++ b/extensions/security/deployment/src/main/java/io/quarkus/security/deployment/PermissionSecurityChecks.java @@ -134,22 +134,13 @@ private static Map getPermissionCheckers(Inde "supported return types are 'boolean' and 'Uni'. ") .formatted(toString(checkerMethod), checkerMethod.returnType().name())); } - var permissionToActions = parsePermissionToActions(annotationInstance.value().asString(), new HashMap<>()) - .entrySet().iterator().next(); - var permissionName = permissionToActions.getKey(); + var permissionName = annotationInstance.value().asString(); if (permissionName.isBlank()) { throw new IllegalArgumentException( "@PermissionChecker annotation placed on the '%s' attribute 'value' must not be blank" .formatted(toString(checkerMethod))); } - var permissionActions = permissionToActions.getValue(); - if (permissionActions != null && !permissionActions.isEmpty()) { - throw new IllegalArgumentException(""" - @PermissionChecker annotation instance placed on the '%s' has attribute 'value' with - permission name '%s' and actions '%s', however actions are currently not supported - """.formatted(toString(checkerMethod), permissionName, permissionActions)); - } boolean isBlocking = checkerMethod.hasDeclaredAnnotation(BLOCKING); if (isBlocking && isReactive) { throw new IllegalArgumentException(""" @@ -588,9 +579,47 @@ private void gatherPermissionKeys(AnnotationInstanc List cache, Map>> targetToPermissionKeys) { // @PermissionsAllowed value is in format permission:action, permission2:action, permission:action2, permission3 // here we transform it to permission -> actions - final var permissionToActions = new HashMap>(); - for (String permissionToAction : instance.value().asStringArray()) { - parsePermissionToActions(permissionToAction, permissionToActions); + record PermissionNameAndChecker(String permissionName, PermissionCheckerMetadata checker) { + } + boolean foundPermissionChecker = false; + final var permissionToActions = new HashMap>(); + for (String permissionValExpression : instance.value().asStringArray()) { + final PermissionCheckerMetadata checker = permissionNameToChecker.get(permissionValExpression); + if (checker != null) { + // matched @PermissionAllowed("value") with @PermissionChecker("value") + foundPermissionChecker = true; + final var permissionNameKey = new PermissionNameAndChecker(permissionValExpression, checker); + if (!permissionToActions.containsKey(permissionNameKey)) { + permissionToActions.put(permissionNameKey, Collections.emptySet()); + } + } else if (permissionValExpression.contains(PERMISSION_TO_ACTION_SEPARATOR)) { + + // expected format: permission:action + final String[] permissionToActionArr = permissionValExpression.split(PERMISSION_TO_ACTION_SEPARATOR); + if (permissionToActionArr.length != 2) { + throw new RuntimeException(String.format( + "PermissionsAllowed value '%s' contains more than one separator '%2$s', expected format is 'permissionName%2$saction'", + permissionValExpression, PERMISSION_TO_ACTION_SEPARATOR)); + } + final PermissionNameAndChecker permissionNameKey = new PermissionNameAndChecker(permissionToActionArr[0], + null); + final String action = permissionToActionArr[1]; + if (permissionToActions.containsKey(permissionNameKey)) { + permissionToActions.get(permissionNameKey).add(action); + } else { + final Set actions = new HashSet<>(); + actions.add(action); + permissionToActions.put(permissionNameKey, actions); + } + } else { + + // expected format: permission + final PermissionNameAndChecker permissionNameKey = new PermissionNameAndChecker(permissionValExpression, + null); + if (!permissionToActions.containsKey(permissionNameKey)) { + permissionToActions.put(permissionNameKey, new HashSet<>()); + } + } } if (permissionToActions.isEmpty()) { @@ -611,12 +640,54 @@ private void gatherPermissionKeys(AnnotationInstanc : instance.value("params").asStringArray(); final Type classType = getPermissionClass(instance); final boolean inclusive = instance.value("inclusive") != null && instance.value("inclusive").asBoolean(); + + if (inclusive && foundPermissionChecker) { + // @PermissionsAllowed({ "read", "read:all", "read:it", "write" } && @PermissionChecker("read") + // require @PermissionChecker for all 'read:action' because determining expected behavior would be too + // complex; similarly for @PermissionChecker("read:all") require 'read' and 'read:it' have checker as well + List checkerPermissions = permissionToActions.keySet().stream() + .filter(k -> k.checker != null).toList(); + for (PermissionNameAndChecker checkerPermission : checkerPermissions) { + // read -> read + // read:all -> read + String permissionName = checkerPermission.permissionName.contains(PERMISSION_TO_ACTION_SEPARATOR) + ? checkerPermission.permissionName.split(PERMISSION_TO_ACTION_SEPARATOR)[0] + : checkerPermission.permissionName; + for (var e : permissionToActions.entrySet()) { + PermissionNameAndChecker permissionNameKey = e.getKey(); + // look for permission names that match our permission checker value (before action-to-perm separator) + // for example: read:it + if (permissionNameKey.checker == null && permissionNameKey.permissionName.equals(permissionName)) { + boolean hasActions = e.getValue() != null && !e.getValue().isEmpty(); + final String permissionsJoinedWithActions; + if (hasActions) { + permissionsJoinedWithActions = e.getValue() + .stream() + .map(action -> permissionNameKey.permissionName + PERMISSION_TO_ACTION_SEPARATOR + + action) + .collect(Collectors.joining(", ")); + } else { + permissionsJoinedWithActions = permissionNameKey.permissionName; + } + throw new RuntimeException( + """ + @PermissionsAllowed annotation placed on the '%s' has inclusive relation between its permissions. + The '%s' permission has been matched with @PermissionChecker '%s', therefore you must also define + a @PermissionChecker for '%s' permissions. + """ + .formatted(toString(annotationTarget), permissionName, + toString(checkerPermission.checker.checkerMethod), + permissionsJoinedWithActions)); + } + } + } + } + for (var permissionToAction : permissionToActions.entrySet()) { - final var permissionName = permissionToAction.getKey(); + final var permissionNameKey = permissionToAction.getKey(); final var permissionActions = permissionToAction.getValue(); - final var permissionChecker = findPermissionChecker(permissionName, permissionActions); - final var key = new PermissionKey(permissionName, permissionActions, params, classType, inclusive, - permissionChecker, annotationTarget); + final var key = new PermissionKey(permissionNameKey.permissionName, permissionActions, params, classType, + inclusive, permissionNameKey.checker, annotationTarget); final int i = cache.indexOf(key); if (i == -1) { orPermissions.add(key); @@ -632,44 +703,6 @@ private void gatherPermissionKeys(AnnotationInstanc .add(List.copyOf(orPermissions)); } - private static HashMap> parsePermissionToActions(String permissionToAction, - HashMap> permissionToActions) { - if (permissionToAction.contains(PERMISSION_TO_ACTION_SEPARATOR)) { - - // expected format: permission:action - final String[] permissionToActionArr = permissionToAction.split(PERMISSION_TO_ACTION_SEPARATOR); - if (permissionToActionArr.length != 2) { - throw new RuntimeException(String.format( - "PermissionsAllowed value '%s' contains more than one separator '%2$s', expected format is 'permissionName%2$saction'", - permissionToAction, PERMISSION_TO_ACTION_SEPARATOR)); - } - final String permissionName = permissionToActionArr[0]; - final String action = permissionToActionArr[1]; - if (permissionToActions.containsKey(permissionName)) { - permissionToActions.get(permissionName).add(action); - } else { - final Set actions = new HashSet<>(); - actions.add(action); - permissionToActions.put(permissionName, actions); - } - } else { - - // expected format: permission - if (!permissionToActions.containsKey(permissionToAction)) { - permissionToActions.put(permissionToAction, new HashSet<>()); - } - } - return permissionToActions; - } - - private PermissionCheckerMetadata findPermissionChecker(String permissionName, Set permissionActions) { - if (permissionActions != null && !permissionActions.isEmpty()) { - // only permission name is supported for now - return null; - } - return permissionNameToChecker.get(permissionName); - } - private static Type getPermissionClass(AnnotationInstance instance) { return instance.value(PERMISSION_ATTR) == null ? Type.create(STRING_PERMISSION, Type.Kind.CLASS) : instance.value(PERMISSION_ATTR).asClass(); @@ -1361,8 +1394,8 @@ private static SecMethodAndPermCtorIdx[] matchPermCtorParamIdxBasedOnNameMatch(M : constructor.declaringClass().name().toString(); throw new RuntimeException(String.format( "No '%s' formal parameter name matches '%s' Permission %s parameter name '%s'", - securedMethod.name(), matchTarget, isQuarkusPermission ? "checker" : "constructor", - constructorParamName)); + PermissionSecurityChecksBuilder.toString(securedMethod), matchTarget, + isQuarkusPermission ? "checker" : "constructor", constructorParamName)); } } return matches; diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/MissingCheckerForInclusivePermsValidationFailureTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/MissingCheckerForInclusivePermsValidationFailureTest.java new file mode 100644 index 0000000000000..7371d84e4909c --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/MissingCheckerForInclusivePermsValidationFailureTest.java @@ -0,0 +1,45 @@ +package io.quarkus.security.test.permissionsallowed.checker; + +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.test.QuarkusUnitTest; + +public class MissingCheckerForInclusivePermsValidationFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .assertException(t -> { + Assertions.assertEquals(RuntimeException.class, t.getClass(), t.getMessage()); + Assertions.assertTrue(t.getMessage().contains("@PermissionsAllowed annotation placed on")); + Assertions.assertTrue( + t.getMessage().contains("SecuredBean#securedBean' has inclusive relation between its permissions")); + Assertions.assertTrue(t.getMessage().contains("you must also define")); + Assertions.assertTrue(t.getMessage().contains("@PermissionChecker for 'checker:missing' permissions")); + }); + + @Test + public void test() { + Assertions.fail(); + } + + @Singleton + public static class SecuredBean { + + @PermissionsAllowed(value = { "checker", "checker:missing" }, inclusive = true) + public void securedBean() { + // EMPTY + } + + @PermissionChecker("checker") + public boolean check(SecurityIdentity identity) { + return false; + } + } +} diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/PermissionCheckerNameWithColonsTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/PermissionCheckerNameWithColonsTest.java new file mode 100644 index 0000000000000..928b7369ea774 --- /dev/null +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/PermissionCheckerNameWithColonsTest.java @@ -0,0 +1,333 @@ +package io.quarkus.security.test.permissionsallowed.checker; + +import static io.quarkus.security.test.utils.IdentityMock.ADMIN; +import static io.quarkus.security.test.utils.IdentityMock.USER; +import static io.quarkus.security.test.utils.SecurityTestUtils.assertFailureFor; +import static io.quarkus.security.test.utils.SecurityTestUtils.assertSuccess; + +import java.util.Set; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.PermissionChecker; +import io.quarkus.security.PermissionsAllowed; +import io.quarkus.security.StringPermission; +import io.quarkus.security.UnauthorizedException; +import io.quarkus.security.test.utils.AuthData; +import io.quarkus.security.test.utils.IdentityMock; +import io.quarkus.security.test.utils.SecurityTestUtils; +import io.quarkus.test.QuarkusUnitTest; + +public class PermissionCheckerNameWithColonsTest { + + private static final AuthData USER_WITH_AUGMENTORS = new AuthData(USER, true); + private static final AuthData ADMIN_WITH_AUGMENTORS = new AuthData(ADMIN, true); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(IdentityMock.class, AuthData.class, SecurityTestUtils.class)); + + @Inject + SecuredBean bean; + + @Test + public void testIdentityPermissionWithActionGrantAccess() { + // @PermissionsAllowed({ "read", "read:all" }) says one of: either 'read' is granted by permission checker + // or 'read:all' is granted by identity permissions + var userWithReadAll = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("read", "all")), true); + assertSuccess(() -> bean.readAndReadAll(false), "readAndReadAll", userWithReadAll); + var userWithReadNothing = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("read", "nothing")), + true); + assertFailureFor(() -> bean.readAndReadAll(false), ForbiddenException.class, userWithReadNothing); + // check 'read' is granted by the checker method + assertSuccess(() -> bean.readAndReadAll(true), "readAndReadAll", userWithReadNothing); + // check 'read' can only be granted by the checker method + var userWithRead = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("read")), true); + assertFailureFor(() -> bean.readAndReadAll(false), ForbiddenException.class, userWithRead); + } + + @Test + public void testIdentityPermissionWithMultipleActionsGrantsAccess() { + // @PermissionsAllowed({ "write:all", "write", "write:essay" }) says one of: either 'write' is granted + // by permission checker or 'write:all' or 'write:essay' is granted by identity permissions + var userWithWriteAll = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("write", "all")), true); + assertSuccess(() -> bean.writeAndWriteAllAndEssay(false), "writeAndWriteAllAndEssay", userWithWriteAll); + var userWithWriteEssay = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("write", "essay")), + true); + assertSuccess(() -> bean.writeAndWriteAllAndEssay(false), "writeAndWriteAllAndEssay", userWithWriteEssay); + var userWithWriteEssayAndAll = new AuthData(Set.of("user"), false, "user", + Set.of(new StringPermission("write", "essay", "all")), true); + assertSuccess(() -> bean.writeAndWriteAllAndEssay(false), "writeAndWriteAllAndEssay", userWithWriteEssayAndAll); + var userWithWriteNothing = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("write", "nothing")), + true); + assertFailureFor(() -> bean.writeAndWriteAllAndEssay(false), ForbiddenException.class, userWithWriteNothing); + // check 'write' is granted by the checker method + assertSuccess(() -> bean.writeAndWriteAllAndEssay(true), "writeAndWriteAllAndEssay", userWithWriteNothing); + // check 'write' can only be granted by the checker method + var userWithWrite = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("write")), true); + assertFailureFor(() -> bean.writeAndWriteAllAndEssay(false), ForbiddenException.class, userWithWrite); + } + + @Test + public void testInclusivePermsAreOnlyGrantedByChecker() { + // @PermissionsAllowed(value = { "execute", "execute:all", "execute:dir" }, inclusive = true) + // check all "execute", "execute:all", "execute:dir" are granted by the checker, + // and they cannot be granted by the identity permissions at all + + // access is denied because permission checkers require 'true' + var userWithExecuteDirAndAll = new AuthData(Set.of("user"), false, "user", + Set.of(new StringPermission("execute", "all", "dir")), true); + assertFailureFor(() -> bean.executeAndAllAndDir(true, true, false), ForbiddenException.class, userWithExecuteDirAndAll); + assertFailureFor(() -> bean.executeAndAllAndDir(true, false, true), ForbiddenException.class, userWithExecuteDirAndAll); + assertFailureFor(() -> bean.executeAndAllAndDir(false, true, true), ForbiddenException.class, userWithExecuteDirAndAll); + + // access is granted because all arguments are true + assertSuccess(() -> bean.executeAndAllAndDir(true, true, true), "executeAndAllAndDir", userWithExecuteDirAndAll); + + // access is denied as anonymous user is not allow access any resources annotated with the @PermissionsAllowed + // because anonymous users can't have permissions + assertFailureFor(() -> bean.executeAndAllAndDir(true, true, true), UnauthorizedException.class, + new AuthData(IdentityMock.ANONYMOUS, true)); + } + + @Test + public void testInclusivePermsAreOnlyGrantedByCheckerAndExtraPermission() { + // @PermissionsAllowed(value = { "delete", "delete:all", "delete:dir", "purge" }, inclusive = true) + // check all "delete", "delete:all", "delete:dir" are granted by the checker, + // and they cannot be granted by the identity permissions at all + // but for the 'purge', identity permission can be used and the checker doesn't need to exist + + // access is denied because permission checkers require 'true' even though user has all the identity permissions + var userWithDeleteDirAndAllAndPurge = new AuthData(Set.of("user"), false, "user", + Set.of(new StringPermission("delete", "all", "dir"), new StringPermission("purge")), true); + assertFailureFor(() -> bean.deleteAndAllAndDir(true, true, false), ForbiddenException.class, + userWithDeleteDirAndAllAndPurge); + assertFailureFor(() -> bean.deleteAndAllAndDir(true, false, true), ForbiddenException.class, + userWithDeleteDirAndAllAndPurge); + assertFailureFor(() -> bean.deleteAndAllAndDir(false, true, true), ForbiddenException.class, + userWithDeleteDirAndAllAndPurge); + var userWithPurge = new AuthData(Set.of("user"), false, "user", Set.of(new StringPermission("purge")), true); + assertFailureFor(() -> bean.deleteAndAllAndDir(true, true, false), ForbiddenException.class, userWithPurge); + assertFailureFor(() -> bean.deleteAndAllAndDir(true, false, true), ForbiddenException.class, userWithPurge); + assertFailureFor(() -> bean.deleteAndAllAndDir(false, true, true), ForbiddenException.class, userWithPurge); + + // access is granted because all arguments are true + assertSuccess(() -> bean.deleteAndAllAndDir(true, true, true), "deleteAndAllAndDir", userWithDeleteDirAndAllAndPurge); + assertSuccess(() -> bean.deleteAndAllAndDir(true, true, true), "deleteAndAllAndDir", userWithPurge); + + // access is not granted because identity doesn't have 'purge' permission + assertFailureFor(() -> bean.deleteAndAllAndDir(true, true, true), ForbiddenException.class, USER_WITH_AUGMENTORS); + } + + @Test + public void testPermissionCheckersForNamesWithActionSeparatorsOnly() { + // @PermissionsAllowed({ "edit:all", "edit", "edit:essay" }) + // permission checker is defined for: edit:all, edit:essay + + // assert that edit:all and edit:essay permission checkers grant access + assertSuccess(() -> bean.editAndEditAllAndEssay(true, false), "editAndEditAllAndEssay", USER_WITH_AUGMENTORS); + assertSuccess(() -> bean.editAndEditAllAndEssay(false, true), "editAndEditAllAndEssay", USER_WITH_AUGMENTORS); + assertSuccess(() -> bean.editAndEditAllAndEssay(true, true), "editAndEditAllAndEssay", USER_WITH_AUGMENTORS); + + // assert that 'edit' can be granted by identity permission + var userWithEdit = new AuthData(USER, true, new StringPermission("edit")); + assertSuccess(() -> bean.editAndEditAllAndEssay(false, false), "editAndEditAllAndEssay", userWithEdit); + + // assert user without either the identity permission or granted access by the checker method cannot access + assertFailureFor(() -> bean.editAndEditAllAndEssay(false, false), ForbiddenException.class, USER_WITH_AUGMENTORS); + + // @PermissionsAllowed({ "list:files", "list:dir" }) + // permission checker is defined for 'list:files' + + // is allowed because the checker grants access + assertSuccess(() -> bean.listFilesAndDir(true), "listFilesAndDir", USER_WITH_AUGMENTORS); + + // is not allowed because the checker does not grant access + assertFailureFor(() -> bean.listFilesAndDir(false), ForbiddenException.class, USER_WITH_AUGMENTORS); + + // is not allowed because identity permission cannot grant access with the 'list:files' when such checker exists + var userListFiles = new AuthData(USER, true, new StringPermission("list", "files")); + assertFailureFor(() -> bean.listFilesAndDir(false), ForbiddenException.class, userListFiles); + + // is allowed because identity permission can grant access with the 'list:dir' as no such checker exists + var userListDir = new AuthData(USER, true, new StringPermission("list", "dir")); + assertSuccess(() -> bean.listFilesAndDir(false), "listFilesAndDir", userListDir); + + // @PermissionsAllowed({ "list:files", "list:links" }) + // there is a permission checker for both permissions + assertSuccess(() -> bean.listFilesAndLinks(true, true), "listFilesAndLinks", USER_WITH_AUGMENTORS); + assertSuccess(() -> bean.listFilesAndLinks(true, false), "listFilesAndLinks", USER_WITH_AUGMENTORS); + assertSuccess(() -> bean.listFilesAndLinks(false, true), "listFilesAndLinks", USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.listFilesAndLinks(false, false), ForbiddenException.class, USER_WITH_AUGMENTORS); + var userWithListFilesAndLinks = new AuthData(USER, true, new StringPermission("list", "files", "links")); + assertFailureFor(() -> bean.listFilesAndLinks(false, false), ForbiddenException.class, userWithListFilesAndLinks); + } + + @Test + public void testInclusivePermissionCheckersAndRepeatedAnnotations() { + // @PermissionsAllowed("cut:array") + // @PermissionsAllowed(value = { "cut:blob", "cut:chars" }, inclusive = true) + // @PermissionsAllowed(value = { "cut:text", "cut:binary" }, inclusive = true) + // @PermissionsAllowed({ "cut:text", "cut:binary" }) - SHOULD be irrelevant + assertSuccess(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, true, true, true, true), + "cutTextAndBinaryAndBlobAndArrayAndChars", USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(false, true, true, true, true), + ForbiddenException.class, USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, false, true, true, true), + ForbiddenException.class, USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, true, false, true, true), + ForbiddenException.class, USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, true, true, false, true), + ForbiddenException.class, USER_WITH_AUGMENTORS); + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(true, true, true, true, false), + ForbiddenException.class, USER_WITH_AUGMENTORS); + var userWithAllCutActions = new AuthData(USER, true, + new StringPermission("cut", "array", "blob", "chars", "text", "binary")); + // assert failures as only checkers can grant these permissions + assertFailureFor(() -> bean.cutTextAndBinaryAndBlobAndArrayAndChars(false, false, false, false, false), + ForbiddenException.class, userWithAllCutActions); + } + + @ApplicationScoped + public static class SecuredBean { + + @PermissionsAllowed({ "read", "read:all" }) + String readAndReadAll(boolean read) { + return "readAndReadAll"; + } + + @PermissionsAllowed({ "write:all", "write", "write:essay" }) + String writeAndWriteAllAndEssay(boolean write) { + return "writeAndWriteAllAndEssay"; + } + + @PermissionsAllowed(value = { "execute", "execute:all", "execute:dir" }, inclusive = true) + String executeAndAllAndDir(boolean execute, boolean executeAll, boolean executeDir) { + return "executeAndAllAndDir"; + } + + @PermissionsAllowed(value = { "delete", "delete:all", "delete:dir", "purge" }, inclusive = true) + String deleteAndAllAndDir(boolean delete, boolean deleteAll, boolean deleteDir) { + return "deleteAndAllAndDir"; + } + + @PermissionsAllowed({ "edit:all", "edit", "edit:essay" }) + String editAndEditAllAndEssay(boolean editAll, boolean editEssay) { + return "editAndEditAllAndEssay"; + } + + @PermissionsAllowed({ "list:files", "list:dir" }) + String listFilesAndDir(boolean listFiles) { + return "listFilesAndDir"; + } + + @PermissionsAllowed({ "list:files", "list:links" }) + String listFilesAndLinks(boolean listFiles, boolean listLinks) { + return "listFilesAndLinks"; + } + + @PermissionsAllowed("cut:array") + @PermissionsAllowed(value = { "cut:blob", "cut:chars" }, inclusive = true) + @PermissionsAllowed(value = { "cut:text", "cut:binary" }, inclusive = true) + @PermissionsAllowed({ "cut:text", "cut:binary" }) // this one SHOULD not have effect due to the instance above + String cutTextAndBinaryAndBlobAndArrayAndChars(boolean cutText, boolean cutBinary, boolean cutArray, boolean cutChars, + boolean cutBlob) { + return "cutTextAndBinaryAndBlobAndArrayAndChars"; + } + } + + @ApplicationScoped + public static class PermissionCheckers { + + @PermissionChecker("read") + boolean canRead(boolean read) { + return read; + } + + @PermissionChecker("write") + boolean canWrite(boolean write) { + return write; + } + + @PermissionChecker("execute") + boolean canExecute(boolean execute) { + return execute; + } + + @PermissionChecker("execute:all") + boolean canExecuteAll(boolean executeAll) { + return executeAll; + } + + @PermissionChecker("execute:dir") + boolean canExecuteDir(boolean executeDir) { + return executeDir; + } + + @PermissionChecker("delete") + boolean canDelete(boolean delete) { + return delete; + } + + @PermissionChecker("delete:all") + boolean canDeleteAll(boolean deleteAll) { + return deleteAll; + } + + @PermissionChecker("delete:dir") + boolean canDeleteDir(boolean deleteDir) { + return deleteDir; + } + + @PermissionChecker("edit:all") + boolean canEditAll(boolean editAll) { + return editAll; + } + + @PermissionChecker("edit:essay") + boolean canEditEssay(boolean editEssay) { + return editEssay; + } + + @PermissionChecker("list:files") + boolean canListFiles(boolean listFiles) { + return listFiles; + } + + @PermissionChecker("list:links") + boolean canListLinks(boolean listLinks) { + return listLinks; + } + + @PermissionChecker("cut:text") + boolean canCutText(boolean cutText) { + return cutText; + } + + @PermissionChecker("cut:binary") + boolean canCutBinary(boolean cutBinary) { + return cutBinary; + } + + @PermissionChecker("cut:blob") + boolean canCutBlob(boolean cutBlob) { + return cutBlob; + } + + @PermissionChecker("cut:array") + boolean canCutArray(boolean cutArray) { + return cutArray; + } + + @PermissionChecker("cut:chars") + boolean canCutChars(boolean cutChars) { + return cutChars; + } + + } +} diff --git a/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/UnknownCheckerParamValidationFailureTest.java b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/UnknownCheckerParamValidationFailureTest.java index 55fa1603bd4c7..42b81fc04ff4f 100644 --- a/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/UnknownCheckerParamValidationFailureTest.java +++ b/extensions/security/deployment/src/test/java/io/quarkus/security/test/permissionsallowed/checker/UnknownCheckerParamValidationFailureTest.java @@ -18,7 +18,8 @@ public class UnknownCheckerParamValidationFailureTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .assertException(t -> { Assertions.assertEquals(RuntimeException.class, t.getClass(), t.getMessage()); - Assertions.assertTrue(t.getMessage().contains("No 'securedBean' formal parameter name matches")); + Assertions.assertTrue(t.getMessage().contains("No '")); + Assertions.assertTrue(t.getMessage().contains("SecuredBean#securedBean' formal parameter name matches")); Assertions.assertTrue(t.getMessage().contains("SecuredBean#check")); Assertions.assertTrue(t.getMessage().contains("parameter name 'unknownParameter'")); }); diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java index d17b2ddcf9e8b..1cb02e5e4e614 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ResourceNotFoundData.java @@ -12,6 +12,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; @@ -39,9 +40,9 @@ public class ResourceNotFoundData { private String baseUrl; private String httpRoot; - private List endpointRoutes; - private Set staticRoots; - private List additionalEndpoints; + private List endpointRoutes = Collections.emptyList(); + private Set staticRoots = Collections.emptySet(); + private List additionalEndpoints = Collections.emptyList(); public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java index b332eb3e72bff..cfcd32c452637 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/bootstrap/model/PlatformImportsImpl.java @@ -67,7 +67,7 @@ void addPlatformRelease(String propertyName, String propertyValue) { final String platformKey = propertyName.substring(PROPERTY_PREFIX.length(), platformKeyStreamSep); final String streamId = propertyName.substring(platformKeyStreamSep + 1, streamVersionSep); final String version = propertyName.substring(streamVersionSep + 1); - allPlatformInfo.computeIfAbsent(platformKey, k -> new PlatformInfo(k)).getOrCreateStream(streamId).addIfNotPresent( + allPlatformInfo.computeIfAbsent(platformKey, PlatformInfo::new).getOrCreateStream(streamId).addIfNotPresent( version, () -> { final PlatformReleaseInfo ri = new PlatformReleaseInfo(platformKey, streamId, version, propertyValue); @@ -81,10 +81,7 @@ public void addPlatformDescriptor(String groupId, String artifactId, String clas artifactId.substring(0, artifactId.length() - BootstrapConstants.PLATFORM_DESCRIPTOR_ARTIFACT_ID_SUFFIX.length()), version); - platformImports.computeIfAbsent(bomCoords, c -> { - platformBoms.add(bomCoords); - return new PlatformImport(); - }).descriptorFound = true; + platformImports.computeIfAbsent(bomCoords, this::newPlatformImport).descriptorFound = true; } public void addPlatformProperties(String groupId, String artifactId, String classifier, String type, String version, @@ -93,7 +90,7 @@ public void addPlatformProperties(String groupId, String artifactId, String clas artifactId.substring(0, artifactId.length() - BootstrapConstants.PLATFORM_PROPERTIES_ARTIFACT_ID_SUFFIX.length()), version); - platformImports.computeIfAbsent(bomCoords, c -> new PlatformImport()); + platformImports.computeIfAbsent(bomCoords, this::newPlatformImport); importedPlatformBoms.computeIfAbsent(groupId, g -> new ArrayList<>()); if (!importedPlatformBoms.get(groupId).contains(bomCoords)) { importedPlatformBoms.get(groupId).add(bomCoords); @@ -117,6 +114,17 @@ public void addPlatformProperties(String groupId, String artifactId, String clas } } + /** + * This method is meant to be called when a new platform BOM import was detected. + * + * @param bom platform BOM coordinates + * @return new platform import instance + */ + private PlatformImport newPlatformImport(ArtifactCoords bom) { + platformBoms.add(bom); + return new PlatformImport(); + } + public void setPlatformProperties(Map platformProps) { this.collectedProps.putAll(platformProps); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java index ea066ae352e12..5ba6fd50d5d77 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/handlers/ClientSendRequestHandler.java @@ -1,6 +1,5 @@ package org.jboss.resteasy.reactive.client.handlers; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -41,6 +40,7 @@ import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.util.MultivaluedTreeMap; +import io.netty.buffer.ByteBufInputStream; import io.netty.handler.codec.DecoderException; import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.LastHttpContent; @@ -361,7 +361,7 @@ public void handle(AsyncResult ar) { try { if (buffer.length() > 0) { requestContext.setResponseEntityStream( - new ByteArrayInputStream(buffer.getBytes())); + new ByteBufInputStream(buffer.getByteBuf())); } else { requestContext.setResponseEntityStream(null); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java index 99fa3837fd647..3234442eb35b2 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientReaderInterceptorContextImpl.java @@ -19,7 +19,7 @@ import jakarta.ws.rs.ext.ReaderInterceptor; import jakarta.ws.rs.ext.ReaderInterceptorContext; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyReader; import org.jboss.resteasy.reactive.client.spi.MissingMessageBodyReaderErrorMessageContextualizer; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; @@ -76,15 +76,16 @@ public Object proceed() throws IOException, WebApplicationException { for (MessageBodyReader> reader : readers) { if (reader.isReadable(entityClass, entityType, annotations, mediaType)) { try { - if (reader instanceof ClientRestHandler) { - try { - ((ClientRestHandler) reader).handle(clientRequestContext); - } catch (Exception e) { - throw new WebApplicationException("Can't inject the client request context", e); - } + if (reader instanceof ClientMessageBodyReader) { + return ((ClientMessageBodyReader) reader).readFrom(entityClass, entityType, annotations, mediaType, + headers, + inputStream, clientRequestContext); + } else { + return ((MessageBodyReader) reader).readFrom(entityClass, entityType, annotations, mediaType, + headers, + inputStream); } - return ((MessageBodyReader) reader).readFrom(entityClass, entityType, annotations, mediaType, headers, - inputStream); + } catch (IOException e) { throw new ProcessingException(e); } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java index fc837e422aae4..298cb8ebf7f5d 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientSerialisers.java @@ -26,7 +26,7 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.reactive.client.providers.serialisers.ClientDefaultTextPlainBodyHandler; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyWriter; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.core.UnmanagedBeanFactory; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; @@ -115,16 +115,13 @@ public static Buffer invokeClientWriter(Entity> entity, Object entityObject, C if (writer.isWriteable(entityClass, entityType, entity.getAnnotations(), entity.getMediaType())) { if ((writerInterceptors == null) || writerInterceptors.length == 0) { VertxBufferOutputStream out = new VertxBufferOutputStream(); - if (writer instanceof ClientRestHandler) { - try { - ((ClientRestHandler) writer).handle(clientRequestContext); - } catch (Exception e) { - throw new WebApplicationException("Can't inject the client request context", e); - } + if (writer instanceof ClientMessageBodyWriter cw) { + cw.writeTo(entityObject, entityClass, entityType, entity.getAnnotations(), + entity.getMediaType(), headerMap, out, clientRequestContext); + } else { + writer.writeTo(entityObject, entityClass, entityType, entity.getAnnotations(), + entity.getMediaType(), headerMap, out); } - - writer.writeTo(entityObject, entityClass, entityType, entity.getAnnotations(), - entity.getMediaType(), headerMap, out); return out.getBuffer(); } else { return runClientWriterInterceptors(entityObject, entityClass, entityType, entity.getAnnotations(), diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientWriterInterceptorContextImpl.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientWriterInterceptorContextImpl.java index 72ca6b69d29a0..ca517339db61e 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientWriterInterceptorContextImpl.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/ClientWriterInterceptorContextImpl.java @@ -16,7 +16,7 @@ import jakarta.ws.rs.ext.WriterInterceptor; import jakarta.ws.rs.ext.WriterInterceptorContext; -import org.jboss.resteasy.reactive.client.spi.ClientRestHandler; +import org.jboss.resteasy.reactive.client.spi.ClientMessageBodyWriter; import org.jboss.resteasy.reactive.common.core.Serialisers; import org.jboss.resteasy.reactive.common.jaxrs.ConfigurationImpl; @@ -70,16 +70,14 @@ public void proceed() throws IOException, WebApplicationException { effectiveWriter = newWriters.get(0); } - if (effectiveWriter instanceof ClientRestHandler) { - try { - ((ClientRestHandler) effectiveWriter).handle(clientRequestContext); - } catch (Exception e) { - throw new WebApplicationException("Can't inject the client request context", e); - } + if (effectiveWriter instanceof ClientMessageBodyWriter cw) { + cw.writeTo(entity, entityClass, entityType, + annotations, mediaType, headers, outputStream, clientRequestContext); + } else { + effectiveWriter.writeTo(entity, entityClass, entityType, + annotations, mediaType, headers, outputStream); } - effectiveWriter.writeTo(entity, entityClass, entityType, - annotations, mediaType, headers, outputStream); outputStream.close(); result = Buffer.buffer(baos.toByteArray()); done = true; diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyReader.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyReader.java new file mode 100644 index 0000000000000..563c6e9e83114 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyReader.java @@ -0,0 +1,20 @@ +package org.jboss.resteasy.reactive.client.spi; + +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyReader; + +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; + +public interface ClientMessageBodyReader extends MessageBodyReader { + + T readFrom(Class type, Type genericType, + Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream, + RestClientRequestContext context) throws java.io.IOException, jakarta.ws.rs.WebApplicationException; +} diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyWriter.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyWriter.java new file mode 100644 index 0000000000000..0a4c396ba5e03 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/spi/ClientMessageBodyWriter.java @@ -0,0 +1,22 @@ +package org.jboss.resteasy.reactive.client.spi; + +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.ext.MessageBodyWriter; + +import org.jboss.resteasy.reactive.client.impl.RestClientRequestContext; + +public interface ClientMessageBodyWriter extends MessageBodyWriter { + + void writeTo(T t, Class> type, Type genericType, Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream, + RestClientRequestContext context) + throws java.io.IOException, jakarta.ws.rs.WebApplicationException; + +} diff --git a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml index 4fbe9a5db08b6..113cbbe4106e0 100644 --- a/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml +++ b/independent-projects/tools/base-codestarts/src/main/resources/codestarts/quarkus-extension/code/quarkiverse/java/.github/workflows/quarkus-snapshot.tpl.qute.yaml @@ -32,9 +32,6 @@ jobs: runs-on: ubuntu-latest steps: - - name: Install yq - uses: dcarbone/install-yq-action@v1.0.1 - - name: Set up Java uses: actions/setup-java@v4 with: diff --git a/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java b/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java index 17ca851fdd315..6c193564584ec 100644 --- a/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java +++ b/integration-tests/elytron-security-ldap/src/main/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapResource.java @@ -20,6 +20,8 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.SecurityContext; @Path("/api") @ApplicationScoped @@ -45,4 +47,11 @@ public String forbidden() { return "authorized"; } + @GET + @Path("/requiresRootRole") + @RolesAllowed("root") + public String getPrincipalName(@Context SecurityContext securityContext) { + return securityContext.getUserPrincipal().getName(); + } + } diff --git a/integration-tests/elytron-security-ldap/src/main/resources/application.properties b/integration-tests/elytron-security-ldap/src/main/resources/application.properties index 4454fc4de0f62..4546c1d3776c1 100644 --- a/integration-tests/elytron-security-ldap/src/main/resources/application.properties +++ b/integration-tests/elytron-security-ldap/src/main/resources/application.properties @@ -14,3 +14,5 @@ quarkus.security.ldap.identity-mapping.attribute-mappings."0".filter-base-dn=ou= quarkus.security.ldap.cache.enabled=true quarkus.security.ldap.cache.max-age=60s quarkus.security.ldap.cache.size=10 + +quarkus.http.auth.roles-mapping."adminRole"=root diff --git a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java deleted file mode 100644 index c0144886d8b05..0000000000000 --- a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronLdapExtensionTestResources.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.quarkus.elytron.security.ldap.it; - -import io.quarkus.test.common.QuarkusTestResource; -import io.quarkus.test.ldap.LdapServerTestResource; - -@QuarkusTestResource(LdapServerTestResource.class) -public class ElytronLdapExtensionTestResources { -} diff --git a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java index e2e107bdb26e6..5d4f2b0518b00 100644 --- a/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java +++ b/integration-tests/elytron-security-ldap/src/test/java/io/quarkus/elytron/security/ldap/it/ElytronSecurityLdapTest.java @@ -6,11 +6,13 @@ import org.junit.jupiter.api.Test; import com.unboundid.ldap.listener.InMemoryDirectoryServer; -import com.unboundid.ldap.sdk.LDAPException; +import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.ldap.LdapServerTestResource; import io.restassured.RestAssured; +@WithTestResource(LdapServerTestResource.class) @QuarkusTest class ElytronSecurityLdapTest { @@ -96,9 +98,31 @@ void admin_role_not_authorized() { .statusCode(403); } - @Test() + @Test @Order(8) - void standard_role_authenticated_cached() throws LDAPException { + void testMappingOfLdapGroupsToIdentityRoles() { + // LDAP groups are added as SecurityIdentity roles + // according to the quarkus.security.ldap.identity-mapping.attribute-mappings + // this test verifies that LDAP groups can be remapped to application-specific SecurityIdentity roles + // role 'adminRole' comes from 'cn' and we remapped it to 'root' + RestAssured.given() + .auth().preemptive().basic("standardUser", "standardUserPassword") + .when() + .get("/api/requiresRootRole") + .then() + .statusCode(403); + RestAssured.given() + .auth().preemptive().basic("adminUser", "adminUserPassword") + .when() + .get("/api/requiresRootRole") + .then() + .statusCode(200) + .body(containsString("adminUser")); // that is uid + } + + @Test + @Order(9) + void standard_role_authenticated_cached() { RestAssured.given() .redirects().follow(false) .when() diff --git a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml index 90b8196c640d0..2b31dd449d072 100644 --- a/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml +++ b/integration-tests/maven/src/test/resources/__snapshots__/CreateExtensionMojoIT/testCreateQuarkiverseExtension/quarkus-my-quarkiverse-ext_.github_workflows_quarkus-snapshot.yaml @@ -32,9 +32,6 @@ jobs: runs-on: ubuntu-latest steps: - - name: Install yq - uses: dcarbone/install-yq-action@v1.0.1 - - name: Set up Java uses: actions/setup-java@v4 with: diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java index 6d54d94df2376..5dd767961d316 100644 --- a/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java +++ b/integration-tests/opentelemetry-redis-instrumentation/src/main/java/io/quarkus/io/opentelemetry/RedisResource.java @@ -68,4 +68,14 @@ public Uni getReactiveInvalidOperation() { .replaceWithVoid(); } + // tainted + @GET + @Path("/tainted") + public String getTainted() { + ds.withConnection(conn -> { + conn.select(7); // taints the connection + conn.value(String.class).get("foobar"); + }); + return "OK"; + } } diff --git a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java index b0ed21891c967..40cc3f7566473 100644 --- a/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java +++ b/integration-tests/opentelemetry-redis-instrumentation/src/test/java/io/quarkus/it/opentelemetry/QuarkusOpenTelemetryRedisTest.java @@ -13,6 +13,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; 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 java.time.Duration; import java.util.List; @@ -195,6 +197,24 @@ public void reactiveInvalidOperation() { checkForException(exception); } + @Test + public void taintedConnection() { + RestAssured.get("redis/tainted") + .then() + .statusCode(200) + .body(CoreMatchers.is("OK")); + + Awaitility.await().atMost(Duration.ofSeconds(10)).until(() -> getSpans().size() == 3); + + Map span = findSpan(getSpans(), m -> SpanKind.CLIENT.name().equals(m.get("kind")) + && "get".equals(m.get("name"))); + Map attributes = (Map) span.get("attributes"); + + assertEquals("redis", attributes.get("db.system")); + assertEquals("get", attributes.get("db.operation")); + assertFalse(span.containsKey("db.redis.database_index")); + } + private List> getSpans() { return get("/opentelemetry/export").body().as(new TypeRef<>() { });