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

Support AuthorizationHandlers on OpenAPI Operations #2285

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,22 @@
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 <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#operationObject">Operation</a>
*/
@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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -28,6 +29,7 @@ public class OperationImpl implements Operation {

private Map<JsonPointer, JsonObject> parameters;
private List<String> tags;
private List<AuthorizationHandler> authzHandlers;
private List<Handler<RoutingContext>> userHandlers;
private List<Handler<RoutingContext>> userFailureHandlers;

Expand Down Expand Up @@ -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<RoutingContext> handler) {
this.userHandlers.add(handler);
Expand Down Expand Up @@ -129,6 +138,10 @@ protected JsonObject getPathModel() {
return pathModel;
}

protected List<AuthorizationHandler> getAuthorizationHandlers() {
return authzHandlers;
}

protected List<Handler<RoutingContext>> getUserHandlers() {
return userHandlers;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RoutingContext> mockHandler) {
return SimpleAuthenticationHandler.create()
.authenticate(ctx -> {
mockHandler.handle(ctx);
return Future.succeededFuture(User.create(new JsonObject()));
});
}
}