diff --git a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc index d0e4bb48dc2c5..723d3aaf27907 100644 --- a/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc +++ b/docs/src/main/asciidoc/security-authorize-web-endpoints-reference.adoc @@ -65,15 +65,41 @@ quarkus.http.auth.permission.roles1.policy=role-policy1 ---- <1> This permission references the default built-in `permit` policy to allow `GET` methods to `/public`. In this case, the demonstrated setting would not affect this example because this request is allowed anyway. -<2> This permission references the built-in `deny` policy for `/forbidden`. +<2> This permission references the built-in `deny` policy for both `/forbidden` and `/forbidden/` paths. It is an exact path match because it does not end with `*`. <3> This permission set references the previously defined policy. `roles1` is an example name; you can call the permission sets whatever you want. -[WARNING] +[IMPORTANT] ==== -The exact path `/forbidden` in the example will not secure the `/forbidden/` path. -It is necessary to add a new exact path for the `/forbidden/` path to ensure proper security coverage. +The exact path pattern `/forbidden` in the example above also secures the `/forbidden/` path. +This way, the `forbidden` endpoint in the example below is secured by the `deny1` permission. + +[source,java] +---- +package org.acme.crud; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +@Path("/forbidden") +public class ForbiddenResource { + @GET + public String forbidden() { <1> + return "No!"; + } +} +---- +<1> Both `/forbidden` and `/forbidden/` paths need to be secured in order to secure the `forbidden` endpoint. + +If you need to permit access to the `/forbidden/` path, please add new permission with more specific exact path like in the example below: + +[source,properties] +---- +quarkus.http.auth.permission.permit1.paths=/forbidden/ <1> +quarkus.http.auth.permission.permit1.policy=permit +---- +<1> The `/forbidden/` path is not secured. ==== [[custom-http-security-policy]] diff --git a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java index ed21f896dc164..a8324f72cfc2c 100644 --- a/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java +++ b/extensions/resteasy-classic/resteasy/deployment/src/test/java/io/quarkus/resteasy/test/security/JakartaRestResourceHttpPermissionTest.java @@ -33,6 +33,8 @@ public class JakartaRestResourceHttpPermissionTest { "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-deny.paths=/api/baz/fum/\n" + + "quarkus.http.auth.permission.baz-fum-deny.policy=authenticated\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" + 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 index ee706eae57662..220102abd6348 100644 --- 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 @@ -33,6 +33,8 @@ public class JakartaRestResourceHttpPermissionTest { "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-deny.paths=/api/baz/fum/\n" + + "quarkus.http.auth.permission.baz-fum-deny.policy=authenticated\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" + 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 index cf185c6c5a32b..1323e9ff3e2ff 100644 --- 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 @@ -203,8 +203,8 @@ public void testHealthCheckPaths(String path) { 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); + // /api/baz/ is different resource than secured /api/baz, but we secure both when there is not more specific exact path pattern + assurePath("/api/baz/", 401); } @Test diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java index 45a2fcea2dac1..25f3052d5bbcc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/ImmutablePathMatcher.java @@ -122,6 +122,14 @@ public static class ImmutablePathMatcherBuilder { private static final String STRING_PATH_SEPARATOR = "/"; private final Map exactPathMatches = new HashMap<>(); + /** + * Exact paths we proactively secure when more specify permissions are not specified. + * For example path for exact path '/api/hello' we add extra pattern for the '/api/hello/'. + * This helps to secure Jakarta REST endpoints by default as both paths may point to the same endpoint there. + * However, we only do that when user didn't declare any permission for the '/api/hello/'. + * This way, user can still forbid access to the `/api/hello' path and permit access to the '/api/hello/' path. + */ + private final Map additionalExactPathMatches = new HashMap<>(); private final Map> pathsWithWildcard = new HashMap<>(); private BiConsumer handlerAccumulator; @@ -188,6 +196,9 @@ public void accept(SubstringMatch match1, SubstringMatch match2) { paths.put(p.path, handler, subPathMatcher); } + for (var e : additionalExactPathMatches.entrySet()) { + exactPathMatches.putIfAbsent(e.getKey(), e.getValue()); + } int[] lengths = buildLengths(paths.keys()); return new ImmutablePathMatcher<>(defaultHandler, paths.asImmutableMap(), exactPathMatches, lengths, hasPathWithInnerWildcard); @@ -289,6 +300,20 @@ private void addExactPath(final String path, final T handler) { } else { exactPathMatches.put(path, handler); } + // when 'path.equals("/api/hello")' then the other path is '/api/hello/' + final String otherPath; + if (path.endsWith(STRING_PATH_SEPARATOR)) { + if (path.length() == 1) { + // path '/' is only valid option, '' is not allowed + return; + } + // drop path separator + otherPath = path.substring(0, path.length() - 1); + } else { + otherPath = path + STRING_PATH_SEPARATOR; + } + // if key is already present, then we have the right handler into which new ones have already been merged + additionalExactPathMatches.putIfAbsent(otherPath, handler); } private static int[] buildLengths(Iterable keys) { diff --git a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java index dfbfe2add67ed..5583cbf32dda7 100644 --- a/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java +++ b/extensions/vertx-http/runtime/src/test/java/io/quarkus/vertx/http/runtime/PathMatcherTest.java @@ -36,7 +36,9 @@ public void testPrefixPathWithEndingWildcard() { final Object prefixPathMatcher2 = new Object(); matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", prefixPathMatcher1) .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two", exactPathMatcher2) - .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3).build(); + .addPath("/one/two/", prefixPathMatcher1).addPath("/one/two/three/", prefixPathMatcher2) + .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four/", prefixPathMatcher2) + .addPath("/one/two/three/four", exactPathMatcher3).build(); assertMatched(matcher, "/one/two/three", exactPathMatcher1); assertMatched(matcher, "/one/two", exactPathMatcher2); assertMatched(matcher, "/one/two/three/four", exactPathMatcher3); @@ -72,8 +74,10 @@ public void testPrefixPathDefaultHandler() { final Object prefixPathMatcher1 = new Object(); final Object prefixPathMatcher2 = new Object(); matcher = ImmutablePathMatcher.builder().addPath("/one/two/*", prefixPathMatcher1).addPath("/*", defaultHandler) - .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two", exactPathMatcher2) - .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3).build(); + .addPath("/one/two/three", exactPathMatcher1).addPath("/one/two/three/", prefixPathMatcher2) + .addPath("/one/two", exactPathMatcher2).addPath("/one/two/", prefixPathMatcher1) + .addPath("/one/two/three*", prefixPathMatcher2).addPath("/one/two/three/four", exactPathMatcher3) + .addPath("/one/two/three/four/", prefixPathMatcher2).build(); assertMatched(matcher, "/one/two/three", exactPathMatcher1); assertMatched(matcher, "/one/two", exactPathMatcher2); assertMatched(matcher, "/one/two/three/four", exactPathMatcher3); @@ -120,7 +124,7 @@ public void testSpecialChars() { ImmutablePathMatcher matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2) .addPath("/one/two?three=four", handler1).addPath("/one/*/three?one\\\\\\=two", handler3) .addPath("/one/two#three*", handler4).addPath("/*/two#three*", handler5).addPath("/*", HANDLER) - .build(); + .addPath("/one/two#three/", handler4).build(); assertMatched(matcher, "/one/two#three", handler2); assertMatched(matcher, "/one/two?three=four", handler1); assertMatched(matcher, "/one/any-value/three?one\\\\\\=two", handler3); @@ -142,7 +146,7 @@ public void testSpecialChars() { assertMatched(matcher, "/one1/two#three/christmas!", handler5); assertMatched(matcher, "/one1/two#thre"); // no default handler - matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2) + matcher = ImmutablePathMatcher.builder().addPath("/one/two#three", handler2).addPath("/one/two#three/", handler4) .addPath("/one/two?three=four", handler1).addPath("/one/*/three?one\\\\\\=two", handler3) .addPath("/one/two#three*", handler4).addPath("/*/two#three*", handler5).build(); assertMatched(matcher, "/one/two#three", handler2); @@ -181,7 +185,7 @@ public void testInnerWildcardsWithExactMatches() { .addPath("/one/two/three", handler2).addPath("/one/two/three/four", handler3) .addPath("/", handler4).addPath("/*", HANDLER).addPath("/one/two/*/four", handler5) .addPath("/one/*/three/four", handler6).addPath("/*/two/three/four", handler7) - .addPath("/*/two", handler8).build(); + .addPath("/*/two", handler8).addPath("/*/two/three/four/", HANDLER).build(); assertMatched(matcher, "/one/two", handler1); assertMatched(matcher, "/one/two/three", handler2); assertMatched(matcher, "/one/two/three/four", handler3); @@ -216,7 +220,7 @@ public void testInnerWildcardsOnly() { assertMatched(matcher, "/one/two/three/4/five", handler5); assertMatched(matcher, "/one/two/three/sergey/five", handler5); assertMatched(matcher, "/one/two/three/sergey/five-ish"); - assertMatched(matcher, "/one/two/three/sergey/five/"); + assertMatched(matcher, "/one/two/three/sergey/five/", handler5); assertMatched(matcher, "/one/two/three/four", handler4); assertMatched(matcher, "/one/two/3/four", handler4); assertMatched(matcher, "/one/two/three", handler3); @@ -228,11 +232,11 @@ public void testInnerWildcardsOnly() { assertMatched(matcher, "/ho-hey/two", handler2); assertMatched(matcher, "/ho-hey/two2"); assertMatched(matcher, "/ho-hey/two2/"); - assertMatched(matcher, "/ho-hey/two/"); + assertMatched(matcher, "/ho-hey/two/", handler2); assertMatched(matcher, "/ho-hey/hey-ho/three", handler1); assertMatched(matcher, "/1/2/three", handler1); assertMatched(matcher, "/1/two/three", handler1); - assertMatched(matcher, "/1/two/three/"); + assertMatched(matcher, "/1/two/three/", handler1); assertMatched(matcher, "/1/two/three/f"); // no default path handler matcher = ImmutablePathMatcher.builder().addPath("/*/two", handler2) @@ -242,8 +246,8 @@ public void testInnerWildcardsOnly() { assertMatched(matcher, "/one/two/three/four/five", handler5); assertMatched(matcher, "/one/two/three/4/five", handler5); assertMatched(matcher, "/one/two/three/sergey/five", handler5); + assertMatched(matcher, "/one/two/three/sergey/five/", handler5); assertNotMatched(matcher, "/one/two/three/sergey/five-ish"); - assertNotMatched(matcher, "/one/two/three/sergey/five/"); assertMatched(matcher, "/one/two/three/four", handler4); assertMatched(matcher, "/one/two/3/four", handler4); assertMatched(matcher, "/one/two/three", handler3); @@ -253,13 +257,13 @@ public void testInnerWildcardsOnly() { assertMatched(matcher, "/two/two", handler2); assertMatched(matcher, "/2/two", handler2); assertMatched(matcher, "/ho-hey/two", handler2); + assertMatched(matcher, "/ho-hey/two/", handler2); assertNotMatched(matcher, "/ho-hey/two2"); assertNotMatched(matcher, "/ho-hey/two2/"); - assertNotMatched(matcher, "/ho-hey/two/"); assertMatched(matcher, "/ho-hey/hey-ho/three", handler1); assertMatched(matcher, "/1/2/three", handler1); assertMatched(matcher, "/1/two/three", handler1); - assertNotMatched(matcher, "/1/two/three/"); + assertMatched(matcher, "/1/two/three/", handler1); assertNotMatched(matcher, "/1/two/three/f"); } @@ -377,7 +381,7 @@ public void testPrefixPathHandlerMerging() { handler6.add("AgentBrown"); var matcher = ImmutablePathMatcher.> builder().handlerAccumulator(List::addAll).addPath("/path*", handler1) .addPath("/path*", handler2).addPath("/path/*", handler3).addPath("/path/", handler4) - .addPath("/path/*/", handler5).addPath("/*", handler6).build(); + .addPath("/path/*/", handler5).addPath("/*", handler6).addPath("/path", handler1).build(); var handler = matcher.match("/path").getValue(); assertNotNull(handler); assertTrue(handler.contains("Neo")); @@ -505,11 +509,11 @@ public void testDefaultHandlerOneInnerWildcard() { assertMatched(matcher, "/3/one"); assertMatched(matcher, "/4/one"); assertMatched(matcher, "/4/one"); + assertMatched(matcher, "/1/one/"); assertNotMatched(matcher, "/"); assertNotMatched(matcher, "/1"); assertNotMatched(matcher, "/1/"); assertNotMatched(matcher, "/1/two"); - assertNotMatched(matcher, "/1/one/"); assertNotMatched(matcher, "/1/one1"); assertNotMatched(matcher, "/1/on"); assertNotMatched(matcher, "/1/one/two");