diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index c6c61717171896..f4c292f2308ab8 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 a727eb1db36044..5e7582d0637508 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 00000000000000..139e93fc77c01e --- /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 00000000000000..39bb45259acc74 --- /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 7ccdb3ae65434a..d259ee4de9d62c 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 00000000000000..a053bf6dfa4eab --- /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 2fd75abc4f5589..594b8b11831e24 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 6583273f9155ef..3e3eaccc681ce9 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 11427ad26d365a..d8ee67439d697b 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 00000000000000..36c6fd5b09ed48 --- /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 00000000000000..9bcb98cb0d73bc --- /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); + } +}