From 794db332e93e58fe3ab4473703e58c2a59818822 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 15 Apr 2024 09:13:45 -0400 Subject: [PATCH 1/9] fix(jmx): restore error codes for auth and TLS connection failures (#350) --- .../java/io/cryostat/ExceptionMappers.java | 19 ---- .../cryostat/recordings/RecordingHelper.java | 2 +- .../targets/TargetConnectionManager.java | 87 +++++++++++-------- src/test/java/itest/TargetEventsGetTest.java | 2 +- 4 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/main/java/io/cryostat/ExceptionMappers.java b/src/main/java/io/cryostat/ExceptionMappers.java index acb7c439a..9a05221c7 100644 --- a/src/main/java/io/cryostat/ExceptionMappers.java +++ b/src/main/java/io/cryostat/ExceptionMappers.java @@ -19,9 +19,6 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; -import org.openjdk.jmc.rjmx.ConnectionException; - -import io.cryostat.targets.TargetConnectionManager; import io.cryostat.util.EntityExistsException; import com.nimbusds.jwt.proc.BadJWTException; @@ -89,22 +86,6 @@ public RestResponse mapIllegalArgumentException(IllegalArgumentException e return RestResponse.status(HttpResponseStatus.BAD_REQUEST.code()); } - @ServerExceptionMapper - public RestResponse mapJmxConnectionException(ConnectionException ex) { - logger.warn(ex); - return RestResponse.status(HttpResponseStatus.BAD_GATEWAY.code()); - } - - @ServerExceptionMapper - public RestResponse mapFlightRecorderException( - org.openjdk.jmc.rjmx.services.jfr.FlightRecorderException ex) { - logger.warn(ex); - if (TargetConnectionManager.isJmxAuthFailure(ex)) { - return RestResponse.status(HttpResponseStatus.FORBIDDEN.code()); - } - return RestResponse.status(HttpResponseStatus.BAD_GATEWAY.code()); - } - @ServerExceptionMapper public RestResponse mapMutinyTimeoutException(TimeoutException ex) { logger.warn(ex); diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java index ca12cb6db..8ba809f86 100644 --- a/src/main/java/io/cryostat/recordings/RecordingHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -957,7 +957,7 @@ public RecordingNotFoundException(Pair key) { } } - static class SnapshotCreationException extends Exception { + public static class SnapshotCreationException extends Exception { public SnapshotCreationException(String message) { super(message); } diff --git a/src/main/java/io/cryostat/targets/TargetConnectionManager.java b/src/main/java/io/cryostat/targets/TargetConnectionManager.java index e48e772b1..17b7791f2 100644 --- a/src/main/java/io/cryostat/targets/TargetConnectionManager.java +++ b/src/main/java/io/cryostat/targets/TargetConnectionManager.java @@ -31,7 +31,6 @@ import java.util.concurrent.Semaphore; import javax.management.remote.JMXServiceURL; -import javax.naming.ServiceUnavailableException; import javax.security.sasl.SaslException; import org.openjdk.jmc.rjmx.ConnectionException; @@ -42,6 +41,7 @@ import io.cryostat.core.net.JFRConnectionToolkit; import io.cryostat.credentials.Credential; import io.cryostat.expressions.MatchExpressionEvaluator; +import io.cryostat.recordings.RecordingHelper.SnapshotCreationException; import io.cryostat.targets.Target.EventKind; import io.cryostat.targets.Target.TargetDiscovery; @@ -54,6 +54,7 @@ import io.quarkus.vertx.ConsumeEvent; import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.unchecked.Unchecked; +import io.vertx.ext.web.handler.HttpException; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.transaction.Transactional; @@ -196,13 +197,24 @@ public Uni executeConnectedTaskUni(Target target, ConnectedTask task) } })) .onFailure(RuntimeException.class) - .transform(this::unwrapRuntimeException) + .transform(t -> unwrapNestedException(RuntimeException.class, t)) .onFailure() .invoke(logger::warn) - .onFailure(t -> isTargetConnectionFailure(t) || isUnknownTargetFailure(t)) + .onFailure(this::isJmxAuthFailure) + .transform(t -> new HttpException(427, t)) + .onFailure(this::isJmxSslFailure) + .transform(t -> new HttpException(502, t)) + .onFailure(this::isServiceTypeFailure) + .transform(t -> new HttpException(504, t)) + .onFailure( + t -> + !(t instanceof HttpException) + && !(t instanceof SnapshotCreationException)) .retry() .withBackOff(failedBackoff) - .expireIn(failedTimeout.plusMillis(System.currentTimeMillis()).toMillis()); + .expireIn(failedTimeout.plusMillis(System.currentTimeMillis()).toMillis()) + .onFailure(this::isTargetConnectionFailure) + .transform(t -> new HttpException(504, t)); } public T executeConnectedTask(Target target, ConnectedTask task) { @@ -220,13 +232,24 @@ public Uni executeDirect( } })) .onFailure(RuntimeException.class) - .transform(this::unwrapRuntimeException) + .transform(t -> unwrapNestedException(RuntimeException.class, t)) .onFailure() .invoke(logger::warn) - .onFailure(t -> isTargetConnectionFailure(t) || isUnknownTargetFailure(t)) + .onFailure(this::isJmxAuthFailure) + .transform(t -> new HttpException(427, t)) + .onFailure(this::isJmxSslFailure) + .transform(t -> new HttpException(502, t)) + .onFailure(this::isServiceTypeFailure) + .transform(t -> new HttpException(504, t)) + .onFailure( + t -> + !(t instanceof HttpException) + && !(t instanceof SnapshotCreationException)) .retry() .withBackOff(failedBackoff) - .expireIn(failedTimeout.plusMillis(System.currentTimeMillis()).toMillis()); + .expireIn(failedTimeout.plusMillis(System.currentTimeMillis()).toMillis()) + .onFailure(this::isTargetConnectionFailure) + .transform(t -> new HttpException(504, t)); } /** @@ -374,17 +397,21 @@ public interface ConnectedTask { T execute(JFRConnection connection) throws Exception; } - public Throwable unwrapRuntimeException(Throwable t) { + public Throwable unwrapNestedException(Class klazz, Throwable t) { final int maxDepth = 10; int depth = 0; Throwable cause = t; - while (cause instanceof RuntimeException && depth++ < maxDepth) { + while (klazz.isInstance(t) && depth++ < maxDepth) { + var c = cause.getCause(); + if (c == null) { + break; + } cause = cause.getCause(); } return cause; } - public static boolean isTargetConnectionFailure(Throwable t) { + private boolean isTargetConnectionFailure(Throwable t) { if (!(t instanceof Exception)) { return false; } @@ -393,16 +420,26 @@ public static boolean isTargetConnectionFailure(Throwable t) { || ExceptionUtils.indexOfType(e, FlightRecorderException.class) >= 0; } - public static boolean isJmxAuthFailure(Throwable t) { + /** + * Check if the exception happened because the connection required authentication, and we had no + * credentials to present. + */ + private boolean isJmxAuthFailure(Throwable t) { if (!(t instanceof Exception)) { return false; } Exception e = (Exception) t; - return ExceptionUtils.indexOfType(e, SecurityException.class) >= 0 + return ExceptionUtils.indexOfType(e, javax.security.auth.login.FailedLoginException.class) + >= 0 + || ExceptionUtils.indexOfType(e, SecurityException.class) >= 0 || ExceptionUtils.indexOfType(e, SaslException.class) >= 0; } - public static boolean isJmxSslFailure(Throwable t) { + /** + * Check if the exception happened because the connection presented an SSL/TLS cert which we + * don't trust. + */ + private boolean isJmxSslFailure(Throwable t) { if (!(t instanceof Exception)) { return false; } @@ -412,7 +449,7 @@ public static boolean isJmxSslFailure(Throwable t) { } /** Check if the exception happened because the port connected to a non-JMX service. */ - public static boolean isServiceTypeFailure(Throwable t) { + private boolean isServiceTypeFailure(Throwable t) { if (!(t instanceof Exception)) { return false; } @@ -421,23 +458,9 @@ public static boolean isServiceTypeFailure(Throwable t) { && ExceptionUtils.indexOfType(e, SocketTimeoutException.class) >= 0; } - public static boolean isUnknownTargetFailure(Throwable t) { - if (!(t instanceof Exception)) { - return false; - } - Exception e = (Exception) t; - return ExceptionUtils.indexOfType(e, java.net.UnknownHostException.class) >= 0 - || ExceptionUtils.indexOfType(e, java.rmi.UnknownHostException.class) >= 0 - || ExceptionUtils.indexOfType(e, ServiceUnavailableException.class) >= 0; - } - - @Name("io.cryostat.net.TargetConnectionManager.TargetConnectionOpened") + @Name("io.cryostat.targets.TargetConnectionManager.TargetConnectionOpened") @Label("Target Connection Opened") @Category("Cryostat") - // @SuppressFBWarnings( - // value = "URF_UNREAD_FIELD", - // justification = "The event fields are recorded with JFR instead of accessed - // directly") public static class TargetConnectionOpened extends Event { String serviceUri; boolean exceptionThrown; @@ -452,13 +475,9 @@ void setExceptionThrown(boolean exceptionThrown) { } } - @Name("io.cryostat.net.TargetConnectionManager.TargetConnectionClosed") + @Name("io.cryostat.targets.TargetConnectionManager.TargetConnectionClosed") @Label("Target Connection Closed") @Category("Cryostat") - // @SuppressFBWarnings( - // value = "URF_UNREAD_FIELD", - // justification = "The event fields are recorded with JFR instead of accessed - // directly") public static class TargetConnectionClosed extends Event { URI serviceUri; boolean exceptionThrown; diff --git a/src/test/java/itest/TargetEventsGetTest.java b/src/test/java/itest/TargetEventsGetTest.java index 0323d8d5a..aa0e6582d 100644 --- a/src/test/java/itest/TargetEventsGetTest.java +++ b/src/test/java/itest/TargetEventsGetTest.java @@ -114,7 +114,7 @@ public void testGetTargetEventsV2WithQueryReturnsRequestedEvents() throws Except LinkedHashMap expectedResults = new LinkedHashMap(); expectedResults.put("name", "Target Connection Opened"); expectedResults.put( - "typeId", "io.cryostat.net.TargetConnectionManager.TargetConnectionOpened"); + "typeId", "io.cryostat.targets.TargetConnectionManager.TargetConnectionOpened"); expectedResults.put("description", ""); expectedResults.put("category", List.of("Cryostat")); expectedResults.put( From 7f3116c8786027efa5db8aae3ac25f8993210793 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 15 Apr 2024 09:26:50 -0400 Subject: [PATCH 2/9] fix(recordingOptions): scope customizers to each Target (#341) --- .../cryostat/recordings/RecordingHelper.java | 2 +- .../RecordingOptionsBuilderFactory.java | 26 ++++++++-- .../RecordingOptionsCustomizerFactory.java | 48 +++++++++++++++++++ .../io/cryostat/recordings/Recordings.java | 45 +++++++++-------- .../cryostat/recordings/RecordingsModule.java | 20 -------- .../java/io/cryostat/rules/RuleService.java | 9 ++-- 6 files changed, 96 insertions(+), 54 deletions(-) create mode 100644 src/main/java/io/cryostat/recordings/RecordingOptionsCustomizerFactory.java diff --git a/src/main/java/io/cryostat/recordings/RecordingHelper.java b/src/main/java/io/cryostat/recordings/RecordingHelper.java index 8ba809f86..506febebb 100644 --- a/src/main/java/io/cryostat/recordings/RecordingHelper.java +++ b/src/main/java/io/cryostat/recordings/RecordingHelper.java @@ -246,7 +246,7 @@ public ActiveRecording createSnapshot(Target target, JFRConnection connection) String rename = String.format("%s-%d", desc.getName().toLowerCase(), desc.getId()); RecordingOptionsBuilder recordingOptionsBuilder = - recordingOptionsBuilderFactory.create(connection.getService()); + recordingOptionsBuilderFactory.create(target); recordingOptionsBuilder.name(rename); connection.getService().updateRecordingOptions(desc, recordingOptionsBuilder.build()); diff --git a/src/main/java/io/cryostat/recordings/RecordingOptionsBuilderFactory.java b/src/main/java/io/cryostat/recordings/RecordingOptionsBuilderFactory.java index 60f7215c9..9b0dde250 100644 --- a/src/main/java/io/cryostat/recordings/RecordingOptionsBuilderFactory.java +++ b/src/main/java/io/cryostat/recordings/RecordingOptionsBuilderFactory.java @@ -17,9 +17,27 @@ import org.openjdk.jmc.common.unit.QuantityConversionException; import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; -import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; -public interface RecordingOptionsBuilderFactory { - RecordingOptionsBuilder create(IFlightRecorderService service) - throws QuantityConversionException; +import io.cryostat.targets.Target; +import io.cryostat.targets.TargetConnectionManager; + +import io.smallrye.common.annotation.Blocking; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +@ApplicationScoped +public class RecordingOptionsBuilderFactory { + + @Inject RecordingOptionsCustomizerFactory customizerFactory; + @Inject TargetConnectionManager connectionManager; + + @Blocking + public RecordingOptionsBuilder create(Target target) throws QuantityConversionException { + return connectionManager.executeConnectedTask( + target, + conn -> + customizerFactory + .create(target) + .apply(new RecordingOptionsBuilder(conn.getService()))); + } } diff --git a/src/main/java/io/cryostat/recordings/RecordingOptionsCustomizerFactory.java b/src/main/java/io/cryostat/recordings/RecordingOptionsCustomizerFactory.java new file mode 100644 index 000000000..9177bf18e --- /dev/null +++ b/src/main/java/io/cryostat/recordings/RecordingOptionsCustomizerFactory.java @@ -0,0 +1,48 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cryostat.recordings; + +import java.util.HashMap; + +import io.cryostat.core.RecordingOptionsCustomizer; +import io.cryostat.targets.Target; +import io.cryostat.targets.Target.EventKind; +import io.cryostat.targets.Target.TargetDiscovery; + +import io.quarkus.vertx.ConsumeEvent; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import org.jboss.logging.Logger; + +@ApplicationScoped +public class RecordingOptionsCustomizerFactory { + + @Inject Logger logger; + + private final HashMap customizers = new HashMap<>(); + + @ConsumeEvent(Target.TARGET_JVM_DISCOVERY) + void onMessage(TargetDiscovery event) { + if (EventKind.LOST.equals(event.kind())) { + customizers.remove(event.serviceRef()); + } + } + + public RecordingOptionsCustomizer create(Target target) { + return customizers.computeIfAbsent( + target, (t) -> new RecordingOptionsCustomizer(logger::debug)); + } +} diff --git a/src/main/java/io/cryostat/recordings/Recordings.java b/src/main/java/io/cryostat/recordings/Recordings.java index 0bcea5e3c..8eea2e436 100644 --- a/src/main/java/io/cryostat/recordings/Recordings.java +++ b/src/main/java/io/cryostat/recordings/Recordings.java @@ -113,7 +113,7 @@ public class Recordings { @Inject TargetConnectionManager connectionManager; @Inject EventBus bus; @Inject RecordingOptionsBuilderFactory recordingOptionsBuilderFactory; - @Inject RecordingOptionsCustomizer recordingOptionsCustomizer; + @Inject RecordingOptionsCustomizerFactory recordingOptionsCustomizerFactory; @Inject EventOptionsBuilder.Factory eventOptionsBuilderFactory; @Inject Clock clock; @Inject S3Client storage; @@ -604,7 +604,7 @@ public Response createRecording( connection -> { RecordingOptionsBuilder optionsBuilder = recordingOptionsBuilderFactory - .create(connection.getService()) + .create(target) .name(recordingName); if (duration.isPresent()) { optionsBuilder.duration(TimeUnit.SECONDS.toMillis(duration.get())); @@ -884,8 +884,7 @@ public Map getRecordingOptions(@RestPath long id) throws Excepti return connectionManager.executeConnectedTask( target, connection -> { - RecordingOptionsBuilder builder = - recordingOptionsBuilderFactory.create(connection.getService()); + RecordingOptionsBuilder builder = recordingOptionsBuilderFactory.create(target); return getRecordingOptions(connection.getService(), builder); }); } @@ -906,6 +905,9 @@ public Response patchRecordingOptionsV1(@RestPath URI connectUrl) { @Blocking @Path("/api/v3/targets/{id}/recordingOptions") @RolesAllowed("read") + @SuppressFBWarnings( + value = "UC_USELESS_OBJECT", + justification = "SpotBugs thinks the options map is unused, but it is used") public Map patchRecordingOptions( @RestPath long id, @RestForm String toDisk, @@ -914,20 +916,20 @@ public Map patchRecordingOptions( throws Exception { final String unsetKeyword = "unset"; - Map form = new HashMap<>(); + Map options = new HashMap<>(); Pattern bool = Pattern.compile("true|false|" + unsetKeyword); if (toDisk != null) { Matcher m = bool.matcher(toDisk); if (!m.matches()) { throw new BadRequestException("Invalid options"); } - form.put("toDisk", toDisk); + options.put("toDisk", toDisk); } if (maxAge != null) { if (!unsetKeyword.equals(maxAge)) { try { Long.parseLong(maxAge); - form.put("maxAge", maxAge); + options.put("maxAge", maxAge); } catch (NumberFormatException e) { throw new BadRequestException("Invalid options"); } @@ -937,31 +939,28 @@ public Map patchRecordingOptions( if (!unsetKeyword.equals(maxSize)) { try { Long.parseLong(maxSize); - form.put("maxSize", maxSize); + options.put("maxSize", maxSize); } catch (NumberFormatException e) { throw new BadRequestException("Invalid options"); } } } - form.entrySet() - .forEach( - e -> { - RecordingOptionsCustomizer.OptionKey optionKey = - RecordingOptionsCustomizer.OptionKey.fromOptionName(e.getKey()) - .get(); - if ("unset".equals(e.getValue())) { - recordingOptionsCustomizer.unset(optionKey); - } else { - recordingOptionsCustomizer.set(optionKey, e.getValue()); - } - }); - Target target = Target.find("id", id).singleResult(); + for (var entry : options.entrySet()) { + RecordingOptionsCustomizer.OptionKey optionKey = + RecordingOptionsCustomizer.OptionKey.fromOptionName(entry.getKey()).get(); + var recordingOptionsCustomizer = recordingOptionsCustomizerFactory.create(target); + if (unsetKeyword.equals(entry.getValue())) { + recordingOptionsCustomizer.unset(optionKey); + } else { + recordingOptionsCustomizer.set(optionKey, entry.getValue()); + } + } + return connectionManager.executeConnectedTask( target, connection -> { - RecordingOptionsBuilder builder = - recordingOptionsBuilderFactory.create(connection.getService()); + var builder = recordingOptionsBuilderFactory.create(target); return getRecordingOptions(connection.getService(), builder); }); } diff --git a/src/main/java/io/cryostat/recordings/RecordingsModule.java b/src/main/java/io/cryostat/recordings/RecordingsModule.java index fa83a5a36..9f04be17c 100644 --- a/src/main/java/io/cryostat/recordings/RecordingsModule.java +++ b/src/main/java/io/cryostat/recordings/RecordingsModule.java @@ -15,37 +15,17 @@ */ package io.cryostat.recordings; -import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; - import io.cryostat.core.EventOptionsBuilder; -import io.cryostat.core.RecordingOptionsCustomizer; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Produces; import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Singleton public class RecordingsModule { - - @Produces - @ApplicationScoped - public RecordingOptionsBuilderFactory provideRecordingOptionsBuilderFactory( - RecordingOptionsCustomizer customizer) { - return service -> customizer.apply(new RecordingOptionsBuilder(service)); - } - @Produces @ApplicationScoped public EventOptionsBuilder.Factory provideEventOptionsBuilderFactory() { return new EventOptionsBuilder.Factory(); } - - @Produces - @ApplicationScoped - public RecordingOptionsCustomizer provideRecordingOptionsCustomizer() { - Logger log = LoggerFactory.getLogger(RecordingOptionsCustomizer.class); - return new RecordingOptionsCustomizer(log::debug); - } } diff --git a/src/main/java/io/cryostat/rules/RuleService.java b/src/main/java/io/cryostat/rules/RuleService.java index bf726e608..e1a7165d6 100644 --- a/src/main/java/io/cryostat/rules/RuleService.java +++ b/src/main/java/io/cryostat/rules/RuleService.java @@ -32,7 +32,6 @@ import org.openjdk.jmc.rjmx.ConnectionException; import org.openjdk.jmc.rjmx.ServiceNotAvailableException; -import io.cryostat.core.net.JFRConnection; import io.cryostat.core.templates.Template; import io.cryostat.core.templates.TemplateType; import io.cryostat.expressions.MatchExpressionEvaluator; @@ -143,7 +142,7 @@ public void activate(Rule rule, Target target) throws Exception { connectionManager.executeConnectedTask( target, connection -> { - var recordingOptions = createRecordingOptions(rule, connection); + var recordingOptions = createRecordingOptions(rule, target); Pair pair = recordingHelper.parseEventSpecifier(rule.eventSpecifier); @@ -173,15 +172,13 @@ public void activate(Rule rule, Target target) throws Exception { } } - private IConstrainedMap createRecordingOptions(Rule rule, JFRConnection connection) + private IConstrainedMap createRecordingOptions(Rule rule, Target target) throws ConnectionException, QuantityConversionException, IOException, ServiceNotAvailableException { RecordingOptionsBuilder optionsBuilder = - recordingOptionsBuilderFactory - .create(connection.getService()) - .name(rule.getRecordingName()); + recordingOptionsBuilderFactory.create(target).name(rule.getRecordingName()); if (rule.maxAgeSeconds > 0) { optionsBuilder.maxAge(rule.maxAgeSeconds); } From 6ec9867d226d3c6f1c560a1d1870576a66de63ac Mon Sep 17 00:00:00 2001 From: Cryostat CI Date: Mon, 15 Apr 2024 15:33:23 +0000 Subject: [PATCH 3/9] build(webui): update submodule to c93e6b4 --- src/main/webui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webui b/src/main/webui index a9c3d73c4..c93e6b4f7 160000 --- a/src/main/webui +++ b/src/main/webui @@ -1 +1 @@ -Subproject commit a9c3d73c421709f5515b71c8742f267b4badc492 +Subproject commit c93e6b4f7a3e7779bb1f9c28a2b7458bb11b1ffa From 28c6c9c2052b75c33713e65d5bc2456f6c8df5cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Apr 2024 13:55:43 -0400 Subject: [PATCH 4/9] build(deps): bump org.projectnessie.cel:cel-bom from 0.3.21 to 0.4.4 (#237) * build(deps): bump org.projectnessie.cel:cel-bom from 0.3.21 to 0.4.4 Bumps [org.projectnessie.cel:cel-bom](https://github.com/projectnessie/cel-java) from 0.3.21 to 0.4.4. - [Release notes](https://github.com/projectnessie/cel-java/releases) - [Commits](https://github.com/projectnessie/cel-java/compare/v0.3.21...v0.4.4) --- updated-dependencies: - dependency-name: org.projectnessie.cel:cel-bom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * force downgrade protobuf version to match -agent projectnessie/protobuf config * Upgrade Protobuf as well --------- Signed-off-by: dependabot[bot] Signed-off-by: Elliott Baron Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Andrew Azores Co-authored-by: Elliott Baron --- pom.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index caf2e76b2..64724543b 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,8 @@ 5.2.1 3.13.0 1.7 - 0.3.21 + 0.4.4 + 3.25.2 9.37.3 1.19.7 quarkus-bom @@ -161,6 +162,12 @@ org.projectnessie.cel cel-jackson + + + com.google.protobuf + protobuf-java + ${com.google.protobuf-java.version} + commons-validator commons-validator From 7cce795dd588a45cb50dbd6a6b58be29be532718 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Mon, 15 Apr 2024 18:48:39 -0400 Subject: [PATCH 5/9] build(arch): use build arch profiles for native dependency classification (#371) --- pom.xml | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 64724543b..6246e085b 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,9 @@ UTF-8 UTF-8 + linux + amd64 + 2.30.1 1.16.1 @@ -31,6 +34,7 @@ io.quarkus.platform 3.2.9.Final 2.3.6 + 4.1.101.Final 3.5.0 4.8.4 @@ -45,6 +49,13 @@ + + io.netty + netty-bom + ${io.netty.version} + pom + import + ${quarkus.platform.group-id} ${quarkus.platform.artifact-id} @@ -190,16 +201,6 @@ io.quarkus quarkus-quartz - - io.netty - netty-transport-native-epoll - linux-x86_64 - - - io.netty - netty-transport-native-kqueue - osx-x86_64 - com.google.googlejavaformat google-java-format @@ -421,5 +422,85 @@ native + + + default-arch + + + !build.arch + + + + ${build.os}-x86_64 + compile + + + + amd64 + + + build.arch + amd64 + + + + ${build.os}-x86_64 + compile + + + + arm64 + + + build.arch + arm64 + + + + ${build.os}-aarch_64 + compile + + + + with-epoll + + + !build.exclude-epoll + + + + compile + + + + io.netty + netty-transport-native-epoll + ${io.netty.version} + ${io.netty.netty-transport-native-epoll.classifier} + ${io.netty.netty-transport-native-epoll.scope} + + + + + no-epoll + + + build.exclude-epoll + + + + ${build.os}-x86_64 + provided + + + + io.netty + netty-transport-native-epoll + ${io.netty.netty-transport-native-epoll.classifier} + ${io.netty.netty-transport-native-epoll.scope} + + + + From 6fb3be36b3ba257c37f10fc4840709f3e14a5762 Mon Sep 17 00:00:00 2001 From: Elliott Baron Date: Wed, 17 Apr 2024 11:33:49 -0400 Subject: [PATCH 6/9] build(assembly): add profile to build assembly (#380) --- pom.xml | 27 +++++++++++++++++++++++++++ src/assembly/quarkus-app.xml | 14 ++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/assembly/quarkus-app.xml diff --git a/pom.xml b/pom.xml index 6246e085b..a0ec801b8 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ 2.3.6 4.1.101.Final 3.5.0 + 3.7.1 4.8.4 4.8.4.0 @@ -502,5 +503,31 @@ + + dist + + + + maven-assembly-plugin + ${assembly-plugin.version} + + + src/assembly/quarkus-app.xml + + posix + + + + assemble-quarkus-app + package + + single + + + + + + + diff --git a/src/assembly/quarkus-app.xml b/src/assembly/quarkus-app.xml new file mode 100644 index 000000000..8b55a03eb --- /dev/null +++ b/src/assembly/quarkus-app.xml @@ -0,0 +1,14 @@ + + quarkus-app + + tar.gz + + false + + + ${project.build.directory}/quarkus-app + + + From ddd65ba77fc52119ec97b743989d3ac386cf77e1 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 18 Apr 2024 17:08:06 -0400 Subject: [PATCH 7/9] chore(pom): remove build.os property that breaks Dependabot scanning (#384) --- pom.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index a0ec801b8..020c9f226 100644 --- a/pom.xml +++ b/pom.xml @@ -16,7 +16,6 @@ UTF-8 UTF-8 - linux amd64 2.30.1 @@ -432,7 +431,7 @@ - ${build.os}-x86_64 + linux-x86_64 compile @@ -445,7 +444,7 @@ - ${build.os}-x86_64 + linux-x86_64 compile @@ -458,7 +457,7 @@ - ${build.os}-aarch_64 + linux-aarch_64 compile @@ -490,7 +489,7 @@ - ${build.os}-x86_64 + linux-x86_64 provided From 3331284364b936afb38c222840f9364463275100 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Apr 2024 17:17:27 -0400 Subject: [PATCH 8/9] build(deps): bump io.cryostat:cryostat-core from 2.30.1 to 2.30.2 (#385) Bumps [io.cryostat:cryostat-core](https://github.com/cryostatio/cryostat-core) from 2.30.1 to 2.30.2. - [Release notes](https://github.com/cryostatio/cryostat-core/releases) - [Commits](https://github.com/cryostatio/cryostat-core/compare/v2.30.1...v2.30.2) --- updated-dependencies: - dependency-name: io.cryostat:cryostat-core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 020c9f226..42e6e8d9a 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ amd64 - 2.30.1 + 2.30.2 1.16.1 2.13.0 From 2c22fac4e6797d437ac620645926179e98b22dfd Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 18 Apr 2024 17:18:31 -0400 Subject: [PATCH 9/9] feat(jmx): custom entrypoint to support copying TLS certs into truststore for JMX (#330) --- compose/cryostat.yml | 5 ++ schema/openapi.yaml | 13 +++ smoketest.bash | 9 ++ src/main/docker/Dockerfile.jvm | 17 +++- src/main/docker/include/entrypoint.bash | 82 +++++++++++++++++++ src/main/docker/include/genpass.bash | 5 ++ src/main/docker/include/truststore-setup.bash | 24 ++++++ .../java/io/cryostat/ConfigProperties.java | 2 + .../java/io/cryostat/security/TrustStore.java | 53 ++++++++++++ src/main/resources/application-dev.properties | 2 +- src/main/resources/application.properties | 5 ++ 11 files changed, 215 insertions(+), 2 deletions(-) create mode 100755 src/main/docker/include/entrypoint.bash create mode 100755 src/main/docker/include/genpass.bash create mode 100755 src/main/docker/include/truststore-setup.bash create mode 100644 src/main/java/io/cryostat/security/TrustStore.java diff --git a/compose/cryostat.yml b/compose/cryostat.yml index c42fb185d..f77c3167a 100644 --- a/compose/cryostat.yml +++ b/compose/cryostat.yml @@ -14,6 +14,7 @@ services: image: ${CRYOSTAT_IMAGE:-quay.io/cryostat/cryostat:3.0.0-snapshot} volumes: - ${XDG_RUNTIME_DIR}/podman/podman.sock:/run/user/1000/podman/podman.sock:Z + - jmxtls_cfg:/truststore:U security_opt: - label:disable hostname: cryostat3 @@ -39,3 +40,7 @@ services: retries: 3 start_period: 30s timeout: 5s + +volumes: + jmxtls_cfg: + external: true diff --git a/schema/openapi.yaml b/schema/openapi.yaml index b7db9ef33..fcda791f2 100644 --- a/schema/openapi.yaml +++ b/schema/openapi.yaml @@ -2184,6 +2184,19 @@ paths: - SecurityScheme: [] tags: - Reports + /api/v3/tls/certs: + get: + responses: + "200": + content: + application/json: + schema: + items: + type: string + type: array + description: OK + tags: + - Trust Store /health: get: responses: diff --git a/smoketest.bash b/smoketest.bash index 6e88e733a..9e77c6478 100755 --- a/smoketest.bash +++ b/smoketest.bash @@ -180,6 +180,8 @@ cleanup() { ${container_engine} rm localstack_cfg_helper || true ${container_engine} volume rm localstack_cfg || true fi + ${container_engine} rm jmxtls_cfg_helper || true + ${container_engine} volume rm jmxtls_cfg || true truncate -s 0 "${HOSTSFILE}" for i in "${PIDS[@]}"; do kill -0 "${i}" && kill "${i}" @@ -212,6 +214,13 @@ if [ "${s3}" = "localstack" ]; then createLocalstackCfgVolume fi +createJmxTlsCertVolume() { + "${container_engine}" volume create jmxtls_cfg + "${container_engine}" container create --name jmxtls_cfg_helper -v jmxtls_cfg:/truststore busybox + "${container_engine}" cp "${DIR}/truststore" jmxtls_cfg_helper:/truststore +} +createJmxTlsCertVolume + setupUserHosts() { # This requires https://github.com/figiel/hosts to work. See README. truncate -s 0 "${HOSTSFILE}" diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm index 8f5b902da..db1431403 100644 --- a/src/main/docker/Dockerfile.jvm +++ b/src/main/docker/Dockerfile.jvm @@ -88,11 +88,26 @@ LABEL io.cryostat.component=cryostat3 ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" -ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] +ENTRYPOINT [ "/deployments/app/entrypoint.bash", "/opt/jboss/container/java/run/run-java.sh" ] # We make distinct layers so if there are application changes the library layers can be re-used COPY --chown=185 src/main/docker/include/cryostat.jfc /usr/lib/jvm/jre/lib/jfr/ +COPY --chown=185 src/main/docker/include/genpass.bash /deployments/app/ +COPY --chown=185 src/main/docker/include/entrypoint.bash /deployments/app/ +COPY --chown=185 src/main/docker/include/truststore-setup.bash /deployments/app/ COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ COPY --chown=185 target/quarkus-app/*.jar /deployments/ COPY --chown=185 target/quarkus-app/app/ /deployments/app/ COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +ENV CONF_DIR=/opt/cryostat.d +ENV SSL_TRUSTSTORE=$CONF_DIR/truststore.p12 \ + SSL_TRUSTSTORE_PASS_FILE=$CONF_DIR/truststore.pass + +USER root +RUN mkdir -p $CONF_DIR \ + && chmod -R g=u $CONF_DIR \ + && chown jboss:root $CONF_DIR +USER 185 + +RUN /deployments/app/truststore-setup.bash diff --git a/src/main/docker/include/entrypoint.bash b/src/main/docker/include/entrypoint.bash new file mode 100755 index 000000000..892f4c71a --- /dev/null +++ b/src/main/docker/include/entrypoint.bash @@ -0,0 +1,82 @@ +#!/bin/bash + +set -e + +DIR="$(dirname "$(realpath "$0")")" +source "${DIR}/genpass.bash" + +function banner() { + echo "+------------------------------------------+" + printf "| %-40s |\n" "$(date)" + echo "| |" + printf "| %-40s |\n" "$@" + echo "+------------------------------------------+" +} + +PWFILE="/tmp/jmxremote.password" +USRFILE="/tmp/jmxremote.access" +function createJmxCredentials() { + if [ -z "$CRYOSTAT_RJMX_USER" ]; then + CRYOSTAT_RJMX_USER="cryostat" + fi + if [ -z "$CRYOSTAT_RJMX_PASS" ]; then + CRYOSTAT_RJMX_PASS="$(genpass)" + fi + + echo -n "$CRYOSTAT_RJMX_USER $CRYOSTAT_RJMX_PASS" > "$PWFILE" + chmod 400 "$PWFILE" + echo -n "$CRYOSTAT_RJMX_USER readwrite" > "$USRFILE" + chmod 400 "$USRFILE" +} + +function importTrustStores() { + if [ -z "$CONF_DIR" ]; then + CONF_DIR="/opt/cryostat.d" + fi + if [ -z "$SSL_TRUSTSTORE_DIR" ]; then + SSL_TRUSTSTORE_DIR="/truststore" + fi + + if [ ! -d "$SSL_TRUSTSTORE_DIR" ]; then + banner "$SSL_TRUSTSTORE_DIR does not exist; no certificates to import" + return 0 + elif [ ! "$(ls -A "$SSL_TRUSTSTORE_DIR")" ]; then + banner "$SSL_TRUSTSTORE_DIR is empty; no certificates to import" + return 0 + fi + + SSL_TRUSTSTORE_PASS="$(cat "${SSL_TRUSTSTORE_PASS_FILE:-$CONF_DIR/truststore.pass}")" + + find "$SSL_TRUSTSTORE_DIR" -type f | while IFS= read -r cert; do + echo "Importing certificate $cert ..." + + keytool -importcert -v \ + -noprompt \ + -alias "imported-$(basename "$cert")" \ + -trustcacerts \ + -keystore "${SSL_TRUSTSTORE:-$CONF_DIR/truststore.p12}" \ + -file "$cert"\ + -storepass "$SSL_TRUSTSTORE_PASS" + done + + FLAGS+=( + "-Djavax.net.ssl.trustStore=$SSL_TRUSTSTORE" + "-Djavax.net.ssl.trustStorePassword=$SSL_TRUSTSTORE_PASS" + ) +} + +FLAGS=() +importTrustStores + +if [ "$CRYOSTAT_DISABLE_JMX_AUTH" = "true" ]; then + banner "JMX Auth Disabled" + FLAGS+=("-Dcom.sun.management.jmxremote.authenticate=false") +else + createJmxCredentials + FLAGS+=("-Dcom.sun.management.jmxremote.authenticate=true") + FLAGS+=("-Dcom.sun.management.jmxremote.password.file=$PWFILE") + FLAGS+=("-Dcom.sun.management.jmxremote.access.file=$USRFILE") +fi + +export JAVA_OPTS_APPEND="${JAVA_OPTS_APPEND} ${FLAGS[*]}" +exec $1 diff --git a/src/main/docker/include/genpass.bash b/src/main/docker/include/genpass.bash new file mode 100755 index 000000000..ba1557267 --- /dev/null +++ b/src/main/docker/include/genpass.bash @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +genpass() { + < /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c32 +} diff --git a/src/main/docker/include/truststore-setup.bash b/src/main/docker/include/truststore-setup.bash new file mode 100755 index 000000000..ab4d80cfa --- /dev/null +++ b/src/main/docker/include/truststore-setup.bash @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -e + +DIR="$(dirname "$(realpath "$0")")" +source "${DIR}/genpass.bash" + +SSL_TRUSTSTORE_PASS="$(genpass)" + +echo "$SSL_TRUSTSTORE_PASS" > "$SSL_TRUSTSTORE_PASS_FILE" + +trap "cd -" EXIT +cd "$CONF_DIR" + +keytool -importkeystore \ + -noprompt \ + -storetype PKCS12 \ + -srckeystore /usr/lib/jvm/jre-17-openjdk/lib/security/cacerts \ + -srcstorepass changeit \ + -destkeystore "$SSL_TRUSTSTORE" \ + -deststorepass "$SSL_TRUSTSTORE_PASS" + +chmod 664 "${SSL_TRUSTSTORE}" +chmod 640 "${SSL_TRUSTSTORE_PASS_FILE}" diff --git a/src/main/java/io/cryostat/ConfigProperties.java b/src/main/java/io/cryostat/ConfigProperties.java index f69efd3f1..0078a977b 100644 --- a/src/main/java/io/cryostat/ConfigProperties.java +++ b/src/main/java/io/cryostat/ConfigProperties.java @@ -51,4 +51,6 @@ public class ConfigProperties { public static final String STORAGE_TRANSIENT_ARCHIVES_ENABLED = "storage.transient-archives.enabled"; public static final String STORAGE_TRANSIENT_ARCHIVES_TTL = "storage.transient-archives.ttl"; + + public static final String SSL_TRUSTSTORE_DIR = "ssl.truststore.dir"; } diff --git a/src/main/java/io/cryostat/security/TrustStore.java b/src/main/java/io/cryostat/security/TrustStore.java new file mode 100644 index 000000000..26f660e40 --- /dev/null +++ b/src/main/java/io/cryostat/security/TrustStore.java @@ -0,0 +1,53 @@ +/* + * Copyright The Cryostat Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cryostat.security; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import io.cryostat.ConfigProperties; + +import io.smallrye.common.annotation.Blocking; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; + +@Path("") +public class TrustStore { + + @ConfigProperty(name = ConfigProperties.SSL_TRUSTSTORE_DIR) + java.nio.file.Path trustStoreDir; + + @Inject Logger logger; + + @Blocking + @GET + @Path("/api/v3/tls/certs") + @Produces(MediaType.APPLICATION_JSON) + public List listCerts() throws IOException { + return Files.walk(trustStoreDir) + .map(java.nio.file.Path::toFile) + .filter(File::isFile) + .map(File::getPath) + .toList(); + } +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 08e564fbe..0467ceee7 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -6,7 +6,7 @@ quarkus.http.cors=true # quarkus.http.cors.origins=http://localhost:9000,http://0.0.0.0:9000 quarkus.http.cors.origins=http://localhost:9000 quarkus.http.cors.access-control-allow-credentials=true -quarkus.http.cors.exposed-headers=X-JMX-Authorization,X-JMX-Authenticate,X-WWW-Authenticate +quarkus.http.cors.exposed-headers=X-WWW-Authenticate # quarkus.http.cors.methods=GET,PUT,POST,PATCH,OPTIONS # quarkus.http.cors.access-control-max-age=1s diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6d6ab697b..6091cdbac 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -32,6 +32,11 @@ cryostat.http.proxy.host=${quarkus.http.host} cryostat.http.proxy.port=${quarkus.http.port} cryostat.http.proxy.path=/ +conf-dir=/opt/cryostat.d +ssl.truststore=${conf-dir}/truststore.p12 +ssl.truststore.dir=/truststore +ssl.truststore.pass-file=${conf-dir}/truststore.pass + quarkus.http.auth.proactive=false quarkus.http.host=0.0.0.0 quarkus.http.port=8181