diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml
index fbd5f4c9aea33..328d2c399f96d 100644
--- a/bom/runtime/pom.xml
+++ b/bom/runtime/pom.xml
@@ -24,7 +24,7 @@
0.2.0
0.0.12
0.34.0
- 3.0.0.Beta2
+ 3.0.0.Beta3
1.0.0.Final
1.3
1.0.1
diff --git a/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java b/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java
index ddcac2cd4a757..bf9efc957e809 100644
--- a/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java
+++ b/extensions/undertow/deployment/src/main/java/io/quarkus/undertow/deployment/UndertowBuildStep.java
@@ -2,6 +2,7 @@
import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;
+import static io.undertow.servlet.api.SecurityInfo.EmptyRoleSemantic.AUTHENTICATE;
import static io.undertow.servlet.api.SecurityInfo.EmptyRoleSemantic.DENY;
import static io.undertow.servlet.api.SecurityInfo.EmptyRoleSemantic.PERMIT;
import static javax.servlet.DispatcherType.REQUEST;
@@ -66,12 +67,14 @@
import org.jboss.metadata.web.spec.HttpMethodConstraintMetaData;
import org.jboss.metadata.web.spec.ListenerMetaData;
import org.jboss.metadata.web.spec.MultipartConfigMetaData;
+import org.jboss.metadata.web.spec.SecurityConstraintMetaData;
import org.jboss.metadata.web.spec.ServletMappingMetaData;
import org.jboss.metadata.web.spec.ServletMetaData;
import org.jboss.metadata.web.spec.ServletSecurityMetaData;
import org.jboss.metadata.web.spec.ServletsMetaData;
import org.jboss.metadata.web.spec.TransportGuaranteeType;
import org.jboss.metadata.web.spec.WebMetaData;
+import org.jboss.metadata.web.spec.WebResourceCollectionMetaData;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
@@ -97,6 +100,7 @@
import io.quarkus.deployment.util.ServiceUtil;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.undertow.runtime.HttpSessionContext;
+import io.quarkus.undertow.runtime.ServletHttpSecurityPolicy;
import io.quarkus.undertow.runtime.ServletProducer;
import io.quarkus.undertow.runtime.ServletRuntimeConfig;
import io.quarkus.undertow.runtime.ServletSecurityInfoProxy;
@@ -110,8 +114,10 @@
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.FilterInfo;
import io.undertow.servlet.api.HttpMethodSecurityInfo;
+import io.undertow.servlet.api.SecurityConstraint;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.api.ServletSecurityInfo;
+import io.undertow.servlet.api.WebResourceCollection;
import io.undertow.servlet.handlers.DefaultServlet;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
@@ -147,8 +153,12 @@ public ServiceStartBuildItem boot(UndertowDeploymentRecorder recorder,
BuildProducer routeProducer,
ExecutorBuildItem executorBuildItem, HttpConfiguration httpConfiguration,
ServletRuntimeConfig servletRuntimeConfig,
- ServletContextPathBuildItem servletContextPathBuildItem) throws Exception {
+ ServletContextPathBuildItem servletContextPathBuildItem,
+ Capabilities capabilities) throws Exception {
+ if (capabilities.isCapabilityPresent(Capabilities.SECURITY)) {
+ recorder.setupSecurity(servletDeploymentManagerBuildItem.getDeploymentManager());
+ }
Handler ut = recorder.startUndertow(shutdown, executorBuildItem.getExecutorProxy(),
servletDeploymentManagerBuildItem.getDeploymentManager(),
wrappers.stream().map(HttpHandlerWrapperBuildItem::getValue).collect(Collectors.toList()), httpConfiguration,
@@ -165,8 +175,12 @@ public ServiceStartBuildItem boot(UndertowDeploymentRecorder recorder,
@BuildStep
void integrateCdi(BuildProducer additionalBeans,
BuildProducer contextRegistrars,
- BuildProducer listeners) {
+ BuildProducer listeners,
+ Capabilities capabilities) {
additionalBeans.produce(new AdditionalBeanBuildItem(ServletProducer.class));
+ if (capabilities.isCapabilityPresent(Capabilities.SECURITY)) {
+ additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(ServletHttpSecurityPolicy.class));
+ }
contextRegistrars.produce(new ContextRegistrarBuildItem(new ContextRegistrar() {
@Override
public void register(RegistrationContext registrationContext) {
@@ -403,6 +417,37 @@ public ServletDeploymentManagerBuildItem build(List servlets,
}
}
}
+ if (webMetaData.getDenyUncoveredHttpMethods() != null) {
+ recorder.setDenyUncoveredHttpMethods(deployment, webMetaData.getDenyUncoveredHttpMethods());
+ }
+ if (webMetaData.getSecurityConstraints() != null) {
+ for (SecurityConstraintMetaData constraint : webMetaData.getSecurityConstraints()) {
+ SecurityConstraint securityConstraint = new SecurityConstraint()
+ .setTransportGuaranteeType(transportGuaranteeType(constraint.getTransportGuarantee()));
+
+ List roleNames = constraint.getRoleNames();
+ if (constraint.getAuthConstraint() == null) {
+ // no auth constraint means we permit the empty roles
+ securityConstraint.setEmptyRoleSemantic(PERMIT);
+ } else if (roleNames.size() == 1 && roleNames.contains("*")) {
+ securityConstraint.setEmptyRoleSemantic(AUTHENTICATE);
+ } else {
+ securityConstraint.addRolesAllowed(roleNames);
+ }
+
+ if (constraint.getResourceCollections() != null) {
+ for (final WebResourceCollectionMetaData resourceCollection : constraint.getResourceCollections()) {
+ securityConstraint.addWebResourceCollection(new WebResourceCollection()
+ .addHttpMethods(resourceCollection.getHttpMethods())
+ .addHttpMethodOmissions(resourceCollection.getHttpMethodOmissions())
+ .addUrlPatterns(resourceCollection.getUrlPatterns()));
+ }
+ }
+ recorder.addSecurityConstraint(deployment, securityConstraint.getEmptyRoleSemantic(),
+ securityConstraint.getTransportGuaranteeType(), securityConstraint.getRolesAllowed(),
+ securityConstraint.getWebResourceCollections());
+ }
+ }
//listeners
if (webMetaData.getListeners() != null) {
diff --git a/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/SecuredServlet.java b/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/SecuredServlet.java
new file mode 100644
index 0000000000000..139e93fc77c01
--- /dev/null
+++ b/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/SecuredServlet.java
@@ -0,0 +1,18 @@
+package io.quarkus.undertow.test;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@WebServlet(urlPatterns = "/secure/servlet")
+public class SecuredServlet extends HttpServlet {
+
+ @Override
+ protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ resp.getWriter().write(req.getUserPrincipal().getName());
+ }
+}
diff --git a/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/ServletWebXmlSecurityTestCase.java b/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/ServletWebXmlSecurityTestCase.java
new file mode 100644
index 0000000000000..39bb45259acc7
--- /dev/null
+++ b/extensions/undertow/deployment/src/test/java/io/quarkus/undertow/test/ServletWebXmlSecurityTestCase.java
@@ -0,0 +1,53 @@
+package io.quarkus.undertow.test;
+
+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.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.quarkus.test.QuarkusUnitTest;
+import io.restassured.RestAssured;
+
+/**
+ * tests that basic web.xml security is applied. We don't actually have
+ * the security subsystem installed here, so this is the fallback behaviour that
+ * will always deny
+ */
+public class ServletWebXmlSecurityTestCase {
+
+ static final String WEB_XML = "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ " \n" +
+ " test\n" +
+ " /secure/*\n" +
+ " GET\n" +
+ " POST\n" +
+ " \n" +
+ " \n" +
+ " admin\n" +
+ " \n" +
+ "" +
+ "";
+
+ @RegisterExtension
+ static QuarkusUnitTest runner = new QuarkusUnitTest()
+ .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
+ .addClasses(SecuredServlet.class)
+ .addAsManifestResource(new StringAsset(WEB_XML), "web.xml"));
+
+ @Test
+ public void testWebXmlSecurityConstraints() {
+ RestAssured.when().get("/secure/servlet").then()
+ .statusCode(401);
+ }
+
+}
diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusAuthMechanism.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusAuthMechanism.java
index 7ccdb3ae65434..d259ee4de9d62 100644
--- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusAuthMechanism.java
+++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/QuarkusAuthMechanism.java
@@ -2,6 +2,7 @@
import io.quarkus.vertx.http.runtime.security.HttpAuthenticator;
import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser;
+import io.undertow.httpcore.StatusCodes;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
@@ -30,6 +31,11 @@ public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContex
VertxHttpExchange delegate = (VertxHttpExchange) exchange.getDelegate();
RoutingContext context = (RoutingContext) delegate.getContext();
HttpAuthenticator authenticator = context.get(HttpAuthenticator.class.getName());
+ if (authenticator == null) {
+ exchange.setStatusCode(StatusCodes.UNAUTHORIZED);
+ exchange.endExchange();
+ return new ChallengeResult(true, exchange.getStatusCode());
+ }
authenticator.sendChallenge(context, new Runnable() {
@Override
public void run() {
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
new file mode 100644
index 0000000000000..a053bf6dfa4ea
--- /dev/null
+++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/ServletHttpSecurityPolicy.java
@@ -0,0 +1,79 @@
+package io.quarkus.undertow.runtime;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+
+import javax.inject.Singleton;
+
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy;
+import io.undertow.servlet.api.Deployment;
+import io.undertow.servlet.api.SecurityInfo;
+import io.undertow.servlet.api.SingleConstraintMatch;
+import io.undertow.servlet.handlers.security.SecurityPathMatch;
+import io.vertx.ext.web.RoutingContext;
+
+@Singleton
+public class ServletHttpSecurityPolicy implements HttpSecurityPolicy {
+
+ private volatile Deployment deployment;
+
+ //the context path, guaranteed to have a trailing /
+ private volatile String contextPath;
+
+ @Override
+ public CompletionStage checkPermission(RoutingContext request, SecurityIdentity identity,
+ AuthorizationRequestContext requestContext) {
+
+ String requestPath = request.request().path();
+ if (!requestPath.startsWith(contextPath)) {
+ //anything outside the context path we don't have anything to do with
+ return CompletableFuture.completedFuture(CheckResult.PERMIT);
+ }
+ if (!contextPath.equals("/")) {
+ requestPath = requestPath.substring(contextPath.length() - 1);
+ }
+ SecurityPathMatch match = deployment.getSecurityPathMatches().getSecurityInfo(requestPath,
+ request.request().rawMethod());
+
+ SingleConstraintMatch mergedConstraint = match.getMergedConstraint();
+ if (mergedConstraint.getRequiredRoles().isEmpty()) {
+ SecurityInfo.EmptyRoleSemantic emptyRoleSemantic = mergedConstraint.getEmptyRoleSemantic();
+ if (emptyRoleSemantic == SecurityInfo.EmptyRoleSemantic.PERMIT) {
+ return CompletableFuture.completedFuture(CheckResult.PERMIT);
+ } else if (emptyRoleSemantic == SecurityInfo.EmptyRoleSemantic.DENY) {
+ return CompletableFuture.completedFuture(CheckResult.DENY);
+ } else if (emptyRoleSemantic == SecurityInfo.EmptyRoleSemantic.AUTHENTICATE) {
+ if (identity.isAnonymous()) {
+ return CompletableFuture.completedFuture(CheckResult.DENY);
+ } else {
+ return CompletableFuture.completedFuture(CheckResult.PERMIT);
+ }
+ } else {
+ CompletableFuture c = new CompletableFuture<>();
+ c.completeExceptionally(new RuntimeException("Unknown empty role semantic " + emptyRoleSemantic));
+ return c;
+ }
+ } else {
+ for (String i : mergedConstraint.getRequiredRoles()) {
+ if (identity.hasRole(i)) {
+ return CompletableFuture.completedFuture(CheckResult.PERMIT);
+ }
+ }
+ return CompletableFuture.completedFuture(CheckResult.DENY);
+ }
+ }
+
+ public Deployment getDeployment() {
+ return deployment;
+ }
+
+ public ServletHttpSecurityPolicy setDeployment(Deployment deployment) {
+ this.deployment = deployment;
+ contextPath = deployment.getDeploymentInfo().getContextPath();
+ if (!contextPath.endsWith("/")) {
+ contextPath = contextPath + "/";
+ }
+ return this;
+ }
+}
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 2fd75abc4f558..594b8b11831e2 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
@@ -64,11 +64,15 @@
import io.undertow.servlet.api.InstanceHandle;
import io.undertow.servlet.api.ListenerInfo;
import io.undertow.servlet.api.LoginConfig;
+import io.undertow.servlet.api.SecurityConstraint;
+import io.undertow.servlet.api.SecurityInfo;
import io.undertow.servlet.api.ServletContainer;
import io.undertow.servlet.api.ServletContainerInitializerInfo;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.api.ServletSecurityInfo;
import io.undertow.servlet.api.ThreadSetupHandler;
+import io.undertow.servlet.api.TransportGuaranteeType;
+import io.undertow.servlet.api.WebResourceCollection;
import io.undertow.servlet.handlers.DefaultServlet;
import io.undertow.servlet.handlers.ServletPathMatches;
import io.undertow.servlet.handlers.ServletRequestContext;
@@ -202,6 +206,7 @@ public void handleNotification(SecurityNotification notification) {
}
}
});
+
return new RuntimeValue<>(d);
}
@@ -308,6 +313,11 @@ public void addServletInitParameter(RuntimeValue info, String na
info.getValue().addInitParameter(name, value);
}
+ public void setupSecurity(DeploymentManager manager) {
+
+ CDI.current().select(ServletHttpSecurityPolicy.class).get().setDeployment(manager.getDeployment());
+ }
+
public Handler startUndertow(ShutdownContext shutdown, ExecutorService executorService,
DeploymentManager manager, List wrappers, HttpConfiguration httpConfiguration,
ServletRuntimeConfig servletRuntimeConfig) throws Exception {
@@ -511,6 +521,27 @@ public void addContextParam(RuntimeValue deployment, String para
deployment.getValue().addInitParameter(paramName, paramValue);
}
+ public void setDenyUncoveredHttpMethods(RuntimeValue deployment, boolean denyUncoveredHttpMethods) {
+ deployment.getValue().setDenyUncoveredHttpMethods(denyUncoveredHttpMethods);
+ }
+
+ public void addSecurityConstraint(RuntimeValue deployment, SecurityConstraint securityConstraint) {
+ deployment.getValue().addSecurityConstraint(securityConstraint);
+ }
+
+ public void addSecurityConstraint(RuntimeValue deployment, SecurityInfo.EmptyRoleSemantic emptyRoleSemantic,
+ TransportGuaranteeType transportGuaranteeType,
+ Set rolesAllowed, Set webResourceCollections) {
+
+ SecurityConstraint securityConstraint = new SecurityConstraint()
+ .setEmptyRoleSemantic(emptyRoleSemantic)
+ .addRolesAllowed(rolesAllowed)
+ .setTransportGuaranteeType(transportGuaranteeType)
+ .addWebResourceCollections(webResourceCollections.toArray(new WebResourceCollection[0]));
+ deployment.getValue().addSecurityConstraint(securityConstraint);
+
+ }
+
/**
* we can't have SecureRandom in the native image heap, so we need to lazy init
*/
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);
+ }
+}