diff --git a/pom.xml b/pom.xml index 852bd8fa95..564e3de7ec 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ 2.34.1 2.26 - 2.3.1 + 2.4.0 3.12.0 1.15 @@ -395,6 +395,8 @@ type=tmpfs,target=/opt/cryostat.d/recordings.d --mount type=tmpfs,target=/opt/cryostat.d/truststore.d + --mount + type=tmpfs,target=/opt/cryostat.d/probes.d --env CRYOSTAT_TARGET_CACHE_TTL=60 --env @@ -416,6 +418,8 @@ --env CRYOSTAT_TEMPLATE_PATH=/opt/cryostat.d/templates.d --env + CRYOSTAT_PROBE_TEMPLATE_PATH=/opt/cryostat.d/probes.d + --env SSL_TRUSTSTORE_DIR=/opt/cryostat.d/truststore.d --env GRAFANA_DATASOURCE_URL=http://${cryostat.itest.webHost}:${cryostat.itest.jfr-datasource.port} diff --git a/run.sh b/run.sh index 3c6e9e450d..8153b565a8 100755 --- a/run.sh +++ b/run.sh @@ -97,6 +97,7 @@ podman run \ --mount type=bind,source="$(dirname $0)/conf",destination=/opt/cryostat.d/conf.d,relabel=shared \ --mount type=bind,source="$(dirname $0)/templates",destination=/opt/cryostat.d/templates.d,relabel=shared \ --mount type=bind,source="$(dirname $0)/truststore",destination=/truststore,relabel=shared \ + --mount type=tmpfs,target=/opt/cryostat.d/probes.d \ -e CRYOSTAT_PLATFORM=$CRYOSTAT_PLATFORM \ -e CRYOSTAT_DISABLE_SSL=$CRYOSTAT_DISABLE_SSL \ -e CRYOSTAT_DISABLE_JMX_AUTH=$CRYOSTAT_DISABLE_JMX_AUTH \ @@ -114,6 +115,7 @@ podman run \ -e CRYOSTAT_CONFIG_PATH="/opt/cryostat.d/conf.d" \ -e CRYOSTAT_ARCHIVE_PATH="/opt/cryostat.d/recordings.d" \ -e CRYOSTAT_TEMPLATE_PATH="/opt/cryostat.d/templates.d" \ + -e CRYOSTAT_PROBE_TEMPLATE_PATH="/opt/cryostat.d/probes.d" \ -e CRYOSTAT_CLIENTLIB_PATH="/clientlib" \ -e CRYOSTAT_REPORT_GENERATION_MAX_HEAP="$CRYOSTAT_REPORT_GENERATION_MAX_HEAP" \ -e GRAFANA_DATASOURCE_URL=$GRAFANA_DATASOURCE_URL \ diff --git a/src/main/java/io/cryostat/net/security/ResourceAction.java b/src/main/java/io/cryostat/net/security/ResourceAction.java index 427c1fbce2..90d31ccfbd 100644 --- a/src/main/java/io/cryostat/net/security/ResourceAction.java +++ b/src/main/java/io/cryostat/net/security/ResourceAction.java @@ -72,6 +72,9 @@ public enum ResourceAction { UPDATE_TEMPLATE(UPDATE, TEMPLATE), DELETE_TEMPLATE(DELETE, TEMPLATE), + CREATE_PROBE_TEMPLATE(CREATE, TEMPLATE), + DELETE_PROBE_TEMPLATE(DELETE, TEMPLATE), + CREATE_REPORT(CREATE, REPORT), READ_REPORT(READ, REPORT), UPDATE_REPORT(UPDATE, REPORT), diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/HttpApiBetaModule.java b/src/main/java/io/cryostat/net/web/http/api/beta/HttpApiBetaModule.java index a447e73580..4cbe2da830 100644 --- a/src/main/java/io/cryostat/net/web/http/api/beta/HttpApiBetaModule.java +++ b/src/main/java/io/cryostat/net/web/http/api/beta/HttpApiBetaModule.java @@ -49,6 +49,31 @@ public abstract class HttpApiBetaModule { @IntoSet abstract RequestHandler bindDiscoveryGetHandler(DiscoveryGetHandler handler); + @Binds + @IntoSet + abstract RequestHandler bindProbeTemplateUploadHandler(ProbeTemplateUploadHandler handler); + + @Binds + @IntoSet + abstract RequestHandler bindProbeTemplateUploadBodyHandler( + ProbeTemplateUploadBodyHandler handler); + + @Binds + @IntoSet + abstract RequestHandler bindProbeTemplateDeleteHandler(ProbeTemplateDeleteHandler handler); + + @Binds + @IntoSet + abstract RequestHandler bindTargetProbePostHandler(TargetProbePostHandler handler); + + @Binds + @IntoSet + abstract RequestHandler bindTargetProbeDeleteHandler(TargetProbeDeleteHandler handler); + + @Binds + @IntoSet + abstract RequestHandler bindTargetProbesGetHandler(TargetProbesGetHandler handler); + @Binds @IntoSet abstract RequestHandler bindAuthTokenPostHandler(AuthTokenPostHandler handler); diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandler.java new file mode 100644 index 0000000000..d612014b3f --- /dev/null +++ b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandler.java @@ -0,0 +1,144 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import io.cryostat.core.agent.LocalProbeTemplateService; +import io.cryostat.core.log.Logger; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.handler.impl.HttpStatusException; + +public class ProbeTemplateDeleteHandler extends AbstractV2RequestHandler { + + static final String PATH = "probes/:probetemplateName"; + + private final Logger logger; + private final NotificationFactory notificationFactory; + private final LocalProbeTemplateService probeTemplateService; + private final FileSystem fs; + private static final String NOTIFICATION_CATEGORY = "ProbeTemplateUploaded"; + + @Inject + ProbeTemplateDeleteHandler( + AuthManager auth, + NotificationFactory notificationFactory, + LocalProbeTemplateService probeTemplateService, + Logger logger, + FileSystem fs, + Gson gson) { + super(auth, gson); + this.notificationFactory = notificationFactory; + this.logger = logger; + this.probeTemplateService = probeTemplateService; + this.fs = fs; + } + + @Override + public ApiVersion apiVersion() { + return ApiVersion.V2; + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.DELETE; + } + + @Override + public String path() { + return basePath() + PATH; + } + + @Override + public boolean isAsync() { + return false; + } + + @Override + public boolean isOrdered() { + return true; + } + + @Override + public boolean requiresAuthentication() { + return true; + } + + @Override + public IntermediateResponse handle(RequestParameters params) throws Exception { + String probeTemplateName = params.getPathParams().get("probetemplateName"); + try { + this.probeTemplateService.deleteTemplate(probeTemplateName); + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message(Map.of("probeTemplate", probeTemplateName)) + .build() + .send(); + } catch (Exception e) { + throw new HttpStatusException(400, e.getMessage(), e); + } + return new IntermediateResponse().body(null); + } + + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.DELETE_PROBE_TEMPLATE); + } + + @Override + public HttpMimeType mimeType() { + return HttpMimeType.PLAINTEXT; + } +} diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadBodyHandler.java new file mode 100644 index 0000000000..e22b84eb00 --- /dev/null +++ b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadBodyHandler.java @@ -0,0 +1,91 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import java.util.Set; + +import javax.inject.Inject; + +import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; +import io.cryostat.net.web.http.api.ApiVersion; + +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; + +public class ProbeTemplateUploadBodyHandler extends AbstractAuthenticatedRequestHandler { + + static final BodyHandler BODY_HANDLER = BodyHandler.create(true); + + @Inject + ProbeTemplateUploadBodyHandler(AuthManager auth) { + super(auth); + } + + @Override + public ApiVersion apiVersion() { + return ApiVersion.V2; + } + + @Override + public int getPriority() { + return DEFAULT_PRIORITY - 1; + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.POST; + } + + @Override + public String path() { + return basePath() + ProbeTemplateUploadHandler.PATH; + } + + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + + @Override + public void handleAuthenticated(RoutingContext ctx) { + BODY_HANDLER.handle(ctx); + } +} diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandler.java new file mode 100644 index 0000000000..bc4546e122 --- /dev/null +++ b/src/main/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandler.java @@ -0,0 +1,163 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import java.io.InputStream; +import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import io.cryostat.core.agent.LocalProbeTemplateService; +import io.cryostat.core.agent.ProbeValidationException; +import io.cryostat.core.log.Logger; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.FileUpload; +import io.vertx.ext.web.handler.impl.HttpStatusException; + +class ProbeTemplateUploadHandler extends AbstractV2RequestHandler { + + static final String PATH = "probes/:probetemplateName"; + + private final Logger logger; + private final NotificationFactory notificationFactory; + private final LocalProbeTemplateService probeTemplateService; + private final FileSystem fs; + private static final String NOTIFICATION_CATEGORY = "ProbeTemplateUploaded"; + + @Inject + ProbeTemplateUploadHandler( + AuthManager auth, + NotificationFactory notificationFactory, + LocalProbeTemplateService probeTemplateService, + Logger logger, + FileSystem fs, + Gson gson) { + super(auth, gson); + this.notificationFactory = notificationFactory; + this.logger = logger; + this.probeTemplateService = probeTemplateService; + this.fs = fs; + } + + @Override + public ApiVersion apiVersion() { + return ApiVersion.V2; + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.POST; + } + + @Override + public String path() { + return basePath() + PATH; + } + + @Override + public boolean isAsync() { + return false; + } + + @Override + public boolean isOrdered() { + return true; + } + + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.CREATE_PROBE_TEMPLATE); + } + + @Override + public boolean requiresAuthentication() { + return true; + } + + @Override + public IntermediateResponse handle(RequestParameters requestParams) throws Exception { + try { + for (FileUpload u : requestParams.getFileUploads()) { + String templateName = requestParams.getPathParams().get("probetemplateName"); + Path path = fs.pathOf(u.uploadedFileName()); + if (!"probeTemplate".equals(u.name())) { + fs.deleteIfExists(path); + continue; + } + try (InputStream is = fs.newInputStream(path)) { + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message(Map.of("probeTemplate", u.uploadedFileName())) + .build() + .send(); + probeTemplateService.addTemplate(is, templateName); + } finally { + fs.deleteIfExists(path); + } + } + } catch (ProbeValidationException pve) { + logger.error(pve.getMessage()); + throw new HttpStatusException(400, pve.getMessage(), pve); + } catch (Exception e) { + logger.error(e.getMessage()); + throw new HttpStatusException(500, e.getMessage(), e); + } + return new IntermediateResponse().body(null); + } + + @Override + public HttpMimeType mimeType() { + return HttpMimeType.PLAINTEXT; + } +} diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandler.java new file mode 100644 index 0000000000..c55e4cf458 --- /dev/null +++ b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandler.java @@ -0,0 +1,159 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import io.cryostat.core.agent.AgentJMXHelper; +import io.cryostat.core.log.Logger; +import io.cryostat.core.sys.Environment; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.handler.impl.HttpStatusException; +import org.apache.commons.lang3.StringUtils; + +public class TargetProbeDeleteHandler extends AbstractV2RequestHandler { + + static final String PATH = "targets/:targetId/probes"; + + private final Logger logger; + private final NotificationFactory notificationFactory; + private final FileSystem fs; + private final TargetConnectionManager connectionManager; + private final Environment env; + private static final String NOTIFICATION_CATEGORY = "ProbeTemplateDeleted"; + + @Inject + TargetProbeDeleteHandler( + Logger logger, + NotificationFactory notificationFactory, + FileSystem fs, + AuthManager auth, + TargetConnectionManager connectionManager, + Environment env, + Gson gson) { + super(auth, gson); + this.logger = logger; + this.notificationFactory = notificationFactory; + this.connectionManager = connectionManager; + this.env = env; + this.fs = fs; + } + + @Override + public ApiVersion apiVersion() { + return ApiVersion.V2; + } + + @Override + public String path() { + return basePath() + PATH; + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.DELETE; + } + + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + + @Override + public boolean requiresAuthentication() { + return true; + } + + @Override + public IntermediateResponse handle(RequestParameters requestParams) throws Exception { + Map pathParams = requestParams.getPathParams(); + String targetId = pathParams.get("targetId"); + StringBuilder sb = new StringBuilder(); + if (StringUtils.isBlank(targetId)) { + sb.append("targetId is required."); + throw new HttpStatusException(400, sb.toString().trim()); + } + try { + return connectionManager.executeConnectedTask( + getConnectionDescriptorFromParams(requestParams), + connection -> { + connection.connect(); + AgentJMXHelper helper = new AgentJMXHelper(connection.getHandle()); + // The convention for removing probes in the agent controller mbean is to + // call + // defineEventProbes with a null argument. + helper.defineEventProbes(null); + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message(Map.of("targetId", targetId)) + .build() + .send(); + return new IntermediateResponse().body(null); + }); + } catch (Exception e) { + throw e; + } + } + + @Override + public HttpMimeType mimeType() { + return HttpMimeType.PLAINTEXT; + } + + @Override + public boolean isAsync() { + return false; + } +} diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandler.java new file mode 100644 index 0000000000..4f6f003bbb --- /dev/null +++ b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandler.java @@ -0,0 +1,190 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import io.cryostat.core.agent.AgentJMXHelper; +import io.cryostat.core.agent.LocalProbeTemplateService; +import io.cryostat.core.log.Logger; +import io.cryostat.core.sys.Environment; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.handler.impl.HttpStatusException; +import org.apache.commons.lang3.StringUtils; + +/** + * TargetProbePostHandler will facilitate adding probes to a target and will have the following form + * and response types: + * + *

POST /api/v2/targets/:targetId/probes/ + * + *

targetId - The location of the target JVM to connect to, in the form of a service:rmi:jmx:// + * JMX Service URL. Should use percent-encoding. + * + *

Parameters + * + *

probeTemplate - name of the probe template to use + * + *

Responses + * + *

200 - No body + * + *

401 - User authentication failed. The body is an error message. There will be an + * X-WWW-Authenticate: $SCHEME header that indicates the authentication scheme that is used. + * + *

404 - The target could not be found. The body is an error message. + * + *

427 - JMX authentication failed. The body is an error message. There will be an + * X-JMX-Authenticate: $SCHEME header that indicates the authentication scheme that is used. + */ +public class TargetProbePostHandler extends AbstractV2RequestHandler { + + static final String PATH = "targets/:targetId/probes/:probeTemplate"; + + private final Logger logger; + private final NotificationFactory notificationFactory; + private final LocalProbeTemplateService probeTemplateService; + private final FileSystem fs; + private final TargetConnectionManager connectionManager; + private final Environment env; + private static final String NOTIFICATION_CATEGORY = "ProbeTemplateUploaded"; + + @Inject + TargetProbePostHandler( + Logger logger, + NotificationFactory notificationFactory, + LocalProbeTemplateService service, + FileSystem fs, + AuthManager auth, + TargetConnectionManager connectionManager, + Environment env, + Gson gson) { + super(auth, gson); + this.logger = logger; + this.notificationFactory = notificationFactory; + this.probeTemplateService = service; + this.connectionManager = connectionManager; + this.env = env; + this.fs = fs; + } + + @Override + public ApiVersion apiVersion() { + return ApiVersion.V2; + } + + @Override + public String path() { + return basePath() + PATH; + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.POST; + } + + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + + @Override + public boolean requiresAuthentication() { + return true; + } + + @Override + public IntermediateResponse handle(RequestParameters requestParams) throws Exception { + Map pathParams = requestParams.getPathParams(); + String targetId = pathParams.get("targetId"); + String probeTemplate = pathParams.get("probeTemplate"); + if (StringUtils.isAnyBlank(targetId, probeTemplate)) { + StringBuilder sb = new StringBuilder(); + if (StringUtils.isBlank(targetId)) { + sb.append("targetId is required."); + } + if (StringUtils.isBlank(probeTemplate)) { + sb.append("\"probeTemplate\" is required."); + } + throw new HttpStatusException(400, sb.toString().trim()); + } + return connectionManager.executeConnectedTask( + getConnectionDescriptorFromParams(requestParams), + connection -> { + connection.connect(); + AgentJMXHelper helper = new AgentJMXHelper(connection.getHandle()); + helper.defineEventProbes(probeTemplateService.getTemplate(probeTemplate)); + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message( + Map.of( + Map.of("targetId", targetId), + Map.of("probeTemplate", probeTemplate))) + .build() + .send(); + return new IntermediateResponse().body(null); + }); + } + + @Override + public HttpMimeType mimeType() { + return HttpMimeType.PLAINTEXT; + } + + @Override + public boolean isAsync() { + return false; + } +} diff --git a/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbesGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbesGetHandler.java new file mode 100644 index 0000000000..e2d8c5a985 --- /dev/null +++ b/src/main/java/io/cryostat/net/web/http/api/beta/TargetProbesGetHandler.java @@ -0,0 +1,140 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import java.util.Map; +import java.util.Set; + +import javax.inject.Inject; + +import io.cryostat.core.agent.AgentJMXHelper; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.AbstractV2RequestHandler; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.handler.impl.HttpStatusException; +import org.apache.commons.lang3.StringUtils; + +public class TargetProbesGetHandler extends AbstractV2RequestHandler { + + static final String PATH = "targets/:targetId/probes"; + + private final TargetConnectionManager connectionManager; + private static final String NOTIFICATION_CATEGORY = "TargetProbesGet"; + private final NotificationFactory notificationFactory; + + @Inject + TargetProbesGetHandler( + AuthManager auth, + TargetConnectionManager connectionManager, + NotificationFactory notificationFactory, + Gson gson) { + super(auth, gson); + this.notificationFactory = notificationFactory; + this.connectionManager = connectionManager; + } + + @Override + public ApiVersion apiVersion() { + return ApiVersion.V2; + } + + @Override + public String path() { + return basePath() + PATH; + } + + @Override + public HttpMethod httpMethod() { + return HttpMethod.GET; + } + + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + + @Override + public boolean requiresAuthentication() { + return true; + } + + @Override + public IntermediateResponse handle(RequestParameters requestParams) throws Exception { + Map pathParams = requestParams.getPathParams(); + String targetId = pathParams.get("targetId"); + StringBuilder sb = new StringBuilder(); + if (StringUtils.isBlank(targetId)) { + sb.append("targetId is required."); + throw new HttpStatusException(400, sb.toString().trim()); + } + return connectionManager.executeConnectedTask( + getConnectionDescriptorFromParams(requestParams), + connection -> { + connection.connect(); + AgentJMXHelper helper = new AgentJMXHelper(connection.getHandle()); + String probes = helper.retrieveEventProbes(); + notificationFactory + .createBuilder() + .metaCategory(NOTIFICATION_CATEGORY) + .metaType(HttpMimeType.JSON) + .message(Map.of("targetId", targetId)) + .build() + .send(); + return new IntermediateResponse().body(probes); + }); + } + + @Override + public HttpMimeType mimeType() { + return HttpMimeType.JSON; + } + + @Override + public boolean isAsync() { + return false; + } +} diff --git a/src/main/java/io/cryostat/templates/TemplatesModule.java b/src/main/java/io/cryostat/templates/TemplatesModule.java index 69a83748a0..fc9bd9b1b8 100644 --- a/src/main/java/io/cryostat/templates/TemplatesModule.java +++ b/src/main/java/io/cryostat/templates/TemplatesModule.java @@ -39,6 +39,7 @@ import javax.inject.Singleton; +import io.cryostat.core.agent.LocalProbeTemplateService; import io.cryostat.core.sys.Environment; import io.cryostat.core.sys.FileSystem; import io.cryostat.core.templates.LocalStorageTemplateService; @@ -55,4 +56,17 @@ static LocalStorageTemplateService provideLocalStorageTemplateService( FileSystem fs, Environment env) { return new LocalStorageTemplateService(fs, env); } + + @Provides + @Singleton + static LocalProbeTemplateService provideLocalProbeTemplateService( + FileSystem fs, Environment env) { + try { + return new LocalProbeTemplateService(fs, env); + } catch (Exception e) { + // Dagger doesn't like constructors that can throw exceptions, the probeTemplateService + // throws an exception if the sanity checks fail so we need to deal with it here + throw new RuntimeException(e); + } + } } diff --git a/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandlerTest.java new file mode 100644 index 0000000000..b625862800 --- /dev/null +++ b/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateDeleteHandlerTest.java @@ -0,0 +1,170 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import static org.mockito.Mockito.lenient; + +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +import io.cryostat.MainModule; +import io.cryostat.core.agent.LocalProbeTemplateService; +import io.cryostat.core.log.Logger; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.handler.impl.HttpStatusException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ProbeTemplateDeleteHandlerTest { + + ProbeTemplateDeleteHandler handler; + @Mock AuthManager auth; + @Mock LocalProbeTemplateService templateService; + @Mock FileSystem fs; + @Mock Logger logger; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; + Gson gson = MainModule.provideGson(logger); + + @BeforeEach + void setup() { + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); + this.handler = + new ProbeTemplateDeleteHandler( + auth, notificationFactory, templateService, logger, fs, gson); + } + + @Nested + class BasicHandlerDefinition { + @Test + void shouldBePOSTHandler() { + MatcherAssert.assertThat(handler.httpMethod(), Matchers.equalTo(HttpMethod.DELETE)); + } + + @Test + void shouldBeAPIV2() { + MatcherAssert.assertThat(handler.apiVersion(), Matchers.equalTo(ApiVersion.V2)); + } + + @Test + void shouldHaveExpectedPath() { + MatcherAssert.assertThat( + handler.path(), Matchers.equalTo("/api/v2/probes/:probetemplateName")); + } + + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.DELETE_PROBE_TEMPLATE))); + } + + @Test + void shouldReturnPlaintextMimeType() { + MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT)); + } + + @Test + void shouldRequireAuthentication() { + MatcherAssert.assertThat(handler.requiresAuthentication(), Matchers.is(true)); + } + } + + @Nested + class RequestHandling { + + @Mock RequestParameters requestParams; + + @Test + void shouldRespond400WhenTemplateNotFound() throws Exception { + Mockito.when(requestParams.getPathParams()) + .thenReturn(Map.of("probetemplateName", "foo.xml")); + Mockito.doThrow(new IOException()).when(templateService).deleteTemplate("foo.xml"); + HttpStatusException ex = + Assertions.assertThrows( + HttpStatusException.class, () -> handler.handle(requestParams)); + MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(400)); + } + + @Test + void shouldCallThroughToService() throws Exception { + Mockito.when(requestParams.getPathParams()) + .thenReturn(Map.of("probetemplateName", "foo.xml")); + IntermediateResponse response = handler.handle(requestParams); + + Mockito.verify(templateService).deleteTemplate("foo.xml"); + Mockito.verifyNoMoreInteractions(templateService); + + MatcherAssert.assertThat(response.getStatusCode(), Matchers.equalTo(200)); + MatcherAssert.assertThat(response.getBody(), Matchers.nullValue()); + } + } +} diff --git a/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandlerTest.java new file mode 100644 index 0000000000..4c3f8f495a --- /dev/null +++ b/src/test/java/io/cryostat/net/web/http/api/beta/ProbeTemplateUploadHandlerTest.java @@ -0,0 +1,223 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import static org.mockito.Mockito.lenient; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; + +import io.cryostat.MainModule; +import io.cryostat.core.agent.LocalProbeTemplateService; +import io.cryostat.core.agent.ProbeValidationException; +import io.cryostat.core.log.Logger; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.FileUpload; +import io.vertx.ext.web.handler.impl.HttpStatusException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ProbeTemplateUploadHandlerTest { + + ProbeTemplateUploadHandler handler; + @Mock AuthManager auth; + @Mock LocalProbeTemplateService templateService; + @Mock FileSystem fs; + @Mock Logger logger; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; + Gson gson = MainModule.provideGson(logger); + + @BeforeEach + void setup() { + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); + this.handler = + new ProbeTemplateUploadHandler( + auth, notificationFactory, templateService, logger, fs, gson); + } + + @Nested + class BasicHandlerDefinition { + @Test + void shouldBePOSTHandler() { + MatcherAssert.assertThat(handler.httpMethod(), Matchers.equalTo(HttpMethod.POST)); + } + + @Test + void shouldBeAPIV2() { + MatcherAssert.assertThat(handler.apiVersion(), Matchers.equalTo(ApiVersion.V2)); + } + + @Test + void shouldHaveExpectedPath() { + MatcherAssert.assertThat( + handler.path(), Matchers.equalTo("/api/v2/probes/:probetemplateName")); + } + + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.CREATE_PROBE_TEMPLATE))); + } + + @Test + void shouldReturnPlaintextMimeType() { + MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT)); + } + + @Test + void shouldRequireAuthentication() { + MatcherAssert.assertThat(handler.requiresAuthentication(), Matchers.is(true)); + } + } + + @Nested + class RequestHandling { + + @Mock RequestParameters requestParams; + + @Test + void shouldRespond500WhenUploadFails() throws Exception { + FileUpload upload = Mockito.mock(FileUpload.class); + Mockito.when(upload.name()).thenReturn("probeTemplate"); + Mockito.when(requestParams.getFileUploads()).thenReturn(Set.of(upload)); + Mockito.when(requestParams.getPathParams()) + .thenReturn(Map.of("probetemplateName", "foo.xml")); + + Mockito.when(upload.uploadedFileName()).thenReturn("/file-uploads/abcd-1234"); + Path uploadPath = Mockito.mock(Path.class); + Mockito.when(fs.pathOf("/file-uploads/abcd-1234")).thenReturn(uploadPath); + + Mockito.when(fs.newInputStream(Mockito.any())).thenThrow(IOException.class); + HttpStatusException ex = + Assertions.assertThrows( + HttpStatusException.class, () -> handler.handle(requestParams)); + MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(500)); + Mockito.verify(fs).deleteIfExists(uploadPath); + } + + @Test + void shouldRespond400IfXmlInvalid() throws Exception { + FileUpload upload = Mockito.mock(FileUpload.class); + Mockito.when(upload.name()).thenReturn("probeTemplate"); + Mockito.when(upload.uploadedFileName()).thenReturn("/file-uploads/abcd-1234"); + + Mockito.when(requestParams.getFileUploads()).thenReturn(Set.of(upload)); + Mockito.when(requestParams.getPathParams()) + .thenReturn(Map.of("probetemplateName", "foo.xml")); + + Path uploadPath = Mockito.mock(Path.class); + Mockito.when(fs.pathOf("/file-uploads/abcd-1234")).thenReturn(uploadPath); + + InputStream stream = Mockito.mock(InputStream.class); + Mockito.when(fs.newInputStream(Mockito.any())).thenReturn(stream); + + Mockito.doThrow(ProbeValidationException.class) + .when(templateService) + .addTemplate(stream, "foo.xml"); + + HttpStatusException ex = + Assertions.assertThrows( + HttpStatusException.class, () -> handler.handle(requestParams)); + MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(400)); + Mockito.verify(fs).deleteIfExists(uploadPath); + } + + @Test + void shouldProcessGoodRequest() throws Exception { + FileUpload upload = Mockito.mock(FileUpload.class); + Mockito.when(upload.name()).thenReturn("probeTemplate"); + Mockito.when(upload.uploadedFileName()).thenReturn("/file-uploads/abcd-1234"); + + Mockito.when(requestParams.getFileUploads()).thenReturn(Set.of(upload)); + Mockito.when(requestParams.getPathParams()) + .thenReturn(Map.of("probetemplateName", "foo.xml")); + + Path uploadPath = Mockito.mock(Path.class); + Mockito.when(fs.pathOf("/file-uploads/abcd-1234")).thenReturn(uploadPath); + + InputStream stream = Mockito.mock(InputStream.class); + Mockito.when(fs.newInputStream(uploadPath)).thenReturn(stream); + + IntermediateResponse response = handler.handle(requestParams); + + Mockito.verify(templateService).addTemplate(stream, "foo.xml"); + Mockito.verifyNoMoreInteractions(templateService); + Mockito.verify(fs).deleteIfExists(uploadPath); + + MatcherAssert.assertThat(response.getStatusCode(), Matchers.equalTo(200)); + MatcherAssert.assertThat(response.getBody(), Matchers.nullValue()); + } + } +} diff --git a/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandlerTest.java new file mode 100644 index 0000000000..d934b6b6d8 --- /dev/null +++ b/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeDeleteHandlerTest.java @@ -0,0 +1,194 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +import java.util.Map; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.openjdk.jmc.rjmx.IConnectionHandle; + +import io.cryostat.MainModule; +import io.cryostat.core.log.Logger; +import io.cryostat.core.net.JFRConnection; +import io.cryostat.core.sys.Environment; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.handler.impl.HttpStatusException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class TargetProbeDeleteHandlerTest { + + TargetProbeDeleteHandler handler; + @Mock AuthManager auth; + @Mock FileSystem fs; + @Mock Logger logger; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; + @Mock TargetConnectionManager targetConnectionManager; + @Mock Environment env; + Gson gson = MainModule.provideGson(logger); + + @BeforeEach + void setup() { + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); + this.handler = + new TargetProbeDeleteHandler( + logger, notificationFactory, fs, auth, targetConnectionManager, env, gson); + } + + @Nested + class BasicHandlerDefinition { + @Test + void shouldBeDELETEHandler() { + MatcherAssert.assertThat(handler.httpMethod(), Matchers.equalTo(HttpMethod.DELETE)); + } + + @Test + void shouldBeBetaAPI() { + MatcherAssert.assertThat(handler.apiVersion(), Matchers.equalTo(ApiVersion.V2)); + } + + @Test + void shouldHaveExpectedPath() { + MatcherAssert.assertThat( + handler.path(), Matchers.equalTo("/api/v2/targets/:targetId/probes")); + } + + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(ResourceAction.NONE)); + } + + @Test + void shouldReturnPlaintextMimeType() { + MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT)); + } + + @Test + void shouldRequireAuthentication() { + MatcherAssert.assertThat(handler.requiresAuthentication(), Matchers.is(true)); + } + } + + @Nested + class Requests { + + @Mock RequestParameters requestParams; + private static final String AGENT_OBJECT_NAME = + "org.openjdk.jmc.jfr.agent:type=AgentController"; + private static final String DEFINE_EVENT_PROBES = "defineEventProbes"; + + @Test + public void shouldRespondOK() throws Exception { + Mockito.when(requestParams.getPathParams()).thenReturn(Map.of("targetId", "foo")); + Mockito.when(requestParams.getHeaders()).thenReturn(MultiMap.caseInsensitiveMultiMap()); + JFRConnection connection = Mockito.mock(JFRConnection.class); + IConnectionHandle handle = Mockito.mock(IConnectionHandle.class); + MBeanServerConnection mbsc = Mockito.mock(MBeanServerConnection.class); + Mockito.when( + targetConnectionManager.executeConnectedTask( + Mockito.any(ConnectionDescriptor.class), Mockito.any())) + .thenAnswer( + arg0 -> + ((TargetConnectionManager.ConnectedTask) + arg0.getArgument(1)) + .execute(connection)); + Mockito.when(connection.getHandle()).thenReturn(handle); + Mockito.when(handle.getServiceOrDummy(MBeanServerConnection.class)).thenReturn(mbsc); + Mockito.when( + mbsc.invoke( + any(ObjectName.class), + any(String.class), + any(Object[].class), + any(String[].class))) + .thenReturn(null); + IntermediateResponse response = handler.handle(requestParams); + MatcherAssert.assertThat(response.getStatusCode(), Matchers.equalTo(200)); + } + + @Test + public void shouldRespond400WhenTargetIdIsMissing() throws Exception { + Mockito.when(requestParams.getPathParams()).thenReturn(Map.of("targetId", "")); + try { + IntermediateResponse response = handler.handle(requestParams); + } catch (HttpStatusException e) { + MatcherAssert.assertThat(e.getStatusCode(), Matchers.equalTo(400)); + } + } + } +} diff --git a/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeGetHandlerTest.java new file mode 100644 index 0000000000..41a3903f39 --- /dev/null +++ b/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbeGetHandlerTest.java @@ -0,0 +1,194 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +import java.util.Map; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.openjdk.jmc.rjmx.IConnectionHandle; + +import io.cryostat.MainModule; +import io.cryostat.core.agent.LocalProbeTemplateService; +import io.cryostat.core.log.Logger; +import io.cryostat.core.net.JFRConnection; +import io.cryostat.core.sys.Environment; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.handler.impl.HttpStatusException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class TargetProbeGetHandlerTest { + + TargetProbesGetHandler handler; + @Mock AuthManager auth; + @Mock LocalProbeTemplateService templateService; + @Mock FileSystem fs; + @Mock Logger logger; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; + @Mock TargetConnectionManager targetConnectionManager; + @Mock Environment env; + Gson gson = MainModule.provideGson(logger); + + @BeforeEach + void setup() { + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); + this.handler = + new TargetProbesGetHandler( + auth, targetConnectionManager, notificationFactory, gson); + } + + @Nested + class BasicHandlerDefinition { + @Test + void shouldBeGETHandler() { + MatcherAssert.assertThat(handler.httpMethod(), Matchers.equalTo(HttpMethod.GET)); + } + + @Test + void shouldBeV2API() { + MatcherAssert.assertThat(handler.apiVersion(), Matchers.equalTo(ApiVersion.V2)); + } + + @Test + void shouldHaveExpectedPath() { + MatcherAssert.assertThat( + handler.path(), Matchers.equalTo("/api/v2/targets/:targetId/probes")); + } + + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(ResourceAction.NONE)); + } + + @Test + void shouldReturnJSONMimeType() { + MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.JSON)); + } + + @Test + void shouldRequireAuthentication() { + MatcherAssert.assertThat(handler.requiresAuthentication(), Matchers.is(true)); + } + } + + @Nested + class Requests { + + @Mock RequestParameters requestParams; + + @Test + public void shouldRespondOK() throws Exception { + Mockito.when(requestParams.getPathParams()).thenReturn(Map.of("targetId", "foo")); + Mockito.when(requestParams.getHeaders()).thenReturn(MultiMap.caseInsensitiveMultiMap()); + JFRConnection connection = Mockito.mock(JFRConnection.class); + IConnectionHandle handle = Mockito.mock(IConnectionHandle.class); + MBeanServerConnection mbsc = Mockito.mock(MBeanServerConnection.class); + Mockito.when( + targetConnectionManager.executeConnectedTask( + Mockito.any(ConnectionDescriptor.class), Mockito.any())) + .thenAnswer( + arg0 -> + ((TargetConnectionManager.ConnectedTask) + arg0.getArgument(1)) + .execute(connection)); + Mockito.when(connection.getHandle()).thenReturn(handle); + Mockito.when(handle.getServiceOrDummy(MBeanServerConnection.class)).thenReturn(mbsc); + Object result = Mockito.mock(Object.class); + Mockito.when( + mbsc.invoke( + any(ObjectName.class), + any(String.class), + any(Object[].class), + any(String[].class))) + .thenReturn(result); + IntermediateResponse response = handler.handle(requestParams); + MatcherAssert.assertThat(response.getStatusCode(), Matchers.equalTo(200)); + } + + @Test + public void shouldRespond400WhenTargetIdIsMissing() throws Exception { + Mockito.when(requestParams.getPathParams()).thenReturn(Map.of("targetId", "")); + try { + IntermediateResponse response = handler.handle(requestParams); + } catch (HttpStatusException e) { + MatcherAssert.assertThat(e.getStatusCode(), Matchers.equalTo(400)); + } + } + } +} diff --git a/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandlerTest.java new file mode 100644 index 0000000000..e87f78538d --- /dev/null +++ b/src/test/java/io/cryostat/net/web/http/api/beta/TargetProbePostHandlerTest.java @@ -0,0 +1,203 @@ +/* + * Copyright The Cryostat Authors + * + * The Universal Permissive License (UPL), Version 1.0 + * + * Subject to the condition set forth below, permission is hereby granted to any + * person obtaining a copy of this software, associated documentation and/or data + * (collectively the "Software"), free of charge and under any and all copyright + * rights in the Software, and any and all patent rights owned or freely + * licensable by each licensor hereunder covering either (i) the unmodified + * Software as contributed to or provided by such licensor, or (ii) the Larger + * Works (as defined below), to deal in both + * + * (a) the Software, and + * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if + * one is included with the Software (each a "Larger Work" to which the Software + * is contributed by such licensors), + * + * without restriction, including without limitation the rights to copy, create + * derivative works of, display, perform, and distribute the Software and make, + * use, sell, offer for sale, import, export, have made, and have sold the + * Software and the Larger Work(s), and to sublicense the foregoing rights on + * either these or other terms. + * + * This license is subject to the following condition: + * The above copyright notice and either this complete permission notice or at + * a minimum a reference to the UPL must be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package io.cryostat.net.web.http.api.beta; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +import java.util.Map; + +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; + +import org.openjdk.jmc.rjmx.IConnectionHandle; + +import io.cryostat.MainModule; +import io.cryostat.core.agent.LocalProbeTemplateService; +import io.cryostat.core.log.Logger; +import io.cryostat.core.net.JFRConnection; +import io.cryostat.core.sys.Environment; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.messaging.notifications.Notification; +import io.cryostat.messaging.notifications.NotificationFactory; +import io.cryostat.net.AuthManager; +import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.web.http.HttpMimeType; +import io.cryostat.net.web.http.api.ApiVersion; +import io.cryostat.net.web.http.api.v2.IntermediateResponse; +import io.cryostat.net.web.http.api.v2.RequestParameters; + +import com.google.gson.Gson; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.ext.web.handler.impl.HttpStatusException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class TargetProbePostHandlerTest { + + TargetProbePostHandler handler; + @Mock AuthManager auth; + @Mock LocalProbeTemplateService templateService; + @Mock FileSystem fs; + @Mock Logger logger; + @Mock NotificationFactory notificationFactory; + @Mock Notification notification; + @Mock Notification.Builder notificationBuilder; + @Mock TargetConnectionManager targetConnectionManager; + @Mock Environment env; + Gson gson = MainModule.provideGson(logger); + + @BeforeEach + void setup() { + lenient().when(notificationFactory.createBuilder()).thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaCategory(Mockito.any())) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(Notification.MetaType.class))) + .thenReturn(notificationBuilder); + lenient() + .when(notificationBuilder.metaType(Mockito.any(HttpMimeType.class))) + .thenReturn(notificationBuilder); + lenient().when(notificationBuilder.message(Mockito.any())).thenReturn(notificationBuilder); + lenient().when(notificationBuilder.build()).thenReturn(notification); + this.handler = + new TargetProbePostHandler( + logger, + notificationFactory, + templateService, + fs, + auth, + targetConnectionManager, + env, + gson); + } + + @Nested + class BasicHandlerDefinition { + @Test + void shouldBePOSTHandler() { + MatcherAssert.assertThat(handler.httpMethod(), Matchers.equalTo(HttpMethod.POST)); + } + + @Test + void shouldBeV2API() { + MatcherAssert.assertThat(handler.apiVersion(), Matchers.equalTo(ApiVersion.V2)); + } + + @Test + void shouldHaveExpectedPath() { + MatcherAssert.assertThat( + handler.path(), + Matchers.equalTo("/api/v2/targets/:targetId/probes/:probeTemplate")); + } + + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(ResourceAction.NONE)); + } + + @Test + void shouldReturnPlaintextMimeType() { + MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT)); + } + + @Test + void shouldRequireAuthentication() { + MatcherAssert.assertThat(handler.requiresAuthentication(), Matchers.is(true)); + } + } + + @Nested + class Requests { + + @Mock RequestParameters requestParams; + + @Test + public void shouldRespondOK() throws Exception { + Mockito.when(requestParams.getPathParams()) + .thenReturn(Map.of("targetId", "foo", "probeTemplate", "bar")); + Mockito.when(requestParams.getHeaders()).thenReturn(MultiMap.caseInsensitiveMultiMap()); + JFRConnection connection = Mockito.mock(JFRConnection.class); + IConnectionHandle handle = Mockito.mock(IConnectionHandle.class); + MBeanServerConnection mbsc = Mockito.mock(MBeanServerConnection.class); + Mockito.when( + targetConnectionManager.executeConnectedTask( + Mockito.any(ConnectionDescriptor.class), Mockito.any())) + .thenAnswer( + arg0 -> + ((TargetConnectionManager.ConnectedTask) + arg0.getArgument(1)) + .execute(connection)); + Mockito.when(connection.getHandle()).thenReturn(handle); + Mockito.when(handle.getServiceOrDummy(MBeanServerConnection.class)).thenReturn(mbsc); + Object result = Mockito.mock(Object.class); + Mockito.when( + mbsc.invoke( + any(ObjectName.class), + any(String.class), + any(Object[].class), + any(String[].class))) + .thenReturn(result); + IntermediateResponse response = handler.handle(requestParams); + MatcherAssert.assertThat(response.getStatusCode(), Matchers.equalTo(200)); + } + + @Test + public void shouldRespond400WhenTargetIdIsMissing() throws Exception { + Mockito.when(requestParams.getPathParams()).thenReturn(Map.of("targetId", "")); + try { + IntermediateResponse response = handler.handle(requestParams); + } catch (HttpStatusException e) { + MatcherAssert.assertThat(e.getStatusCode(), Matchers.equalTo(400)); + } + } + } +}