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())); + }); + } +}