From ba5a9990f11f0dc9ece99c0bee359665744af2a0 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Tue, 29 Oct 2019 12:54:59 +1100 Subject: [PATCH] Refactor the way HTTP permissions are applied --- .../pep/KeycloakPolicyEnforcerAuthorizer.java | 35 ++-- .../deployment/HttpSecurityProcessor.java | 9 +- .../AuthenticatedHttpSecurityPolicy.java | 5 +- .../runtime/security/DenySecurityPolicy.java | 5 +- .../http/runtime/security/HttpAuthorizer.java | 198 +++++++----------- .../runtime/security/HttpSecurityPolicy.java | 64 +++++- .../security/HttpSecurityRecorder.java | 21 +- .../PathMatchingHttpSecurityPolicy.java | 141 +++++++++++++ .../security/PermitSecurityPolicy.java | 6 +- .../RolesAllowedHttpSecurityPolicy.java | 5 +- 10 files changed, 316 insertions(+), 173 deletions(-) create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/security/PathMatchingHttpSecurityPolicy.java 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 df4eb95815587..344a738c6f587 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.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/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);