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