checkPermission(RoutingContext request, SecurityIdentity identity,
+ AuthorizationRequestContext requestContext);
/**
* The results of a permission check
*/
- enum CheckResult {
+ class CheckResult {
+
+ public static CheckResult DENY = new CheckResult(false);
+ public static CheckResult PERMIT = new CheckResult(true);
+
/**
- * If this is returned then the request is allowed. All permission checkers must permit a request for it
- * to proceed.
+ * If this check was sucessful
*/
- PERMIT,
+ private final boolean permitted;
+
/**
- * Denies the request. If the {@link SecurityIdentity} represents the anonymous user then
- * {@link HttpAuthenticationMechanism#sendChallenge(RoutingContext)} will be invoked, otherwise
- * a 403 forbidden error code will be returned.
+ * The new security identity, this allows the policy to add additional context
+ * information to the identity. If this is null no change is made
*/
- DENY
+ private final SecurityIdentity augmentedIdentity;
+
+ public CheckResult(boolean permitted) {
+ this.permitted = permitted;
+ this.augmentedIdentity = null;
+ }
+
+ public CheckResult(boolean permitted, SecurityIdentity augmentedIdentity) {
+ this.permitted = permitted;
+ this.augmentedIdentity = augmentedIdentity;
+ }
+
+ public boolean isPermitted() {
+ return permitted;
+ }
+
+ public SecurityIdentity getAugmentedIdentity() {
+ return augmentedIdentity;
+ }
+ }
+
+ /**
+ * A context object that can be used to run blocking tasks
+ *
+ * Blocking identity providers should used this context object to run blocking tasks, to prevent excessive and
+ * unnecessary delegation to thread pools
+ */
+ interface AuthorizationRequestContext {
+
+ CompletionStage runBlocking(RoutingContext context, SecurityIdentity identity,
+ BiFunction function);
+
}
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java
index 23c2d1457c88b..3bc24a6ae9b9e 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityRecorder.java
@@ -75,24 +75,7 @@ public void handle(RoutingContext event) {
if (authorizer == null) {
authorizer = CDI.current().select(HttpAuthorizer.class).get();
}
- authorizer.checkPermission(event).handle(new BiFunction() {
- @Override
- public SecurityIdentity apply(SecurityIdentity identity, Throwable throwable) {
- if (throwable != null) {
- event.fail(throwable);
- return null;
- }
- if (identity != null) {
- if (!identity.isAnonymous()) {
- event.setUser(new QuarkusHttpUser(identity));
- }
- event.next();
- return identity;
- }
- event.response().end();
- return null;
- }
- });
+ authorizer.checkPermission(event);
}
};
}
@@ -102,7 +85,7 @@ public BeanContainerListener initPermissions(HttpBuildTimeConfig permissions,
return new BeanContainerListener() {
@Override
public void created(BeanContainer container) {
- container.instance(HttpAuthorizer.class).init(permissions, policies);
+ container.instance(PathMatchingHttpSecurityPolicy.class).init(permissions, policies);
}
};
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java
new file mode 100644
index 0000000000000..1d1b754453afd
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java
@@ -0,0 +1,141 @@
+package io.quarkus.vertx.http.runtime.security;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.function.BiFunction;
+import java.util.function.Supplier;
+
+import javax.inject.Singleton;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.vertx.http.runtime.HttpBuildTimeConfig;
+import io.quarkus.vertx.http.runtime.PolicyMappingConfig;
+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.
+ */
+@Singleton
+public class PathMatchingHttpSecurityPolicy implements HttpSecurityPolicy {
+
+ private final PathMatcher> pathMatcher = new PathMatcher<>();
+
+ @Override
+ public CompletionStage checkPermission(RoutingContext routingContext, SecurityIdentity identity,
+ AuthorizationRequestContext requestContext) {
+ CompletableFuture latch = new CompletableFuture<>();
+ List permissionCheckers = findPermissionCheckers(routingContext.request());
+ doPermissionCheck(routingContext, latch, identity, 0, permissionCheckers, requestContext);
+ return latch;
+ }
+
+ private void doPermissionCheck(RoutingContext routingContext, CompletableFuture latch,
+ SecurityIdentity identity, int index,
+ List permissionCheckers, AuthorizationRequestContext requestContext) {
+ if (index == permissionCheckers.size()) {
+ latch.complete(new CheckResult(true, identity));
+ return;
+ }
+ //get the current checker
+ HttpSecurityPolicy res = permissionCheckers.get(index);
+ res.checkPermission(routingContext, identity, requestContext)
+ .handle(new BiFunction() {
+ @Override
+ public Object apply(CheckResult checkResult, Throwable throwable) {
+ if (throwable != null) {
+ latch.completeExceptionally(throwable);
+ } else {
+ if (!checkResult.isPermitted()) {
+ latch.complete(CheckResult.DENY);
+ } else {
+ SecurityIdentity newIdentity = checkResult.getAugmentedIdentity() != null
+ ? checkResult.getAugmentedIdentity()
+ : identity;
+ //attempt to run the next checker
+ doPermissionCheck(routingContext, latch, newIdentity, index + 1, permissionCheckers,
+ requestContext);
+ }
+ }
+ return null;
+ }
+ });
+ }
+
+ void init(HttpBuildTimeConfig config, Map> supplierMap) {
+ Map permissionCheckers = new HashMap<>();
+ for (Map.Entry> i : supplierMap.entrySet()) {
+ permissionCheckers.put(i.getKey(), i.getValue().get());
+ }
+
+ Map> tempMap = new HashMap<>();
+ for (Map.Entry entry : config.auth.permissions.entrySet()) {
+ HttpSecurityPolicy checker = permissionCheckers.get(entry.getValue().policy);
+ if (checker == null) {
+ throw new RuntimeException("Unable to find HTTP security policy " + entry.getValue().policy);
+ }
+
+ for (String path : entry.getValue().paths) {
+ if (tempMap.containsKey(path)) {
+ HttpMatcher m = new HttpMatcher(new HashSet<>(entry.getValue().methods), checker);
+ tempMap.get(path).add(m);
+ } else {
+ HttpMatcher m = new HttpMatcher(new HashSet<>(entry.getValue().methods), checker);
+ List perms = new ArrayList<>();
+ tempMap.put(path, perms);
+ perms.add(m);
+ if (path.endsWith("*")) {
+ pathMatcher.addPrefixPath(path.substring(0, path.length() - 1), perms);
+ } else {
+ pathMatcher.addExactPath(path, perms);
+ }
+ }
+ }
+ }
+ }
+
+ public List findPermissionCheckers(HttpServerRequest request) {
+ PathMatcher.PathMatch> toCheck = pathMatcher.match(request.path());
+ if (toCheck.getValue() == null || toCheck.getValue().isEmpty()) {
+ return Collections.emptyList();
+ }
+ List methodMatch = new ArrayList<>();
+ List noMethod = new ArrayList<>();
+ for (HttpMatcher i : toCheck.getValue()) {
+ if (i.methods == null || i.methods.isEmpty()) {
+ noMethod.add(i.checker);
+ } else if (i.methods.contains(request.method().toString())) {
+ methodMatch.add(i.checker);
+ }
+ }
+ if (!methodMatch.isEmpty()) {
+ return methodMatch;
+ } else if (!noMethod.isEmpty()) {
+ return noMethod;
+ } else {
+ //we deny if we did not match due to method filtering
+ return Collections.singletonList(DenySecurityPolicy.INSTANCE);
+ }
+
+ }
+
+ static class HttpMatcher {
+
+ final Set methods;
+ final HttpSecurityPolicy checker;
+
+ HttpMatcher(Set methods, HttpSecurityPolicy checker) {
+ this.methods = methods;
+ this.checker = checker;
+ }
+ }
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PermitSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PermitSecurityPolicy.java
index 72a296aa65242..31b5ff1bf6337 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PermitSecurityPolicy.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PermitSecurityPolicy.java
@@ -4,11 +4,13 @@
import java.util.concurrent.CompletionStage;
import io.quarkus.security.identity.SecurityIdentity;
-import io.vertx.core.http.HttpServerRequest;
+import io.vertx.ext.web.RoutingContext;
public class PermitSecurityPolicy implements HttpSecurityPolicy {
+
@Override
- public CompletionStage checkPermission(HttpServerRequest request, SecurityIdentity identity) {
+ public CompletionStage checkPermission(RoutingContext request, SecurityIdentity identity,
+ AuthorizationRequestContext requestContext) {
return CompletableFuture.completedFuture(CheckResult.PERMIT);
}
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java
index c53f21f90d150..f1ba44f932197 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/RolesAllowedHttpSecurityPolicy.java
@@ -5,7 +5,7 @@
import java.util.concurrent.CompletionStage;
import io.quarkus.security.identity.SecurityIdentity;
-import io.vertx.core.http.HttpServerRequest;
+import io.vertx.ext.web.RoutingContext;
/**
* permission checker that handles role based permissions
@@ -30,7 +30,8 @@ public RolesAllowedHttpSecurityPolicy setRolesAllowed(List rolesAllowed)
}
@Override
- public CompletionStage checkPermission(HttpServerRequest request, SecurityIdentity identity) {
+ public CompletionStage checkPermission(RoutingContext request, SecurityIdentity identity,
+ AuthorizationRequestContext requestContext) {
for (String i : rolesAllowed) {
if (identity.hasRole(i)) {
return CompletableFuture.completedFuture(CheckResult.PERMIT);
diff --git a/integration-tests/elytron-undertow/pom.xml b/integration-tests/elytron-undertow/pom.xml
index 6583273f9155e..3e3eaccc681ce 100644
--- a/integration-tests/elytron-undertow/pom.xml
+++ b/integration-tests/elytron-undertow/pom.xml
@@ -25,6 +25,14 @@
io.quarkus
quarkus-undertow
+
+ io.quarkus
+ quarkus-resteasy
+
+
+ io.quarkus
+ quarkus-smallrye-openapi
+
io.quarkus
diff --git a/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/GreetingServlet.java b/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/GreetingServlet.java
index 11427ad26d365..d8ee67439d697 100644
--- a/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/GreetingServlet.java
+++ b/integration-tests/elytron-undertow/src/main/java/io/quarkus/it/undertow/elytron/GreetingServlet.java
@@ -8,7 +8,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-@WebServlet(name = "ServletGreeting", urlPatterns = "/")
+@WebServlet(name = "ServletGreeting", urlPatterns = "/*")
public class GreetingServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
diff --git a/integration-tests/elytron-undertow/src/main/resources/META-INF/web.xml b/integration-tests/elytron-undertow/src/main/resources/META-INF/web.xml
new file mode 100644
index 0000000000000..36c6fd5b09ed4
--- /dev/null
+++ b/integration-tests/elytron-undertow/src/main/resources/META-INF/web.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ test
+ /secure/*
+ /openapi/*
+ GET
+ POST
+
+
+
+ managers
+
+
+
\ No newline at end of file
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
new file mode 100644
index 0000000000000..9bcb98cb0d73b
--- /dev/null
+++ b/integration-tests/elytron-undertow/src/test/java/io/quarkus/it/undertow/elytron/WebXmlPermissionsTestCase.java
@@ -0,0 +1,89 @@
+package io.quarkus.it.undertow.elytron;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.Matchers.is;
+
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.restassured.http.ContentType;
+
+@QuarkusTest
+class WebXmlPermissionsTestCase {
+
+ @Test
+ void testPost() {
+ // This is a regression test in that we had a problem where the Vert.x request was not paused
+ // before the authentication filters ran and the post message was thrown away by Vert.x because
+ // RESTEasy hadn't registered its request handlers yet.
+ given()
+ .header("Authorization", "Basic am9objpqb2hu")
+ .body("Bill")
+ .contentType(ContentType.TEXT)
+ .when()
+ .post("/")
+ .then()
+ .statusCode(200)
+ .body(is("hello Bill"));
+ }
+
+ @Test
+ void testOpenApiNoPermissions() {
+ given()
+ .when()
+ .get("/openapi")
+ .then()
+ .statusCode(401);
+ }
+
+ @Test
+ void testOpenApiWithWrongAuth() {
+ given()
+ .header("Authorization", "Basic am9objpqb2hu")
+ .when()
+ .get("/openapi")
+ .then()
+ .statusCode(403);
+ }
+
+ @Test
+ void testOpenApiWithAuth() {
+ given()
+ .auth()
+ .basic("mary", "mary")
+ .when()
+ .get("/openapi")
+ .then()
+ .statusCode(200);
+ }
+
+ @Test
+ void testSecuredServletWithWrongAuth() {
+ given()
+ .header("Authorization", "Basic am9objpqb2hu")
+ .when()
+ .get("/secure/a")
+ .then()
+ .statusCode(403);
+ }
+
+ @Test
+ void testSecuredServletWithNoAuth() {
+ given()
+ .when()
+ .get("/secure/a")
+ .then()
+ .statusCode(401);
+ }
+
+ @Test
+ void testSecuredServletWithAuth() {
+ given()
+ .auth()
+ .basic("mary", "mary")
+ .when()
+ .get("/secure/a")
+ .then()
+ .statusCode(200);
+ }
+}