Skip to content

Commit

Permalink
Filter out disabled REST methods from the OpenAPI document
Browse files Browse the repository at this point in the history
  • Loading branch information
jmartisk committed Jun 27, 2023
1 parent 01ccac6 commit 1e779c2
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.quarkus.runtime.rest;

import java.util.List;
import java.util.Map;

/**
* This class serves for passing a list of disabled REST paths (via the `@EndpointDisabled` annotation)
* so that an OpenAPI filter can omit them from the generated OpenAPI document.
*/
public class DisabledRestEndpoints {

// keys are REST paths, values are HTTP methods disabled on the given path
private static Map<String, List<String>> endpoints;

public static void set(Map<String, List<String>> endpoints) {
DisabledRestEndpoints.endpoints = endpoints;
}

public static Map<String, List<String>> get() {
return endpoints;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.ShutdownContext;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.runtime.rest.DisabledRestEndpoints;
import io.quarkus.security.AuthenticationCompletionException;
import io.quarkus.security.AuthenticationException;
import io.quarkus.security.AuthenticationFailedException;
Expand Down Expand Up @@ -182,6 +183,7 @@ public ResteasyReactiveRequestContext createContext(Deployment deployment,
closeTaskHandler, contextFactory, new ArcThreadSetupAction(beanContainer.requestContext()),
vertxConfig.rootPath);
Deployment deployment = runtimeDeploymentManager.deploy();
DisabledRestEndpoints.set(deployment.getDisabledEndpoints());
initClassFactory.createInstance().getInstance().init(deployment);
currentDeployment = deployment;

Expand Down
2 changes: 1 addition & 1 deletion extensions/smallrye-openapi/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
<!-- Test -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-deployment</artifactId>
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.quarkus.smallrye.openapi.test.jaxrs;

import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.resteasy.reactive.server.EndpointDisabled;
import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

/**
* Verify that REST endpoints that are disabled via the {@link EndpointDisabled} annotation are not included in the OpenAPI
* document.
*/
public class DisabledEndpointTestCase {
@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(DisabledEndpoint.class,
EnabledEndpoint.class)
.add(new StringAsset("quarkus.http.root-path=/root\n"), "application.properties"));

@EndpointDisabled(name = "xxx", disableIfMissing = true, stringValue = "xxx")
@Path("/disabled")
public static class DisabledEndpoint {

@Path("/hello")
@GET
public String hello() {
return null;
}

@Path("/hello2/{param1}")
@GET
public String hello2(@QueryParam("param1") String param1) {
return null;
}

@Path("/hello5")
@PUT
public String hello5() {
return null;
}

}

@EndpointDisabled(name = "xxx", disableIfMissing = false, stringValue = "xxx")
@Path("/enabled")
public static class EnabledEndpoint {

@Path("/hello3")
@GET
public String hello() {
return null;
}

@Path("/hello4/{param1}")
@GET
public String hello4(@QueryParam("param1") String param1) {
return null;
}

@Path("/hello5")
@POST
public String hello5() {
return null;
}

}

@Test
public void testDisabledEndpoint() {
RestAssured.given().header("Accept", "application/json")
.when().get("/q/openapi")
.prettyPeek().then()
.body("paths.\"/root/disabled/hello\".get", nullValue())
.body("paths.\"/root/disabled/hello2/{param1}\".get", nullValue())
.body("paths.\"/root/enabled/hello3\".get", notNullValue())
.body("paths.\"/root/enabled/hello4/{param1}\".get", notNullValue())
.body("paths.\"/root/enabled/hello5\".post", notNullValue())
.body("paths.\"/root/enabled/hello5\".put", nullValue());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class OpenApiWithResteasyPathHttpRootDefaultPathTestCase {
.withApplicationRoot((jar) -> jar
.addClasses(OpenApiResource.class, ResourceBean.class)
.addAsResource(new StringAsset("quarkus.http.root-path=/http-root-path\n" +
"quarkus.resteasy.path=/resteasy-path"),
"quarkus.resteasy-reactive.path=/resteasy-path"),
"application.properties"));

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class OpenApiWithResteasyPathTestCase {
static QuarkusUnitTest runner = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(OpenApiResource.class, ResourceBean.class)
.addAsResource(new StringAsset("quarkus.resteasy.path=/foo/bar"),
.addAsResource(new StringAsset("quarkus.resteasy-reactive.path=/foo/bar"),
"application.properties"));

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.eclipse.microprofile.openapi.models.OpenAPI;

import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.smallrye.openapi.runtime.filter.DisabledRestEndpointsFilter;
import io.smallrye.openapi.api.OpenApiConfig;
import io.smallrye.openapi.api.OpenApiConfigImpl;
import io.smallrye.openapi.api.OpenApiDocument;
Expand Down Expand Up @@ -88,6 +89,7 @@ static class StaticDocument implements OpenApiDocumentHolder {
if (autoFilter != null) {
document.filter(autoFilter);
}
document.filter(new DisabledRestEndpointsFilter());
document.filter(OpenApiProcessor.getFilter(openApiConfig, cl));
document.initialize();

Expand Down Expand Up @@ -122,6 +124,7 @@ static class DynamicDocument implements OpenApiDocumentHolder {
private OpenApiConfig openApiConfig;
private OASFilter userFilter;
private OASFilter autoFilter;
private DisabledRestEndpointsFilter disabledEndpointsFilter;

DynamicDocument(Config config, OASFilter autoFilter) {
ClassLoader cl = OpenApiConstants.classLoader == null ? Thread.currentThread().getContextClassLoader()
Expand All @@ -133,6 +136,7 @@ static class DynamicDocument implements OpenApiDocumentHolder {
this.userFilter = OpenApiProcessor.getFilter(openApiConfig, cl);
this.autoFilter = autoFilter;
this.generatedOnBuild = OpenApiProcessor.modelFromStaticFile(this.openApiConfig, staticFile);
this.disabledEndpointsFilter = new DisabledRestEndpointsFilter();
}
}
} catch (IOException ex) {
Expand Down Expand Up @@ -174,6 +178,7 @@ private OpenApiDocument getOpenApiDocument() {
if (this.autoFilter != null) {
document.filter(this.autoFilter);
}
document.filter(this.disabledEndpointsFilter);
document.filter(this.userFilter);
document.initialize();
return document;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.quarkus.smallrye.openapi.runtime.filter;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.eclipse.microprofile.openapi.OASFilter;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.PathItem;

import io.quarkus.runtime.rest.DisabledRestEndpoints;

/**
* If the RESTEasy Reactive extension passed us a list of REST paths that are disabled via the @DisabledRestEndpoint
* annotation, remove them from the OpenAPI document. This has to be done at runtime because
* the annotation is controlled by a runtime config property.
*/
public class DisabledRestEndpointsFilter implements OASFilter {

public void filterOpenAPI(OpenAPI openAPI) {
Map<String, List<String>> disabledEndpointsMap = DisabledRestEndpoints.get();
if (disabledEndpointsMap != null) {
Map<String, PathItem> pathItems = openAPI.getPaths().getPathItems();
List<String> emptyPathItems = new ArrayList<>();
if (pathItems != null) {
for (Map.Entry<String, PathItem> entry : pathItems.entrySet()) {
String path = entry.getKey();
PathItem pathItem = entry.getValue();
List<String> disabledMethodsForThisPath = disabledEndpointsMap.get(path);
if (disabledMethodsForThisPath != null) {
disabledMethodsForThisPath.forEach(method -> {
pathItem.setOperation(PathItem.HttpMethod.valueOf(method), null);
});
// if the pathItem is now empty, remove it
if (pathItem.getOperations().isEmpty()) {
emptyPathItems.add(path);
}
}
}
emptyPathItems.forEach(openAPI.getPaths()::removePathItem);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import jakarta.ws.rs.core.Application;
Expand Down Expand Up @@ -48,6 +49,7 @@ public class Deployment {
private final RuntimeExceptionMapper exceptionMapper;
private final boolean resumeOn404;
private final ResteasyReactiveConfig resteasyReactiveConfig;
private final Map<String, List<String>> disabledEndpoints;
//this is not final, as it is set after startup
private RuntimeConfiguration runtimeConfiguration;

Expand All @@ -62,7 +64,8 @@ public Deployment(ExceptionMapping exceptionMapping, ContextResolvers contextRes
List<GenericRuntimeConfigurableServerRestHandler<?>> runtimeConfigurableServerRestHandlers,
RuntimeExceptionMapper exceptionMapper,
boolean resumeOn404,
ResteasyReactiveConfig resteasyReactiveConfig) {
ResteasyReactiveConfig resteasyReactiveConfig,
Map<String, List<String>> disabledEndpoints) {
this.exceptionMapping = exceptionMapping;
this.contextResolvers = contextResolvers;
this.serialisers = serialisers;
Expand All @@ -80,6 +83,7 @@ public Deployment(ExceptionMapping exceptionMapping, ContextResolvers contextRes
this.exceptionMapper = exceptionMapper;
this.resumeOn404 = resumeOn404;
this.resteasyReactiveConfig = resteasyReactiveConfig;
this.disabledEndpoints = disabledEndpoints;
}

public RuntimeExceptionMapper getExceptionMapper() {
Expand Down Expand Up @@ -206,4 +210,8 @@ public Deployment setRuntimeConfiguration(RuntimeConfiguration runtimeConfigurat
this.runtimeConfiguration = runtimeConfiguration;
return this;
}

public Map<String, List<String>> getDisabledEndpoints() {
return disabledEndpoints;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,31 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) {
return info.getFactoryCreator().apply(aClass).createInstance();
}
});

// sanitise the prefix for our usage to make it either an empty string, or something which starts with a / and does not
// end with one
String prefix = rootPath;
if (prefix != null) {
prefix = sanitizePathPrefix(prefix);
} else {
prefix = "";
}
if ((applicationPath != null) && !applicationPath.isEmpty()) {
prefix = prefix + sanitizePathPrefix(applicationPath);
}
// to use it inside lambdas
String finalPrefix = prefix;

List<GenericRuntimeConfigurableServerRestHandler<?>> runtimeConfigurableServerRestHandlers = new ArrayList<>();
RuntimeResourceDeployment runtimeResourceDeployment = new RuntimeResourceDeployment(info, executorSupplier,
virtualExecutorSupplier,
interceptorDeployment, dynamicEntityWriter, resourceLocatorHandler, requestContextFactory.isDefaultBlocking());
List<ResourceClass> possibleSubResource = new ArrayList<>(locatableResourceClasses);
possibleSubResource.addAll(resourceClasses); //the TCK uses normal resources also as sub resources
Map<String, List<String>> disabledEndpoints = new HashMap<>();
for (int i = 0; i < possibleSubResource.size(); i++) {
ResourceClass clazz = possibleSubResource.get(i);
if ((clazz.getIsDisabled() != null) && clazz.getIsDisabled().get()) {
continue;
}

Map<String, TreeMap<URITemplate, List<RequestMapper.RequestPath<RuntimeResource>>>> templates = new HashMap<>();
URITemplate classPathTemplate = clazz.getPath() == null ? null : new URITemplate(clazz.getPath(), true);
for (int j = 0; j < clazz.getMethods().size(); j++) {
Expand All @@ -127,6 +141,24 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) {
}
Map<String, RequestMapper<RuntimeResource>> mappersByMethod = new RuntimeMappingDeployment(templates)
.buildClassMapper();
mappersByMethod.forEach((method, mapper) -> {
for (RequestMapper.RequestPath<RuntimeResource> path : mapper.getTemplates()) {
if ((clazz.getIsDisabled() != null) && clazz.getIsDisabled().get()) {
String templateWithoutSlash = path.template.template.startsWith("/")
? path.template.template.substring(1)
: path.template.template;
String fullPath = clazz.getPath().endsWith("/") ? finalPrefix + clazz.getPath() + templateWithoutSlash
: finalPrefix + clazz.getPath() + "/" + templateWithoutSlash;
if (!disabledEndpoints.containsKey(fullPath)) {
disabledEndpoints.put(fullPath, new ArrayList<>());
}
disabledEndpoints.get(fullPath).add(method);
}
}
});
if ((clazz.getIsDisabled() != null) && clazz.getIsDisabled().get()) {
continue;
}
resourceLocatorHandler.addResource(loadClass(clazz.getClassName()), mappersByMethod);
}

Expand Down Expand Up @@ -172,17 +204,6 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) {
abortHandlingChain.addAll(interceptorDeployment.getGlobalResponseInterceptorHandlers());
}
abortHandlingChain.add(new ResponseWriterHandler(dynamicEntityWriter));
// sanitise the prefix for our usage to make it either an empty string, or something which starts with a / and does not
// end with one
String prefix = rootPath;
if (prefix != null) {
prefix = sanitizePathPrefix(prefix);
} else {
prefix = "";
}
if ((applicationPath != null) && !applicationPath.isEmpty()) {
prefix = prefix + sanitizePathPrefix(applicationPath);
}

//pre matching interceptors are run first
List<ServerRestHandler> preMatchHandlers = new ArrayList<>();
Expand Down Expand Up @@ -210,7 +231,8 @@ public BeanFactory.BeanInstance<?> apply(Class<?> aClass) {
abortHandlingChain.toArray(EMPTY_REST_HANDLER_ARRAY), dynamicEntityWriter,
prefix, paramConverterProviders, configurationImpl, applicationSupplier,
threadSetupAction, requestContextFactory, preMatchHandlers, classMappers,
runtimeConfigurableServerRestHandlers, exceptionMapper, info.isResumeOn404(), info.getResteasyReactiveConfig());
runtimeConfigurableServerRestHandlers, exceptionMapper, info.isResumeOn404(), info.getResteasyReactiveConfig(),
disabledEndpoints);
}

private void forEachMapperEntry(MappersKey key,
Expand Down

0 comments on commit 1e779c2

Please sign in to comment.