diff --git a/pom.xml b/pom.xml index 3b7740c869..868a9456d8 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 1.7 2.8.0 4.5.13 - 5.3.0 + 5.4.1 3.9.7 1.7.30 2.8.6 @@ -201,6 +201,18 @@ ${com.github.spotbugs.version} provided + + io.fabric8 + kubernetes-server-mock + ${io.fabric8.client.version} + test + + + io.fabric8 + openshift-server-mock + ${io.fabric8.client.version} + test + diff --git a/src/main/java/io/cryostat/messaging/MessagingServer.java b/src/main/java/io/cryostat/messaging/MessagingServer.java index b8ac612108..799e985ba2 100644 --- a/src/main/java/io/cryostat/messaging/MessagingServer.java +++ b/src/main/java/io/cryostat/messaging/MessagingServer.java @@ -56,6 +56,7 @@ import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.HttpServer; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import com.google.gson.Gson; @@ -147,7 +148,12 @@ public void start() throws SocketException, UnknownHostException { authManager .doAuthenticated( sws::subProtocol, - authManager::validateWebSocketSubProtocol) + p -> + authManager + .validateWebSocketSubProtocol( + p, + ResourceAction + .READ_ALL)) .onSuccess( () -> { logger.info( diff --git a/src/main/java/io/cryostat/net/AuthManager.java b/src/main/java/io/cryostat/net/AuthManager.java index f173fce192..7883ba1ca7 100644 --- a/src/main/java/io/cryostat/net/AuthManager.java +++ b/src/main/java/io/cryostat/net/AuthManager.java @@ -37,18 +37,24 @@ */ package io.cryostat.net; +import java.util.Set; import java.util.concurrent.Future; import java.util.function.Function; import java.util.function.Supplier; +import io.cryostat.net.security.ResourceAction; + public interface AuthManager { AuthenticationScheme getScheme(); - Future validateToken(Supplier tokenProvider); + Future validateToken( + Supplier tokenProvider, Set resourceActions); - Future validateHttpHeader(Supplier headerProvider); + Future validateHttpHeader( + Supplier headerProvider, Set resourceActions); - Future validateWebSocketSubProtocol(Supplier subProtocolProvider); + Future validateWebSocketSubProtocol( + Supplier subProtocolProvider, Set resourceActions); AuthenticatedAction doAuthenticated( Supplier provider, Function, Future> validator); diff --git a/src/main/java/io/cryostat/net/BasicAuthManager.java b/src/main/java/io/cryostat/net/BasicAuthManager.java index 7ef6cb45f8..5b83f55b94 100644 --- a/src/main/java/io/cryostat/net/BasicAuthManager.java +++ b/src/main/java/io/cryostat/net/BasicAuthManager.java @@ -44,6 +44,7 @@ import java.util.Base64; import java.util.Objects; import java.util.Properties; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.function.Supplier; @@ -52,6 +53,7 @@ import io.cryostat.core.log.Logger; import io.cryostat.core.sys.FileSystem; +import io.cryostat.net.security.ResourceAction; import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.lang3.StringUtils; @@ -77,7 +79,8 @@ public AuthenticationScheme getScheme() { } @Override - public Future validateToken(Supplier tokenProvider) { + public Future validateToken( + Supplier tokenProvider, Set resourceActions) { if (!configLoaded) { this.loadConfig(); } @@ -90,12 +93,21 @@ public Future validateToken(Supplier tokenProvider) { String user = matcher.group(1); String pass = matcher.group(2); String passHashHex = DigestUtils.sha256Hex(pass); - return CompletableFuture.completedFuture( - Objects.equals(users.getProperty(user), passHashHex)); + boolean granted = Objects.equals(users.getProperty(user), passHashHex); + // FIXME actually implement this + resourceActions.forEach( + action -> + logger.trace( + "user {} granted {} {}", + user, + action.getVerb(), + action.getResource())); + return CompletableFuture.completedFuture(granted); } @Override - public Future validateHttpHeader(Supplier headerProvider) { + public Future validateHttpHeader( + Supplier headerProvider, Set resourceActions) { String authorization = headerProvider.get(); if (StringUtils.isBlank(authorization)) { return CompletableFuture.completedFuture(false); @@ -109,14 +121,15 @@ public Future validateHttpHeader(Supplier headerProvider) { try { String decoded = new String(Base64.getUrlDecoder().decode(b64), StandardCharsets.UTF_8).trim(); - return validateToken(() -> decoded); + return validateToken(() -> decoded, resourceActions); } catch (IllegalArgumentException e) { return CompletableFuture.completedFuture(false); } } @Override - public Future validateWebSocketSubProtocol(Supplier subProtocolProvider) { + public Future validateWebSocketSubProtocol( + Supplier subProtocolProvider, Set resourceActions) { String subprotocol = subProtocolProvider.get(); if (StringUtils.isBlank(subprotocol)) { return CompletableFuture.completedFuture(false); @@ -132,7 +145,7 @@ public Future validateWebSocketSubProtocol(Supplier subProtocol try { String decoded = new String(Base64.getUrlDecoder().decode(b64), StandardCharsets.UTF_8).trim(); - return validateToken(() -> decoded); + return validateToken(() -> decoded, resourceActions); } catch (IllegalArgumentException e) { return CompletableFuture.completedFuture(false); } diff --git a/src/main/java/io/cryostat/net/NetworkModule.java b/src/main/java/io/cryostat/net/NetworkModule.java index 3f8a3b7abd..48ed6f9db0 100644 --- a/src/main/java/io/cryostat/net/NetworkModule.java +++ b/src/main/java/io/cryostat/net/NetworkModule.java @@ -55,6 +55,8 @@ import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoSet; +import io.fabric8.openshift.client.DefaultOpenShiftClient; +import io.fabric8.openshift.client.OpenShiftConfigBuilder; import io.vertx.core.Vertx; import io.vertx.ext.web.client.WebClient; import io.vertx.ext.web.client.WebClientOptions; @@ -159,7 +161,12 @@ static BasicAuthManager provideBasicAuthManager(Logger logger, FileSystem fs) { @Provides @Singleton static OpenShiftAuthManager provideOpenShiftAuthManager(Logger logger, FileSystem fs) { - return new OpenShiftAuthManager(logger, fs); + return new OpenShiftAuthManager( + logger, + fs, + token -> + new DefaultOpenShiftClient( + new OpenShiftConfigBuilder().withOauthToken(token).build())); } @Binds diff --git a/src/main/java/io/cryostat/net/NoopAuthManager.java b/src/main/java/io/cryostat/net/NoopAuthManager.java index ce7e3f3870..7295ffea2f 100644 --- a/src/main/java/io/cryostat/net/NoopAuthManager.java +++ b/src/main/java/io/cryostat/net/NoopAuthManager.java @@ -37,11 +37,13 @@ */ package io.cryostat.net; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.function.Supplier; import io.cryostat.core.log.Logger; +import io.cryostat.net.security.ResourceAction; public class NoopAuthManager extends AbstractAuthManager { @@ -56,17 +58,20 @@ public AuthenticationScheme getScheme() { } @Override - public Future validateToken(Supplier tokenProvider) { + public Future validateToken( + Supplier tokenProvider, Set resourceActions) { return CompletableFuture.completedFuture(true); } @Override - public Future validateHttpHeader(Supplier headerProvider) { + public Future validateHttpHeader( + Supplier headerProvider, Set resourceActions) { return CompletableFuture.completedFuture(true); } @Override - public Future validateWebSocketSubProtocol(Supplier subProtocolProvider) { + public Future validateWebSocketSubProtocol( + Supplier subProtocolProvider, Set resourceActions) { return CompletableFuture.completedFuture(true); } } diff --git a/src/main/java/io/cryostat/net/OpenShiftAuthManager.java b/src/main/java/io/cryostat/net/OpenShiftAuthManager.java index 5e9d50daef..72033b99a0 100644 --- a/src/main/java/io/cryostat/net/OpenShiftAuthManager.java +++ b/src/main/java/io/cryostat/net/OpenShiftAuthManager.java @@ -41,22 +41,33 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Base64; +import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import io.cryostat.core.log.Logger; import io.cryostat.core.sys.FileSystem; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.security.ResourceType; +import io.cryostat.net.security.ResourceVerb; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.fabric8.kubernetes.api.model.authentication.TokenReview; +import io.fabric8.kubernetes.api.model.authentication.TokenReviewBuilder; +import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReview; +import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReviewBuilder; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.KubernetesClientException; -import io.fabric8.openshift.client.DefaultOpenShiftClient; import io.fabric8.openshift.client.OpenShiftClient; -import io.fabric8.openshift.client.OpenShiftConfigBuilder; import jdk.jfr.Category; import jdk.jfr.Event; import jdk.jfr.Label; @@ -65,11 +76,16 @@ public class OpenShiftAuthManager extends AbstractAuthManager { + private static final Set PERMISSION_NOT_REQUIRED = Set.of("PERMISSION_NOT_REQUIRED"); + private final FileSystem fs; + private final Function clientProvider; - public OpenShiftAuthManager(Logger logger, FileSystem fs) { + OpenShiftAuthManager( + Logger logger, FileSystem fs, Function clientProvider) { super(logger); this.fs = fs; + this.clientProvider = clientProvider; } @Override @@ -78,65 +94,113 @@ public AuthenticationScheme getScheme() { } @Override - public Future validateToken(Supplier tokenProvider) { + public Future validateToken( + Supplier tokenProvider, Set resourceActions) { String token = tokenProvider.get(); if (StringUtils.isBlank(token)) { return CompletableFuture.completedFuture(false); } - return CompletableFuture.supplyAsync( - () -> { - AuthRequest evt = new AuthRequest(); - evt.begin(); + if (resourceActions.isEmpty()) { + return reviewToken(token); + } + + try (OpenShiftClient client = clientProvider.apply(token)) { + String namespace = getNamespace(); + List> results = + resourceActions + .parallelStream() + .flatMap( + resourceAction -> + validateAction(client, namespace, resourceAction)) + .collect(Collectors.toList()); - try (OpenShiftClient authClient = - new DefaultOpenShiftClient( - new OpenShiftConfigBuilder() - .withOauthToken(token) - .build())) { - // only an authenticated user should be allowed to list routes - // in the namespace - // TODO find a better way to authenticate tokens - authClient.routes().inNamespace(getNamespace()).list(); + CompletableFuture.allOf(results.toArray(new CompletableFuture[0])) + .get(15, TimeUnit.SECONDS); + // if we get here then all requests were successful and granted, otherwise an exception + // was thrown on allOf().get() above + return CompletableFuture.completedFuture(true); + } catch (KubernetesClientException | ExecutionException e) { + logger.info(e); + return CompletableFuture.failedFuture(e); + } catch (Exception e) { + logger.error(e); + return CompletableFuture.failedFuture(e); + } + } + + Future reviewToken(String token) { + try (OpenShiftClient client = clientProvider.apply(getServiceAccountToken())) { + TokenReview review = + new TokenReviewBuilder().withNewSpec().withToken(token).endSpec().build(); + review = client.tokenReviews().create(review); + Boolean authenticated = review.getStatus().getAuthenticated(); + return CompletableFuture.completedFuture(authenticated != null && authenticated); + } catch (KubernetesClientException e) { + logger.info(e); + return CompletableFuture.failedFuture(e); + } catch (Exception e) { + logger.error(e); + return CompletableFuture.failedFuture(e); + } + } + private Stream> validateAction( + OpenShiftClient client, String namespace, ResourceAction resourceAction) { + Set resources = map(resourceAction.getResource()); + if (PERMISSION_NOT_REQUIRED.equals(resources) || resources.isEmpty()) { + return Stream.of(CompletableFuture.completedFuture(null)); + } + String group = "operator.cryostat.io"; + String verb = map(resourceAction.getVerb()); + return resources + .parallelStream() + .map( + resource -> { + AuthRequest evt = new AuthRequest(); + evt.begin(); + try { + SelfSubjectAccessReview accessReview = + new SelfSubjectAccessReviewBuilder() + .withNewSpec() + .withNewResourceAttributes() + .withNamespace(namespace) + .withGroup(group) + .withResource(resource) + .withVerb(verb) + .endResourceAttributes() + .endSpec() + .build(); + accessReview = + client.authorization() + .v1() + .selfSubjectAccessReview() + .create(accessReview); evt.setRequestSuccessful(true); - return true; - } catch (KubernetesClientException e) { - logger.info(e); + if (!accessReview.getStatus().getAllowed()) { + return CompletableFuture.failedFuture( + new PermissionDeniedException( + namespace, + group, + resource, + verb, + accessReview.getStatus().getReason())); + } else { + return CompletableFuture.completedFuture(null); + } } catch (Exception e) { - logger.error(e); + return CompletableFuture.failedFuture(e); } finally { if (evt.shouldCommit()) { evt.end(); evt.commit(); } } - - return false; - }) - .orTimeout(15, TimeUnit.SECONDS); - } - - @Name("io.cryostat.net.OpenShiftAuthManager.AuthRequest") - @Label("AuthRequest") - @Category("Cryostat") - @SuppressFBWarnings( - value = "URF_UNREAD_FIELD", - justification = "Event fields are recorded with JFR instead of accessed directly") - public static class AuthRequest extends Event { - - boolean requestSuccessful; - - public AuthRequest() { - this.requestSuccessful = false; - } - - public void setRequestSuccessful(boolean requestSuccessful) { - this.requestSuccessful = requestSuccessful; - } + }); } @Override - public Future validateHttpHeader(Supplier headerProvider) { + public Future validateHttpHeader( + Supplier headerProvider, Set resourceActions) { String authorization = headerProvider.get(); if (StringUtils.isBlank(authorization)) { return CompletableFuture.completedFuture(false); @@ -146,11 +210,12 @@ public Future validateHttpHeader(Supplier headerProvider) { if (!matcher.matches()) { return CompletableFuture.completedFuture(false); } - return validateToken(() -> matcher.group(1)); + return validateToken(() -> matcher.group(1), resourceActions); } @Override - public Future validateWebSocketSubProtocol(Supplier subProtocolProvider) { + public Future validateWebSocketSubProtocol( + Supplier subProtocolProvider, Set resourceActions) { String subprotocol = subProtocolProvider.get(); if (StringUtils.isBlank(subprotocol)) { return CompletableFuture.completedFuture(false); @@ -167,7 +232,7 @@ public Future validateWebSocketSubProtocol(Supplier subProtocol try { String decoded = new String(Base64.getUrlDecoder().decode(b64), StandardCharsets.UTF_8).trim(); - return validateToken(() -> decoded); + return validateToken(() -> decoded, resourceActions); } catch (IllegalArgumentException e) { return CompletableFuture.completedFuture(false); } @@ -183,4 +248,98 @@ private String getNamespace() throws IOException { .findFirst() .get(); } + + @SuppressFBWarnings( + value = "DMI_HARDCODED_ABSOLUTE_FILENAME", + justification = "Kubernetes serviceaccount file path is well-known and absolute") + private String getServiceAccountToken() throws IOException { + return fs.readFile(Paths.get(Config.KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)) + .lines() + .filter(StringUtils::isNotBlank) + .findFirst() + .get(); + } + + private static Set map(ResourceType resource) { + switch (resource) { + case TARGET: + return Set.of("flightrecorders"); + case RECORDING: + return Set.of("recordings"); + case CERTIFICATE: + return Set.of("deployments", "pods", "cryostats"); + case CREDENTIALS: + return Set.of("cryostats"); + case TEMPLATE: + case REPORT: + case RULE: + default: + return PERMISSION_NOT_REQUIRED; + } + } + + private static String map(ResourceVerb verb) { + switch (verb) { + case CREATE: + return "create"; + case READ: + return "get"; + case UPDATE: + return "patch"; + case DELETE: + return "delete"; + default: + throw new IllegalArgumentException( + String.format("Unknown resource verb \"%s\"", verb)); + } + } + + @SuppressWarnings("serial") + public static class PermissionDeniedException extends Exception { + private final String namespace; + private final String resource; + private final String verb; + + public PermissionDeniedException( + String namespace, String group, String resource, String verb, String reason) { + super( + String.format( + "Requesting client in namespace \"%s\" cannot %s %s.%s: %s", + namespace, verb, resource, group, reason)); + this.namespace = namespace; + this.resource = resource; + this.verb = verb; + } + + public String getNamespace() { + return namespace; + } + + public String getResourceType() { + return resource; + } + + public String getVerb() { + return verb; + } + } + + @Name("io.cryostat.net.OpenShiftAuthManager.AuthRequest") + @Label("AuthRequest") + @Category("Cryostat") + @SuppressFBWarnings( + value = "URF_UNREAD_FIELD", + justification = "Event fields are recorded with JFR instead of accessed directly") + public static class AuthRequest extends Event { + + boolean requestSuccessful; + + public AuthRequest() { + this.requestSuccessful = false; + } + + public void setRequestSuccessful(boolean requestSuccessful) { + this.requestSuccessful = requestSuccessful; + } + } } diff --git a/src/main/java/io/cryostat/net/security/ResourceAction.java b/src/main/java/io/cryostat/net/security/ResourceAction.java new file mode 100644 index 0000000000..427c1fbce2 --- /dev/null +++ b/src/main/java/io/cryostat/net/security/ResourceAction.java @@ -0,0 +1,120 @@ +/* + * 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.security; + +import static io.cryostat.net.security.ResourceType.CERTIFICATE; +import static io.cryostat.net.security.ResourceType.CREDENTIALS; +import static io.cryostat.net.security.ResourceType.RECORDING; +import static io.cryostat.net.security.ResourceType.REPORT; +import static io.cryostat.net.security.ResourceType.RULE; +import static io.cryostat.net.security.ResourceType.TARGET; +import static io.cryostat.net.security.ResourceType.TEMPLATE; +import static io.cryostat.net.security.ResourceVerb.CREATE; +import static io.cryostat.net.security.ResourceVerb.DELETE; +import static io.cryostat.net.security.ResourceVerb.READ; +import static io.cryostat.net.security.ResourceVerb.UPDATE; + +import java.util.EnumSet; +import java.util.stream.Collectors; + +/** + * API action types(), at a high level, which are mapped to underlying platform permissions and + * checked by AuthManagers. + */ +public enum ResourceAction { + CREATE_TARGET(CREATE, TARGET), + READ_TARGET(READ, TARGET), + UPDATE_TARGET(UPDATE, TARGET), + DELETE_TARGET(DELETE, TARGET), + + CREATE_RECORDING(CREATE, RECORDING), + READ_RECORDING(READ, RECORDING), + UPDATE_RECORDING(UPDATE, RECORDING), + DELETE_RECORDING(DELETE, RECORDING), + + CREATE_TEMPLATE(CREATE, TEMPLATE), + READ_TEMPLATE(READ, TEMPLATE), + UPDATE_TEMPLATE(UPDATE, TEMPLATE), + DELETE_TEMPLATE(DELETE, TEMPLATE), + + CREATE_REPORT(CREATE, REPORT), + READ_REPORT(READ, REPORT), + UPDATE_REPORT(UPDATE, REPORT), + DELETE_REPORT(DELETE, REPORT), + + CREATE_CREDENTIALS(CREATE, CREDENTIALS), + READ_CREDENTIALS(READ, CREDENTIALS), + UPDATE_CREDENTIALS(UPDATE, CREDENTIALS), + DELETE_CREDENTIALS(DELETE, CREDENTIALS), + + CREATE_RULE(CREATE, RULE), + READ_RULE(READ, RULE), + UPDATE_RULE(UPDATE, RULE), + DELETE_RULE(DELETE, RULE), + + CREATE_CERTIFICATE(CREATE, CERTIFICATE), + READ_CERTIFICATE(READ, CERTIFICATE), + UPDATE_CERTIFICATE(UPDATE, CERTIFICATE), + DELETE_CERTIFICATE(DELETE, CERTIFICATE), + ; + + public static final EnumSet ALL = EnumSet.allOf(ResourceAction.class); + public static final EnumSet NONE = EnumSet.noneOf(ResourceAction.class); + public static final EnumSet READ_ALL = + EnumSet.copyOf( + ALL.stream() + .filter(p -> ResourceVerb.READ == p.verb) + .collect(Collectors.toSet())); + public static final EnumSet WRITE_ALL = EnumSet.complementOf(READ_ALL); + + private final ResourceVerb verb; + private final ResourceType resource; + + ResourceAction(ResourceVerb verb, ResourceType resource) { + this.verb = verb; + this.resource = resource; + } + + public ResourceVerb getVerb() { + return verb; + } + + public ResourceType getResource() { + return resource; + } +} diff --git a/src/main/java/io/cryostat/net/security/ResourceType.java b/src/main/java/io/cryostat/net/security/ResourceType.java new file mode 100644 index 0000000000..3d53bbbd59 --- /dev/null +++ b/src/main/java/io/cryostat/net/security/ResourceType.java @@ -0,0 +1,49 @@ +/* + * 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.security; + +public enum ResourceType { + TARGET, + RECORDING, + TEMPLATE, + REPORT, + CREDENTIALS, + RULE, + CERTIFICATE, + ; +} diff --git a/src/main/java/io/cryostat/net/security/ResourceVerb.java b/src/main/java/io/cryostat/net/security/ResourceVerb.java new file mode 100644 index 0000000000..4d20d555ae --- /dev/null +++ b/src/main/java/io/cryostat/net/security/ResourceVerb.java @@ -0,0 +1,46 @@ +/* + * 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.security; + +public enum ResourceVerb { + CREATE, + READ, + UPDATE, + DELETE, + ; +} diff --git a/src/main/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandler.java b/src/main/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandler.java index 1a9c8443a3..3cbbefc1c2 100644 --- a/src/main/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandler.java +++ b/src/main/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandler.java @@ -41,6 +41,7 @@ import java.nio.charset.StandardCharsets; import java.rmi.ConnectIOException; import java.util.Base64; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -52,7 +53,9 @@ import io.cryostat.core.net.Credentials; import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.OpenShiftAuthManager.PermissionDeniedException; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.web.RoutingContext; @@ -77,12 +80,21 @@ protected AbstractAuthenticatedRequestHandler(AuthManager auth) { @Override public void handle(RoutingContext ctx) { try { - if (!validateRequestAuthorization(ctx.request()).get()) { - throw new HttpStatusException(401); + boolean permissionGranted = validateRequestAuthorization(ctx.request()).get(); + if (!permissionGranted) { + // expected to go into catch clause below + throw new HttpStatusException(401, "HTTP Authorization Failure"); } // set Content-Type: text/plain by default. Handler implementations may replace this. ctx.response().putHeader(HttpHeaders.CONTENT_TYPE, HttpMimeType.PLAINTEXT.mime()); handleAuthenticated(ctx); + } catch (ExecutionException ee) { + Throwable cause = ee.getCause(); + if (cause instanceof PermissionDeniedException + || cause instanceof KubernetesClientException) { + throw new HttpStatusException(401, "HTTP Authorization Failure", ee); + } + throw new HttpStatusException(500, ee.getMessage(), ee); } catch (HttpStatusException e) { throw e; } catch (ConnectionException e) { @@ -105,7 +117,8 @@ public void handle(RoutingContext ctx) { } protected Future validateRequestAuthorization(HttpServerRequest req) throws Exception { - return auth.validateHttpHeader(() -> req.getHeader(HttpHeaders.AUTHORIZATION)); + return auth.validateHttpHeader( + () -> req.getHeader(HttpHeaders.AUTHORIZATION), resourceActions()); } protected ConnectionDescriptor getConnectionDescriptorFromContext(RoutingContext ctx) { diff --git a/src/main/java/io/cryostat/net/web/http/RequestHandler.java b/src/main/java/io/cryostat/net/web/http/RequestHandler.java index 2b68058d83..3eb32b175d 100644 --- a/src/main/java/io/cryostat/net/web/http/RequestHandler.java +++ b/src/main/java/io/cryostat/net/web/http/RequestHandler.java @@ -37,6 +37,9 @@ */ package io.cryostat.net.web.http; +import java.util.Set; + +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.api.ApiVersion; import io.vertx.core.Handler; @@ -68,6 +71,8 @@ default String basePath() { HttpMethod httpMethod(); + Set resourceActions(); + default boolean isAvailable() { return true; } diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/AuthPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/AuthPostHandler.java index 59c5e213b7..50fd415645 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/AuthPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/AuthPostHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v1; +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; @@ -68,6 +71,11 @@ public String path() { return basePath() + "auth"; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + // This handler is not async, but it's simple enough that it doesn't need // to be run in a seperate worker thread. @Override diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/GrafanaDashboardUrlGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/GrafanaDashboardUrlGetHandler.java index 0d99e8a2f7..850e4b368c 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/GrafanaDashboardUrlGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/GrafanaDashboardUrlGetHandler.java @@ -38,10 +38,12 @@ package io.cryostat.net.web.http.api.v1; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import io.cryostat.core.sys.Environment; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.net.web.http.api.ApiVersion; @@ -80,6 +82,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + // This handler is not async, but it's simple enough that it doesn't need // to be run in a seperate worker thread. @Override diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/GrafanaDatasourceUrlGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/GrafanaDatasourceUrlGetHandler.java index 42b89f2944..efa0a532dd 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/GrafanaDatasourceUrlGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/GrafanaDatasourceUrlGetHandler.java @@ -38,10 +38,12 @@ package io.cryostat.net.web.http.api.v1; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import io.cryostat.core.sys.Environment; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.net.web.http.api.ApiVersion; @@ -75,6 +77,11 @@ public String path() { return basePath() + "grafana_datasource_url"; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public HttpMethod httpMethod() { return HttpMethod.GET; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/NotificationsUrlGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/NotificationsUrlGetHandler.java index 31a751ff4f..cde8b0ea7e 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/NotificationsUrlGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/NotificationsUrlGetHandler.java @@ -40,11 +40,13 @@ import java.net.SocketException; import java.net.UnknownHostException; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import io.cryostat.net.HttpServer; import io.cryostat.net.NetworkConfiguration; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.net.web.http.api.ApiVersion; @@ -78,6 +80,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + "notifications_url"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandler.java index 665d9bfb19..a11d3d612f 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandler.java @@ -39,7 +39,9 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.EnumSet; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import javax.inject.Named; @@ -49,6 +51,7 @@ import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -89,6 +92,11 @@ public HttpMethod httpMethod() { return HttpMethod.DELETE; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.DELETE_RECORDING); + } + @Override public String path() { return basePath() + "recordings/:recordingName"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingGetHandler.java index c90959e3ef..d1e884e050 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingGetHandler.java @@ -38,12 +38,15 @@ package io.cryostat.net.web.http.api.v1; import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Set; import javax.inject.Inject; import javax.inject.Named; import io.cryostat.MainModule; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -73,6 +76,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_RECORDING); + } + @Override public String path() { return basePath() + "recordings/:recordingName"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingUploadPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingUploadPostHandler.java index 619061bbb2..6c4dcdaf4d 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingUploadPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingUploadPostHandler.java @@ -41,7 +41,9 @@ import java.net.URL; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.EnumSet; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; @@ -51,6 +53,7 @@ import io.cryostat.core.sys.Environment; import io.cryostat.core.sys.FileSystem; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -97,6 +100,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_RECORDING); + } + @Override public String path() { return basePath() + "recordings/:recordingName/upload"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsGetHandler.java index ba0b7711ad..7be11739d8 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsGetHandler.java @@ -41,9 +41,11 @@ import java.net.URISyntaxException; import java.net.UnknownHostException; import java.nio.file.Path; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; import javax.inject.Inject; @@ -54,6 +56,7 @@ import io.cryostat.core.log.Logger; import io.cryostat.core.sys.FileSystem; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.WebServer; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; @@ -99,6 +102,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_RECORDING); + } + @Override public String path() { return basePath() + "recordings"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostBodyHandler.java index 39771f0b43..899b316f69 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v1; +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; @@ -67,6 +70,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + RecordingsPostHandler.PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandler.java index 16be53f11f..72f5c910fe 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandler.java @@ -40,7 +40,9 @@ import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.util.EnumSet; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -56,6 +58,7 @@ import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.HttpServer; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -118,6 +121,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.CREATE_RECORDING); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/ReportGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/ReportGetHandler.java index 1d7a918781..19f9260a27 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/ReportGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/ReportGetHandler.java @@ -38,6 +38,8 @@ package io.cryostat.net.web.http.api.v1; import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Set; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; @@ -46,6 +48,7 @@ import io.cryostat.core.log.Logger; import io.cryostat.net.AuthManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -76,6 +79,14 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of( + ResourceAction.READ_RECORDING, + ResourceAction.CREATE_REPORT, + ResourceAction.READ_REPORT); + } + @Override public String path() { return basePath() + "reports/:recordingName"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandler.java index 8534ab0bc4..7b3cd741a8 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandler.java @@ -39,7 +39,9 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.EnumSet; import java.util.List; +import java.util.Set; import javax.inject.Inject; @@ -48,6 +50,7 @@ import io.cryostat.jmc.serialization.SerializableEventTypeInfo; import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -79,6 +82,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET); + } + @Override public String path() { return basePath() + "targets/:targetId/events"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandler.java index dba4db4135..9c7527218d 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandler.java @@ -37,13 +37,16 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.EnumSet; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -80,6 +83,11 @@ public HttpMethod httpMethod() { return HttpMethod.DELETE; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.DELETE_RECORDING, ResourceAction.READ_TARGET); + } + @Override public String path() { return basePath() + "targets/:targetId/recordings/:recordingName"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandler.java index 073c28d795..47ccf304b0 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandler.java @@ -39,8 +39,10 @@ import java.io.IOException; import java.io.InputStream; +import java.util.EnumSet; import java.util.Objects; import java.util.Optional; +import java.util.Set; import javax.inject.Inject; @@ -48,6 +50,7 @@ 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.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -82,6 +85,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET, ResourceAction.READ_RECORDING); + } + @Override public String path() { return basePath() + "targets/:targetId/recordings/:recordingName"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandler.java index 10107ecff6..cab103a1fb 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandler.java @@ -37,8 +37,10 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.Set; import javax.inject.Inject; @@ -49,6 +51,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -93,6 +96,11 @@ public String path() { return basePath() + PATH; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchBodyHandler.java index a663dcd844..a5e168f957 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v1; +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; @@ -71,6 +74,11 @@ public HttpMethod httpMethod() { return HttpMethod.PATCH; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + TargetRecordingOptionsPatchHandler.PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandler.java index 4ac8484978..6ba1c0d8fb 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandler.java @@ -38,7 +38,9 @@ package io.cryostat.net.web.http.api.v1; import java.util.Arrays; +import java.util.EnumSet; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,6 +52,7 @@ import io.cryostat.core.RecordingOptionsCustomizer.OptionKey; import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -99,6 +102,11 @@ public String path() { return basePath() + PATH; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET, ResourceAction.UPDATE_TARGET); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchBodyHandler.java index 320c82ef4d..b98995ef78 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v1; +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; @@ -71,6 +74,11 @@ public HttpMethod httpMethod() { return HttpMethod.PATCH; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + TargetRecordingPatchHandler.PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchHandler.java index cd559140db..1daa52e47c 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchHandler.java @@ -37,9 +37,13 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.EnumSet; +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; @@ -79,6 +83,14 @@ public String path() { return basePath() + PATH; } + @Override + public Set resourceActions() { + return EnumSet.of( + ResourceAction.READ_TARGET, + ResourceAction.READ_RECORDING, + ResourceAction.UPDATE_RECORDING); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandler.java index 9b10b9faf5..0ddda75c4e 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandler.java @@ -43,7 +43,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; +import java.util.EnumSet; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; @@ -54,6 +56,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -106,6 +109,11 @@ public String path() { return basePath() + "targets/:targetId/recordings/:recordingName/upload"; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET, ResourceAction.READ_RECORDING); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandler.java index 68cee7e912..69a4e1bd4d 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandler.java @@ -38,7 +38,9 @@ package io.cryostat.net.web.http.api.v1; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; +import java.util.Set; import javax.inject.Inject; import javax.inject.Provider; @@ -48,6 +50,7 @@ import io.cryostat.jmc.serialization.HyperlinkedSerializableRecordingDescriptor; import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.WebServer; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; @@ -91,6 +94,11 @@ public String path() { return basePath() + "targets/:targetId/recordings"; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET, ResourceAction.READ_RECORDING); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostBodyHandler.java index b70a07612e..b2693594b0 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v1; +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; @@ -72,6 +75,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + TargetRecordingsPostHandler.PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java index 3f48dc2214..97466f298b 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandler.java @@ -39,7 +39,9 @@ import java.io.IOException; import java.net.URISyntaxException; +import java.util.EnumSet; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -57,6 +59,7 @@ 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.WebServer; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; @@ -113,6 +116,16 @@ public String path() { return basePath() + PATH; } + @Override + public Set resourceActions() { + return EnumSet.of( + ResourceAction.READ_TARGET, + ResourceAction.UPDATE_TARGET, + ResourceAction.CREATE_RECORDING, + ResourceAction.READ_RECORDING, + ResourceAction.READ_TEMPLATE); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetReportGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetReportGetHandler.java index 3abf289e9a..3667dec74d 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetReportGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetReportGetHandler.java @@ -37,6 +37,8 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.EnumSet; +import java.util.Set; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -47,6 +49,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.reports.ReportService; import io.cryostat.net.reports.SubprocessReportGenerator; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -85,6 +88,15 @@ public String path() { return basePath() + "targets/:targetId/reports/:recordingName"; } + @Override + public Set resourceActions() { + return EnumSet.of( + ResourceAction.READ_TARGET, + ResourceAction.READ_RECORDING, + ResourceAction.CREATE_REPORT, + ResourceAction.READ_REPORT); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetSnapshotPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetSnapshotPostHandler.java index d48145bd73..20c79744c4 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetSnapshotPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetSnapshotPostHandler.java @@ -37,6 +37,9 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.EnumSet; +import java.util.Set; + import javax.inject.Inject; import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; @@ -44,6 +47,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.api.ApiVersion; import io.cryostat.recordings.RecordingOptionsBuilderFactory; @@ -76,6 +80,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET, ResourceAction.UPDATE_RECORDING); + } + @Override public String path() { return basePath() + "targets/:targetId/snapshot"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetTemplateGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetTemplateGetHandler.java index 35027a9c98..7e9128ae94 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetTemplateGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetTemplateGetHandler.java @@ -37,11 +37,15 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.EnumSet; +import java.util.Set; + import javax.inject.Inject; import io.cryostat.core.templates.TemplateType; import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -76,6 +80,11 @@ public String path() { return basePath() + "targets/:targetId/templates/:templateName/type/:templateType"; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET, ResourceAction.READ_TEMPLATE); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetTemplatesGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetTemplatesGetHandler.java index 9d84c760b6..79d3c68dac 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetTemplatesGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetTemplatesGetHandler.java @@ -38,7 +38,9 @@ package io.cryostat.net.web.http.api.v1; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; +import java.util.Set; import javax.inject.Inject; @@ -46,6 +48,7 @@ import io.cryostat.core.templates.TemplateType; import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -90,6 +93,11 @@ public String path() { return basePath() + "targets/:targetId/templates"; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET, ResourceAction.READ_TEMPLATE); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TargetsGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TargetsGetHandler.java index b74affe297..70307cf718 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TargetsGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TargetsGetHandler.java @@ -37,9 +37,13 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.EnumSet; +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.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -77,6 +81,11 @@ public String path() { return basePath() + "targets"; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandler.java index f5b995133a..4601407b85 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandler.java @@ -37,7 +37,9 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.EnumSet; import java.util.Map; +import java.util.Set; import javax.inject.Inject; @@ -45,6 +47,7 @@ import io.cryostat.core.templates.MutableTemplateService.InvalidEventTemplateException; import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -84,6 +87,11 @@ public String path() { return basePath() + "templates/:templateName"; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.DELETE_TEMPLATE); + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostBodyHandler.java index b49789a5eb..592f7e9bcc 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v1; +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; @@ -76,6 +79,11 @@ public String path() { return basePath() + TemplatesPostHandler.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/v1/TemplatesPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandler.java index 90fbb5b630..1e8bcb9a7f 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandler.java @@ -39,7 +39,9 @@ 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; @@ -50,6 +52,7 @@ import io.cryostat.core.templates.MutableTemplateService.InvalidXmlException; import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -93,6 +96,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.CREATE_TEMPLATE); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandler.java index 35c8e4345c..02a11ea5a0 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandler.java @@ -41,6 +41,7 @@ import java.nio.charset.StandardCharsets; import java.rmi.ConnectIOException; import java.util.Base64; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,6 +51,7 @@ import io.cryostat.core.net.Credentials; import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.OpenShiftAuthManager.PermissionDeniedException; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.net.web.http.api.ApiMeta; @@ -57,6 +59,7 @@ import io.cryostat.net.web.http.api.ApiResultData; import com.google.gson.Gson; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; @@ -87,11 +90,26 @@ protected AbstractV2RequestHandler(AuthManager auth, Gson gson) { public final void handle(RoutingContext ctx) { RequestParameters requestParams = RequestParameters.from(ctx); try { - if (requiresAuthentication() - && !validateRequestAuthorization( - requestParams.getHeaders().get(HttpHeaders.AUTHORIZATION)) - .get()) { - throw new ApiException(401, "HTTP Authorization Failure"); + if (requiresAuthentication()) { + try { + boolean permissionGranted = + validateRequestAuthorization( + requestParams + .getHeaders() + .get(HttpHeaders.AUTHORIZATION)) + .get(); + if (!permissionGranted) { + // expected to go into catch clause below + throw new ApiException(401, "HTTP Authorization Failure"); + } + } catch (ExecutionException ee) { + Throwable cause = ee.getCause(); + if (cause instanceof PermissionDeniedException + || cause instanceof KubernetesClientException) { + throw new ApiException(401, "HTTP Authorization Failure", ee); + } + throw new ApiException(500, ee); + } } writeResponse(ctx, handle(requestParams)); } catch (ApiException e) { @@ -117,7 +135,7 @@ public final void handle(RoutingContext ctx) { } protected Future validateRequestAuthorization(String authHeader) throws Exception { - return auth.validateHttpHeader(() -> authHeader); + return auth.validateHttpHeader(() -> authHeader, resourceActions()); } protected ConnectionDescriptor getConnectionDescriptorFromParams(RequestParameters params) { diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/ApiGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/ApiGetHandler.java index 24be80630c..0e7eecfd70 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/ApiGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/ApiGetHandler.java @@ -47,6 +47,7 @@ import javax.inject.Inject; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.WebServer; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; @@ -90,6 +91,11 @@ public String path() { return basePath() + "api"; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override HttpMimeType mimeType() { return HttpMimeType.JSON; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/CertificatePostBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/CertificatePostBodyHandler.java index 5a24beb104..9bb214c11a 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/CertificatePostBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/CertificatePostBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v2; +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; @@ -71,6 +74,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + CertificatePostHandler.PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/CertificatePostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/CertificatePostHandler.java index b251fee83c..52d4a360ee 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/CertificatePostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/CertificatePostHandler.java @@ -46,6 +46,8 @@ import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.util.Collection; +import java.util.EnumSet; +import java.util.Set; import java.util.function.Function; import javax.inject.Inject; @@ -55,6 +57,7 @@ import io.cryostat.core.sys.FileSystem; import io.cryostat.net.AuthManager; import io.cryostat.net.security.CertificateValidator; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -104,6 +107,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.CREATE_CERTIFICATE); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/RuleDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/RuleDeleteHandler.java index f76995d021..7baa39227a 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/RuleDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/RuleDeleteHandler.java @@ -38,11 +38,14 @@ package io.cryostat.net.web.http.api.v2; import java.io.IOException; +import java.util.EnumSet; +import java.util.Set; import javax.inject.Inject; import io.cryostat.core.log.Logger; 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.rules.Rule; @@ -80,6 +83,11 @@ public HttpMethod httpMethod() { return HttpMethod.DELETE; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.DELETE_RULE); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/RuleGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/RuleGetHandler.java index a8603c1cff..fdd37b0487 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/RuleGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/RuleGetHandler.java @@ -37,10 +37,14 @@ */ package io.cryostat.net.web.http.api.v2; +import java.util.EnumSet; +import java.util.Set; + import javax.inject.Inject; import io.cryostat.core.log.Logger; 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.rules.Rule; @@ -78,6 +82,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_RULE); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/RulesGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/RulesGetHandler.java index 0f40004e3a..ff546437ba 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/RulesGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/RulesGetHandler.java @@ -37,12 +37,14 @@ */ package io.cryostat.net.web.http.api.v2; +import java.util.EnumSet; import java.util.Set; import javax.inject.Inject; import io.cryostat.core.log.Logger; 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.rules.Rule; @@ -80,6 +82,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_RULE); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/RulesPostBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/RulesPostBodyHandler.java index 32a82aef78..a72b593b4d 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/RulesPostBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/RulesPostBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v2; +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; @@ -71,6 +74,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + RulesPostHandler.PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/RulesPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/RulesPostHandler.java index bd8bcec7cf..4751526d0f 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/RulesPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/RulesPostHandler.java @@ -38,11 +38,14 @@ package io.cryostat.net.web.http.api.v2; import java.io.IOException; +import java.util.EnumSet; +import java.util.Set; import javax.inject.Inject; import io.cryostat.core.log.Logger; 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.rules.MatchExpressionValidationException; @@ -89,6 +92,16 @@ public String path() { return basePath() + PATH; } + @Override + public Set resourceActions() { + return EnumSet.of( + ResourceAction.CREATE_RULE, + ResourceAction.READ_TARGET, + ResourceAction.CREATE_RECORDING, + ResourceAction.UPDATE_RECORDING, + ResourceAction.READ_TEMPLATE); + } + @Override public HttpMimeType mimeType() { return HttpMimeType.PLAINTEXT; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsDeleteHandler.java index aaa455c2f7..223a163aa6 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsDeleteHandler.java @@ -38,11 +38,14 @@ package io.cryostat.net.web.http.api.v2; import java.io.IOException; +import java.util.EnumSet; +import java.util.Set; import javax.inject.Inject; import io.cryostat.configuration.CredentialsManager; 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; @@ -77,6 +80,11 @@ public HttpMethod httpMethod() { return HttpMethod.DELETE; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.DELETE_CREDENTIALS); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostBodyHandler.java index 49f6ef4fa1..6f88b83fe6 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v2; +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; @@ -71,6 +74,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + TargetCredentialsPostHandler.PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostHandler.java index 3193550db7..5fbccab4e2 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostHandler.java @@ -38,6 +38,8 @@ package io.cryostat.net.web.http.api.v2; import java.io.IOException; +import java.util.EnumSet; +import java.util.Set; import javax.inject.Inject; @@ -45,6 +47,7 @@ import io.cryostat.core.log.Logger; import io.cryostat.core.net.Credentials; 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; @@ -82,6 +85,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.CREATE_CREDENTIALS); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetDeleteHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetDeleteHandler.java index 09268b0a1e..a7cda66d0d 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetDeleteHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetDeleteHandler.java @@ -40,10 +40,13 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.EnumSet; +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.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; import io.cryostat.platform.internal.CustomTargetPlatformClient; @@ -79,6 +82,11 @@ public HttpMethod httpMethod() { return HttpMethod.DELETE; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.DELETE_TARGET); + } + @Override public String path() { return basePath() + "targets/:targetId"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandler.java index 9bbba64520..e72eeb8757 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandler.java @@ -38,6 +38,7 @@ package io.cryostat.net.web.http.api.v2; import java.util.Arrays; +import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -50,6 +51,7 @@ import io.cryostat.jmc.serialization.SerializableEventTypeInfo; 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; @@ -83,6 +85,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET); + } + @Override public String path() { return basePath() + "targets/:targetId/events"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandler.java index 91b971eb99..b22386950c 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandler.java @@ -38,8 +38,10 @@ package io.cryostat.net.web.http.api.v2; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Set; import javax.inject.Inject; @@ -48,6 +50,7 @@ import io.cryostat.jmc.serialization.SerializableOptionDescriptor; 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; @@ -81,6 +84,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET); + } + @Override public String path() { return basePath() + "targets/:targetId/recordingOptionsList"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetSnapshotPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetSnapshotPostHandler.java index c80b75c883..e8957e7adc 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetSnapshotPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetSnapshotPostHandler.java @@ -37,6 +37,9 @@ */ package io.cryostat.net.web.http.api.v2; +import java.util.EnumSet; +import java.util.Set; + import javax.inject.Inject; import org.openjdk.jmc.common.unit.QuantityConversionException; @@ -46,6 +49,7 @@ import io.cryostat.jmc.serialization.HyperlinkedSerializableRecordingDescriptor; import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.WebServer; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; @@ -91,6 +95,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.READ_TARGET, ResourceAction.UPDATE_RECORDING); + } + @Override public String path() { return basePath() + "targets/:targetId/snapshot"; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetsPostBodyHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetsPostBodyHandler.java index 6e44692ee2..b9254889da 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetsPostBodyHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetsPostBodyHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.api.v2; +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; @@ -71,6 +74,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + TargetsPostHandler.PATH; diff --git a/src/main/java/io/cryostat/net/web/http/api/v2/TargetsPostHandler.java b/src/main/java/io/cryostat/net/web/http/api/v2/TargetsPostHandler.java index e3c5917803..5c364c86de 100644 --- a/src/main/java/io/cryostat/net/web/http/api/v2/TargetsPostHandler.java +++ b/src/main/java/io/cryostat/net/web/http/api/v2/TargetsPostHandler.java @@ -40,13 +40,16 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Objects; +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.HttpMimeType; import io.cryostat.net.web.http.api.ApiVersion; import io.cryostat.platform.PlatformClient; @@ -93,6 +96,11 @@ public HttpMethod httpMethod() { return HttpMethod.POST; } + @Override + public Set resourceActions() { + return EnumSet.of(ResourceAction.CREATE_TARGET); + } + @Override public String path() { return basePath() + PATH; diff --git a/src/main/java/io/cryostat/net/web/http/generic/CorsEnablingHandler.java b/src/main/java/io/cryostat/net/web/http/generic/CorsEnablingHandler.java index 52e882898a..bc7ff58dd4 100644 --- a/src/main/java/io/cryostat/net/web/http/generic/CorsEnablingHandler.java +++ b/src/main/java/io/cryostat/net/web/http/generic/CorsEnablingHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.generic; +import java.util.Set; + import javax.inject.Inject; import io.cryostat.core.sys.Environment; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.WebServer; import io.cryostat.net.web.http.AbstractAuthenticatedRequestHandler; import io.cryostat.net.web.http.RequestHandler; @@ -93,6 +96,11 @@ public HttpMethod httpMethod() { return HttpMethod.OTHER; // unused for ALL_PATHS handlers } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return ALL_PATHS; diff --git a/src/main/java/io/cryostat/net/web/http/generic/CorsOptionsHandler.java b/src/main/java/io/cryostat/net/web/http/generic/CorsOptionsHandler.java index 3ab89b38be..e7a56c711d 100644 --- a/src/main/java/io/cryostat/net/web/http/generic/CorsOptionsHandler.java +++ b/src/main/java/io/cryostat/net/web/http/generic/CorsOptionsHandler.java @@ -37,9 +37,12 @@ */ package io.cryostat.net.web.http.generic; +import java.util.Set; + import javax.inject.Inject; import io.cryostat.core.sys.Environment; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.api.ApiVersion; import io.vertx.core.http.HttpMethod; @@ -66,6 +69,11 @@ public HttpMethod httpMethod() { return HttpMethod.OPTIONS; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + "*"; diff --git a/src/main/java/io/cryostat/net/web/http/generic/HealthGetHandler.java b/src/main/java/io/cryostat/net/web/http/generic/HealthGetHandler.java index f8c3701b55..eab6486904 100644 --- a/src/main/java/io/cryostat/net/web/http/generic/HealthGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/generic/HealthGetHandler.java @@ -41,12 +41,14 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; import io.cryostat.core.log.Logger; import io.cryostat.core.sys.Environment; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.net.web.http.api.ApiVersion; @@ -92,6 +94,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public boolean isAsync() { return false; diff --git a/src/main/java/io/cryostat/net/web/http/generic/StaticAssetsGetHandler.java b/src/main/java/io/cryostat/net/web/http/generic/StaticAssetsGetHandler.java index c3fcc03026..a945fa9e06 100644 --- a/src/main/java/io/cryostat/net/web/http/generic/StaticAssetsGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/generic/StaticAssetsGetHandler.java @@ -37,8 +37,11 @@ */ package io.cryostat.net.web.http.generic; +import java.util.Set; + import javax.inject.Inject; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.net.web.http.api.ApiVersion; @@ -65,6 +68,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + "*"; diff --git a/src/main/java/io/cryostat/net/web/http/generic/TimeoutHandler.java b/src/main/java/io/cryostat/net/web/http/generic/TimeoutHandler.java index e12c3d7f2c..ee5b683e5e 100644 --- a/src/main/java/io/cryostat/net/web/http/generic/TimeoutHandler.java +++ b/src/main/java/io/cryostat/net/web/http/generic/TimeoutHandler.java @@ -37,8 +37,11 @@ */ package io.cryostat.net.web.http.generic; +import java.util.Set; + import javax.inject.Inject; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.net.web.http.api.ApiVersion; @@ -70,6 +73,11 @@ public HttpMethod httpMethod() { return HttpMethod.OTHER; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return ALL_PATHS; diff --git a/src/main/java/io/cryostat/net/web/http/generic/WebClientAssetsGetHandler.java b/src/main/java/io/cryostat/net/web/http/generic/WebClientAssetsGetHandler.java index 9f4705ad43..a8be9cac28 100644 --- a/src/main/java/io/cryostat/net/web/http/generic/WebClientAssetsGetHandler.java +++ b/src/main/java/io/cryostat/net/web/http/generic/WebClientAssetsGetHandler.java @@ -38,9 +38,11 @@ package io.cryostat.net.web.http.generic; import java.nio.file.Path; +import java.util.Set; import javax.inject.Inject; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.WebServer; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; @@ -81,6 +83,11 @@ public HttpMethod httpMethod() { return HttpMethod.GET; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public String path() { return basePath() + "*"; diff --git a/src/test/java/io/cryostat/net/BasicAuthManagerTest.java b/src/test/java/io/cryostat/net/BasicAuthManagerTest.java index 476d271405..a2d838fd4e 100644 --- a/src/test/java/io/cryostat/net/BasicAuthManagerTest.java +++ b/src/test/java/io/cryostat/net/BasicAuthManagerTest.java @@ -44,6 +44,7 @@ import io.cryostat.core.log.Logger; import io.cryostat.core.sys.FileSystem; +import io.cryostat.net.security.ResourceAction; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; @@ -153,7 +154,7 @@ void shouldLogFileReadErrors() throws Exception { class TokenValidationTest { @Test void shouldFailAuthenticationWhenCredentialsMalformed() throws Exception { - Assertions.assertFalse(mgr.validateToken(() -> "user").get()); + Assertions.assertFalse(mgr.validateToken(() -> "user", ResourceAction.NONE).get()); ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor objectCaptor = ArgumentCaptor.forClass(Object.class); Mockito.verify(logger).warn(messageCaptor.capture(), objectCaptor.capture()); @@ -164,7 +165,7 @@ void shouldFailAuthenticationWhenCredentialsMalformed() throws Exception { @Test void shouldFailAuthenticationWhenNoMatchFound() throws Exception { - Assertions.assertFalse(mgr.validateToken(() -> "user:pass").get()); + Assertions.assertFalse(mgr.validateToken(() -> "user:pass", ResourceAction.NONE).get()); ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor objectCaptor = ArgumentCaptor.forClass(Object.class); Mockito.verify(logger).warn(messageCaptor.capture(), objectCaptor.capture()); @@ -189,7 +190,7 @@ void shouldPassAuthenticationWhenMatchFound() throws Exception { new StringReader( "user:d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1")); Mockito.when(fs.readFile(mockPath)).thenReturn(props); - Assertions.assertTrue(mgr.validateToken(() -> "user:pass").get()); + Assertions.assertTrue(mgr.validateToken(() -> "user:pass", ResourceAction.NONE).get()); Mockito.verifyNoInteractions(logger); } @@ -209,9 +210,10 @@ void shouldHandleMultipleAuthentications() throws Exception { new StringReader( "user:d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1")); Mockito.when(fs.readFile(mockPath)).thenReturn(props); - Assertions.assertTrue(mgr.validateToken(() -> "user:pass").get()); - Assertions.assertFalse(mgr.validateToken(() -> "user:sass").get()); - Assertions.assertFalse(mgr.validateToken(() -> "user2:pass").get()); + Assertions.assertTrue(mgr.validateToken(() -> "user:pass", ResourceAction.NONE).get()); + Assertions.assertFalse(mgr.validateToken(() -> "user:sass", ResourceAction.NONE).get()); + Assertions.assertFalse( + mgr.validateToken(() -> "user2:pass", ResourceAction.NONE).get()); Mockito.verifyNoInteractions(logger); } @@ -236,9 +238,10 @@ void shouldIgnoreMalformedPropertiesLines() throws Exception { new StringReader( String.join(System.lineSeparator(), creds1, creds2, creds3))); Mockito.when(fs.readFile(mockPath)).thenReturn(props); - Assertions.assertTrue(mgr.validateToken(() -> "user:pass").get()); - Assertions.assertFalse(mgr.validateToken(() -> "foo:bar").get()); - Assertions.assertTrue(mgr.validateToken(() -> "admin:admin").get()); + Assertions.assertTrue(mgr.validateToken(() -> "user:pass", ResourceAction.NONE).get()); + Assertions.assertFalse(mgr.validateToken(() -> "foo:bar", ResourceAction.NONE).get()); + Assertions.assertTrue( + mgr.validateToken(() -> "admin:admin", ResourceAction.NONE).get()); Mockito.verifyNoInteractions(logger); } } @@ -261,7 +264,8 @@ void shouldPassKnownCredentials() throws Exception { new StringReader( "user:d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1")); Mockito.when(fs.readFile(mockPath)).thenReturn(props); - Assertions.assertTrue(mgr.validateHttpHeader(() -> "Basic dXNlcjpwYXNz").get()); + Assertions.assertTrue( + mgr.validateHttpHeader(() -> "Basic dXNlcjpwYXNz", ResourceAction.NONE).get()); } @Test @@ -280,14 +284,15 @@ void shouldFailUnknownCredentials() throws Exception { new StringReader( "user:d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1")); Mockito.when(fs.readFile(mockPath)).thenReturn(props); - Assertions.assertFalse(mgr.validateHttpHeader(() -> "Basic foo").get()); + Assertions.assertFalse( + mgr.validateHttpHeader(() -> "Basic foo", ResourceAction.NONE).get()); } @ParameterizedTest @ValueSource(strings = {"", "Bearer sometoken", "Basic (not_b64)"}) @NullSource void shouldFailBadCredentials(String s) throws Exception { - Assertions.assertFalse(mgr.validateHttpHeader(() -> s).get()); + Assertions.assertFalse(mgr.validateHttpHeader(() -> s, ResourceAction.NONE).get()); } } @@ -312,7 +317,8 @@ void shouldPassKnownCredentials() throws Exception { Mockito.when(fs.readFile(mockPath)).thenReturn(props); Assertions.assertTrue( mgr.validateWebSocketSubProtocol( - () -> "basic.authorization.cryostat.dXNlcjpwYXNz") + () -> "basic.authorization.cryostat.dXNlcjpwYXNz", + ResourceAction.NONE) .get()); } @@ -335,7 +341,8 @@ void shouldPassKnownCredentialsWithPadding() throws Exception { Mockito.when(fs.readFile(mockPath)).thenReturn(props); Assertions.assertTrue( mgr.validateWebSocketSubProtocol( - () -> "basic.authorization.cryostat.dXNlcjpwYXNzMTIzNA==") + () -> "basic.authorization.cryostat.dXNlcjpwYXNzMTIzNA==", + ResourceAction.NONE) .get()); } @@ -362,7 +369,8 @@ void shouldPassKnownCredentialsAndStrippedPadding() throws Exception { // specification for the Sec-WebSocket-Protocol header Assertions.assertTrue( mgr.validateWebSocketSubProtocol( - () -> "basic.authorization.cryostat.dXNlcjpwYXNzMTIzNA") + () -> "basic.authorization.cryostat.dXNlcjpwYXNzMTIzNA", + ResourceAction.NONE) .get()); } @@ -384,7 +392,8 @@ void shouldFailUnknownCredentials() throws Exception { "user:d74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1")); Mockito.when(fs.readFile(mockPath)).thenReturn(props); Assertions.assertFalse( - mgr.validateWebSocketSubProtocol(() -> "basic.authorization.cryostat.foo") + mgr.validateWebSocketSubProtocol( + () -> "basic.authorization.cryostat.foo", ResourceAction.NONE) .get()); } @@ -393,7 +402,8 @@ void shouldFailUnknownCredentials() throws Exception { strings = {"", "basic.credentials.foo", "basic.authorization.cryostat.user:pass"}) @NullSource void shouldFailBadCredentials(String s) throws Exception { - Assertions.assertFalse(mgr.validateWebSocketSubProtocol(() -> s).get()); + Assertions.assertFalse( + mgr.validateWebSocketSubProtocol(() -> s, ResourceAction.NONE).get()); } } } diff --git a/src/test/java/io/cryostat/net/OpenShiftAuthManagerTest.java b/src/test/java/io/cryostat/net/OpenShiftAuthManagerTest.java new file mode 100644 index 0000000000..73ab7b381e --- /dev/null +++ b/src/test/java/io/cryostat/net/OpenShiftAuthManagerTest.java @@ -0,0 +1,373 @@ +/* + * 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; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.net.HttpURLConnection; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import io.cryostat.MainModule; +import io.cryostat.core.log.Logger; +import io.cryostat.core.sys.FileSystem; +import io.cryostat.net.OpenShiftAuthManager.PermissionDeniedException; +import io.cryostat.net.security.ResourceAction; +import io.cryostat.net.security.ResourceType; +import io.cryostat.net.security.ResourceVerb; + +import com.google.gson.Gson; +import io.fabric8.kubernetes.api.model.authentication.TokenReview; +import io.fabric8.kubernetes.api.model.authentication.TokenReviewBuilder; +import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReview; +import io.fabric8.kubernetes.api.model.authorization.v1.SelfSubjectAccessReviewBuilder; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.openshift.client.OpenShiftClient; +import io.fabric8.openshift.client.server.mock.EnableOpenShiftMockClient; +import io.fabric8.openshift.client.server.mock.OpenShiftMockServer; +import io.fabric8.openshift.client.server.mock.OpenShiftMockServerExtension; +import okhttp3.mockwebserver.RecordedRequest; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith({MockitoExtension.class, OpenShiftMockServerExtension.class}) +@EnableOpenShiftMockClient(https = false, crud = false) +class OpenShiftAuthManagerTest { + + static final String SUBJECT_REVIEW_API_PATH = + "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews"; + static final String TOKEN_REVIEW_API_PATH = "/apis/authentication.k8s.io/v1/tokenreviews"; + + OpenShiftAuthManager mgr; + @Mock FileSystem fs; + @Mock Logger logger; + OpenShiftClient client; + OpenShiftMockServer server; + TokenProvider tokenProvider; + Gson gson = MainModule.provideGson(logger); + + @BeforeAll + static void disableKubeConfig() { + // FIXME Disable reading ~/.kube/config. Remove once updated to 5.5.0 or newer. + System.setProperty(Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false"); + System.setProperty(Config.KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, "false"); + } + + @BeforeEach + void setup() { + client = Mockito.spy(client); + tokenProvider = new TokenProvider(client); + mgr = new OpenShiftAuthManager(logger, fs, tokenProvider); + } + + @Test + void shouldHandleBearerAuthentication() { + MatcherAssert.assertThat(mgr.getScheme(), Matchers.equalTo(AuthenticationScheme.BEARER)); + } + + @ParameterizedTest + @NullAndEmptySource + void shouldNotValidateBlankToken(String tok) throws Exception { + MatcherAssert.assertThat( + mgr.validateToken(() -> tok, ResourceAction.NONE).get(), Matchers.is(false)); + } + + @Test + void shouldValidateTokenWithNoRequiredPermissions() throws Exception { + TokenReview tokenReview = + new TokenReviewBuilder() + .withNewStatus() + .withAuthenticated(true) + .endStatus() + .build(); + server.expect() + .post() + .withPath(TOKEN_REVIEW_API_PATH) + .andReturn(HttpURLConnection.HTTP_CREATED, tokenReview) + .once(); + + Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH))) + .thenReturn(new BufferedReader(new StringReader("serviceAccountToken"))); + + MatcherAssert.assertThat( + mgr.validateToken(() -> "userToken", ResourceAction.NONE).get(), Matchers.is(true)); + } + + @Test + void shouldNotValidateTokenWithNoRequiredPermissionsButNoTokenAccess() throws Exception { + TokenReview tokenReview = + new TokenReviewBuilder() + .withNewStatus() + .withAuthenticated(false) + .endStatus() + .build(); + server.expect() + .post() + .withPath(TOKEN_REVIEW_API_PATH) + .andReturn(HttpURLConnection.HTTP_CREATED, tokenReview) + .once(); + + Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH))) + .thenReturn(new BufferedReader(new StringReader("serviceAccountToken"))); + + MatcherAssert.assertThat( + mgr.validateToken(() -> "userToken", ResourceAction.NONE).get(), + Matchers.is(false)); + } + + @Test + void shouldValidateTokenWithSufficientPermissions() throws Exception { + SelfSubjectAccessReview accessReview = + new SelfSubjectAccessReviewBuilder() + .withNewStatus() + .withAllowed(true) + .endStatus() + .build(); + server.expect() + .post() + .withPath(SUBJECT_REVIEW_API_PATH) + .andReturn(HttpURLConnection.HTTP_CREATED, accessReview) + .once(); + + Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_NAMESPACE_PATH))) + .thenReturn(new BufferedReader(new StringReader("mynamespace"))); + + MatcherAssert.assertThat( + mgr.validateToken(() -> "token", Set.of(ResourceAction.READ_TARGET)).get(), + Matchers.is(true)); + + ArgumentCaptor nsPathCaptor = ArgumentCaptor.forClass(Path.class); + Mockito.verify(fs).readFile(nsPathCaptor.capture()); + MatcherAssert.assertThat( + nsPathCaptor.getValue(), + Matchers.equalTo(Paths.get(Config.KUBERNETES_NAMESPACE_PATH))); + } + + @Test + void shouldNotValidateTokenWithInsufficientPermissions() throws Exception { + SelfSubjectAccessReview accessReview = + new SelfSubjectAccessReviewBuilder() + .withNewStatus() + .withAllowed(false) + .endStatus() + .build(); + server.expect() + .post() + .withPath(SUBJECT_REVIEW_API_PATH) + .andReturn(HttpURLConnection.HTTP_CREATED, accessReview) + .once(); + + String namespace = "mynamespace"; + Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_NAMESPACE_PATH))) + .thenReturn(new BufferedReader(new StringReader(namespace))); + + ExecutionException ee = + Assertions.assertThrows( + ExecutionException.class, + () -> + mgr.validateToken(() -> "token", Set.of(ResourceAction.READ_TARGET)) + .get()); + MatcherAssert.assertThat( + ExceptionUtils.getRootCause(ee), + Matchers.instanceOf(PermissionDeniedException.class)); + PermissionDeniedException pde = (PermissionDeniedException) ExceptionUtils.getRootCause(ee); + MatcherAssert.assertThat(pde.getNamespace(), Matchers.equalTo(namespace)); + MatcherAssert.assertThat(pde.getResourceType(), Matchers.equalTo("flightrecorders")); + MatcherAssert.assertThat(pde.getVerb(), Matchers.equalTo("get")); + + ArgumentCaptor nsPathCaptor = ArgumentCaptor.forClass(Path.class); + Mockito.verify(fs).readFile(nsPathCaptor.capture()); + MatcherAssert.assertThat( + nsPathCaptor.getValue(), + Matchers.equalTo(Paths.get(Config.KUBERNETES_NAMESPACE_PATH))); + } + + @ParameterizedTest + @EnumSource( + mode = EnumSource.Mode.MATCH_ANY, + names = "^([a-zA-Z]+_(RECORDING|TARGET|CERTIFICATE|CREDENTIALS))$") + void shouldValidateExpectedPermissionsPerSecuredResource(ResourceAction resourceAction) + throws Exception { + Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_NAMESPACE_PATH))) + .thenReturn(new BufferedReader(new StringReader("mynamespace"))); + + String expectedVerb; + if (resourceAction.getVerb() == ResourceVerb.CREATE) { + expectedVerb = "create"; + } else if (resourceAction.getVerb() == ResourceVerb.READ) { + expectedVerb = "get"; + } else if (resourceAction.getVerb() == ResourceVerb.UPDATE) { + expectedVerb = "patch"; + } else if (resourceAction.getVerb() == ResourceVerb.DELETE) { + expectedVerb = "delete"; + } else { + throw new IllegalArgumentException(resourceAction.getVerb().toString()); + } + + Set expectedResources; + if (resourceAction.getResource() == ResourceType.TARGET) { + expectedResources = Set.of("flightrecorders"); + } else if (resourceAction.getResource() == ResourceType.RECORDING) { + expectedResources = Set.of("recordings"); + } else if (resourceAction.getResource() == ResourceType.CERTIFICATE) { + expectedResources = Set.of("deployments", "pods", "cryostats"); + } else if (resourceAction.getResource() == ResourceType.CREDENTIALS) { + expectedResources = Set.of("cryostats"); + } else { + throw new IllegalArgumentException(resourceAction.getResource().toString()); + } + + SelfSubjectAccessReview accessReview = + new SelfSubjectAccessReviewBuilder() + .withNewStatus() + .withAllowed(true) + .endStatus() + .build(); + server.expect() + .post() + .withPath(SUBJECT_REVIEW_API_PATH) + .andReturn(HttpURLConnection.HTTP_CREATED, accessReview) + .times(expectedResources.size()); + + String token = "abcd1234"; + MatcherAssert.assertThat( + mgr.validateToken(() -> token, Set.of(resourceAction)).get(), Matchers.is(true)); + + // server.takeRequest() returns each request fired in order, so do that repeatedly and drop + // any initial requests that are made by the OpenShiftClient that aren't directly + // SelfSubjectAccessReview requests made by the OpenShiftAuthManager + int maxDroppedRequests = 2; + int requestCount = 0; + RecordedRequest req = server.takeRequest(); + while (true) { + if (++requestCount > maxDroppedRequests) { + throw new IllegalStateException(); + } + String path = req.getPath(); + if (SUBJECT_REVIEW_API_PATH.equals(path)) { + break; + } + req = server.takeRequest(); + } + MatcherAssert.assertThat(req.getPath(), Matchers.equalTo(SUBJECT_REVIEW_API_PATH)); + MatcherAssert.assertThat(tokenProvider.token, Matchers.equalTo(token)); + MatcherAssert.assertThat(req.getMethod(), Matchers.equalTo("POST")); + + SelfSubjectAccessReview body = + gson.fromJson(req.getBody().readUtf8(), SelfSubjectAccessReview.class); + MatcherAssert.assertThat( + body.getSpec().getResourceAttributes().getVerb(), Matchers.equalTo(expectedVerb)); + + Set actualResources = new HashSet<>(); + actualResources.add(body.getSpec().getResourceAttributes().getResource()); + // start at 1 because we've already checked the first request above + for (int i = 1; i < expectedResources.size(); i++) { + // request should already have been made, so there should be no time waiting for a + // request to come in + req = server.takeRequest(1, TimeUnit.SECONDS); + if (req == null) { + throw new IllegalStateException("Expected request not received in time"); + } + body = gson.fromJson(req.getBody().readUtf8(), SelfSubjectAccessReview.class); + + MatcherAssert.assertThat(req.getPath(), Matchers.equalTo(SUBJECT_REVIEW_API_PATH)); + MatcherAssert.assertThat(tokenProvider.token, Matchers.equalTo(token)); + MatcherAssert.assertThat(req.getMethod(), Matchers.equalTo("POST")); + MatcherAssert.assertThat( + body.getSpec().getResourceAttributes().getVerb(), + Matchers.equalTo(expectedVerb)); + actualResources.add(body.getSpec().getResourceAttributes().getResource()); + } + + MatcherAssert.assertThat(actualResources, Matchers.equalTo(expectedResources)); + } + + @ParameterizedTest + @EnumSource( + mode = EnumSource.Mode.MATCH_ALL, + names = { + "^[a-zA-Z]+_(?!TARGET).*$", + "^[a-zA-Z]+_(?!RECORDING).*$", + "^[a-zA-Z]+_(?!CERTIFICATE).*$", + "^[a-zA-Z]+_(?!CREDENTIALS).*$" + }) + void shouldValidateExpectedPermissionsForUnsecuredResources(ResourceAction resourceAction) + throws Exception { + Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_NAMESPACE_PATH))) + .thenReturn(new BufferedReader(new StringReader("mynamespace"))); + MatcherAssert.assertThat( + mgr.validateToken(() -> "token", Set.of(resourceAction)).get(), Matchers.is(true)); + } + + private static class TokenProvider implements Function { + + private final OpenShiftClient osc; + String token; + + TokenProvider(OpenShiftClient osc) { + this.osc = osc; + } + + @Override + public OpenShiftClient apply(String token) { + if (this.token != null) { + throw new IllegalStateException("Token was already set!"); + } + this.token = token; + return osc; + } + } +} diff --git a/src/test/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandlerTest.java b/src/test/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandlerTest.java index 3c43cbe88a..1f25d8027d 100644 --- a/src/test/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/AbstractAuthenticatedRequestHandlerTest.java @@ -41,14 +41,18 @@ import java.net.UnknownHostException; import java.rmi.ConnectIOException; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.openjdk.jmc.rjmx.ConnectionException; import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.OpenShiftAuthManager.PermissionDeniedException; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.api.ApiVersion; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; @@ -93,7 +97,7 @@ void setup() { @Test void shouldPutDefaultContentTypeHeader() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); handler.handle(ctx); Mockito.verify(resp).putHeader(HttpHeaders.CONTENT_TYPE, "text/plain"); @@ -101,7 +105,7 @@ void shouldPutDefaultContentTypeHeader() { @Test void shouldThrow401IfAuthFails() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(false)); HttpStatusException ex = @@ -109,9 +113,32 @@ void shouldThrow401IfAuthFails() { MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(401)); } + @Test + void shouldThrow401IfAuthFails2() { + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) + .thenReturn( + CompletableFuture.failedFuture( + new PermissionDeniedException( + "namespace", "group", "resource", "verb", "reason"))); + + HttpStatusException ex = + Assertions.assertThrows(HttpStatusException.class, () -> handler.handle(ctx)); + MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(401)); + } + + @Test + void shouldThrow401IfAuthFails3() { + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) + .thenReturn(CompletableFuture.failedFuture(new KubernetesClientException("test"))); + + HttpStatusException ex = + Assertions.assertThrows(HttpStatusException.class, () -> handler.handle(ctx)); + MatcherAssert.assertThat(ex.getStatusCode(), Matchers.equalTo(401)); + } + @Test void shouldThrow500IfAuthThrows() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.failedFuture(new NullPointerException())); HttpStatusException ex = @@ -124,7 +151,7 @@ class WithHandlerThrownException { @BeforeEach void setup2() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); } @@ -212,7 +239,7 @@ void setup3() { handler = new ConnectionDescriptorHandler(auth); Mockito.when(ctx.request()).thenReturn(req); when(req.headers()).thenReturn(headers); - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); } @@ -376,6 +403,11 @@ public HttpMethod httpMethod() { return null; } + @Override + public Set resourceActions() { + return ResourceAction.NONE; + } + @Override public void handleAuthenticated(RoutingContext ctx) throws Exception {} } diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/AuthPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/AuthPostHandlerTest.java index 5dfa33a6c0..f2b454f51b 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/AuthPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/AuthPostHandlerTest.java @@ -40,6 +40,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.util.Set; import java.util.concurrent.CompletableFuture; import io.cryostat.net.AuthManager; @@ -81,9 +82,14 @@ void shouldHandleExpectedPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v1/auth")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat(handler.resourceActions(), Matchers.equalTo(Set.of())); + } + @Test void shouldRespond200IfAuthPasses() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); @@ -101,7 +107,7 @@ void shouldRespond200IfAuthPasses() { @Test void shouldThrow401IfAuthFails() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(false)); RoutingContext ctx = mock(RoutingContext.class); @@ -115,7 +121,7 @@ void shouldThrow401IfAuthFails() { @Test void shouldThrow500IfAuthThrows() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.failedFuture(new NullPointerException())); RoutingContext ctx = mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/GrafanaDashboardUrlGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/GrafanaDashboardUrlGetHandlerTest.java index 48e0361ad9..314744d740 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/GrafanaDashboardUrlGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/GrafanaDashboardUrlGetHandlerTest.java @@ -41,6 +41,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Set; + import io.cryostat.MainModule; import io.cryostat.core.log.Logger; import io.cryostat.core.sys.Environment; @@ -85,6 +87,11 @@ void shouldHandleCorrectPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v1/grafana_dashboard_url")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat(handler.resourceActions(), Matchers.equalTo(Set.of())); + } + @Test void shouldBeAsync() { Assertions.assertTrue(handler.isAsync()); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/GrafanaDatasourceUrlGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/GrafanaDatasourceUrlGetHandlerTest.java index b0dec2f4a9..91afbf59fd 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/GrafanaDatasourceUrlGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/GrafanaDatasourceUrlGetHandlerTest.java @@ -41,6 +41,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Set; + import io.cryostat.MainModule; import io.cryostat.core.log.Logger; import io.cryostat.core.sys.Environment; @@ -86,6 +88,11 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/grafana_datasource_url")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat(handler.resourceActions(), Matchers.equalTo(Set.of())); + } + @Test void shouldBeAsync() { Assertions.assertTrue(handler.isAsync()); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/NotificationsUrlGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/NotificationsUrlGetHandlerTest.java index a724a9e66a..fca0459ab3 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/NotificationsUrlGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/NotificationsUrlGetHandlerTest.java @@ -43,6 +43,7 @@ import java.net.SocketException; import java.net.UnknownHostException; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; @@ -94,6 +95,11 @@ void shouldHaveCorrectPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v1/notifications_url")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat(handler.resourceActions(), Matchers.equalTo(Set.of())); + } + @Test void shouldBeAsync() { Assertions.assertTrue(handler.isAsync()); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandlerTest.java index 0d1e62bca8..a9568c0462 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingDeleteHandlerTest.java @@ -43,6 +43,7 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import io.cryostat.core.sys.FileSystem; @@ -50,6 +51,7 @@ import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.vertx.core.http.HttpMethod; @@ -111,9 +113,16 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/recordings/:recordingName")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.DELETE_RECORDING))); + } + @Test void shouldThrow404IfNoMatchingRecordingFound() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(fs.listDirectoryChildren(Mockito.any())).thenReturn(List.of()); @@ -131,7 +140,7 @@ void shouldThrow404IfNoMatchingRecordingFound() throws Exception { @Test void shouldDeleteIfRecordingFound() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); String recordingName = "someRecording"; @@ -161,7 +170,7 @@ void shouldDeleteIfRecordingFound() throws Exception { @Test void shouldThrowExceptionIfDeletionFails() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); String recordingName = "someRecording"; diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingGetHandlerTest.java index 72cb929e3b..b9ad8916c7 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingGetHandlerTest.java @@ -38,8 +38,10 @@ package io.cryostat.net.web.http.api.v1; import java.nio.file.Path; +import java.util.Set; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.vertx.core.http.HttpMethod; import org.hamcrest.MatcherAssert; @@ -72,4 +74,10 @@ void shouldHandleCorrectPath() { MatcherAssert.assertThat( handler.path(), Matchers.equalTo("/api/v1/recordings/:recordingName")); } + + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_RECORDING))); + } } diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingUploadPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingUploadPostHandlerTest.java index 8dc2ad3e57..c4bf42fb89 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingUploadPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingUploadPostHandlerTest.java @@ -38,11 +38,13 @@ package io.cryostat.net.web.http.api.v1; import java.nio.file.Path; +import java.util.Set; import java.util.concurrent.CompletableFuture; import io.cryostat.core.sys.Environment; import io.cryostat.core.sys.FileSystem; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; @@ -101,6 +103,12 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/recordings/:recordingName/upload")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_RECORDING))); + } + @ParameterizedTest @ValueSource( strings = { @@ -121,7 +129,7 @@ void shouldThrow501IfDatasourceUrlMalformed(String rawUrl) { resp.putHeader( Mockito.any(CharSequence.class), Mockito.any(CharSequence.class))) .thenReturn(resp); - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(env.getEnv("GRAFANA_DATASOURCE_URL")).thenReturn(rawUrl); @@ -138,7 +146,7 @@ void shouldThrowExceptionIfRecordingNotFound() throws Exception { resp.putHeader( Mockito.any(CharSequence.class), Mockito.any(CharSequence.class))) .thenReturn(resp); - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(env.getEnv("GRAFANA_DATASOURCE_URL")).thenReturn(DATASOURCE_URL); Mockito.when(fs.isRegularFile(Mockito.any())).thenReturn(false); @@ -150,7 +158,7 @@ void shouldThrowExceptionIfRecordingNotFound() throws Exception { @Test void shouldDoUpload() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(env.getEnv("GRAFANA_DATASOURCE_URL")).thenReturn(DATASOURCE_URL); @@ -204,7 +212,7 @@ public Void answer(InvocationOnMock args) throws Throwable { @Test void shouldHandleInvalidResponseStatusCode() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(env.getEnv("GRAFANA_DATASOURCE_URL")).thenReturn(DATASOURCE_URL); @@ -267,7 +275,7 @@ void shouldHandleNullStatusMessage() throws Exception { resp.putHeader( Mockito.any(CharSequence.class), Mockito.any(CharSequence.class))) .thenReturn(resp); - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(env.getEnv("GRAFANA_DATASOURCE_URL")).thenReturn(DATASOURCE_URL); @@ -323,7 +331,7 @@ void shouldHandleNullResponseBody() throws Exception { resp.putHeader( Mockito.any(CharSequence.class), Mockito.any(CharSequence.class))) .thenReturn(resp); - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(env.getEnv("GRAFANA_DATASOURCE_URL")).thenReturn(DATASOURCE_URL); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsGetHandlerTest.java index 773a36c952..3717babef5 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsGetHandlerTest.java @@ -41,11 +41,13 @@ import java.nio.file.Path; import java.util.List; import java.util.Map; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; import io.cryostat.core.sys.FileSystem; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.WebServer; import com.google.gson.Gson; @@ -94,6 +96,12 @@ void shouldHandleCorrectPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v1/recordings")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_RECORDING))); + } + @Test void shouldRespondWith501IfDirectoryDoesNotExist() throws IOException { RoutingContext ctx = Mockito.mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandlerTest.java index a583fe0991..4709631698 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/RecordingsPostHandlerTest.java @@ -38,8 +38,10 @@ package io.cryostat.net.web.http.api.v1; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.nio.file.Path; import java.util.HashSet; @@ -54,6 +56,7 @@ import io.cryostat.messaging.notifications.NotificationFactory; import io.cryostat.net.AuthManager; import io.cryostat.net.HttpServer; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; @@ -121,6 +124,13 @@ void shouldBeLowerPriority() { handler.getPriority(), Matchers.greaterThan(RequestHandler.DEFAULT_PRIORITY)); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.CREATE_RECORDING))); + } + @Test void shouldHandleRecordingUploadRequest() throws Exception { String basename = "localhost_test_20191219T213834Z"; @@ -129,7 +139,7 @@ void shouldHandleRecordingUploadRequest() throws Exception { RoutingContext ctx = mock(RoutingContext.class); - when(authManager.validateHttpHeader(any())) + when(authManager.validateHttpHeader(any(), any())) .thenReturn(CompletableFuture.completedFuture(true)); HttpServerRequest req = mock(HttpServerRequest.class); when(ctx.request()).thenReturn(req); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/ReportGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/ReportGetHandlerTest.java index a5a8529071..4fd2263aa8 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/ReportGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/ReportGetHandlerTest.java @@ -42,11 +42,13 @@ import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Set; import java.util.concurrent.CompletableFuture; import io.cryostat.core.log.Logger; import io.cryostat.net.AuthManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.security.ResourceAction; import io.vertx.core.http.HttpHeaders; import io.vertx.core.http.HttpMethod; @@ -88,6 +90,17 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/reports/:recordingName")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of( + ResourceAction.READ_REPORT, + ResourceAction.CREATE_REPORT, + ResourceAction.READ_RECORDING))); + } + @Test void shouldNotBeAsync() { Assertions.assertFalse(handler.isAsync()); @@ -100,7 +113,7 @@ void shouldBeOrdered() { @Test void shouldRespondBySendingFile() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); @@ -126,7 +139,7 @@ void shouldRespondBySendingFile() throws Exception { @Test void shouldRespond404IfRecordingNameNotFound() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandlerTest.java index b401209b8b..dea0332a0d 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetEventsGetHandlerTest.java @@ -41,6 +41,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; import org.openjdk.jmc.flightrecorder.configuration.events.IEventTypeID; import org.openjdk.jmc.rjmx.services.jfr.IEventTypeInfo; @@ -53,6 +54,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -97,6 +99,12 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/targets/:targetId/events")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_TARGET))); + } + @Test void shouldRespondWithErrorIfExceptionThrown() throws Exception { Mockito.when( diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandlerTest.java index 34a444d355..5500731862 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingDeleteHandlerTest.java @@ -40,6 +40,7 @@ import static org.mockito.Mockito.lenient; import java.util.Map; +import java.util.Set; import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; @@ -49,6 +50,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.TargetConnectionManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.recordings.RecordingArchiveHelper; import io.cryostat.recordings.RecordingNotFoundException; @@ -118,6 +120,14 @@ void shouldHandleCorrectPath() { Matchers.equalTo("/api/v1/targets/:targetId/recordings/:recordingName")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of(ResourceAction.READ_TARGET, ResourceAction.DELETE_RECORDING))); + } + @Test void shouldDeleteRecording() throws Exception { Mockito.when(ctx.pathParam("targetId")).thenReturn("fooTarget"); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandlerTest.java index cef21c0274..69d23a4563 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingGetHandlerTest.java @@ -44,6 +44,7 @@ import java.io.ByteArrayInputStream; import java.util.List; import java.util.Random; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; @@ -53,6 +54,7 @@ import io.cryostat.core.net.JFRConnection; 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.vertx.core.MultiMap; @@ -102,6 +104,14 @@ void shouldHandleCorrectPath() { Matchers.equalTo("/api/v1/targets/:targetId/recordings/:recordingName")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of(ResourceAction.READ_TARGET, ResourceAction.READ_RECORDING))); + } + @Test void shouldNotBeAsync() { Assertions.assertFalse(handler.isAsync()); @@ -109,7 +119,7 @@ void shouldNotBeAsync() { @Test void shouldHandleRecordingDownloadRequest() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); when(connection.getService()).thenReturn(service); @@ -161,7 +171,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { @Test void shouldHandleRecordingDownloadRequestWithJfrSuffix() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); when(connection.getService()).thenReturn(service); @@ -213,7 +223,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { @Test void shouldRespond404IfRecordingNameNotFound() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); @@ -248,7 +258,7 @@ public Object answer(InvocationOnMock invocation) throws Throwable { @Test void shouldRespond500IfUnexpectedExceptionThrown() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandlerTest.java index f364c76b13..0d2c843f6b 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsGetHandlerTest.java @@ -38,6 +38,7 @@ package io.cryostat.net.web.http.api.v1; import java.util.Map; +import java.util.Set; import org.openjdk.jmc.common.unit.IConstrainedMap; import org.openjdk.jmc.flightrecorder.configuration.recording.RecordingOptionsBuilder; @@ -49,6 +50,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.recordings.RecordingOptionsBuilderFactory; import com.google.gson.Gson; @@ -102,6 +104,12 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/targets/:targetId/recordingOptions")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_TARGET))); + } + @Test void shouldRespondWithErrorIfExceptionThrown() throws Exception { Mockito.when( diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandlerTest.java index 05062ebf0f..2d0bcf61d0 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingOptionsPatchHandlerTest.java @@ -38,6 +38,7 @@ package io.cryostat.net.web.http.api.v1; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.openjdk.jmc.common.unit.IConstrainedMap; @@ -50,6 +51,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.recordings.RecordingOptionsBuilderFactory; import com.google.gson.Gson; @@ -104,6 +106,13 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/targets/:targetId/recordingOptions")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.READ_TARGET, ResourceAction.UPDATE_TARGET))); + } + @Test void shouldSetRecordingOptions() throws Exception { Map defaultValues = diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchHandlerTest.java index 507e204919..2b8b79fe25 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingPatchHandlerTest.java @@ -37,10 +37,12 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.Set; import java.util.concurrent.CompletableFuture; import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.security.ResourceAction; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; @@ -90,6 +92,17 @@ void shouldHandleCorrectPath() { Matchers.equalTo("/api/v1/targets/:targetId/recordings/:recordingName")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of( + ResourceAction.READ_TARGET, + ResourceAction.READ_RECORDING, + ResourceAction.UPDATE_RECORDING))); + } + @Test void shouldNotBeAsync() { // recording saving is a blocking operation, so the handler should be marked as such @@ -98,7 +111,7 @@ void shouldNotBeAsync() { @Test void shouldThrow401IfAuthFails() { - Mockito.when(authManager.validateHttpHeader(Mockito.any())) + Mockito.when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(false)); HttpStatusException ex = @@ -110,7 +123,7 @@ void shouldThrow401IfAuthFails() { @ValueSource(strings = {"unknown", "start", "dump"}) @NullAndEmptySource void shouldThrow400InvalidOperations(String mtd) { - Mockito.when(authManager.validateHttpHeader(Mockito.any())) + Mockito.when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(ctx.getBodyAsString()).thenReturn(mtd); Mockito.when(ctx.response()).thenReturn(resp); @@ -127,7 +140,7 @@ void shouldThrow400InvalidOperations(String mtd) { @ParameterizedTest @ValueSource(strings = {"save", "stop"}) void shouldDelegateSupportedOperations(String mtd) throws Exception { - Mockito.when(authManager.validateHttpHeader(Mockito.any())) + Mockito.when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(ctx.pathParam("targetId")).thenReturn("fooHost:1234"); Mockito.when(ctx.request()).thenReturn(req); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandlerTest.java index 747f796b19..46dcde4e93 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingUploadPostHandlerTest.java @@ -40,6 +40,7 @@ import java.io.InputStream; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.openjdk.jmc.rjmx.services.jfr.IFlightRecorderService; @@ -52,6 +53,7 @@ import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; import io.cryostat.net.reports.ReportService; +import io.cryostat.net.security.ResourceAction; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; @@ -117,6 +119,14 @@ void shouldHandleCorrectPath() { Matchers.equalTo("/api/v1/targets/:targetId/recordings/:recordingName/upload")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of(ResourceAction.READ_TARGET, ResourceAction.READ_RECORDING))); + } + @ParameterizedTest @ValueSource( strings = { @@ -131,7 +141,7 @@ void shouldHandleCorrectPath() { }) @NullAndEmptySource void shouldThrow501IfDatasourceUrlMalformed(String rawUrl) { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(env.getEnv("GRAFANA_DATASOURCE_URL")).thenReturn(rawUrl); Mockito.when(ctx.response()).thenReturn(resp); @@ -155,7 +165,7 @@ void shouldThrowExceptionIfRecordingNotFound() throws Exception { .thenReturn(resp); Mockito.when(ctx.pathParam("targetId")).thenReturn("fooHost:1234"); Mockito.when(req.headers()).thenReturn(MultiMap.caseInsensitiveMultiMap()); - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when( targetConnectionManager.executeConnectedTask( @@ -183,7 +193,7 @@ void shouldThrowExceptionIfRecordingNotFound() throws Exception { @Test void shouldDoUpload() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when( targetConnectionManager.executeConnectedTask( @@ -249,7 +259,7 @@ public Void answer(InvocationOnMock args) throws Throwable { @Test void shouldHandleInvalidResponseStatusCode() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when( targetConnectionManager.executeConnectedTask( @@ -318,7 +328,7 @@ public Void answer(InvocationOnMock args) throws Throwable { @Test void shouldHandleNullStatusMessage() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when( targetConnectionManager.executeConnectedTask( @@ -387,7 +397,7 @@ public Void answer(InvocationOnMock args) throws Throwable { @Test void shouldHandleNullResponseBody() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when( targetConnectionManager.executeConnectedTask( diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandlerTest.java index 60b30ee0cd..6566eba5ce 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsGetHandlerTest.java @@ -39,6 +39,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Set; import org.openjdk.jmc.common.unit.IQuantity; import org.openjdk.jmc.common.unit.QuantityConversionException; @@ -52,6 +53,7 @@ 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.WebServer; import com.google.gson.Gson; @@ -101,6 +103,14 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/targets/:targetId/recordings")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of(ResourceAction.READ_TARGET, ResourceAction.READ_RECORDING))); + } + @Test void shouldRespondWithErrorIfExceptionThrown() throws Exception { Mockito.when( diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java index 02753542d3..73855ad288 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetRecordingsPostHandlerTest.java @@ -38,6 +38,7 @@ package io.cryostat.net.web.http.api.v1; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; @@ -57,6 +58,7 @@ 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.WebServer; import io.cryostat.recordings.RecordingOptionsBuilderFactory; import io.cryostat.recordings.RecordingTargetHelper; @@ -124,9 +126,22 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/targets/:targetId/recordings")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of( + ResourceAction.READ_TARGET, + ResourceAction.READ_TEMPLATE, + ResourceAction.CREATE_RECORDING, + ResourceAction.READ_RECORDING, + ResourceAction.UPDATE_TARGET))); + } + @Test void shouldStartRecording() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(targetConnectionManager.executeConnectedTask(Mockito.any(), Mockito.any())) .thenAnswer( @@ -232,7 +247,7 @@ void shouldStartRecording() throws Exception { @Test void shouldHandleNameCollision() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); Mockito.when(targetConnectionManager.executeConnectedTask(Mockito.any(), Mockito.any())) @@ -301,7 +316,7 @@ private static IRecordingDescriptor createDescriptor(String name) @ParameterizedTest @MethodSource("getRequestMaps") void shouldThrowInvalidOptionException(Map requestValues) throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); IRecordingDescriptor existingRecording = createDescriptor("someRecording"); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetReportGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetReportGetHandlerTest.java index 215e8bf167..f89c17754c 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetReportGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetReportGetHandlerTest.java @@ -41,6 +41,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; @@ -50,6 +51,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.reports.ReportService; import io.cryostat.net.reports.SubprocessReportGenerator; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.vertx.core.MultiMap; @@ -94,9 +96,21 @@ void shouldHandleCorrectPath() { Matchers.equalTo("/api/v1/targets/:targetId/reports/:recordingName")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of( + ResourceAction.READ_TARGET, + ResourceAction.READ_RECORDING, + ResourceAction.CREATE_REPORT, + ResourceAction.READ_REPORT))); + } + @Test void shouldHandleRecordingDownloadRequest() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); @@ -122,7 +136,7 @@ void shouldHandleRecordingDownloadRequest() throws Exception { @Test void shouldRespond404IfRecordingNameNotFound() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); @@ -148,7 +162,7 @@ void shouldRespond404IfRecordingNameNotFound() throws Exception { @Test void shouldRespond404IfTargetNotFound() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); @@ -178,7 +192,7 @@ void shouldRespond404IfTargetNotFound() throws Exception { @Test void shouldRespond404IfRecordingNotFound() throws Exception { - when(authManager.validateHttpHeader(Mockito.any())) + when(authManager.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetSnapshotPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetSnapshotPostHandlerTest.java index f1d8a2a0d3..07d45f4a61 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetSnapshotPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetSnapshotPostHandlerTest.java @@ -37,6 +37,7 @@ */ package io.cryostat.net.web.http.api.v1; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.openjdk.jmc.common.unit.IConstrainedMap; @@ -48,12 +49,15 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.recordings.RecordingOptionsBuilderFactory; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.ext.web.RoutingContext; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -78,9 +82,17 @@ void setup() { auth, targetConnectionManager, recordingOptionsBuilderFactory); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + snapshot.resourceActions(), + Matchers.equalTo( + Set.of(ResourceAction.READ_TARGET, ResourceAction.UPDATE_RECORDING))); + } + @Test void shouldCreateSnapshot() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = Mockito.mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetTemplateGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetTemplateGetHandlerTest.java index 15195c88fc..0ef78f6550 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetTemplateGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetTemplateGetHandlerTest.java @@ -38,6 +38,7 @@ package io.cryostat.net.web.http.api.v1; import java.util.Optional; +import java.util.Set; import io.cryostat.core.FlightRecorderException; import io.cryostat.core.net.JFRConnection; @@ -46,6 +47,7 @@ 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.vertx.core.MultiMap; @@ -95,6 +97,13 @@ void shouldHandleCorrectPath() { "/api/v1/targets/:targetId/templates/:templateName/type/:templateType")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.READ_TEMPLATE, ResourceAction.READ_TARGET))); + } + @Test void shouldThrowIfTargetConnectionManagerThrows() throws Exception { RoutingContext ctx = Mockito.mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetTemplatesGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetTemplatesGetHandlerTest.java index 6d359e9c3d..8c7441a77a 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetTemplatesGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetTemplatesGetHandlerTest.java @@ -39,6 +39,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; @@ -49,6 +50,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -93,6 +95,13 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/targets/:targetId/templates")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.READ_TEMPLATE, ResourceAction.READ_TARGET))); + } + @Test void shouldRespondWithErrorIfExceptionThrown() throws Exception { Mockito.when( diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TargetsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TargetsGetHandlerTest.java index 22bf63b31f..1f21a41971 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TargetsGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TargetsGetHandlerTest.java @@ -39,6 +39,7 @@ import java.util.Collections; import java.util.List; +import java.util.Set; import javax.management.remote.JMXServiceURL; @@ -46,6 +47,7 @@ import io.cryostat.core.log.Logger; import io.cryostat.core.net.JFRConnectionToolkit; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.platform.PlatformClient; import io.cryostat.platform.ServiceRef; import io.cryostat.util.URIUtil; @@ -93,6 +95,12 @@ void shouldHandleCorrectPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v1/targets")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_TARGET))); + } + @Test void shouldReturnListOfTargets() throws Exception { Mockito.when(connectionToolkit.createServiceURL(Mockito.anyString(), Mockito.anyInt())) diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandlerTest.java index faf497ee1d..c91b3b391c 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TemplateDeleteHandlerTest.java @@ -41,11 +41,13 @@ import java.io.IOException; import java.util.Map; +import java.util.Set; import io.cryostat.core.templates.LocalStorageTemplateService; 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.vertx.core.http.HttpMethod; @@ -99,6 +101,13 @@ void sholdHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v1/templates/:templateName")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.DELETE_TEMPLATE))); + } + @Test void shouldThrowIfServiceThrows() throws Exception { RoutingContext ctx = Mockito.mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandlerTest.java index f22ee36cdc..36bc6f4d96 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v1/TemplatesPostHandlerTest.java @@ -52,6 +52,7 @@ 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.vertx.core.http.HttpMethod; @@ -109,6 +110,13 @@ void shouldHandleCorrectPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v1/templates")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.CREATE_TEMPLATE))); + } + @Test void shouldThrowIfWriteFails() throws Exception { RoutingContext ctx = Mockito.mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandlerTest.java index e798a6ce3a..6667fd4da4 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/AbstractV2RequestHandlerTest.java @@ -43,6 +43,7 @@ import java.rmi.ConnectIOException; import java.util.Collections; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.openjdk.jmc.rjmx.ConnectionException; @@ -51,6 +52,7 @@ import io.cryostat.core.log.Logger; import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; import io.cryostat.net.web.http.api.ApiVersion; @@ -104,9 +106,14 @@ void setup() { this.handler = new AuthenticatedHandler(auth, gson); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat(handler.resourceActions(), Matchers.equalTo(Set.of())); + } + @Test void shouldThrow401IfAuthFails() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(false)); ApiException ex = Assertions.assertThrows(ApiException.class, () -> handler.handle(ctx)); @@ -115,7 +122,7 @@ void shouldThrow401IfAuthFails() { @Test void shouldThrow500IfAuthThrows() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.failedFuture(new NullPointerException())); ApiException ex = Assertions.assertThrows(ApiException.class, () -> handler.handle(ctx)); @@ -127,7 +134,7 @@ class WithHandlerThrownException { @BeforeEach void setup2() { - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); } @@ -210,7 +217,7 @@ class WithTargetAuth { @BeforeEach void setup3() { handler = new ConnectionDescriptorHandler(auth, gson); - when(auth.validateHttpHeader(Mockito.any())) + when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); } @@ -351,6 +358,11 @@ public HttpMethod httpMethod() { return null; } + @Override + public Set resourceActions() { + return Set.of(); + } + @Override public HttpMimeType mimeType() { return HttpMimeType.PLAINTEXT; diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/ApiGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/ApiGetHandlerTest.java index b8c969815b..040966f3a9 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/ApiGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/ApiGetHandlerTest.java @@ -45,6 +45,7 @@ import io.cryostat.MainModule; import io.cryostat.core.log.Logger; import io.cryostat.net.AuthManager; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.WebServer; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.RequestHandler; @@ -96,6 +97,11 @@ void shouldHaveExpectedPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat(handler.resourceActions(), Matchers.equalTo(Set.of())); + } + @Test void shouldReturnJsonMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.JSON)); @@ -137,6 +143,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.GET; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; requestHandlers.add(testHandler1); @@ -174,6 +185,11 @@ public HttpMethod httpMethod() { public boolean isAvailable() { return false; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; RequestHandler testHandler2 = new TestRequestHandler() { @@ -191,6 +207,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.GET; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; requestHandlers.add(testHandler1); requestHandlers.add(testHandler2); @@ -222,6 +243,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.GET; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; RequestHandler testHandler2 = new TestRequestHandler() { @@ -239,6 +265,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.GET; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; requestHandlers.add(testHandler1); requestHandlers.add(testHandler2); @@ -270,6 +301,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.GET; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; RequestHandler testHandler2 = new TestRequestHandler() { @@ -287,6 +323,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.GET; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; RequestHandler testHandler3 = new TestRequestHandler() { @@ -304,6 +345,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.POST; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; RequestHandler testHandler4 = new TestRequestHandler() { @@ -321,6 +367,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.PATCH; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; requestHandlers.add(testHandler1); requestHandlers.add(testHandler2); @@ -359,6 +410,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.POST; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; // duplicate on purpose - this will serialize identically. This simulates ex. // TargtPostHandler and TargetPostBodyHandler, which also serialize identically. @@ -378,6 +434,11 @@ public String path() { public HttpMethod httpMethod() { return HttpMethod.POST; } + + @Override + public Set resourceActions() { + return Set.of(); + } }; requestHandlers.add(testHandler1); requestHandlers.add(testHandler2); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/CertificatePostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/CertificatePostHandlerTest.java index e3a0188507..2599a3b926 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/CertificatePostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/CertificatePostHandlerTest.java @@ -57,6 +57,7 @@ import io.cryostat.core.sys.FileSystem; import io.cryostat.net.AuthManager; import io.cryostat.net.security.CertificateValidator; +import io.cryostat.net.security.ResourceAction; import io.cryostat.net.web.http.HttpMimeType; import io.cryostat.net.web.http.api.ApiData; import io.cryostat.net.web.http.api.ApiMeta; @@ -116,7 +117,7 @@ void setup() { Mockito.lenient().when(ctx.request()).thenReturn(req); Mockito.lenient() - .when(auth.validateHttpHeader(Mockito.any())) + .when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); } @@ -130,6 +131,13 @@ void shouldHandleCorrectPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v2/certificates")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.CREATE_CERTIFICATE))); + } + @Test void shouldThrow400IfNoCertInRequest() { Mockito.when(ctx.fileUploads()).thenReturn(Collections.emptySet()); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/RuleDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/RuleDeleteHandlerTest.java index 95ff0533fb..3f777575f1 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/RuleDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/RuleDeleteHandlerTest.java @@ -38,10 +38,12 @@ package io.cryostat.net.web.http.api.v2; import java.util.Map; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; 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.rules.RuleRegistry; @@ -95,6 +97,13 @@ void shouldHaveExpectedApiPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v2/rules/:name")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.DELETE_RULE))); + } + @Test void shouldHavePlaintextMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT)); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/RuleGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/RuleGetHandlerTest.java index 70ff927f67..fd8a364e42 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/RuleGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/RuleGetHandlerTest.java @@ -39,10 +39,12 @@ import java.util.Map; import java.util.Optional; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; 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.rules.Rule; @@ -97,6 +99,12 @@ void shouldHaveExpectedApiPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v2/rules/:name")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_RULE))); + } + @Test void shouldHavePlaintextMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.JSON)); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/RulesGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/RulesGetHandlerTest.java index ec4d86889e..084a2393b2 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/RulesGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/RulesGetHandlerTest.java @@ -42,6 +42,7 @@ import io.cryostat.MainModule; import io.cryostat.core.log.Logger; 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.rules.Rule; @@ -96,6 +97,12 @@ void shouldHaveExpectedApiPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v2/rules")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_RULE))); + } + @Test void shouldHavePlaintextMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.JSON)); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/RulesPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/RulesPostHandlerTest.java index 7181554145..0c05a7bcbd 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/RulesPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/RulesPostHandlerTest.java @@ -39,10 +39,12 @@ import java.io.IOException; import java.util.Map; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; 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.rules.Rule; @@ -113,6 +115,19 @@ void shouldHaveExpectedApiPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v2/rules")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo( + Set.of( + ResourceAction.CREATE_RULE, + ResourceAction.READ_TARGET, + ResourceAction.CREATE_RECORDING, + ResourceAction.UPDATE_RECORDING, + ResourceAction.READ_TEMPLATE))); + } + @Test void shouldHavePlaintextMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT)); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetCredentialsDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetCredentialsDeleteHandlerTest.java index d95c4583ef..ddbb1286b7 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetCredentialsDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetCredentialsDeleteHandlerTest.java @@ -39,11 +39,13 @@ import java.io.IOException; import java.util.Map; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.configuration.CredentialsManager; import io.cryostat.core.log.Logger; 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; @@ -98,6 +100,13 @@ void shouldHaveTargetsPath() { handler.path(), Matchers.equalTo("/api/v2/targets/:targetId/credentials")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.DELETE_CREDENTIALS))); + } + @Test void shouldHaveJsonMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT)); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostHandlerTest.java index 807043460c..7a80dc67d4 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetCredentialsPostHandlerTest.java @@ -39,12 +39,14 @@ import java.io.IOException; import java.util.Map; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.configuration.CredentialsManager; import io.cryostat.core.log.Logger; import io.cryostat.core.net.Credentials; 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; @@ -96,6 +98,13 @@ void shouldHaveExpectedPath() { handler.path(), Matchers.equalTo("/api/v2/targets/:targetId/credentials")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), + Matchers.equalTo(Set.of(ResourceAction.CREATE_CREDENTIALS))); + } + @Test void shouldReturnPlaintextMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.PLAINTEXT)); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetDeleteHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetDeleteHandlerTest.java index e551055ce7..65c09ba8d4 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetDeleteHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetDeleteHandlerTest.java @@ -42,10 +42,12 @@ import java.net.URISyntaxException; import java.util.HashMap; import java.util.Map; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; 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.platform.internal.CustomTargetPlatformClient; @@ -100,6 +102,12 @@ void shouldHaveTargetsPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v2/targets/:targetId")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.DELETE_TARGET))); + } + @Test void shouldHaveJsonMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.JSON)); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandlerTest.java index 662ede1d68..5020d68a3d 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetEventsGetHandlerTest.java @@ -57,6 +57,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import com.google.gson.Gson; import io.vertx.core.MultiMap; @@ -97,6 +98,12 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v2/targets/:targetId/events")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_TARGET))); + } + @Test void shouldHandleNoMatches() throws Exception { when(targetConnectionManager.executeConnectedTask( diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandlerTest.java index cb1b85621c..6faf555e83 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetRecordingOptionsListGetHandlerTest.java @@ -44,6 +44,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.openjdk.jmc.common.unit.IOptionDescriptor; @@ -55,6 +56,7 @@ import io.cryostat.net.AuthManager; import io.cryostat.net.ConnectionDescriptor; import io.cryostat.net.TargetConnectionManager; +import io.cryostat.net.security.ResourceAction; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; @@ -101,6 +103,12 @@ void shouldHandleCorrectPath() { handler.path(), Matchers.equalTo("/api/v2/targets/:targetId/recordingOptionsList")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.READ_TARGET))); + } + @Test void shouldRespondWithRecordingOptionsList() throws Exception { IOptionDescriptor descriptor = mock(IOptionDescriptor.class); @@ -127,7 +135,7 @@ void shouldRespondWithRecordingOptionsList() throws Exception { Mockito.when(ctx.request()).thenReturn(req); Mockito.when(req.headers()).thenReturn(MultiMap.caseInsensitiveMultiMap()); - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); try { diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetSnapshotPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetSnapshotPostHandlerTest.java index 2fd70083ff..b61e788235 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetSnapshotPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetSnapshotPostHandlerTest.java @@ -39,6 +39,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import org.openjdk.jmc.common.unit.IConstrainedMap; @@ -54,6 +55,7 @@ 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.WebServer; import io.cryostat.recordings.RecordingOptionsBuilderFactory; @@ -98,9 +100,17 @@ void setup() { gson); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + snapshot.resourceActions(), + Matchers.equalTo( + Set.of(ResourceAction.READ_TARGET, ResourceAction.UPDATE_RECORDING))); + } + @Test void shouldCreateSnapshot() throws Exception { - Mockito.when(auth.validateHttpHeader(Mockito.any())) + Mockito.when(auth.validateHttpHeader(Mockito.any(), Mockito.any())) .thenReturn(CompletableFuture.completedFuture(true)); RoutingContext ctx = Mockito.mock(RoutingContext.class); diff --git a/src/test/java/io/cryostat/net/web/http/api/v2/TargetsPostHandlerTest.java b/src/test/java/io/cryostat/net/web/http/api/v2/TargetsPostHandlerTest.java index cc63a05d82..4032db25a8 100644 --- a/src/test/java/io/cryostat/net/web/http/api/v2/TargetsPostHandlerTest.java +++ b/src/test/java/io/cryostat/net/web/http/api/v2/TargetsPostHandlerTest.java @@ -42,10 +42,12 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import io.cryostat.MainModule; import io.cryostat.core.log.Logger; 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.platform.PlatformClient; @@ -106,6 +108,12 @@ void shouldHaveTargetsPath() { MatcherAssert.assertThat(handler.path(), Matchers.equalTo("/api/v2/targets")); } + @Test + void shouldHaveExpectedRequiredPermissions() { + MatcherAssert.assertThat( + handler.resourceActions(), Matchers.equalTo(Set.of(ResourceAction.CREATE_TARGET))); + } + @Test void shouldHaveJsonMimeType() { MatcherAssert.assertThat(handler.mimeType(), Matchers.equalTo(HttpMimeType.JSON));