From f5197f52886d988b313f82e2e0bc11b6bf98035a Mon Sep 17 00:00:00 2001 From: Thierry Boileau Date: Thu, 9 Jul 2026 22:06:44 +0200 Subject: [PATCH] Enhance support of openAPI specification (Restlet routers/sub-routers, security) --- org.restlet.ext.openapi/pom.xml | 6 + .../ext/openapi/OpenApiApplication.java | 31 ++++-- .../openapi/OpenApiSpecificationRestlet.java | 13 ++- .../internal/RestletOpenApiContext.java | 8 +- .../RestletOpenApiContextBuilder.java | 12 +- .../internal/RestletOpenApiReader.java | 104 ++++++++++++++++-- .../ext/openapi/OpenApiGenerationTest.java | 75 +++++++++++-- .../BasicAuthenticationRootRouterExample.java | 57 ++++++++++ .../BasicAuthenticationSubRouterExample.java | 58 ++++++++++ .../BookResources.java} | 15 +-- .../ext/openapi/example/LibraryExample.java | 25 +++++ .../ext/openapi/example/SubRouterExample.java | 37 +++++++ ...ic-authentication-root-router-openapi.yaml | 94 ++++++++++++++++ ...asic-authentication-subrouter-openapi.yaml | 94 ++++++++++++++++ .../test/resources/sub-router-openapi.yaml | 82 ++++++++++++++ 15 files changed, 665 insertions(+), 46 deletions(-) create mode 100644 org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BasicAuthenticationRootRouterExample.java create mode 100644 org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BasicAuthenticationSubRouterExample.java rename org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/{LibraryExample.java => example/BookResources.java} (86%) create mode 100644 org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/LibraryExample.java create mode 100644 org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/SubRouterExample.java create mode 100644 org.restlet.ext.openapi/src/test/resources/basic-authentication-root-router-openapi.yaml create mode 100644 org.restlet.ext.openapi/src/test/resources/basic-authentication-subrouter-openapi.yaml create mode 100644 org.restlet.ext.openapi/src/test/resources/sub-router-openapi.yaml diff --git a/org.restlet.ext.openapi/pom.xml b/org.restlet.ext.openapi/pom.xml index 9d32ce128d..930b4551f0 100644 --- a/org.restlet.ext.openapi/pom.xml +++ b/org.restlet.ext.openapi/pom.xml @@ -76,6 +76,12 @@ ${lib-junit-version} test + + org.junit.jupiter + junit-jupiter-params + ${lib-junit-version} + test + org.restlet diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiApplication.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiApplication.java index 90c2a58553..0dfea02b7f 100644 --- a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiApplication.java +++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiApplication.java @@ -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 { /** @@ -30,10 +33,11 @@ public Restlet getInboundRoot() { if (!documented) { synchronized (this) { if (!documented) { - Router rootRouter = getNextRouter(inboundRoot); + List authenticators = new ArrayList<>(); + Router rootRouter = getNextRouter(inboundRoot, authenticators); if (!documented && rootRouter != null) { - attachOpenApiSpecificationRestlet(rootRouter); + attachOpenApiSpecificationRestlet(rootRouter, authenticators); documented = true; } } @@ -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 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 authenticators) { + getOpenApiSpecificationRestlet(router, authenticators) + .attach(router, getOpenApiSpecificationPath()); documented = true; } @@ -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 authenticators) { + return new OpenApiSpecificationRestlet(router, authenticators); } } diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiSpecificationRestlet.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiSpecificationRestlet.java index bf3039f0c7..ab570942f3 100644 --- a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiSpecificationRestlet.java +++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/OpenApiSpecificationRestlet.java @@ -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); @@ -36,9 +37,17 @@ public class OpenApiSpecificationRestlet extends Restlet { private final Router router; - public OpenApiSpecificationRestlet(Router router) { + private final List authenticators; + + private final String openApiContextId; + + public OpenApiSpecificationRestlet(Router router, List 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 @@ -66,7 +75,9 @@ private Representation getOpenApiDefinitionAsRepresentation(Request request) { try { var context = new RestletOpenApiContextBuilder() + .ctxId(openApiContextId) .router(router) + .authenticators(authenticators) .openApiConfiguration(oasConfig) .buildContext(true); diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContext.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContext.java index 1945545d2d..7751b429d7 100644 --- a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContext.java +++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContext.java @@ -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 { private final Router router; - public RestletOpenApiContext(Router router) { + private final List authenticators; + + public RestletOpenApiContext(Router router, List authenticators) { this.router = router; + this.authenticators = authenticators; } @Override @@ -36,6 +41,7 @@ protected OpenApiReader buildReader(OpenAPIConfiguration openApiConfiguration) if (reader instanceof RestletOpenApiReader restletOpenApiReader) { restletOpenApiReader.setRouter(router); + restletOpenApiReader.setAuthenticators(authenticators); } reader.setConfiguration(openApiConfiguration); diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContextBuilder.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContextBuilder.java index d688d0e7f4..5f85bf2bd2 100644 --- a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContextBuilder.java +++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiContextBuilder.java @@ -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 { private Router router; + private List authenticators = List.of(); + public RestletOpenApiContextBuilder router(Router router) { this.router = router; return this; } + public RestletOpenApiContextBuilder authenticators( + List authenticators) { + this.authenticators = authenticators; + return this; + } + @Override public OpenApiContext buildContext(boolean init) throws OpenApiConfigurationException { if (StringUtils.isNullOrEmpty(ctxId)) { @@ -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); diff --git a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiReader.java b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiReader.java index 5b0fdc875e..231adec3e7 100644 --- a/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiReader.java +++ b/org.restlet.ext.openapi/src/main/java/org/restlet/ext/openapi/internal/RestletOpenApiReader.java @@ -26,6 +26,8 @@ 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; @@ -33,6 +35,8 @@ 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; @@ -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 { @@ -50,6 +55,8 @@ public class RestletOpenApiReader implements OpenApiReader { private Router router; + private List authenticators = List.of(); + private OpenAPIConfiguration config; private final Paths paths = new Paths(); @@ -62,6 +69,10 @@ public void setRouter(Router router) { this.router = router; } + public void setAuthenticators(List authenticators) { + this.authenticators = authenticators == null ? List.of() : authenticators; + } + @Override public void setConfiguration(OpenAPIConfiguration openApiConfiguration) { this.config = openApiConfiguration; @@ -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 activeAuthenticators) { List 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 activeAuthenticators) { if (route instanceof TemplateRoute templateRoute) { - String path = templateRoute.getTemplate().getPattern(); + String path = pathPrefix + templateRoute.getTemplate().getPattern(); - if (route.getNext() instanceof Finder finder) { + List 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 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 collected) { + if (current instanceof ChallengeAuthenticator challengeAuthenticator) { + collected.add(challengeAuthenticator); + return unwrapAuthenticators(challengeAuthenticator.getNext(), collected); + } + + return current; + } + private void processServerResource( - ServerResource serverResource, String operationPath, List pathVariableNames) { + ServerResource serverResource, + String operationPath, + List pathVariableNames, + List activeAuthenticators) { List annotations = serverResource.isAnnotated() ? AnnotationUtils.getInstance().getAnnotations(serverResource.getClass()) @@ -146,6 +191,7 @@ private void processServerResource( completePathParameters(operation, pathVariableNames); completeOperation(serverResource, operation, methodAnnotationInfo); + applySecurity(operation, activeAuthenticators); PathItem pathItem = Optional.ofNullable(openApi.getPaths()) @@ -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 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(); diff --git a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/OpenApiGenerationTest.java b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/OpenApiGenerationTest.java index 155d9a4bda..fff9e4114b 100644 --- a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/OpenApiGenerationTest.java +++ b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/OpenApiGenerationTest.java @@ -15,30 +15,58 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import org.junit.jupiter.api.Test; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.restlet.Application; import org.restlet.Component; +import org.restlet.Server; +import org.restlet.data.ChallengeResponse; +import org.restlet.data.ChallengeScheme; import org.restlet.data.Protocol; import org.restlet.ext.openapi.OpenApiSpecifications.ValidationResult.Invalid; +import org.restlet.ext.openapi.example.BasicAuthenticationRootRouterExample; +import org.restlet.ext.openapi.example.BasicAuthenticationSubRouterExample; +import org.restlet.ext.openapi.example.LibraryExample; +import org.restlet.ext.openapi.example.SubRouterExample; import org.restlet.representation.Representation; import org.restlet.resource.ClientResource; class OpenApiGenerationTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()); - @Test - void testLibraryApplicationOpenApi() throws Exception { - var application = new LibraryExample.LibraryApplication(); - var component = new Component(); + Component component; + Server server; - var server = + @BeforeEach + void setup() throws Exception { + component = new Component(); + server = component .getServers() .add( Protocol.HTTP, 0 // 0 = let the OS find an ephemeral port ); + component.start(); + } + @AfterEach + void tearDown() throws Exception { + component.stop(); + } + + @ParameterizedTest(name = "test {0}") + @MethodSource("openApiApplicationTestCases") + void testOpenApi( + final String testCase, + final Application application, + final String oas3File, + final ChallengeResponse challengeResponse) + throws Exception { component.getDefaultHost().attach(application); - component.start(); int actualPort = server.getEphemeralPort(); @@ -48,6 +76,10 @@ void testLibraryApplicationOpenApi() throws Exception { new ClientResource( "http://localhost:" + actualPort + OPENAPI_SPECIFICATION_DEFAULT_PATH); + if (challengeResponse != null) { + clientResource.setChallengeResponse(challengeResponse); + } + Representation representation = clientResource.get(); if (!clientResource.getStatus().isSuccess()) { @@ -56,8 +88,7 @@ void testLibraryApplicationOpenApi() throws Exception { String actualYamlResponse = parseAndFormatYaml(representation.getText()); String expectedYamlResponse = - parseAndFormatYaml( - OpenApiSpecifications.readFromClasspath("/library-openapi.yaml")); + parseAndFormatYaml(OpenApiSpecifications.readFromClasspath(oas3File)); assertEquals(expectedYamlResponse, actualYamlResponse); @@ -68,6 +99,32 @@ void testLibraryApplicationOpenApi() throws Exception { } } + public static Stream openApiApplicationTestCases() { + return Stream.of( + Arguments.of( + "library", + new LibraryExample.LibraryApplication(), + "/library-openapi.yaml", + null), + Arguments.of( + "sub-router", + new SubRouterExample.SubRouterApplication(), + "/sub-router-openapi.yaml", + null), + Arguments.of( + "basic-authentication-root-router", + new BasicAuthenticationRootRouterExample + .BasicAuthenticationRootRouterApplication(), + "/basic-authentication-root-router-openapi.yaml", + new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "login", "password")), + Arguments.of( + "basic-authentication-subrouter", + new BasicAuthenticationSubRouterExample + .BasicAuthenticationSubRouterApplication(), + "/basic-authentication-subrouter-openapi.yaml", + new ChallengeResponse(ChallengeScheme.HTTP_BASIC, "login", "password"))); + } + private String parseAndFormatYaml(String yaml) { try { var tree = OBJECT_MAPPER.readTree(yaml); diff --git a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BasicAuthenticationRootRouterExample.java b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BasicAuthenticationRootRouterExample.java new file mode 100644 index 0000000000..dbe7b6240c --- /dev/null +++ b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BasicAuthenticationRootRouterExample.java @@ -0,0 +1,57 @@ +/** + * Copyright 2005-2026 Qlik + *

+ * The content of this file is subject to the terms of the Apache 2.0 open + * source license available at https://www.opensource.org/licenses/apache-2.0 + *

+ * Restlet is a registered trademark of QlikTech International AB. + */ +package org.restlet.ext.openapi.example; + +import java.util.Arrays; +import org.restlet.data.ChallengeScheme; +import org.restlet.ext.openapi.OpenApiApplication; +import org.restlet.routing.Router; +import org.restlet.security.ChallengeAuthenticator; +import org.restlet.security.MapVerifier; + +public class BasicAuthenticationRootRouterExample { + + public static class BasicAuthenticationRootRouterApplication extends OpenApiApplication { + @Override + public org.restlet.Restlet createInboundRoot() { + var router = new Router(getContext()); + router.attach("/api", booksRouter()); + + var authenticator = + new ChallengeAuthenticator(getContext(), ChallengeScheme.HTTP_BASIC, "realm"); + authenticator.setVerifier(new TestVerifier()); + authenticator.setNext(router); + + return authenticator; + } + + private Router booksRouter() { + var router = new Router(getContext()); + router.attach("/books", BookResources.BooksResource.class); + router.attach("/books/{bookId}", BookResources.BookResource.class); + return router; + } + } + + public static class TestVerifier extends MapVerifier { + public TestVerifier() { + getLocalSecrets().put("login", "password".toCharArray()); + } + + @Override + public int verify(String identifier, char[] inputSecret) { + try { + return super.verify(identifier, inputSecret); + } finally { + // Clear secret from memory as soon as possible + Arrays.fill(inputSecret, '\000'); + } + } + } +} diff --git a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BasicAuthenticationSubRouterExample.java b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BasicAuthenticationSubRouterExample.java new file mode 100644 index 0000000000..667e8c2d2f --- /dev/null +++ b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BasicAuthenticationSubRouterExample.java @@ -0,0 +1,58 @@ +/** + * Copyright 2005-2026 Qlik + *

+ * The content of this file is subject to the terms of the Apache 2.0 open + * source license available at https://www.opensource.org/licenses/apache-2.0 + *

+ * Restlet is a registered trademark of QlikTech International AB. + */ +package org.restlet.ext.openapi.example; + +import java.util.Arrays; +import org.restlet.data.ChallengeScheme; +import org.restlet.ext.openapi.OpenApiApplication; +import org.restlet.routing.Router; +import org.restlet.security.ChallengeAuthenticator; +import org.restlet.security.MapVerifier; + +public class BasicAuthenticationSubRouterExample { + + public static class BasicAuthenticationSubRouterApplication extends OpenApiApplication { + @Override + public org.restlet.Restlet createInboundRoot() { + + var authenticator = + new ChallengeAuthenticator(getContext(), ChallengeScheme.HTTP_BASIC, "realm"); + authenticator.setVerifier(new TestVerifier()); + authenticator.setNext(booksRouter()); + + var router = new Router(getContext()); + router.attach("/api", authenticator); + + return router; + } + + private Router booksRouter() { + var router = new Router(getContext()); + router.attach("/books", BookResources.BooksResource.class); + router.attach("/books/{bookId}", BookResources.BookResource.class); + return router; + } + } + + public static class TestVerifier extends MapVerifier { + public TestVerifier() { + getLocalSecrets().put("login", "password".toCharArray()); + } + + @Override + public int verify(String identifier, char[] inputSecret) { + try { + return super.verify(identifier, inputSecret); + } finally { + // Clear secret from memory as soon as possible + Arrays.fill(inputSecret, '\000'); + } + } + } +} diff --git a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/LibraryExample.java b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BookResources.java similarity index 86% rename from org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/LibraryExample.java rename to org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BookResources.java index 0657b8c09a..06329ec01f 100644 --- a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/LibraryExample.java +++ b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/BookResources.java @@ -6,7 +6,7 @@ *

* Restlet is a registered trademark of QlikTech International AB. */ -package org.restlet.ext.openapi; +package org.restlet.ext.openapi.example; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -22,9 +22,8 @@ import org.restlet.resource.Get; import org.restlet.resource.Post; import org.restlet.resource.ServerResource; -import org.restlet.routing.Router; -public class LibraryExample { +public class BookResources { private static final List BOOKS = List.of( new Book("1", "The Great Gatsby", "F. Scott Fitzgerald"), @@ -79,14 +78,4 @@ public void addBook(Book book) { getResponse().setLocationRef(locationRef); } } - - public static class LibraryApplication extends OpenApiApplication { - @Override - public org.restlet.Restlet createInboundRoot() { - var router = new Router(getContext()); - router.attach("/books", BooksResource.class); - router.attach("/books/{bookId}", BookResource.class); - return router; - } - } } diff --git a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/LibraryExample.java b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/LibraryExample.java new file mode 100644 index 0000000000..041ed7387f --- /dev/null +++ b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/LibraryExample.java @@ -0,0 +1,25 @@ +/** + * Copyright 2005-2026 Qlik + *

+ * The content of this file is subject to the terms of the Apache 2.0 open + * source license available at https://www.opensource.org/licenses/apache-2.0 + *

+ * Restlet is a registered trademark of QlikTech International AB. + */ +package org.restlet.ext.openapi.example; + +import org.restlet.ext.openapi.OpenApiApplication; +import org.restlet.routing.Router; + +public class LibraryExample { + + public static class LibraryApplication extends OpenApiApplication { + @Override + public org.restlet.Restlet createInboundRoot() { + var router = new Router(getContext()); + router.attach("/books", BookResources.BooksResource.class); + router.attach("/books/{bookId}", BookResources.BookResource.class); + return router; + } + } +} diff --git a/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/SubRouterExample.java b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/SubRouterExample.java new file mode 100644 index 0000000000..56095ced90 --- /dev/null +++ b/org.restlet.ext.openapi/src/test/java/org/restlet/ext/openapi/example/SubRouterExample.java @@ -0,0 +1,37 @@ +/** + * Copyright 2005-2026 Qlik + *

+ * The content of this file is subject to the terms of the Apache 2.0 open + * source license available at https://www.opensource.org/licenses/apache-2.0 + *

+ * Restlet is a registered trademark of QlikTech International AB. + */ +package org.restlet.ext.openapi.example; + +import org.restlet.ext.openapi.OpenApiApplication; +import org.restlet.routing.Router; + +public class SubRouterExample { + + public static class SubRouterApplication extends OpenApiApplication { + @Override + public org.restlet.Restlet createInboundRoot() { + var router = new Router(getContext()); + router.attach("/api", apiRouter()); + return router; + } + + private Router apiRouter() { + var router = new Router(getContext()); + router.attach("/items", booksRouter()); + return router; + } + + private Router booksRouter() { + var router = new Router(getContext()); + router.attach("/books", BookResources.BooksResource.class); + router.attach("/books/{bookId}", BookResources.BookResource.class); + return router; + } + } +} diff --git a/org.restlet.ext.openapi/src/test/resources/basic-authentication-root-router-openapi.yaml b/org.restlet.ext.openapi/src/test/resources/basic-authentication-root-router-openapi.yaml new file mode 100644 index 0000000000..b58e5b2bbd --- /dev/null +++ b/org.restlet.ext.openapi/src/test/resources/basic-authentication-root-router-openapi.yaml @@ -0,0 +1,94 @@ +--- +openapi: "3.1.0" +info: + title: "BasicAuthenticationRootRouter REST API" + version: "1.0.0" +paths: + /api/books: + get: + summary: "Get a list of all books" + operationId: "getBooks" + parameters: + - name: "filter" + in: "query" + description: "Filter books" + required: false + schema: + type: "string" + responses: + "200": + description: "Success" + content: + application/json: + schema: + type: "array" + items: + $ref: "#/components/schemas/Book" + security: + - HTTP_BASIC: [] + post: + summary: "Add a new book" + operationId: "addBook" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + responses: + "201": + description: "201 response" + headers: + Location: + style: "simple" + schema: + type: "string" + security: + - HTTP_BASIC: [] + /api/books/{bookId}: + get: + summary: "Get a book by ID" + operationId: "getBook" + parameters: + - name: "bookId" + in: "path" + required: true + schema: + type: "string" + responses: + "200": + description: "Success" + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + security: + - HTTP_BASIC: [] + delete: + summary: "Delete a book by ID" + operationId: "deleteBook" + parameters: + - name: "bookId" + in: "path" + required: true + schema: + type: "string" + responses: + "200": + description: "Success" + security: + - HTTP_BASIC: [] +components: + schemas: + Book: + type: "object" + properties: + id: + type: "string" + title: + type: "string" + author: + type: "string" + securitySchemes: + HTTP_BASIC: + type: "http" + scheme: "basic" diff --git a/org.restlet.ext.openapi/src/test/resources/basic-authentication-subrouter-openapi.yaml b/org.restlet.ext.openapi/src/test/resources/basic-authentication-subrouter-openapi.yaml new file mode 100644 index 0000000000..2d8b9981b3 --- /dev/null +++ b/org.restlet.ext.openapi/src/test/resources/basic-authentication-subrouter-openapi.yaml @@ -0,0 +1,94 @@ +--- +openapi: "3.1.0" +info: + title: "BasicAuthenticationSubRouter REST API" + version: "1.0.0" +paths: + /api/books: + get: + summary: "Get a list of all books" + operationId: "getBooks" + parameters: + - name: "filter" + in: "query" + description: "Filter books" + required: false + schema: + type: "string" + responses: + "200": + description: "Success" + content: + application/json: + schema: + type: "array" + items: + $ref: "#/components/schemas/Book" + security: + - HTTP_BASIC: [] + post: + summary: "Add a new book" + operationId: "addBook" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + responses: + "201": + description: "201 response" + headers: + Location: + style: "simple" + schema: + type: "string" + security: + - HTTP_BASIC: [] + /api/books/{bookId}: + get: + summary: "Get a book by ID" + operationId: "getBook" + parameters: + - name: "bookId" + in: "path" + required: true + schema: + type: "string" + responses: + "200": + description: "Success" + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + security: + - HTTP_BASIC: [] + delete: + summary: "Delete a book by ID" + operationId: "deleteBook" + parameters: + - name: "bookId" + in: "path" + required: true + schema: + type: "string" + responses: + "200": + description: "Success" + security: + - HTTP_BASIC: [] +components: + schemas: + Book: + type: "object" + properties: + id: + type: "string" + title: + type: "string" + author: + type: "string" + securitySchemes: + HTTP_BASIC: + type: "http" + scheme: "basic" diff --git a/org.restlet.ext.openapi/src/test/resources/sub-router-openapi.yaml b/org.restlet.ext.openapi/src/test/resources/sub-router-openapi.yaml new file mode 100644 index 0000000000..5888fc5c8e --- /dev/null +++ b/org.restlet.ext.openapi/src/test/resources/sub-router-openapi.yaml @@ -0,0 +1,82 @@ +--- +openapi: "3.1.0" +info: + title: "SubRouter REST API" + version: "1.0.0" +paths: + /api/items/books: + get: + summary: "Get a list of all books" + operationId: "getBooks" + parameters: + - name: "filter" + in: "query" + description: "Filter books" + required: false + schema: + type: "string" + responses: + "200": + description: "Success" + content: + application/json: + schema: + type: "array" + items: + $ref: "#/components/schemas/Book" + post: + summary: "Add a new book" + operationId: "addBook" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + responses: + "201": + description: "201 response" + headers: + Location: + style: "simple" + schema: + type: "string" + /api/items/books/{bookId}: + get: + summary: "Get a book by ID" + operationId: "getBook" + parameters: + - name: "bookId" + in: "path" + required: true + schema: + type: "string" + responses: + "200": + description: "Success" + content: + application/json: + schema: + $ref: "#/components/schemas/Book" + delete: + summary: "Delete a book by ID" + operationId: "deleteBook" + parameters: + - name: "bookId" + in: "path" + required: true + schema: + type: "string" + responses: + "200": + description: "Success" +components: + schemas: + Book: + type: "object" + properties: + id: + type: "string" + title: + type: "string" + author: + type: "string"