Skip to content

Commit

Permalink
Implement web.xml based permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartwdouglas authored and mmusgrov committed Dec 12, 2019
1 parent f84e389 commit 1e5c9ad
Show file tree
Hide file tree
Showing 11 changed files with 356 additions and 4 deletions.
2 changes: 1 addition & 1 deletion bom/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<opentracing-concurrent.version>0.2.0</opentracing-concurrent.version>
<opentracing-jdbc.version>0.0.12</opentracing-jdbc.version>
<jaeger.version>0.34.0</jaeger.version>
<quarkus-http.version>3.0.0.Beta2</quarkus-http.version>
<quarkus-http.version>3.0.0.Beta3</quarkus-http.version>
<jboss-servlet-api_4.0_spec.version>1.0.0.Final</jboss-servlet-api_4.0_spec.version>
<microprofile-config-api.version>1.3</microprofile-config-api.version>
<microprofile-context-propagation.version>1.0.1</microprofile-context-propagation.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -147,8 +153,12 @@ public ServiceStartBuildItem boot(UndertowDeploymentRecorder recorder,
BuildProducer<RouteBuildItem> 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<RoutingContext> ut = recorder.startUndertow(shutdown, executorBuildItem.getExecutorProxy(),
servletDeploymentManagerBuildItem.getDeploymentManager(),
wrappers.stream().map(HttpHandlerWrapperBuildItem::getValue).collect(Collectors.toList()), httpConfiguration,
Expand All @@ -165,8 +175,12 @@ public ServiceStartBuildItem boot(UndertowDeploymentRecorder recorder,
@BuildStep
void integrateCdi(BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<ContextRegistrarBuildItem> contextRegistrars,
BuildProducer<ListenerBuildItem> listeners) {
BuildProducer<ListenerBuildItem> 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) {
Expand Down Expand Up @@ -403,6 +417,37 @@ public ServletDeploymentManagerBuildItem build(List<ServletBuildItem> 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<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"\n" +
"<web-app version=\"3.0\"\n" +
" xmlns=\"http://java.sun.com/xml/ns/javaee\"\n" +
" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
" xsi:schemaLocation=\"http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd\"\n"
+
" metadata-complete=\"false\">\n" +
"\n" +
"<security-constraint>\n" +
" <web-resource-collection>\n" +
" <web-resource-name>test</web-resource-name>\n" +
" <url-pattern>/secure/*</url-pattern>\n" +
" <http-method>GET</http-method>\n" +
" <http-method>POST</http-method>\n" +
" </web-resource-collection>\n" +
" <auth-constraint>\n" +
" <role-name>admin</role-name>\n" +
" </auth-constraint>\n" +
"</security-constraint>" +
"</web-app>";

@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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CheckResult> 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<CheckResult> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -202,6 +206,7 @@ public void handleNotification(SecurityNotification notification) {
}
}
});

return new RuntimeValue<>(d);
}

Expand Down Expand Up @@ -308,6 +313,11 @@ public void addServletInitParameter(RuntimeValue<DeploymentInfo> info, String na
info.getValue().addInitParameter(name, value);
}

public void setupSecurity(DeploymentManager manager) {

CDI.current().select(ServletHttpSecurityPolicy.class).get().setDeployment(manager.getDeployment());
}

public Handler<RoutingContext> startUndertow(ShutdownContext shutdown, ExecutorService executorService,
DeploymentManager manager, List<HandlerWrapper> wrappers, HttpConfiguration httpConfiguration,
ServletRuntimeConfig servletRuntimeConfig) throws Exception {
Expand Down Expand Up @@ -511,6 +521,27 @@ public void addContextParam(RuntimeValue<DeploymentInfo> deployment, String para
deployment.getValue().addInitParameter(paramName, paramValue);
}

public void setDenyUncoveredHttpMethods(RuntimeValue<DeploymentInfo> deployment, boolean denyUncoveredHttpMethods) {
deployment.getValue().setDenyUncoveredHttpMethods(denyUncoveredHttpMethods);
}

public void addSecurityConstraint(RuntimeValue<DeploymentInfo> deployment, SecurityConstraint securityConstraint) {
deployment.getValue().addSecurityConstraint(securityConstraint);
}

public void addSecurityConstraint(RuntimeValue<DeploymentInfo> deployment, SecurityInfo.EmptyRoleSemantic emptyRoleSemantic,
TransportGuaranteeType transportGuaranteeType,
Set<String> rolesAllowed, Set<WebResourceCollection> 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
*/
Expand Down
8 changes: 8 additions & 0 deletions integration-tests/elytron-undertow/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-undertow</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 1e5c9ad

Please sign in to comment.