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/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/KeycloakPolicyEnforcerAuthorizer.java b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/KeycloakPolicyEnforcerAuthorizer.java index 42579507d7ca5..8706578889f9b 100644 --- a/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/KeycloakPolicyEnforcerAuthorizer.java +++ b/extensions/keycloak-authorization/runtime/src/main/java/io/quarkus/keycloak/pep/KeycloakPolicyEnforcerAuthorizer.java @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -18,45 +19,45 @@ import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.adapters.config.PolicyEnforcerConfig; -import io.quarkus.arc.AlternativePriority; import io.quarkus.oidc.runtime.OidcConfig; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.QuarkusSecurityIdentity; -import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; -import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.vertx.ext.web.RoutingContext; @Singleton -@AlternativePriority(1) -public class KeycloakPolicyEnforcerAuthorizer extends HttpAuthorizer { +public class KeycloakPolicyEnforcerAuthorizer + implements HttpSecurityPolicy, BiFunction { private KeycloakAdapterPolicyEnforcer delegate; @Override - public CompletionStage checkPermission(RoutingContext routingContext) { + public CompletionStage checkPermission(RoutingContext request, SecurityIdentity identity, + AuthorizationRequestContext requestContext) { + return requestContext.runBlocking(request, identity, this); + } + + @Override + public CheckResult apply(RoutingContext routingContext, SecurityIdentity identity) { + VertxHttpFacade httpFacade = new VertxHttpFacade(routingContext); AuthorizationContext result = delegate.authorize(httpFacade); if (result.isGranted()) { - QuarkusHttpUser user = (QuarkusHttpUser) routingContext.user(); - - if (user == null) { - return attemptAnonymousAuthentication(routingContext); - } - - return enhanceSecurityIdentity(user.getSecurityIdentity(), result); + SecurityIdentity newIdentity = enhanceSecurityIdentity(identity, result); + return new CheckResult(true, newIdentity); } - return CompletableFuture.completedFuture(null); + return CheckResult.DENY; } - private CompletableFuture enhanceSecurityIdentity(SecurityIdentity current, + private SecurityIdentity enhanceSecurityIdentity(SecurityIdentity current, AuthorizationContext context) { Map attributes = new HashMap<>(current.getAttributes()); attributes.put("permissions", context.getPermissions()); - return CompletableFuture.completedFuture(new QuarkusSecurityIdentity.Builder() + return new QuarkusSecurityIdentity.Builder() .addAttributes(attributes) .setPrincipal(current.getPrincipal()) .addRoles(current.getRoles()) @@ -82,7 +83,7 @@ public CompletionStage apply(Permission permission) { return CompletableFuture.completedFuture(false); } - }).build()); + }).build(); } public void init(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config) { diff --git a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java index 4c49fd2f85550..29a08c94c8127 100644 --- a/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java +++ b/extensions/security/runtime/src/main/java/io/quarkus/security/runtime/QuarkusIdentityProviderManagerImpl.java @@ -13,6 +13,7 @@ import org.jboss.logging.Logger; +import io.quarkus.runtime.BlockingOperationControl; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.IdentityProvider; @@ -32,17 +33,33 @@ public class QuarkusIdentityProviderManagerImpl implements IdentityProviderManag private final List augmenters; private final Executor blockingExecutor; - private static final AuthenticationRequestContext blockingRequestContext = new AuthenticationRequestContext() { + private final AuthenticationRequestContext blockingRequestContext = new AuthenticationRequestContext() { @Override public CompletionStage runBlocking(Supplier function) { - CompletableFuture ret = new CompletableFuture<>(); - try { - SecurityIdentity result = function.get(); - ret.complete(result); - } catch (Throwable t) { - ret.completeExceptionally(t); + + if (BlockingOperationControl.isBlockingAllowed()) { + CompletableFuture ret = new CompletableFuture<>(); + try { + SecurityIdentity result = function.get(); + ret.complete(result); + } catch (Throwable t) { + ret.completeExceptionally(t); + } + return ret; + } else { + CompletableFuture cf = new CompletableFuture<>(); + blockingExecutor.execute(new Runnable() { + @Override + public void run() { + try { + cf.complete(function.get()); + } catch (Throwable t) { + cf.completeExceptionally(t); + } + } + }); + return cf; } - return ret; } }; @@ -69,7 +86,7 @@ public CompletionStage authenticate(AuthenticationRequest requ "No IdentityProviders were registered to handle AuthenticationRequest " + request)); return cf; } - return handleProvider(0, (List) providers, request, new AsyncAuthenticationRequestContext()); + return handleProvider(0, (List) providers, request, blockingRequestContext); } /** @@ -211,31 +228,4 @@ public int compare(SecurityIdentityAugmentor o1, SecurityIdentityAugmentor o2) { } } - private class AsyncAuthenticationRequestContext implements AuthenticationRequestContext { - - private boolean inBlocking = false; - - @Override - public CompletionStage runBlocking(Supplier function) { - if (inBlocking) { - return blockingRequestContext.runBlocking(function); - } - CompletableFuture cf = new CompletableFuture<>(); - blockingExecutor.execute(new Runnable() { - @Override - public void run() { - try { - inBlocking = true; - cf.complete(function.get()); - } catch (Throwable t) { - cf.completeExceptionally(t); - } finally { - inBlocking = false; - } - } - }); - - return cf; - } - } } 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/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java index 333083eec6099..19e0675b59600 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/HttpSecurityProcessor.java @@ -24,6 +24,7 @@ import io.quarkus.vertx.http.runtime.security.HttpAuthorizer; import io.quarkus.vertx.http.runtime.security.HttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.HttpSecurityRecorder; +import io.quarkus.vertx.http.runtime.security.PathMatchingHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.PermitSecurityPolicy; import io.quarkus.vertx.http.runtime.security.RolesAllowedHttpSecurityPolicy; import io.quarkus.vertx.http.runtime.security.SupplierImpl; @@ -31,16 +32,20 @@ public class HttpSecurityProcessor { @BuildStep - public void builtins(BuildProducer producer, HttpBuildTimeConfig buildTimeConfig) { + public void builtins(BuildProducer producer, HttpBuildTimeConfig buildTimeConfig, + BuildProducer beanProducer) { producer.produce(new HttpSecurityPolicyBuildItem("deny", new SupplierImpl<>(new DenySecurityPolicy()))); producer.produce(new HttpSecurityPolicyBuildItem("permit", new SupplierImpl<>(new PermitSecurityPolicy()))); producer.produce( new HttpSecurityPolicyBuildItem("authenticated", new SupplierImpl<>(new AuthenticatedHttpSecurityPolicy()))); - + if (!buildTimeConfig.auth.permissions.isEmpty()) { + beanProducer.produce(AdditionalBeanBuildItem.unremovableOf(PathMatchingHttpSecurityPolicy.class)); + } for (Map.Entry e : buildTimeConfig.auth.rolePolicy.entrySet()) { producer.produce(new HttpSecurityPolicyBuildItem(e.getKey(), new SupplierImpl<>(new RolesAllowedHttpSecurityPolicy(e.getValue().rolesAllowed)))); } + } @BuildStep diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AuthenticatedHttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AuthenticatedHttpSecurityPolicy.java index 42cc431b36754..795a4559c3aa3 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AuthenticatedHttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/AuthenticatedHttpSecurityPolicy.java @@ -4,7 +4,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 checks if the user is authenticated @@ -12,7 +12,8 @@ public class AuthenticatedHttpSecurityPolicy implements HttpSecurityPolicy { @Override - public CompletionStage checkPermission(HttpServerRequest request, SecurityIdentity identity) { + public CompletionStage checkPermission(RoutingContext request, SecurityIdentity identity, + AuthorizationRequestContext requestContext) { return CompletableFuture.completedFuture(identity.isAnonymous() ? CheckResult.DENY : CheckResult.PERMIT); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/DenySecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/DenySecurityPolicy.java index f9bc52d01048f..ff4e80fdabedc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/DenySecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/DenySecurityPolicy.java @@ -4,14 +4,15 @@ 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 DenySecurityPolicy implements HttpSecurityPolicy { public static final DenySecurityPolicy INSTANCE = new DenySecurityPolicy(); @Override - public CompletionStage checkPermission(HttpServerRequest request, SecurityIdentity identity) { + public CompletionStage checkPermission(RoutingContext request, SecurityIdentity identity, + AuthorizationRequestContext requestContext) { return CompletableFuture.completedFuture(CheckResult.DENY); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java index 8e4c369ddf853..f4e03a5e8b3c8 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpAuthorizer.java @@ -1,26 +1,20 @@ 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.enterprise.inject.Instance; import javax.inject.Inject; import javax.inject.Singleton; +import io.quarkus.runtime.BlockingOperationControl; +import io.quarkus.runtime.ExecutorRecorder; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.request.AnonymousAuthenticationRequest; -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; /** @@ -29,80 +23,120 @@ @Singleton public class HttpAuthorizer { - private final PathMatcher> pathMatcher = new PathMatcher<>(); @Inject HttpAuthenticator httpAuthenticator; + @Inject IdentityProviderManager identityProviderManager; - public CompletionStage checkPermission(RoutingContext routingContext) { + final List policies; + + @Inject + HttpAuthorizer(Instance installedPolicies) { + policies = new ArrayList<>(); + for (HttpSecurityPolicy i : installedPolicies) { + policies.add(i); + } + } + + /** + * context that allows for running blocking tasks + */ + private static final HttpSecurityPolicy.AuthorizationRequestContext CONTEXT = new HttpSecurityPolicy.AuthorizationRequestContext() { + @Override + public CompletionStage runBlocking(RoutingContext context, SecurityIdentity identity, + BiFunction function) { + if (BlockingOperationControl.isBlockingAllowed()) { + try { + HttpSecurityPolicy.CheckResult res = function.apply(context, identity); + return CompletableFuture.completedFuture(res); + } catch (Throwable t) { + CompletableFuture res = new CompletableFuture<>(); + res.completeExceptionally(t); + return res; + } + } + try { + CompletableFuture res = new CompletableFuture<>(); + ExecutorRecorder.getCurrent().execute(new Runnable() { + @Override + public void run() { + try { + HttpSecurityPolicy.CheckResult val = function.apply(context, identity); + res.complete(val); + } catch (Throwable t) { + res.completeExceptionally(t); + } + } + }); + return res; + } catch (Exception e) { + CompletableFuture res = new CompletableFuture<>(); + res.completeExceptionally(e); + return res; + } + } + }; + + /** + * Checks that the request is allowed to proceed. If it is then {@link RoutingContext#next()} will + * be invoked, if not appropriate action will be taken to either report the failure or attempt authentication. + * + */ + public void checkPermission(RoutingContext routingContext) { QuarkusHttpUser user = (QuarkusHttpUser) routingContext.user(); if (user == null) { //check the anonymous identity - return attemptAnonymousAuthentication(routingContext); + attemptAnonymousAuthentication(routingContext); + } else { + //we have a user, check their permissions + doPermissionCheck(routingContext, user.getSecurityIdentity(), 0, policies); } - //we have a user, check their permissions - return doPermissionCheck(routingContext, user.getSecurityIdentity()); } - protected CompletableFuture attemptAnonymousAuthentication(RoutingContext routingContext) { - CompletableFuture latch = new CompletableFuture<>(); + private void attemptAnonymousAuthentication(RoutingContext routingContext) { identityProviderManager.authenticate(AnonymousAuthenticationRequest.INSTANCE) .handle(new BiFunction() { @Override public Object apply(SecurityIdentity identity, Throwable throwable) { if (throwable != null) { - latch.completeExceptionally(throwable); + routingContext.fail(throwable); } else { - doPermissionCheck(routingContext, identity).handle( - new BiFunction() { - @Override - public SecurityIdentity apply(SecurityIdentity identity, - Throwable throwable) { - if (throwable != null) { - latch.completeExceptionally(throwable); - return null; - } - latch.complete(identity); - return identity; - } - }); + doPermissionCheck(routingContext, identity, 0, policies); } return null; } }); - return latch; - } - - private CompletionStage doPermissionCheck(RoutingContext routingContext, - SecurityIdentity identity) { - CompletableFuture latch = new CompletableFuture<>(); - List permissionCheckers = findPermissionCheckers(routingContext.request()); - doPermissionCheck(routingContext, latch, identity, 0, permissionCheckers); - return latch; } - private void doPermissionCheck(RoutingContext routingContext, CompletableFuture latch, + private void doPermissionCheck(RoutingContext routingContext, SecurityIdentity identity, int index, List permissionCheckers) { if (index == permissionCheckers.size()) { - latch.complete(identity); + QuarkusHttpUser currentUser = (QuarkusHttpUser) routingContext.user(); + if (!identity.isAnonymous() && (currentUser == null || currentUser.getSecurityIdentity() != identity)) { + routingContext.setUser(new QuarkusHttpUser(identity)); + } + routingContext.next(); return; } //get the current checker HttpSecurityPolicy res = permissionCheckers.get(index); - res.checkPermission(routingContext.request(), identity) + res.checkPermission(routingContext, identity, CONTEXT) .handle(new BiFunction() { @Override public Object apply(HttpSecurityPolicy.CheckResult checkResult, Throwable throwable) { if (throwable != null) { - latch.completeExceptionally(throwable); + routingContext.fail(throwable); } else { - if (checkResult == HttpSecurityPolicy.CheckResult.DENY) { - doDeny(identity, routingContext, latch); + if (!checkResult.isPermitted()) { + doDeny(identity, routingContext); } else { + SecurityIdentity newIdentity = checkResult.getAugmentedIdentity() != null + ? checkResult.getAugmentedIdentity() + : identity; //attempt to run the next checker - doPermissionCheck(routingContext, latch, identity, index + 1, permissionCheckers); + doPermissionCheck(routingContext, newIdentity, index + 1, permissionCheckers); } } return null; @@ -110,8 +144,7 @@ public Object apply(HttpSecurityPolicy.CheckResult checkResult, Throwable throwa }); } - private void doDeny(SecurityIdentity identity, RoutingContext routingContext, - CompletableFuture latch) { + private void doDeny(SecurityIdentity identity, RoutingContext routingContext) { //if we were denied we send a challenge if we are not authenticated, otherwise we send a 403 if (identity.isAnonymous()) { httpAuthenticator.sendChallenge(routingContext, new Runnable() { @@ -123,74 +156,5 @@ public void run() { } else { routingContext.fail(403); } - latch.complete(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/HttpSecurityPolicy.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java index 4c34ba2697de4..7ea18ab25c608 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/HttpSecurityPolicy.java @@ -1,30 +1,74 @@ package io.quarkus.vertx.http.runtime.security; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import io.quarkus.security.identity.SecurityIdentity; -import io.vertx.core.http.HttpServerRequest; import io.vertx.ext.web.RoutingContext; +/** + * A HTTP Security policy, that controls which requests are allowed to proceeed. + * + * There are two different ways these policies can be installed. The easiest is to just create a CDI bean, in which + * case the policy will be invoked on every request. + * + * Alternatively HttpSecurityPolicyBuildItem can be used to create a named policy. This policy can then be referenced + * in the application.properties path matching rules, which allows this policy to be applied to specific requests. + */ public interface HttpSecurityPolicy { - CompletionStage checkPermission(HttpServerRequest request, SecurityIdentity identity); + CompletionStage 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); + } +}