From aaf059462691d834fda3c1377ce6528d8a732f75 Mon Sep 17 00:00:00 2001 From: Ian Stewart Date: Tue, 18 Oct 2022 15:13:17 -0700 Subject: [PATCH] Support AuthorizationHandlers on OpenAPI Operations When attempting to register an AuthorizationHandler on an Operation within the OpenAPI3RouterBuilder the resultant router will fail to build because the AuthZ handler is registered after the generated validation handler. Adding the ability to register AuthorizationHandlers on an Operation. All handlers added using the new authorizationHandler method will be registered after the security handlers but before the validation and user handlers. --- .../io/vertx/ext/web/openapi/Operation.java | 9 ++ .../impl/OpenAPI3RouterBuilderImpl.java | 3 + .../ext/web/openapi/impl/OperationImpl.java | 13 ++ .../web/openapi/RouterBuilderAuthZTest.java | 122 ++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 vertx-web-openapi/src/test/java/io/vertx/ext/web/openapi/RouterBuilderAuthZTest.java diff --git a/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/Operation.java b/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/Operation.java index 68768e42ea..154f2f6c13 100644 --- a/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/Operation.java +++ b/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/Operation.java @@ -7,6 +7,7 @@ import io.vertx.core.http.HttpMethod; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.AuthorizationHandler; /** * Interface representing an Operation @@ -14,6 +15,14 @@ @VertxGen public interface Operation { + /** + * Mount an {@link io.vertx.ext.web.handler.AuthorizationHandler} for this operation + * + * @param handler + * @return + */ + @Fluent Operation authorizationHandler(AuthorizationHandler handler); + /** * Mount an handler for this operation * diff --git a/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/impl/OpenAPI3RouterBuilderImpl.java b/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/impl/OpenAPI3RouterBuilderImpl.java index 6e8c648c2d..98480a5152 100644 --- a/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/impl/OpenAPI3RouterBuilderImpl.java +++ b/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/impl/OpenAPI3RouterBuilderImpl.java @@ -290,6 +290,9 @@ public Router createRouter() { handlersToLoad.add(authnHandler); } + // Authorization Handlers + handlersToLoad.addAll(operation.getAuthorizationHandlers()); + // Generate ValidationHandler ValidationHandlerImpl validationHandler = validationHandlerGenerator.create(operation); handlersToLoad.add(validationHandler); diff --git a/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/impl/OperationImpl.java b/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/impl/OperationImpl.java index 5fe6518615..96f363487c 100644 --- a/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/impl/OperationImpl.java +++ b/vertx-web-openapi/src/main/java/io/vertx/ext/web/openapi/impl/OperationImpl.java @@ -7,6 +7,7 @@ import io.vertx.core.json.JsonObject; import io.vertx.core.json.pointer.JsonPointer; import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.AuthorizationHandler; import io.vertx.ext.web.openapi.OpenAPIHolder; import io.vertx.ext.web.openapi.Operation; @@ -28,6 +29,7 @@ public class OperationImpl implements Operation { private Map parameters; private List tags; + private List authzHandlers; private List> userHandlers; private List> userFailureHandlers; @@ -69,10 +71,17 @@ protected OperationImpl(String operationId, HttpMethod method, String path, Json .noneMatch(j -> j.getString("in").equalsIgnoreCase(paramIn) && j.getString("name").equals(paramName))) this.parameters.put(pathPointer.copy().append(i), parameterModel); } + this.authzHandlers = new ArrayList<>(); this.userHandlers = new ArrayList<>(); this.userFailureHandlers = new ArrayList<>(); } + @Override + public Operation authorizationHandler(AuthorizationHandler handler) { + this.authzHandlers.add(handler); + return this; + } + @Override public Operation handler(Handler handler) { this.userHandlers.add(handler); @@ -129,6 +138,10 @@ protected JsonObject getPathModel() { return pathModel; } + protected List getAuthorizationHandlers() { + return authzHandlers; + } + protected List> getUserHandlers() { return userHandlers; } diff --git a/vertx-web-openapi/src/test/java/io/vertx/ext/web/openapi/RouterBuilderAuthZTest.java b/vertx-web-openapi/src/test/java/io/vertx/ext/web/openapi/RouterBuilderAuthZTest.java new file mode 100644 index 0000000000..23ee0b72c0 --- /dev/null +++ b/vertx-web-openapi/src/test/java/io/vertx/ext/web/openapi/RouterBuilderAuthZTest.java @@ -0,0 +1,122 @@ +package io.vertx.ext.web.openapi; + +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.auth.authorization.AuthorizationContext; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.AuthenticationHandler; +import io.vertx.ext.web.handler.AuthorizationHandler; +import io.vertx.ext.web.handler.SimpleAuthenticationHandler; +import io.vertx.junit5.Checkpoint; +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static io.vertx.ext.web.validation.testutils.TestRequest.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@ExtendWith(VertxExtension.class) +@Timeout(1000) +public class RouterBuilderAuthZTest extends BaseRouterBuilderTest { + + private static final String SECURITY_TESTS = "src/test/resources/specs/security_test.yaml"; + + private static final RouterBuilderOptions FACTORY_OPTIONS = new RouterBuilderOptions() + .setRequireSecurityHandlers(true) + .setMountNotImplementedHandler(false); + + @Test + public void routerBuilderFailsWithAuthZ(Vertx vertx, VertxTestContext testContext) { + Checkpoint checkpoint = testContext.checkpoint(); + loadBuilderAndStartServer(vertx, SECURITY_TESTS, testContext, routerBuilder -> { + routerBuilder + .setOptions(FACTORY_OPTIONS) + .securityHandler("api_key") + .bindBlocking(config -> mockSuccessfulAuthHandler(routingContext -> routingContext.put("api_key", "1"))) + .operation("listPetsSingleSecurity") + .handler(mockAuthorizationHandler(true)); + + testContext.verify(() -> { + assertThatCode(routerBuilder::createRouter) + .isInstanceOfSatisfying(IllegalStateException.class, ise -> + assertThat(ise.getMessage()) + .contains("AUTHORIZATION")); + checkpoint.flag(); + }); + }); + } + + @Test + public void mountAuthZSuccess(Vertx vertx, VertxTestContext testContext) { + Checkpoint checkpoint = testContext.checkpoint(); + loadBuilderAndStartServer(vertx, SECURITY_TESTS, testContext, routerBuilder -> + routerBuilder + .setOptions(FACTORY_OPTIONS) + .securityHandler("api_key") + .bindBlocking(config -> mockSuccessfulAuthHandler(routingContext -> routingContext.put("api_key", "1"))) + .operation("listPetsSingleSecurity") + .authorizationHandler(mockAuthorizationHandler(true)) + .handler(routingContext -> + routingContext + .response() + .setStatusCode(200) + .setStatusMessage(routingContext.get("api_key")) + .end())) + .onComplete(h -> + testRequest(client, HttpMethod.GET, "/pets_single_security") + .expect(statusCode(200), statusMessage("1")) + .send(testContext, checkpoint)); + } + + @Test + public void mountAuthZFailure(Vertx vertx, VertxTestContext testContext) { + Checkpoint checkpoint = testContext.checkpoint(); + loadBuilderAndStartServer(vertx, SECURITY_TESTS, testContext, routerBuilder -> + routerBuilder + .setOptions(FACTORY_OPTIONS) + .securityHandler("api_key") + .bindBlocking(config -> mockSuccessfulAuthHandler(routingContext -> routingContext.put("api_key", "1"))) + .operation("listPetsSingleSecurity") + .authorizationHandler(mockAuthorizationHandler(false)) + .handler(routingContext -> + routingContext + .response() + .setStatusCode(200) + .setStatusMessage(routingContext.get("api_key")) + .end())) + .onComplete(h -> + testRequest(client, HttpMethod.GET, "/pets_single_security") + .expect(statusCode(403), statusMessage("Forbidden")) + .send(testContext, checkpoint)); + } + + private AuthorizationHandler mockAuthorizationHandler(boolean authorized) { + return AuthorizationHandler.create(new Authorization() { + @Override + public boolean match(AuthorizationContext authorizationContext) { + return authorized; + } + + @Override + public boolean verify(Authorization authorization) { + return authorized; + } + }); + } + + private AuthenticationHandler mockSuccessfulAuthHandler(Handler mockHandler) { + return SimpleAuthenticationHandler.create() + .authenticate(ctx -> { + mockHandler.handle(ctx); + return Future.succeededFuture(User.create(new JsonObject())); + }); + } +}