Skip to content
Merged
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
6 changes: 6 additions & 0 deletions org.restlet.ext.openapi/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
<version>${lib-junit-version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${lib-junit-version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.restlet</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
*/
package org.restlet.ext.openapi;

import java.util.ArrayList;
import java.util.List;
import org.restlet.Application;
import org.restlet.Restlet;
import org.restlet.routing.Filter;
import org.restlet.routing.Router;
import org.restlet.security.ChallengeAuthenticator;

public class OpenApiApplication extends Application {
/**
Expand All @@ -30,10 +33,11 @@ public Restlet getInboundRoot() {
if (!documented) {
synchronized (this) {
if (!documented) {
Router rootRouter = getNextRouter(inboundRoot);
List<ChallengeAuthenticator> authenticators = new ArrayList<>();
Router rootRouter = getNextRouter(inboundRoot, authenticators);

if (!documented && rootRouter != null) {
attachOpenApiSpecificationRestlet(rootRouter);
attachOpenApiSpecificationRestlet(rootRouter, authenticators);
documented = true;
}
}
Expand All @@ -49,25 +53,33 @@ protected String getOpenApiSpecificationPath() {
}

/**
* Returns the next router available.
* Returns the next router available, collecting any {@link ChallengeAuthenticator} found while
* unwrapping the filter chain along the way.
*
* @param current The current Restlet to inspect.
* @param authenticators The list of authenticators found so far, to be completed.
* @return The first router available.
*/
private static Router getNextRouter(Restlet current) {
private static Router getNextRouter(
Restlet current, List<ChallengeAuthenticator> authenticators) {
Router result = null;

if (current instanceof Router router) {
result = router;
} else if (current instanceof Filter filter) {
result = getNextRouter(filter.getNext());
if (filter instanceof ChallengeAuthenticator challengeAuthenticator) {
authenticators.add(challengeAuthenticator);
}
result = getNextRouter(filter.getNext(), authenticators);
}

return result;
}

private void attachOpenApiSpecificationRestlet(Router router) {
getOpenApiSpecificationRestlet(router).attach(router, getOpenApiSpecificationPath());
private void attachOpenApiSpecificationRestlet(
Router router, List<ChallengeAuthenticator> authenticators) {
getOpenApiSpecificationRestlet(router, authenticators)
.attach(router, getOpenApiSpecificationPath());
documented = true;
}

Expand All @@ -76,7 +88,8 @@ private void attachOpenApiSpecificationRestlet(Router router) {
*
* @return The {@link Restlet} able to generate the Swagger specification formats.
*/
OpenApiSpecificationRestlet getOpenApiSpecificationRestlet(Router router) {
return new OpenApiSpecificationRestlet(router);
OpenApiSpecificationRestlet getOpenApiSpecificationRestlet(
Router router, List<ChallengeAuthenticator> authenticators) {
return new OpenApiSpecificationRestlet(router, authenticators);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.restlet.representation.StringRepresentation;
import org.restlet.representation.Variant;
import org.restlet.routing.Router;
import org.restlet.security.ChallengeAuthenticator;

public class OpenApiSpecificationRestlet extends Restlet {
private static final VariantInfo VARIANT_JSON = new VariantInfo(MediaType.APPLICATION_JSON);
Expand All @@ -36,9 +37,17 @@ public class OpenApiSpecificationRestlet extends Restlet {

private final Router router;

public OpenApiSpecificationRestlet(Router router) {
private final List<ChallengeAuthenticator> authenticators;

private final String openApiContextId;

public OpenApiSpecificationRestlet(Router router, List<ChallengeAuthenticator> authenticators) {
super(router.getContext());
this.router = router;
this.authenticators = authenticators;
// Unique per router so that swagger-core's global OpenApiContextLocator doesn't hand back
// another application's cached context (and therefore its router/paths).
this.openApiContextId = "restlet-openapi-" + System.identityHashCode(router);
}

@Override
Expand Down Expand Up @@ -66,7 +75,9 @@ private Representation getOpenApiDefinitionAsRepresentation(Request request) {
try {
var context =
new RestletOpenApiContextBuilder()
.ctxId(openApiContextId)
.router(router)
.authenticators(authenticators)
.openApiConfiguration(oasConfig)
.buildContext(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@
import io.swagger.v3.oas.integration.GenericOpenApiContext;
import io.swagger.v3.oas.integration.api.OpenAPIConfiguration;
import io.swagger.v3.oas.integration.api.OpenApiReader;
import java.util.List;
import org.restlet.engine.util.StringUtils;
import org.restlet.routing.Router;
import org.restlet.security.ChallengeAuthenticator;

public class RestletOpenApiContext extends GenericOpenApiContext<RestletOpenApiContext> {
private final Router router;

public RestletOpenApiContext(Router router) {
private final List<ChallengeAuthenticator> authenticators;

public RestletOpenApiContext(Router router, List<ChallengeAuthenticator> authenticators) {
this.router = router;
this.authenticators = authenticators;
}

@Override
Expand All @@ -36,6 +41,7 @@ protected OpenApiReader buildReader(OpenAPIConfiguration openApiConfiguration)

if (reader instanceof RestletOpenApiReader restletOpenApiReader) {
restletOpenApiReader.setRouter(router);
restletOpenApiReader.setAuthenticators(authenticators);
}

reader.setConfiguration(openApiConfiguration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,28 @@
import io.swagger.v3.oas.integration.OpenApiConfigurationException;
import io.swagger.v3.oas.integration.OpenApiContextLocator;
import io.swagger.v3.oas.integration.api.OpenApiContext;
import java.util.List;
import org.restlet.engine.util.StringUtils;
import org.restlet.routing.Router;
import org.restlet.security.ChallengeAuthenticator;

public class RestletOpenApiContextBuilder
extends GenericOpenApiContextBuilder<RestletOpenApiContextBuilder> {
private Router router;

private List<ChallengeAuthenticator> authenticators = List.of();

public RestletOpenApiContextBuilder router(Router router) {
this.router = router;
return this;
}

public RestletOpenApiContextBuilder authenticators(
List<ChallengeAuthenticator> authenticators) {
this.authenticators = authenticators;
return this;
}

@Override
public OpenApiContext buildContext(boolean init) throws OpenApiConfigurationException {
if (StringUtils.isNullOrEmpty(ctxId)) {
Expand All @@ -38,7 +48,7 @@ public OpenApiContext buildContext(boolean init) throws OpenApiConfigurationExce
.getOpenApiContext(OpenApiContext.OPENAPI_CONTEXT_ID_DEFAULT);

ctx =
new RestletOpenApiContext(router)
new RestletOpenApiContext(router, authenticators)
.id(ctxId)
.openApiConfiguration(openApiConfiguration)
.parent(rootCtx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@
import io.swagger.v3.oas.models.media.MediaType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.restlet.Context;
import org.restlet.Restlet;
import org.restlet.data.ChallengeScheme;
import org.restlet.engine.resource.AnnotationInfo;
import org.restlet.engine.resource.AnnotationUtils;
import org.restlet.engine.resource.MethodAnnotationInfo;
Expand All @@ -42,6 +46,7 @@
import org.restlet.routing.Route;
import org.restlet.routing.Router;
import org.restlet.routing.TemplateRoute;
import org.restlet.security.ChallengeAuthenticator;
import org.restlet.service.MetadataService;

public class RestletOpenApiReader implements OpenApiReader {
Expand All @@ -50,6 +55,8 @@ public class RestletOpenApiReader implements OpenApiReader {

private Router router;

private List<ChallengeAuthenticator> authenticators = List.of();

private OpenAPIConfiguration config;

private final Paths paths = new Paths();
Expand All @@ -62,6 +69,10 @@ public void setRouter(Router router) {
this.router = router;
}

public void setAuthenticators(List<ChallengeAuthenticator> authenticators) {
this.authenticators = authenticators == null ? List.of() : authenticators;
}

@Override
public void setConfiguration(OpenAPIConfiguration openApiConfiguration) {
this.config = openApiConfiguration;
Expand Down Expand Up @@ -94,42 +105,76 @@ private OpenAPI processRouter(Router router) {

completeOpenApiInfo(router);

processRoutes(router, "", authenticators);

openApi.setComponents(components);
openApi.setOpenapi("3.1.0");
openApi.setSpecVersion(SpecVersion.V31);

return openApi;
}

private void processRoutes(
Router router, String pathPrefix, List<ChallengeAuthenticator> activeAuthenticators) {
List<Route> allRoutes = new ArrayList<>(router.getRoutes());
if (router.getDefaultRoute() != null) {
allRoutes.add(router.getDefaultRoute());
}

for (Route route : allRoutes) {
processRoute(route);
processRoute(route, pathPrefix, activeAuthenticators);
}

openApi.setComponents(components);
openApi.setOpenapi("3.1.0");
openApi.setSpecVersion(SpecVersion.V31);

return openApi;
}

private void processRoute(Route route) {
private void processRoute(
Route route, String pathPrefix, List<ChallengeAuthenticator> activeAuthenticators) {
if (route instanceof TemplateRoute templateRoute) {
String path = templateRoute.getTemplate().getPattern();
String path = pathPrefix + templateRoute.getTemplate().getPattern();

if (route.getNext() instanceof Finder finder) {
List<ChallengeAuthenticator> branchAuthenticators =
new ArrayList<>(activeAuthenticators);
Restlet next = unwrapAuthenticators(route.getNext(), branchAuthenticators);

if (next instanceof Finder finder) {
ServerResource serverResource = finder.find(null, null);

if (serverResource != null) {
List<String> pathVariableNames = templateRoute.getTemplate().getVariableNames();

processServerResource(serverResource, path, pathVariableNames);
processServerResource(
serverResource, path, pathVariableNames, branchAuthenticators);
}
} else if (next instanceof Router childRouter) {
processRoutes(childRouter, path, branchAuthenticators);
}
} else {
Context.getCurrentLogger().info("Route type ignored: " + route.getClass());
}
}

/**
* Unwraps {@link ChallengeAuthenticator}s found on the way to the route's actual target (a
* {@link Finder} or a child {@link Router}), collecting them along the way so that the
* operations they guard can be documented accordingly.
*
* @param current The current Restlet to inspect.
* @param collected The list of authenticators found so far, to be completed.
* @return The first non-authenticator Restlet found.
*/
private Restlet unwrapAuthenticators(Restlet current, List<ChallengeAuthenticator> collected) {
if (current instanceof ChallengeAuthenticator challengeAuthenticator) {
collected.add(challengeAuthenticator);
return unwrapAuthenticators(challengeAuthenticator.getNext(), collected);
}

return current;
}

private void processServerResource(
ServerResource serverResource, String operationPath, List<String> pathVariableNames) {
ServerResource serverResource,
String operationPath,
List<String> pathVariableNames,
List<ChallengeAuthenticator> activeAuthenticators) {
List<AnnotationInfo> annotations =
serverResource.isAnnotated()
? AnnotationUtils.getInstance().getAnnotations(serverResource.getClass())
Expand All @@ -146,6 +191,7 @@ private void processServerResource(

completePathParameters(operation, pathVariableNames);
completeOperation(serverResource, operation, methodAnnotationInfo);
applySecurity(operation, activeAuthenticators);

PathItem pathItem =
Optional.ofNullable(openApi.getPaths())
Expand All @@ -165,6 +211,40 @@ private void processServerResource(
}
}

/**
* Documents every {@link ChallengeAuthenticator} guarding this operation: registers a {@link
* SecurityScheme} for each of them, and requires the mandatory ones (those for which {@link
* ChallengeAuthenticator#isOptional()} is {@code false}) on the operation.
*/
private void applySecurity(
Operation operation, List<ChallengeAuthenticator> activeAuthenticators) {
if (activeAuthenticators.isEmpty()) {
return;
}

SecurityRequirement mandatoryRequirement = new SecurityRequirement();

for (ChallengeAuthenticator authenticator : activeAuthenticators) {
ChallengeScheme scheme = authenticator.getScheme();

components.addSecuritySchemes(scheme.getName(), toSecurityScheme(scheme));

if (!authenticator.isOptional()) {
mandatoryRequirement.addList(scheme.getName());
}
}

if (!mandatoryRequirement.isEmpty()) {
operation.setSecurity(List.of(mandatoryRequirement));
}
}

private SecurityScheme toSecurityScheme(ChallengeScheme scheme) {
return new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme(scheme.getTechnicalName().toLowerCase());
}

private void completeOpenApiInfo(Router router) {
var applicationClassName = router.getApplication().getClass().getSimpleName();

Expand Down
Loading
Loading