Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP Permissions Refactor #4956

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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;

Expand All @@ -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<RoutingContext, SecurityIdentity, HttpSecurityPolicy.CheckResult> {

private KeycloakAdapterPolicyEnforcer delegate;

@Override
public CompletionStage<SecurityIdentity> checkPermission(RoutingContext routingContext) {
public CompletionStage<CheckResult> 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<SecurityIdentity> enhanceSecurityIdentity(SecurityIdentity current,
private SecurityIdentity enhanceSecurityIdentity(SecurityIdentity current,
AuthorizationContext context) {
Map<String, Object> 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())
Expand All @@ -82,7 +83,7 @@ public CompletionStage<Boolean> apply(Permission permission) {

return CompletableFuture.completedFuture(false);
}
}).build());
}).build();
}

public void init(OidcConfig oidcConfig, KeycloakPolicyEnforcerConfig config) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,17 +33,33 @@ public class QuarkusIdentityProviderManagerImpl implements IdentityProviderManag
private final List<SecurityIdentityAugmentor> augmenters;
private final Executor blockingExecutor;

private static final AuthenticationRequestContext blockingRequestContext = new AuthenticationRequestContext() {
private final AuthenticationRequestContext blockingRequestContext = new AuthenticationRequestContext() {
@Override
public CompletionStage<SecurityIdentity> runBlocking(Supplier<SecurityIdentity> function) {
CompletableFuture<SecurityIdentity> ret = new CompletableFuture<>();
try {
SecurityIdentity result = function.get();
ret.complete(result);
} catch (Throwable t) {
ret.completeExceptionally(t);

if (BlockingOperationControl.isBlockingAllowed()) {
CompletableFuture<SecurityIdentity> ret = new CompletableFuture<>();
try {
SecurityIdentity result = function.get();
ret.complete(result);
} catch (Throwable t) {
ret.completeExceptionally(t);
}
return ret;
} else {
CompletableFuture<SecurityIdentity> 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;
}
};

Expand All @@ -69,7 +86,7 @@ public CompletionStage<SecurityIdentity> 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);
}

/**
Expand Down Expand Up @@ -211,31 +228,4 @@ public int compare(SecurityIdentityAugmentor o1, SecurityIdentityAugmentor o2) {
}
}

private class AsyncAuthenticationRequestContext implements AuthenticationRequestContext {

private boolean inBlocking = false;

@Override
public CompletionStage<SecurityIdentity> runBlocking(Supplier<SecurityIdentity> function) {
if (inBlocking) {
return blockingRequestContext.runBlocking(function);
}
CompletableFuture<SecurityIdentity> 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;
}
}
}
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
Loading