From d816b83cbb267dedf8a520a33ee027cd77fb5666 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Tue, 12 Sep 2023 10:18:57 +0200 Subject: [PATCH] Apply HTTP Security Policy on normalized path - Use normalized path to check Keycloak authorization policies - Use normalised path for Servlet - Use normalized path to check CSRF token path constraint Co-authored-by: Michal Vavrik Co-authored-by: Stuart Douglas Co-authored-by: Sergey Beryozkin Co-authored-by: Clement Escoffier --- .../CsrfRequestResponseReactiveFilter.java | 3 +- .../KeycloakPolicyEnforcerAuthorizer.java | 3 +- .../keycloak/pep/runtime/VertxHttpFacade.java | 3 +- ...JakartaRestResourceHttpPermissionTest.java | 230 +++++++++++++ ...JakartaRestResourceHttpPermissionTest.java | 224 +++++++++++++ .../runtime/ServletHttpSecurityPolicy.java | 2 +- .../runtime/UndertowDeploymentRecorder.java | 2 + .../PathMatchingHttpSecurityPolicyTest.java | 187 +++++++++++ ...bstractPathMatchingHttpSecurityPolicy.java | 13 +- integration-tests/csrf-reactive/pom.xml | 10 + .../io/quarkus/it/csrf/CsrfReactiveTest.java | 2 +- .../it/undertow/elytron/OpenApiServlet.java | 33 ++ ...SecurityAnnotationPermissionsTestCase.java | 5 + .../elytron/WebXmlPermissionsTestCase.java | 10 + .../keycloak-authorization/pom.xml | 5 + .../it/keycloak/PolicyEnforcerTest.java | 313 +++++++++--------- 16 files changed, 882 insertions(+), 163 deletions(-) create mode 100644 extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java create mode 100644 integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/OpenApiServlet.java diff --git a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java index 4c5eaa1517719..cb04191fb040d 100644 --- a/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java +++ b/extensions/csrf-reactive/runtime/src/main/java/io/quarkus/csrf/reactive/runtime/CsrfRequestResponseReactiveFilter.java @@ -227,7 +227,8 @@ private String getCookieToken(RoutingContext routing, CsrfReactiveConfig config) } private boolean isCsrfTokenRequired(RoutingContext routing, CsrfReactiveConfig config) { - return config.createTokenPath.isPresent() ? config.createTokenPath.get().contains(routing.request().path()) : true; + return config.createTokenPath + .map(value -> value.contains(routing.normalizedPath())).orElse(true); } private void createCookie(String csrfToken, RoutingContext routing, CsrfReactiveConfig config) { diff --git a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java index abd5fa3db6d43..fa7831fb6e266 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/runtime/KeycloakPolicyEnforcerAuthorizer.java @@ -44,7 +44,8 @@ public Uni checkPermission(RoutingContext request, Uni jar + .addClasses(TestIdentityProvider.class, TestIdentityController.class, ApiResource.class, + RootResource.class, PublicResource.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + @TestHTTPResource + URL url; + + @Inject + Vertx vertx; + + @ParameterizedTest + @ValueSource(strings = { + // path without wildcard, with leading slashes in both policy and @Path + "/api/foo/", "/api/foo", + // path with wildcard, without leading slashes in both policy and @Path + "/api/bar", "/api/bar/", "/api/bar/irish", + // combination of permit and authenticated policies, paths are resolved to /api/baz/fum/ and auth required + "/api/baz/fum/" + }) + public void testEmptyPathSegments(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, getLastNonEmptySegmentContent(path)); + } + + @ParameterizedTest + @ValueSource(strings = { "/", "///", "/?stuff", "" }) + public void testRootPath(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { "/one/", "/three?stuff" }) + public void testNotSecuredPaths(String path) { + // negative testing - all paths are public unless auth policy is applied + assurePath(path, 200); + } + + @ParameterizedTest + @ValueSource(strings = { "/api/foo///", "////api/foo", "/api//foo", "/api/bar///irish", "/api/bar///irish/", + "/api//baz/fum//", + "/api///foo", "////api/bar", "/api///bar", "/api//bar" }) + public void testSecuredNotFound(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, 404); + } + + private static String getLastNonEmptySegmentContent(String path) { + while (path.endsWith("/") || path.endsWith(".")) { + path = path.substring(0, path.length() - 1); + } + return path.substring(path.lastIndexOf('/') + 1); + } + + @Path("/api") + public static class ApiResource { + + @GET + @Path("/foo") + public String foo() { + return "foo"; + } + + @GET + @Path("/bar") + public String bar() { + return "bar"; + } + + @GET + @Path("/bar/irish") + public String irishBar() { + return "irish"; + } + + @GET + @Path("/baz/fum") + public String bazFum() { + return "fum"; + } + + } + + @Path("/") + public static class RootResource { + + @GET + public String get() { + return "root"; + } + + } + + @Path("/") + public static class PublicResource { + + @Path("one") + @GET + public String one() { + return "one"; + } + + @Path("/two") + @GET + public String two() { + return "two"; + } + + @Path("/three") + @GET + public String three() { + return "three"; + } + + @Path("four") + @GET + public String four() { + return "four"; + } + + @Path("/four#stuff") + @GET + public String fourWitFragment() { + return "four#stuff"; + } + + @Path("five") + @GET + public String five() { + return "five"; + } + + } + + private void assurePath(String path, int expectedStatusCode) { + assurePath(path, expectedStatusCode, null, false); + } + + private void assurePathAuthenticated(String path) { + assurePath(path, 200, null, true); + } + + private void assurePathAuthenticated(String path, int statusCode) { + assurePath(path, statusCode, null, true); + } + + private void assurePathAuthenticated(String path, String body) { + assurePath(path, 200, body, true); + } + + private void assurePath(String path, int expectedStatusCode, String body, boolean auth) { + var httpClient = vertx.createHttpClient(); + try { + httpClient + .request(HttpMethod.GET, url.getPort(), url.getHost(), path) + .map(r -> { + if (auth) { + r.putHeader("Authorization", "Basic " + encodeBase64URLSafeString("test:test".getBytes())); + } + return r; + }) + .flatMap(HttpClientRequest::send) + .invoke(r -> assertEquals(expectedStatusCode, r.statusCode(), path)) + .flatMap(r -> { + if (body != null) { + return r.body().invoke(b -> assertEquals(b.toString(), body, path)); + } else { + return Uni.createFrom().nullItem(); + } + }) + .await() + .atMost(REQUEST_TIMEOUT); + } finally { + httpClient + .close() + .await() + .atMost(REQUEST_TIMEOUT); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java new file mode 100644 index 0000000000000..ee706eae57662 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/security/JakartaRestResourceHttpPermissionTest.java @@ -0,0 +1,224 @@ +package io.quarkus.resteasy.reactive.server.test.security; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.time.Duration; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.vertx.core.Vertx; +import io.vertx.ext.web.client.WebClient; + +public class JakartaRestResourceHttpPermissionTest { + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20); + private static final String APP_PROPS = "quarkus.http.auth.permission.foo.paths=/api/foo,/api/foo/\n" + + "quarkus.http.auth.permission.foo.policy=authenticated\n" + + "quarkus.http.auth.permission.bar.paths=api/bar*\n" + + "quarkus.http.auth.permission.bar.policy=authenticated\n" + + "quarkus.http.auth.permission.baz-fum-pub.paths=/api/baz/fum\n" + + "quarkus.http.auth.permission.baz-fum-pub.policy=permit\n" + + "quarkus.http.auth.permission.baz-fum.paths=/api/baz/fum*\n" + + "quarkus.http.auth.permission.baz-fum.policy=authenticated\n" + + "quarkus.http.auth.permission.root.paths=/\n" + + "quarkus.http.auth.permission.root.policy=authenticated\n" + + "quarkus.http.auth.permission.fragment.paths=/#stuff,/#stuff/\n" + + "quarkus.http.auth.permission.fragment.policy=authenticated\n"; + private static WebClient client; + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TestIdentityProvider.class, TestIdentityController.class, ApiResource.class, + RootResource.class, PublicResource.class) + .addAsResource(new StringAsset(APP_PROPS), "application.properties")); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + @AfterAll + public static void cleanup() { + if (client != null) { + client.close(); + } + } + + @Inject + Vertx vertx; + + @TestHTTPResource + URL url; + + private WebClient getClient() { + if (client == null) { + client = WebClient.create(vertx); + } + return client; + } + + @ParameterizedTest + @ValueSource(strings = { + // path without wildcard, with leading slashes in both policy and @Path + "////api/foo", "/api/foo", "/api//foo", "/api//foo", "/api///foo", "/api/foo/", "/api/foo///", + "/api/foo///.", "/api/foo/./", + // path with wildcard, without leading slashes in both policy and @Path + "////api/bar", "/api///bar", "/api//bar", "/api/bar", "/api/bar/", "/api/bar/irish", + "/api/bar///irish", "/api/bar///irish/.", "/../api/bar///irish/.", + // combination of permit and authenticated policies, paths are resolved to /api/baz/fum/ and auth required + "/api/baz/fum/", "/api//baz/fum//", "/api//baz/fum/." + }) + public void testEmptyPathSegments(String path) { + assurePath(path, 401); + + assurePathAuthenticated(path, getLastNonEmptySegmentContent(path)); + } + + @ParameterizedTest + @ValueSource(strings = { "/", "///", "/?stuff", "/#stuff/", "" }) + public void testRootPath(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { "/one/", "///two", "/three?stuff", "/four#stuff", "/.////five" }) + public void testNotSecuredPaths(String path) { + // negative testing - all paths are public unless auth policy is applied + assurePathAuthenticated(path); + } + + private static String getLastNonEmptySegmentContent(String path) { + while (path.endsWith("/") || path.endsWith(".")) { + path = path.substring(0, path.length() - 1); + } + return path.substring(path.lastIndexOf('/') + 1); + } + + @Path("/api") + public static class ApiResource { + + @GET + @Path("/foo") + public String foo() { + return "foo"; + } + + @GET + @Path("bar") + public String bar() { + return "bar"; + } + + @GET + @Path("bar/irish") + public String irishBar() { + return "irish"; + } + + @GET + @Path("baz/fum") + public String bazFum() { + return "fum"; + } + + } + + @Path("/") + public static class RootResource { + + @GET + public String get() { + return "root"; + } + + @Path("#stuff") + @GET + public String fragment() { + return "#stuff"; + } + + } + + @Path("/") + public static class PublicResource { + + @Path("one") + @GET + public String one() { + return "one"; + } + + @Path("/two") + @GET + public String two() { + return "two"; + } + + @Path("/three") + @GET + public String three() { + return "three"; + } + + @Path("four") + @GET + public String four() { + return "four"; + } + + @Path("four#stuff") + @GET + public String fourFragment() { + return "four#stuff"; + } + + @Path("five") + @GET + public String five() { + return "five"; + } + + } + + private void assurePath(String path, int expectedStatusCode) { + assurePath(path, expectedStatusCode, null, false); + } + + private void assurePathAuthenticated(String path) { + assurePath(path, 200, null, true); + } + + private void assurePathAuthenticated(String path, String body) { + assurePath(path, 200, body, true); + } + + private void assurePath(String path, int expectedStatusCode, String body, boolean auth) { + var req = getClient().get(url.getPort(), url.getHost(), path); + if (auth) { + req.basicAuthentication("test", "test"); + } + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (body != null) { + Assertions.assertTrue(result.result().bodyAsString().contains(body), path); + } + } +} diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java index 1e47c7ceb7859..747314b32c23e 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java @@ -25,7 +25,7 @@ public class ServletHttpSecurityPolicy implements HttpSecurityPolicy { public Uni checkPermission(RoutingContext request, Uni identity, AuthorizationRequestContext requestContext) { - String requestPath = request.request().path(); + String requestPath = request.normalizedPath(); if (!requestPath.startsWith(contextPath)) { //anything outside the context path we don't have anything to do with return Uni.createFrom().item(CheckResult.PERMIT); diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index b2c0692265d0d..1e02df7acaf31 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -67,6 +67,7 @@ import io.undertow.server.HandlerWrapper; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.CanonicalPathHandler; import io.undertow.server.handlers.PathHandler; import io.undertow.server.handlers.ResponseCodeHandler; import io.undertow.server.handlers.resource.CachingResourceManager; @@ -374,6 +375,7 @@ public void run() { .addPrefixPath(manager.getDeployment().getDeploymentInfo().getContextPath(), main); main = pathHandler; } + main = new CanonicalPathHandler(main); currentRoot = main; DefaultExchangeHandler defaultHandler = new DefaultExchangeHandler(ROOT_HANDLER); diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java new file mode 100644 index 0000000000000..08679d345bcaa --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/security/PathMatchingHttpSecurityPolicyTest.java @@ -0,0 +1,187 @@ +package io.quarkus.vertx.http.security; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URL; +import java.time.Duration; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.security.test.utils.TestIdentityController; +import io.quarkus.security.test.utils.TestIdentityProvider; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.vertx.core.Vertx; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.client.WebClient; + +public class PathMatchingHttpSecurityPolicyTest { + + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(20); + private static final String APP_PROPS = "quarkus.http.auth.permission.authenticated.paths=/\n" + + "quarkus.http.auth.permission.authenticated.policy=authenticated\n" + + "quarkus.http.auth.permission.public.paths=/api*\n" + + "quarkus.http.auth.permission.public.policy=permit\n" + + "quarkus.http.auth.permission.foo.paths=/api/foo/bar\n" + + "quarkus.http.auth.permission.foo.policy=authenticated\n" + + "quarkus.http.auth.permission.baz.paths=/api/baz\n" + + "quarkus.http.auth.permission.baz.policy=authenticated\n" + + "quarkus.http.auth.permission.static-resource.paths=/static-file.html\n" + + "quarkus.http.auth.permission.static-resource.policy=authenticated\n" + + "quarkus.http.auth.permission.fubar.paths=/api/fubar/baz*\n" + + "quarkus.http.auth.permission.fubar.policy=authenticated\n" + + "quarkus.http.auth.permission.management.paths=/q/*\n" + + "quarkus.http.auth.permission.management.policy=authenticated\n"; + private static WebClient client; + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest().setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(TestIdentityController.class, TestIdentityProvider.class, PathHandler.class, + RouteHandler.class) + .addAsResource("static-file.html", "META-INF/resources/static-file.html") + .addAsResource(new StringAsset(APP_PROPS), "application.properties")).setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-smallrye-health", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-smallrye-openapi", Version.getVersion()))); + + @BeforeAll + public static void setup() { + TestIdentityController.resetRoles().add("test", "test", "test"); + } + + @AfterAll + public static void cleanup() { + if (client != null) { + client.close(); + } + } + + @Inject + Vertx vertx; + + @TestHTTPResource + URL url; + + private WebClient getClient() { + if (client == null) { + client = WebClient.create(vertx); + } + return client; + } + + @ParameterizedTest + @ValueSource(strings = { + // path policy without wildcard + "/api/foo//bar", "/api/foo///bar", "/api/foo////bar", "/api/foo/////bar", "//api/foo/bar", "///api/foo/bar", + "////api/foo/bar", "//api//foo//bar", "//api/foo//bar", + // path policy with wildcard + "/api/fubar/baz", "/api/fubar/baz/", "/api/fubar/baz//", "/api/fubar/baz/.", "/api/fubar/baz////.", + "/api/fubar/baz/bar", + // routes defined for exact paths + "/api/baz", "//api/baz", "///api////baz", "/api//baz", + // zero length path + "", "/?one=two", + // empty segments only are match with path policy for '/' + "/", "///", "////", "/////" + }) + public void testEmptyPathSegments(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { + "/api/foo/./bar", "/../api/foo///bar", "/api/./foo/.///bar", "/api/foo/./////bar", "/api/fubar/baz/.", + "/..///api/foo/bar", "////../../api/foo/bar", "/./api//foo//bar", "//api/foo/./bar", + "/.", "/..", "/./", "/..//", "/.///", "/..////", "/./////" + }) + public void testDotPathSegments(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { + "/static-file.html", "//static-file.html", "///static-file.html" + }) + public void testStaticResource(String path) { + assurePath(path, 401); + assurePathAuthenticated(path); + } + + @ParameterizedTest + @ValueSource(strings = { + "///q/openapi", "/q///openapi", "/q/openapi/", "/q/openapi///" + }) + public void testOpenApiPath(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, "openapi"); + } + + @ParameterizedTest + @ValueSource(strings = { + "/q/health", "/q/health/live", "/q/health/ready", "//q/health", "///q/health", "///q///health", + "/q/health/", "/q///health/", "/q///health////live" + }) + public void testHealthCheckPaths(String path) { + assurePath(path, 401); + assurePathAuthenticated(path, "UP"); + } + + @Test + public void testMiscellaneousPaths() { + // /api/baz with segment indicating version shouldn't match /api/baz path policy + assurePath("/api/baz;v=1.1", 200); + // /api/baz/ is different resource than secured /api/baz, therefore request should succeed + assurePath("/api/baz/", 200); + } + + @ApplicationScoped + public static class RouteHandler { + public void setup(@Observes Router router) { + router.route("/api/baz").order(-1).handler(rc -> rc.response().end("/api/baz response")); + } + } + + private void assurePath(String path, int expectedStatusCode) { + assurePath(path, expectedStatusCode, null, false); + } + + private void assurePathAuthenticated(String path) { + assurePath(path, 200, null, true); + } + + private void assurePathAuthenticated(String path, String body) { + assurePath(path, 200, body, true); + } + + private void assurePath(String path, int expectedStatusCode, String body, boolean auth) { + var req = getClient().get(url.getPort(), url.getHost(), path); + if (auth) { + req.basicAuthentication("test", "test"); + } + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (body != null) { + Assertions.assertTrue(result.result().bodyAsString().contains(body), path); + } + } + +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java index 185387e80061a..14aa4d607cd29 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AbstractPathMatchingHttpSecurityPolicy.java @@ -15,12 +15,11 @@ import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.AuthorizationRequestContext; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy.CheckResult; import io.smallrye.mutiny.Uni; -import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.web.RoutingContext; /** * A security policy that allows for matching of other security policies based on paths. - * + *

* This is used for the default path/method based RBAC. */ public class AbstractPathMatchingHttpSecurityPolicy { @@ -28,7 +27,7 @@ public class AbstractPathMatchingHttpSecurityPolicy { private final PathMatcher> pathMatcher = new PathMatcher<>(); public String getAuthMechanismName(RoutingContext routingContext) { - PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.request().path()); + PathMatcher.PathMatch> toCheck = pathMatcher.match(routingContext.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { return null; } @@ -42,7 +41,7 @@ public String getAuthMechanismName(RoutingContext routingContext) { public Uni checkPermission(RoutingContext routingContext, Uni identity, AuthorizationRequestContext requestContext) { - List permissionCheckers = findPermissionCheckers(routingContext.request()); + List permissionCheckers = findPermissionCheckers(routingContext); return doPermissionCheck(routingContext, identity, 0, null, permissionCheckers, requestContext); } @@ -126,8 +125,8 @@ public void init(Map permissions, } } - public List findPermissionCheckers(HttpServerRequest request) { - PathMatcher.PathMatch> toCheck = pathMatcher.match(request.path()); + public List findPermissionCheckers(RoutingContext context) { + PathMatcher.PathMatch> toCheck = pathMatcher.match(context.normalizedPath()); if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) { return Collections.emptyList(); } @@ -136,7 +135,7 @@ public List findPermissionCheckers(HttpServerRequest request for (HttpMatcher i : toCheck.getValue()) { if (i.methods == null || i.methods.isEmpty()) { noMethod.add(i.checker); - } else if (i.methods.contains(request.method().toString())) { + } else if (i.methods.contains(context.request().method().toString())) { methodMatch.add(i.checker); } } diff --git a/integration-tests/csrf-reactive/pom.xml b/integration-tests/csrf-reactive/pom.xml index 1e9a7921b18c5..0f561b6a6428d 100644 --- a/integration-tests/csrf-reactive/pom.xml +++ b/integration-tests/csrf-reactive/pom.xml @@ -28,6 +28,16 @@ quarkus-junit5 test + + org.awaitility + awaitility + test + + + io.smallrye.reactive + smallrye-mutiny-vertx-web-client + test + net.sourceforge.htmlunit htmlunit diff --git a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java index 69f4e06ab7573..29fc9bbda0946 100644 --- a/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java +++ b/integration-tests/csrf-reactive/src/test/java/io/quarkus/it/csrf/CsrfReactiveTest.java @@ -218,4 +218,4 @@ private WebClient createWebClient() { private String basicAuth(String user, String password) { return "Basic " + Base64.getEncoder().encodeToString((user + ":" + password).getBytes()); } -} +} \ No newline at end of file diff --git a/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/OpenApiServlet.java b/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/OpenApiServlet.java new file mode 100644 index 0000000000000..0b77b18c46fd7 --- /dev/null +++ b/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/OpenApiServlet.java @@ -0,0 +1,33 @@ +package io.quarkus.it.undertow.elytron; + +import java.io.IOException; + +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet(name = "ServletGreeting", urlPatterns = "/openapi/*") +public class OpenApiServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (req.getUserPrincipal().getName() == null) { + throw new RuntimeException("principal was null"); + } + resp.setStatus(200); + resp.addHeader("Content-Type", "text/plain"); + resp.getWriter().write("hello"); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + if (req.getUserPrincipal().getName() == null) { + throw new RuntimeException("principal was null"); + } + String name = req.getReader().readLine(); + resp.setStatus(200); + resp.addHeader("Content-Type", "text/plain"); + resp.getWriter().write("hello " + name); + } +} diff --git a/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/ServletSecurityAnnotationPermissionsTestCase.java b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/ServletSecurityAnnotationPermissionsTestCase.java index 9da4fd39aa7a9..9cd6454f3ad25 100644 --- a/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/ServletSecurityAnnotationPermissionsTestCase.java +++ b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/ServletSecurityAnnotationPermissionsTestCase.java @@ -26,6 +26,11 @@ void testSecuredServletWithNoAuth() { .get("/foo/annotation-secure") .then() .statusCode(401); + given() + .when() + .get("/foo/bar/../annotation-secure") + .then() + .statusCode(401); } @Test diff --git a/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java index 34825a00a2127..9ee6d2c6dd06f 100644 --- a/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java +++ b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java @@ -34,6 +34,16 @@ void testOpenApiNoPermissions() { .get("/foo/openapi") .then() .statusCode(401); + given() + .when() + .get("/r/../foo/openapi") + .then() + .statusCode(401); + given() + .when() + .get("/foo/bar/../openapi") + .then() + .statusCode(401); } @Test diff --git a/integration-tests/keycloak-authorization/pom.xml b/integration-tests/keycloak-authorization/pom.xml index e82cb7d5e5d65..54316c44f9665 100644 --- a/integration-tests/keycloak-authorization/pom.xml +++ b/integration-tests/keycloak-authorization/pom.xml @@ -106,6 +106,11 @@ + + org.awaitility + awaitility + test + net.sourceforge.htmlunit htmlunit diff --git a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java index cdf127a6f5e1f..880b9299d6ac5 100644 --- a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java +++ b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/PolicyEnforcerTest.java @@ -1,24 +1,31 @@ package io.quarkus.it.keycloak; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import org.hamcrest.Matchers; +import java.net.URL; +import java.time.Duration; + +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.keycloak.representations.AccessTokenResponse; import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException; import com.gargoylesoftware.htmlunit.SilentCssErrorHandler; -import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.html.HtmlForm; import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.util.Cookie; import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; -import io.restassured.http.ContentType; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClient; /** * @author Pedro Igor @@ -26,24 +33,37 @@ @QuarkusTest @QuarkusTestResource(KeycloakLifecycleManager.class) public class PolicyEnforcerTest { - + private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(10); private static final String KEYCLOAK_REALM = "quarkus"; + @TestHTTPResource + URL url; + + static Vertx vertx = Vertx.vertx(); + static WebClient client = WebClient.create(vertx); + + @AfterAll + public static void closeVertxClient() { + if (client != null) { + client.close(); + client = null; + } + if (vertx != null) { + vertx.close().toCompletionStage().toCompletableFuture().join(); + vertx = null; + } + } + @Test public void testUserHasAdminRoleServiceTenant() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api-permission-tenant") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api-permission-tenant") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("admin")) - .when().get("/api-permission-tenant") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Permission Resource Tenant")); + assureGetPath("/api-permission-tenant", 403, getAccessToken("alice"), null); + assureGetPath("//api-permission-tenant", 403, getAccessToken("alice"), null); + + assureGetPath("/api-permission-tenant", 403, getAccessToken("jdoe"), null); + assureGetPath("//api-permission-tenant", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api-permission-tenant", 200, getAccessToken("admin"), "Permission Resource Tenant"); + assureGetPath("//api-permission-tenant", 200, getAccessToken("admin"), "Permission Resource Tenant"); } @Test @@ -54,7 +74,7 @@ public void testUserHasSuperUserRoleWebTenant() throws Exception { } private void testWebAppTenantAllowed(String user) throws Exception { - try (final WebClient webClient = createWebClient()) { + try (final com.gargoylesoftware.htmlunit.WebClient webClient = createWebClient()) { HtmlPage page = webClient.getPage("http://localhost:8081/api-permission-webapp"); assertEquals("Sign in to quarkus", page.getTitleText()); @@ -66,12 +86,18 @@ private void testWebAppTenantAllowed(String user) throws Exception { WebResponse response = loginForm.getInputByName("login").click().getWebResponse(); assertEquals(200, response.getStatusCode()); assertTrue(response.getContentAsString().contains("Permission Resource WebApp")); + + // Token is encrypted in the cookie + Cookie cookie = webClient.getCookieManager().getCookie("q_session_api-permission-webapp"); + assureGetPathWithCookie("/api-permission-webapp", cookie, 200, null, "Permission Resource WebApp"); + assureGetPathWithCookie("//api-permission-webapp", cookie, 200, null, "Permission Resource WebApp"); + webClient.getCookieManager().clearCookies(); } } private void testWebAppTenantForbidden(String user) throws Exception { - try (final WebClient webClient = createWebClient()) { + try (final com.gargoylesoftware.htmlunit.WebClient webClient = createWebClient()) { HtmlPage page = webClient.getPage("http://localhost:8081/api-permission-webapp"); assertEquals("Sign in to quarkus", page.getTitleText()); @@ -86,186 +112,130 @@ private void testWebAppTenantForbidden(String user) throws Exception { } catch (FailingHttpStatusCodeException ex) { assertEquals(403, ex.getStatusCode()); } + + // Token is encrypted in the cookie + Cookie cookie = webClient.getCookieManager().getCookie("q_session_api-permission-webapp"); + assureGetPathWithCookie("/api-permission-webapp", cookie, 403, null, null); + assureGetPathWithCookie("//api-permission-webapp", cookie, 403, null, null); + webClient.getCookieManager().clearCookies(); } } - private WebClient createWebClient() { - WebClient webClient = new WebClient(); + private com.gargoylesoftware.htmlunit.WebClient createWebClient() { + com.gargoylesoftware.htmlunit.WebClient webClient = new com.gargoylesoftware.htmlunit.WebClient(); webClient.setCssErrorHandler(new SilentCssErrorHandler()); return webClient; } @Test public void testUserHasRoleConfidential() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Permission Resource")); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scope?scope=write") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/annotation/scope-write") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scope?scope=read") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/annotation/scope-read") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); - - RestAssured.given().auth().oauth2(getAccessToken("admin")) - .when().get("/api/permission") - .then() - .statusCode(403); - - RestAssured.given().auth().oauth2(getAccessToken("admin")) - .when().get("/api/permission/entitlements") - .then() - .statusCode(200); + assureGetPath("/api/permission", 403, getAccessToken("alice"), null); + assureGetPath("//api/permission", 403, getAccessToken("alice"), null); + + assureGetPath("/api/permission", 200, getAccessToken("jdoe"), "Permission Resource"); + assureGetPath("//api/permission", 200, getAccessToken("jdoe"), "Permission Resource"); + + assureGetPath("/api/permission/scope?scope=write", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/scope?scope=write", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api/permission/annotation/scope-write", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/annotation/scope-write", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api/permission/scope?scope=read", 200, getAccessToken("jdoe"), "read"); + assureGetPath("//api/permission/scope?scope=read", 200, getAccessToken("jdoe"), "read"); + + assureGetPath("/api/permission", 403, getAccessToken("admin"), null); + assureGetPath("//api/permission", 403, getAccessToken("admin"), null); + + assureGetPath("/api/permission/entitlements", 200, getAccessToken("admin"), null); + assureGetPath("//api/permission/entitlements", 200, getAccessToken("admin"), null); } @Test public void testRequestParameterAsClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected?grant=true") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Claim Protected Resource")); - ; - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected?grant=false") - .then() - .statusCode(403); - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/claim-protected") - .then() - .statusCode(403); + assureGetPath("/api/permission/claim-protected?grant=true", 200, getAccessToken("alice"), + "Claim Protected Resource"); + assureGetPath("//api/permission/claim-protected?grant=true", 200, getAccessToken("alice"), + "Claim Protected Resource"); + + assureGetPath("/api/permission/claim-protected?grant=false", 403, getAccessToken("alice"), null); + assureGetPath("//api/permission/claim-protected?grant=false", 403, getAccessToken("alice"), null); + + assureGetPath("/api/permission/claim-protected", 403, getAccessToken("alice"), null); + assureGetPath("//api/permission/claim-protected", 403, getAccessToken("alice"), null); } @Test public void testHttpResponseFromExternalServiceAsClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/permission/http-response-claim-protected") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Http Response Claim Protected Resource")); - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/http-response-claim-protected") - .then() - .statusCode(403); + assureGetPath("/api/permission/http-response-claim-protected", 200, getAccessToken("alice"), null); + assureGetPath("//api/permission/http-response-claim-protected", 200, getAccessToken("alice"), null); + + assureGetPath("/api/permission/http-response-claim-protected", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/http-response-claim-protected", 403, getAccessToken("jdoe"), null); } @Test public void testBodyClaim() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .contentType(ContentType.JSON) - .body("{\"from-body\": \"grant\"}") - .when() - .post("/api/permission/body-claim") - .then() - .statusCode(200) - .and().body(Matchers.containsString("Body Claim Protected Resource")); + assurePostPath("/api/permission/body-claim", "{\"from-body\": \"grant\"}", 200, getAccessToken("alice"), + "Body Claim Protected Resource"); } @Test public void testPublicResource() { - RestAssured.given() - .when().get("/api/public") - .then() - .statusCode(204); + assureGetPath("/api/public", 204, null, null); } @Test public void testPublicResourceWithEnforcingPolicy() { - RestAssured.given() - .when().get("/api/public-enforcing") - .then() - .statusCode(401); + assureGetPath("/api/public-enforcing", 401, null, null); + assureGetPath("//api/public-enforcing", 401, null, null); } @Test public void testPublicResourceWithEnforcingPolicyAndToken() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/public-enforcing") - .then() - .statusCode(403); + assureGetPath("/api/public-enforcing", 403, getAccessToken("alice"), null); + assureGetPath("//api/public-enforcing", 403, getAccessToken("alice"), null); } @Test public void testPublicResourceWithDisabledPolicyAndToken() { - RestAssured.given().auth().oauth2(getAccessToken("alice")) - .when().get("/api/public-token") - .then() - .statusCode(204); + assureGetPath("/api/public-token", 204, getAccessToken("alice"), null); } @Test public void testPathConfigurationPrecedenceWhenPathCacheNotDefined() { - RestAssured.given() - .when().get("/api2/resource") - .then() - .statusCode(401); - - RestAssured.given() - .when().get("/hello") - .then() - .statusCode(404); - - RestAssured.given() - .when().get("/") - .then() - .statusCode(404); + assureGetPath("/api2/resource", 401, null, null); + assureGetPath("//api2/resource", 401, null, null); + + assureGetPath("/hello", 404, null, null); + assureGetPath("//hello", 404, null, null); + + assureGetPath("/", 404, null, null); + assureGetPath("//", 400, null, null); } @Test public void testPermissionScopes() { // 'jdoe' has scope 'read' and 'read' is required - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/standard-way") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); + assureGetPath("/api/permission/scopes/standard-way", 200, getAccessToken("jdoe"), "read"); + assureGetPath("//api/permission/scopes/standard-way", 200, getAccessToken("jdoe"), "read"); // 'jdoe' has scope 'read' while 'write' is required - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/standard-way-denied") - .then() - .statusCode(403); - - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/programmatic-way") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); - - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/programmatic-way-denied") - .then() - .statusCode(403); - - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/annotation-way") - .then() - .statusCode(200) - .and().body(Matchers.containsString("read")); - - RestAssured.given().auth().oauth2(getAccessToken("jdoe")) - .when().get("/api/permission/scopes/annotation-way-denied") - .then() - .statusCode(403); + assureGetPath("/api/permission/scopes/standard-way-denied", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/scopes/standard-way-denied", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api/permission/scopes/programmatic-way", 200, getAccessToken("jdoe"), "read"); + assureGetPath("//api/permission/scopes/programmatic-way", 200, getAccessToken("jdoe"), "read"); + + assureGetPath("/api/permission/scopes/programmatic-way-denied", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/scopes/programmatic-way-denied", 403, getAccessToken("jdoe"), null); + + assureGetPath("/api/permission/scopes/annotation-way", 200, getAccessToken("jdoe"), "read"); + assureGetPath("//api/permission/scopes/annotation-way", 200, getAccessToken("jdoe"), "read"); + + assureGetPath("/api/permission/scopes/annotation-way-denied", 403, getAccessToken("jdoe"), null); + assureGetPath("//api/permission/scopes/annotation-way-denied", 403, getAccessToken("jdoe"), null); } protected String getAccessToken(String userName) { @@ -281,4 +251,47 @@ protected String getAccessToken(String userName) { + "/protocol/openid-connect/token") .as(AccessTokenResponse.class).getToken(); } + + private void assureGetPath(String path, int expectedStatusCode, String token, String body) { + var req = client.get(url.getPort(), url.getHost(), path); + if (token != null) { + req.bearerTokenAuthentication(token); + } + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (body != null) { + assertTrue(result.result().bodyAsString().contains(body), path); + } + } + + private void assureGetPathWithCookie(String path, Cookie cookie, int expectedStatusCode, String token, String body) { + var req = client.get(url.getPort(), url.getHost(), path); + if (token != null) { + req.bearerTokenAuthentication(token); + } + req.putHeader("Cookie", cookie.getName() + "=" + cookie.getValue()); + var result = req.send(); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (body != null) { + assertTrue(result.result().bodyAsString().contains(body), path); + } + } + + private void assurePostPath(String path, String requestBody, int expectedStatusCode, String token, + String responseBody) { + var req = client.post(url.getPort(), url.getHost(), path); + if (token != null) { + req.bearerTokenAuthentication(token); + } + req.putHeader("Content-Type", "application/json"); + var result = req.sendJson(new JsonObject(requestBody)); + await().atMost(REQUEST_TIMEOUT).until(result::isComplete); + assertEquals(expectedStatusCode, result.result().statusCode(), path); + if (responseBody != null) { + assertTrue(result.result().bodyAsString().contains(responseBody), path); + } + } + }