From d17195a675fbc72247fa3178c9ab039ea7703c17 Mon Sep 17 00:00:00 2001 From: joseegarcia Date: Fri, 24 Jul 2026 13:34:41 +0200 Subject: [PATCH 1/5] Support loading specifications from a remote URL (Apicurio / HTTP) `specFile.filePath` may now be an http/https/ftp/file URL; the spec is fetched from that URL at generation time instead of the filesystem/classpath. This enables consuming specs published in an Apicurio Registry (artifacts served over HTTP) and any URL-addressable source, for both the OpenAPI and AsyncAPI generators. - PathUtil.isRemoteUri detects URL-scheme paths. - SchemaUtil.readFile fetches remote specs directly (shared main-spec reader for both generators). - New RemoteFileLocation resolves external $refs relative to a remote spec's URL. - AsyncApiGenerator / BaseAsyncApiHandler resolve remote spec locations. - ApiTool.nodeFromFile handles URL references. - OpenApiGenerator uses the URL parent as the $ref base when the spec is remote. Adds PathUtilTest and a SchemaUtil URL-loading test (file:// URL, network-free). Documents the Apicurio/URL usage in the README. Bumps version 6.5.1 -> 6.6.0 (new feature). Closes joseegman-idoneea/scs-multiapi-plugin#7 Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 28 ++++++++++++++ multiapi-engine/pom.xml | 2 +- .../plugin/asyncapi/AsyncApiGenerator.java | 10 +++++ .../asyncapi/handler/BaseAsyncApiHandler.java | 5 +++ .../common/files/RemoteFileLocation.java | 37 +++++++++++++++++++ .../plugin/common/tools/ApiTool.java | 6 ++- .../plugin/common/tools/PathUtil.java | 13 +++++++ .../plugin/common/tools/SchemaUtil.java | 10 +++++ .../plugin/openapi/OpenApiGenerator.java | 7 +++- .../plugin/common/tools/PathUtilTest.java | 34 +++++++++++++++++ .../plugin/common/tools/SchemaUtilTest.java | 22 +++++++++++ scs-multiapi-gradle-plugin/build.gradle | 6 +-- scs-multiapi-maven-plugin/pom.xml | 4 +- 13 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/files/RemoteFileLocation.java create mode 100644 multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java diff --git a/README.md b/README.md index 0805516f..9970232d 100644 --- a/README.md +++ b/README.md @@ -834,3 +834,31 @@ from a local JAR containing `contracts/event-api.yml` in its resources: ``` + +## Loading specifications from a remote URL (Apicurio Registry, HTTP) + +`filePath` also accepts a remote URL (`http`, `https`, `ftp` or `file` scheme). When it does, +the specification is downloaded at generation time instead of being read from the filesystem or +the classpath. This works for both OpenAPI and AsyncAPI specs. + +This is the mechanism to consume a spec published in an **Apicurio Registry**, whose artifacts are +served over HTTP. Point `filePath` at the artifact's content endpoint, for example: + +```xml + + https://my-apicurio-host/apis/registry/v2/groups/default/artifacts/my-api + com.sngular.apigenerator.openapi.api + com.sngular.apigenerator.openapi.model + +``` + +Gradle: + +```groovy +filePath = 'https://my-apicurio-host/apis/registry/v2/groups/default/artifacts/my-api' +``` + +Notes: +- Only publicly reachable URLs are supported for now; authenticated registries (tokens/headers) + are not yet handled. +- External `$ref`s are resolved relative to the spec's URL when the spec itself is remote. diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index 4a6918a1..be065722 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 6.5.3 + 6.6.0 jar diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java index fd3e49b5..0d87631a 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java @@ -26,6 +26,7 @@ import com.sngular.api.generator.plugin.common.files.ClasspathFileLocation; import com.sngular.api.generator.plugin.common.files.DirectoryFileLocation; import com.sngular.api.generator.plugin.common.files.FileLocation; +import com.sngular.api.generator.plugin.common.files.RemoteFileLocation; import com.sngular.api.generator.plugin.common.tools.PathUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.ImmutablePair; @@ -85,6 +86,15 @@ public final void processFileSpec(final List specsListFile) { private static Pair resolveYmlLocation(final String ymlFilePath) throws FileNotFoundException { log.debug("Resolving YAML file location:{}", ymlFilePath); + if (PathUtil.isRemoteUri(ymlFilePath)) { + log.debug("Loading spec from remote URL"); + try { + final URI uri = URI.create(ymlFilePath); + return new ImmutablePair<>(uri.toURL().openStream(), new RemoteFileLocation(uri.resolve("."))); + } catch (final IOException e) { + throw new FileNotFoundException("Could not open remote YAML file: " + ymlFilePath); + } + } final InputStream classPathInput = AsyncApiGenerator.class.getClassLoader().getResourceAsStream(ymlFilePath); final InputStream ymlFile; final FileLocation ymlParentPath; diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/handler/BaseAsyncApiHandler.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/handler/BaseAsyncApiHandler.java index b3d314b6..ff030d8f 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/handler/BaseAsyncApiHandler.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/handler/BaseAsyncApiHandler.java @@ -21,6 +21,8 @@ import com.sngular.api.generator.plugin.asyncapi.parameter.SpecFile; import com.sngular.api.generator.plugin.asyncapi.template.TemplateFactory; import com.sngular.api.generator.plugin.common.files.ClasspathFileLocation; +import com.sngular.api.generator.plugin.common.files.RemoteFileLocation; +import com.sngular.api.generator.plugin.common.tools.PathUtil; import com.sngular.api.generator.plugin.common.files.DirectoryFileLocation; import com.sngular.api.generator.plugin.common.files.FileLocation; import com.sngular.api.generator.plugin.common.model.CommonSpecFile; @@ -108,6 +110,9 @@ protected BaseAsyncApiHandler( } protected static FileLocation resolveYmlLocation(final String ymlFilePath) throws IOException, URISyntaxException { + if (PathUtil.isRemoteUri(ymlFilePath)) { + return new RemoteFileLocation(URI.create(ymlFilePath).resolve(".")); + } final var classPathInput = BaseAsyncApiHandler.class.getClassLoader().getResource(ymlFilePath); if (Objects.nonNull(classPathInput)) { return new ClasspathFileLocation(getParentUri(classPathInput.toURI())); diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/files/RemoteFileLocation.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/files/RemoteFileLocation.java new file mode 100644 index 00000000..91c8d70e --- /dev/null +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/files/RemoteFileLocation.java @@ -0,0 +1,37 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * * License, v. 2.0. If a copy of the MPL was not distributed with this + * * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package com.sngular.api.generator.plugin.common.files; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; + +/** + * {@link FileLocation} for specifications loaded from a remote URL (e.g. an Apicurio Registry + * artifact served over HTTP). Relative sibling files (external {@code $ref}s) are resolved + * against the spec's base URI and fetched over the same protocol. + */ +public class RemoteFileLocation implements FileLocation { + + private final URI baseUri; + + public RemoteFileLocation(final URI baseUri) { + this.baseUri = baseUri; + } + + @Override + public InputStream getFileAtLocation(final String filename) throws IOException { + final URL target = baseUri.resolve(filename).toURL(); + return target.openStream(); + } + + @Override + public URI path() { + return baseUri; + } +} diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java index d6872a2e..57353ab3 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java @@ -453,8 +453,10 @@ public static boolean hasComponents(final JsonNode node) { public static JsonNode nodeFromFile(final FileLocation ymlParent, final String filePath, final FactoryTypeEnum factoryTypeEnum) throws IOException { final InputStream file; - // Check if path is absolute first - if (PathUtil.isAbsolutePath(filePath)) { + // Remote references (http/https/ftp/file URLs) are fetched directly from their URL. + if (PathUtil.isRemoteUri(filePath)) { + file = new java.net.URL(filePath).openStream(); + } else if (PathUtil.isAbsolutePath(filePath)) { // For absolute paths, open directly file = new FileInputStream(filePath); } else if (filePath.startsWith(PACKAGE_SEPARATOR_STR) || filePath.matches("^\\w.*$")) { diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java index 2c8eee6c..6513b48d 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java @@ -37,4 +37,17 @@ public static boolean isAbsolutePath(final String filePath) { return false; } } + + /** + * Checks whether a spec path points to a remote/URL location that must be fetched via a + * {@link java.net.URL} stream rather than the filesystem or classpath. Enables loading + * specifications from HTTP(S) endpoints such as an Apicurio Registry artifact. + * + * @param filePath the spec path to check + * @return true if the path uses an http, https, ftp or file scheme + */ + public static boolean isRemoteUri(final String filePath) { + return StringUtils.isNotEmpty(filePath) + && filePath.matches("^(?i)(https?|ftp|file)://.*"); + } } diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java index 95fd95ac..1360a470 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java @@ -137,6 +137,12 @@ private static String readFile(final URI rootFilePath, final String filePath) th throw new IllegalArgumentException("File Path cannot be empty"); } + // Remote specifications (http/https/ftp/file URLs, e.g. an Apicurio Registry artifact) are + // fetched directly from their URL, bypassing classpath and filesystem resolution. + if (PathUtil.isRemoteUri(filePath)) { + return readFromUrl(new URL(filePath)); + } + // Normalize the incoming filePath: remove leading './' and replace backslashes with forward slashes final String cleaned = cleanUpPath(filePath).replace('\\', '/'); @@ -161,6 +167,10 @@ private static String readFile(final URI rootFilePath, final String filePath) th } } } + return readFromUrl(fileURL); + } + + private static String readFromUrl(final URL fileURL) { final var sb = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(fileURL.openStream()))) { String inputLine; diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java index 6add6dd0..fcbffce1 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/OpenApiGenerator.java @@ -8,6 +8,7 @@ import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; @@ -23,6 +24,7 @@ import com.sngular.api.generator.plugin.common.tools.ApiTool; import com.sngular.api.generator.plugin.common.tools.MapperContentUtil; import com.sngular.api.generator.plugin.common.tools.MapperUtil; +import com.sngular.api.generator.plugin.common.tools.PathUtil; import com.sngular.api.generator.plugin.exception.GeneratorTemplateException; import com.sngular.api.generator.plugin.openapi.exception.DuplicateModelClassException; import com.sngular.api.generator.plugin.openapi.model.AuthObject; @@ -99,7 +101,10 @@ private void processFile(final SpecFile specFile) { final JsonNode openAPI = OpenApiUtil.getPojoFromSpecFile(baseDir, specFile); OpenApiUtil.mergeWebhooksIntoPaths(openAPI); - OpenApiUtil.solvePathRefs(openAPI, baseDir.resolve(specFile.getFilePath()).getParent().toUri()); + final URI specBaseUri = PathUtil.isRemoteUri(specFile.getFilePath()) + ? URI.create(specFile.getFilePath()).resolve(".") + : baseDir.resolve(specFile.getFilePath()).getParent().toUri(); + OpenApiUtil.solvePathRefs(openAPI, specBaseUri); final String clientPackage = specFile.getClientPackage(); if (specFile.isCallMode()) { diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java new file mode 100644 index 00000000..a5be4cc5 --- /dev/null +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java @@ -0,0 +1,34 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * * License, v. 2.0. If a copy of the MPL was not distributed with this + * * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package com.sngular.api.generator.plugin.common.tools; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class PathUtilTest { + + @Test + void isRemoteUriDetectsUrlSchemes() { + assertTrue(PathUtil.isRemoteUri("http://registry/apis/registry/v2/groups/g/artifacts/a")); + assertTrue(PathUtil.isRemoteUri("https://example.com/openapi.yml")); + assertTrue(PathUtil.isRemoteUri("HTTPS://EXAMPLE.COM/openapi.yml")); + assertTrue(PathUtil.isRemoteUri("ftp://host/spec.yml")); + assertTrue(PathUtil.isRemoteUri("file:///tmp/spec.yml")); + } + + @Test + void isRemoteUriRejectsLocalPaths() { + assertFalse(PathUtil.isRemoteUri("./src/main/resources/api/api.yml")); + assertFalse(PathUtil.isRemoteUri("/absolute/path/api.yml")); + assertFalse(PathUtil.isRemoteUri("contracts/event-api.yml")); + assertFalse(PathUtil.isRemoteUri("C:\\api\\api.yml")); + assertFalse(PathUtil.isRemoteUri("")); + assertFalse(PathUtil.isRemoteUri(null)); + } +} diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/SchemaUtilTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/SchemaUtilTest.java index b28d25c8..1656c199 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/SchemaUtilTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/SchemaUtilTest.java @@ -42,4 +42,26 @@ void shouldResolveBackslashRelativePath() throws Exception { try { Files.walk(tmp).map(Path::toFile).sorted((a,b)->-a.compareTo(b)).forEach(java.io.File::delete); } catch (Exception ignored) {} } } + + @Test + void shouldLoadSpecFromUrl() throws Exception { + // Loading a spec from a URL is the mechanism used for remote sources (e.g. an Apicurio + // Registry artifact). A file:// URL exercises the same URL code path without network access. + final Path tmp = Files.createTempDirectory("schema-util-url-test"); + try { + final Path openapiFile = tmp.resolve("openapi.yml"); + Files.writeString(openapiFile, "components:\n schemas:\n Pet:\n type: object\n"); + + final String url = openapiFile.toUri().toString(); // file:///... + assertTrue(PathUtil.isRemoteUri(url), "A file:// path must be detected as a remote URI"); + + // rootFilePath is irrelevant for a remote URL; it is fetched directly. + final JsonNode node = SchemaUtil.getPojoFromRef(tmp.toUri(), url); + + assertNotNull(node, "El nodo no debe ser nulo"); + assertTrue(node.path("components").path("schemas").has("Pet"), "Debe contener el schema 'Pet'"); + } finally { + try { Files.walk(tmp).map(Path::toFile).sorted((a, b) -> -a.compareTo(b)).forEach(java.io.File::delete); } catch (Exception ignored) {} + } + } } diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index 82437b6d..92bca8f8 100644 --- a/scs-multiapi-gradle-plugin/build.gradle +++ b/scs-multiapi-gradle-plugin/build.gradle @@ -21,7 +21,7 @@ repositories { } group = 'com.sngular' -version = '6.5.3' +version = '6.6.0' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -31,7 +31,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:6.5.3' + implementation 'com.sngular:multiapi-engine:6.6.0' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3' testImplementation 'org.junit.platform:junit-platform-launcher:1.9.2' @@ -100,7 +100,7 @@ testing { integrationTest(JvmTestSuite) { dependencies { - implementation 'com.sngular:scs-multiapi-gradle-plugin:6.5.3' + implementation 'com.sngular:scs-multiapi-gradle-plugin:6.6.0' implementation 'org.assertj:assertj-core:3.24.2' } diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index ce1f5329..b671b797 100644 --- a/scs-multiapi-maven-plugin/pom.xml +++ b/scs-multiapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ com.sngular scs-multiapi-maven-plugin - 6.5.3 + 6.6.0 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -271,7 +271,7 @@ com.sngular multiapi-engine - 6.5.3 + 6.6.0 org.apache.maven From ca18ffb509f5e094d5f01066a9f5ca16cdb6fc04 Mon Sep 17 00:00:00 2001 From: joseegarcia Date: Fri, 24 Jul 2026 14:11:00 +0200 Subject: [PATCH 2/5] Harden remote spec loading: timeouts and narrower scheme allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add bounded connect (15s) / read (30s) timeouts to all URL fetches via PathUtil.openUrlStream, so an unresponsive remote spec server cannot hang the build. Timeouts are harmless for file:/jar: schemes. - Drop `ftp` from the accepted URL schemes (plaintext, effectively unused for API specs) — remote specs are now http/https/file only, reducing attack surface. - Document TLS expectations for https registries with internal/self-signed certs (must be trusted by the build JVM; validation is not disabled). Full multiapi-engine suite: 113/113 (mvn clean test). Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 8 +++++-- .../common/files/RemoteFileLocation.java | 4 +++- .../plugin/common/tools/ApiTool.java | 2 +- .../plugin/common/tools/PathUtil.java | 24 ++++++++++++++++++- .../plugin/common/tools/SchemaUtil.java | 2 +- .../plugin/common/tools/PathUtilTest.java | 2 +- 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9970232d..f8341377 100644 --- a/README.md +++ b/README.md @@ -837,9 +837,10 @@ from a local JAR containing `contracts/event-api.yml` in its resources: ## Loading specifications from a remote URL (Apicurio Registry, HTTP) -`filePath` also accepts a remote URL (`http`, `https`, `ftp` or `file` scheme). When it does, +`filePath` also accepts a remote URL (`http`, `https` or `file` scheme). When it does, the specification is downloaded at generation time instead of being read from the filesystem or -the classpath. This works for both OpenAPI and AsyncAPI specs. +the classpath. This works for both OpenAPI and AsyncAPI specs. Remote fetches use bounded +connect/read timeouts so an unresponsive server cannot hang the build. This is the mechanism to consume a spec published in an **Apicurio Registry**, whose artifacts are served over HTTP. Point `filePath` at the artifact's content endpoint, for example: @@ -862,3 +863,6 @@ Notes: - Only publicly reachable URLs are supported for now; authenticated registries (tokens/headers) are not yet handled. - External `$ref`s are resolved relative to the spec's URL when the spec itself is remote. +- For an `https` registry using an internally-issued/self-signed certificate, the certificate + must be trusted by the JVM running the build (e.g. imported into its truststore); certificate + validation is not disabled. diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/files/RemoteFileLocation.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/files/RemoteFileLocation.java index 91c8d70e..ad05e567 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/files/RemoteFileLocation.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/files/RemoteFileLocation.java @@ -11,6 +11,8 @@ import java.net.URI; import java.net.URL; +import com.sngular.api.generator.plugin.common.tools.PathUtil; + /** * {@link FileLocation} for specifications loaded from a remote URL (e.g. an Apicurio Registry * artifact served over HTTP). Relative sibling files (external {@code $ref}s) are resolved @@ -27,7 +29,7 @@ public RemoteFileLocation(final URI baseUri) { @Override public InputStream getFileAtLocation(final String filename) throws IOException { final URL target = baseUri.resolve(filename).toURL(); - return target.openStream(); + return PathUtil.openUrlStream(target); } @Override diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java index 57353ab3..2ad5a691 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java @@ -455,7 +455,7 @@ public static JsonNode nodeFromFile(final FileLocation ymlParent, final String f final InputStream file; // Remote references (http/https/ftp/file URLs) are fetched directly from their URL. if (PathUtil.isRemoteUri(filePath)) { - file = new java.net.URL(filePath).openStream(); + file = PathUtil.openUrlStream(new java.net.URL(filePath)); } else if (PathUtil.isAbsolutePath(filePath)) { // For absolute paths, open directly file = new FileInputStream(filePath); diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java index 6513b48d..233e20c8 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java @@ -48,6 +48,28 @@ public static boolean isAbsolutePath(final String filePath) { */ public static boolean isRemoteUri(final String filePath) { return StringUtils.isNotEmpty(filePath) - && filePath.matches("^(?i)(https?|ftp|file)://.*"); + && filePath.matches("^(?i)(https?|file)://.*"); + } + + /** Connection timeout (ms) applied when fetching a remote specification. */ + public static final int REMOTE_CONNECT_TIMEOUT_MS = 15_000; + + /** Read timeout (ms) applied when fetching a remote specification. */ + public static final int REMOTE_READ_TIMEOUT_MS = 30_000; + + /** + * Opens a stream to a URL with bounded connect/read timeouts, so an unresponsive remote + * spec server (e.g. an Apicurio Registry) cannot hang the build indefinitely. Timeouts are + * harmless for non-network schemes such as {@code file:} and {@code jar:}. + * + * @param url the URL to open + * @return the input stream + * @throws java.io.IOException if the connection cannot be established or times out + */ + public static java.io.InputStream openUrlStream(final java.net.URL url) throws java.io.IOException { + final java.net.URLConnection connection = url.openConnection(); + connection.setConnectTimeout(REMOTE_CONNECT_TIMEOUT_MS); + connection.setReadTimeout(REMOTE_READ_TIMEOUT_MS); + return connection.getInputStream(); } } diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java index 1360a470..067b0dcd 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java @@ -172,7 +172,7 @@ private static String readFile(final URI rootFilePath, final String filePath) th private static String readFromUrl(final URL fileURL) { final var sb = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(fileURL.openStream()))) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(PathUtil.openUrlStream(fileURL)))) { String inputLine; while ((inputLine = reader.readLine()) != null) { sb.append(inputLine).append(System.lineSeparator()); diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java index a5be4cc5..3e8de4f6 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java @@ -18,12 +18,12 @@ void isRemoteUriDetectsUrlSchemes() { assertTrue(PathUtil.isRemoteUri("http://registry/apis/registry/v2/groups/g/artifacts/a")); assertTrue(PathUtil.isRemoteUri("https://example.com/openapi.yml")); assertTrue(PathUtil.isRemoteUri("HTTPS://EXAMPLE.COM/openapi.yml")); - assertTrue(PathUtil.isRemoteUri("ftp://host/spec.yml")); assertTrue(PathUtil.isRemoteUri("file:///tmp/spec.yml")); } @Test void isRemoteUriRejectsLocalPaths() { + assertFalse(PathUtil.isRemoteUri("ftp://host/spec.yml")); assertFalse(PathUtil.isRemoteUri("./src/main/resources/api/api.yml")); assertFalse(PathUtil.isRemoteUri("/absolute/path/api.yml")); assertFalse(PathUtil.isRemoteUri("contracts/event-api.yml")); From 341d3b37e0540c3635837a8fa6e9f7af82a726f9 Mon Sep 17 00:00:00 2001 From: joseegarcia Date: Fri, 24 Jul 2026 14:56:56 +0200 Subject: [PATCH 3/5] Authenticate against protected remote registries (Apicurio) Adds authentication for remote spec fetching, resolving credentials from system properties (preferred) or environment variables - never from build files: bearer token, basic auth, and an arbitrary header (e.g. X-Registry-ApiKey). Headers are applied only to http/https fetches. Optional host scoping (scs.multiapi.remote.host) sends credentials only to the configured host, so a token is never leaked to a different host via an external $ref or cross-host redirect. Credentials are never logged; TLS validation is unchanged. Adds PathUtilTest coverage and documents the setup in the README. Full multiapi-engine suite: 118/118 (mvn clean test). Closes joseegman-idoneea/scs-multiapi-plugin#8 Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 29 ++++++- .../plugin/common/tools/PathUtil.java | 87 ++++++++++++++++--- .../plugin/common/tools/PathUtilTest.java | 54 ++++++++++++ 3 files changed, 157 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f8341377..4a2627d4 100644 --- a/README.md +++ b/README.md @@ -859,9 +859,34 @@ Gradle: filePath = 'https://my-apicurio-host/apis/registry/v2/groups/default/artifacts/my-api' ``` +### Authenticated registries + +For a protected registry (e.g. an Apicurio Registry with security enabled), credentials are read +from **system properties** (preferred) or **environment variables** — never from the build files — +and sent as request headers on `http`/`https` fetches. Supported schemes: + +| Purpose | System property | Environment variable | +| --- | --- | --- | +| Bearer token | `scs.multiapi.remote.token` | `SCS_MULTIAPI_REMOTE_TOKEN` | +| Basic user | `scs.multiapi.remote.user` | `SCS_MULTIAPI_REMOTE_USER` | +| Basic password | `scs.multiapi.remote.password` | `SCS_MULTIAPI_REMOTE_PASSWORD` | +| Custom header name | `scs.multiapi.remote.header.name` | `SCS_MULTIAPI_REMOTE_HEADER_NAME` | +| Custom header value | `scs.multiapi.remote.header.value` | `SCS_MULTIAPI_REMOTE_HEADER_VALUE` | +| Restrict creds to host | `scs.multiapi.remote.host` | `SCS_MULTIAPI_REMOTE_HOST` | + +A bearer token takes precedence over basic auth; the custom header (e.g. `X-Registry-ApiKey`) is +additive. Example (bearer token from the CI environment): + +```bash +export SCS_MULTIAPI_REMOTE_TOKEN="$APICURIO_TOKEN" +export SCS_MULTIAPI_REMOTE_HOST="my-apicurio-host" # optional but recommended +mvn generate-sources +``` + Notes: -- Only publicly reachable URLs are supported for now; authenticated registries (tokens/headers) - are not yet handled. +- Set `scs.multiapi.remote.host` to the registry host so the token is sent **only** to that host + and never leaked to a different host reached through an external `$ref` or a cross-host redirect. +- Provide credentials via CI secrets / environment variables; they are never logged. - External `$ref`s are resolved relative to the spec's URL when the spec itself is remote. - For an `https` registry using an internally-issued/self-signed certificate, the certificate must be trusted by the JVM running the build (e.g. imported into its truststore); certificate diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java index 233e20c8..ae64437f 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java @@ -6,8 +6,16 @@ package com.sngular.api.generator.plugin.common.tools; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; import java.nio.file.InvalidPathException; import java.nio.file.Paths; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.Map; import org.apache.commons.lang3.StringUtils; @@ -16,6 +24,12 @@ */ public final class PathUtil { + /** Connection timeout (ms) applied when fetching a remote specification. */ + public static final int REMOTE_CONNECT_TIMEOUT_MS = 15_000; + + /** Read timeout (ms) applied when fetching a remote specification. */ + public static final int REMOTE_READ_TIMEOUT_MS = 30_000; + private PathUtil() { // Utility class } @@ -44,32 +58,83 @@ public static boolean isAbsolutePath(final String filePath) { * specifications from HTTP(S) endpoints such as an Apicurio Registry artifact. * * @param filePath the spec path to check - * @return true if the path uses an http, https, ftp or file scheme + * @return true if the path uses an http, https or file scheme */ public static boolean isRemoteUri(final String filePath) { return StringUtils.isNotEmpty(filePath) && filePath.matches("^(?i)(https?|file)://.*"); } - /** Connection timeout (ms) applied when fetching a remote specification. */ - public static final int REMOTE_CONNECT_TIMEOUT_MS = 15_000; - - /** Read timeout (ms) applied when fetching a remote specification. */ - public static final int REMOTE_READ_TIMEOUT_MS = 30_000; - /** * Opens a stream to a URL with bounded connect/read timeouts, so an unresponsive remote * spec server (e.g. an Apicurio Registry) cannot hang the build indefinitely. Timeouts are - * harmless for non-network schemes such as {@code file:} and {@code jar:}. + * harmless for non-network schemes such as {@code file:} and {@code jar:}. For {@code http}/ + * {@code https} URLs any configured authentication headers (see {@link #remoteAuthHeaders}) + * are applied. * * @param url the URL to open * @return the input stream - * @throws java.io.IOException if the connection cannot be established or times out + * @throws IOException if the connection cannot be established or times out */ - public static java.io.InputStream openUrlStream(final java.net.URL url) throws java.io.IOException { - final java.net.URLConnection connection = url.openConnection(); + public static InputStream openUrlStream(final URL url) throws IOException { + final URLConnection connection = url.openConnection(); connection.setConnectTimeout(REMOTE_CONNECT_TIMEOUT_MS); connection.setReadTimeout(REMOTE_READ_TIMEOUT_MS); + final String protocol = url.getProtocol(); + if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) { + remoteAuthHeaders(url.getHost()).forEach(connection::setRequestProperty); + } return connection.getInputStream(); } + + /** + * Resolves the authentication headers to attach when fetching a remote spec, from system + * properties (preferred) or environment variables. Credentials are never read from the build + * files. Supported schemes (first match wins for {@code Authorization}): + *
    + *
  • Bearer token: {@code scs.multiapi.remote.token} / {@code SCS_MULTIAPI_REMOTE_TOKEN}
  • + *
  • Basic auth: {@code scs.multiapi.remote.user}(+{@code .password}) / + * {@code SCS_MULTIAPI_REMOTE_USER}(+{@code _PASSWORD})
  • + *
+ * plus an optional arbitrary header ({@code scs.multiapi.remote.header.name}/{@code .value} or + * {@code SCS_MULTIAPI_REMOTE_HEADER_NAME}/{@code _VALUE}), e.g. an {@code X-Registry-ApiKey}. + * + *

When {@code scs.multiapi.remote.host} / {@code SCS_MULTIAPI_REMOTE_HOST} is set, credentials + * are only sent to that host, so a token is never leaked to a different host reached through an + * external {@code $ref} or a cross-host redirect. + * + * @param host the host of the URL being fetched + * @return the headers to apply (empty if none configured, or scoped out by host) + */ + public static Map remoteAuthHeaders(final String host) { + final Map headers = new LinkedHashMap<>(); + + final String scopedHost = config("scs.multiapi.remote.host", "SCS_MULTIAPI_REMOTE_HOST"); + if (StringUtils.isNotBlank(scopedHost) && !scopedHost.equalsIgnoreCase(host)) { + return headers; + } + + final String token = config("scs.multiapi.remote.token", "SCS_MULTIAPI_REMOTE_TOKEN"); + final String user = config("scs.multiapi.remote.user", "SCS_MULTIAPI_REMOTE_USER"); + if (StringUtils.isNotBlank(token)) { + headers.put("Authorization", "Bearer " + token); + } else if (StringUtils.isNotBlank(user)) { + final String password = StringUtils.defaultString(config("scs.multiapi.remote.password", "SCS_MULTIAPI_REMOTE_PASSWORD")); + final String basic = Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8)); + headers.put("Authorization", "Basic " + basic); + } + + final String headerName = config("scs.multiapi.remote.header.name", "SCS_MULTIAPI_REMOTE_HEADER_NAME"); + final String headerValue = config("scs.multiapi.remote.header.value", "SCS_MULTIAPI_REMOTE_HEADER_VALUE"); + if (StringUtils.isNotBlank(headerName) && StringUtils.isNotBlank(headerValue)) { + headers.put(headerName, headerValue); + } + + return headers; + } + + private static String config(final String systemProperty, final String environmentVariable) { + final String fromProperty = System.getProperty(systemProperty); + return StringUtils.isNotBlank(fromProperty) ? fromProperty : System.getenv(environmentVariable); + } } diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java index 3e8de4f6..0770684d 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java @@ -6,13 +6,30 @@ package com.sngular.api.generator.plugin.common.tools; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Base64; +import java.util.Map; + +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; class PathUtilTest { + private static final String[] AUTH_PROPS = { + "scs.multiapi.remote.host", "scs.multiapi.remote.token", "scs.multiapi.remote.user", + "scs.multiapi.remote.password", "scs.multiapi.remote.header.name", "scs.multiapi.remote.header.value" + }; + + @AfterEach + void clearAuthProps() { + for (final String prop : AUTH_PROPS) { + System.clearProperty(prop); + } + } + @Test void isRemoteUriDetectsUrlSchemes() { assertTrue(PathUtil.isRemoteUri("http://registry/apis/registry/v2/groups/g/artifacts/a")); @@ -31,4 +48,41 @@ void isRemoteUriRejectsLocalPaths() { assertFalse(PathUtil.isRemoteUri("")); assertFalse(PathUtil.isRemoteUri(null)); } + + @Test + void remoteAuthHeadersEmptyWhenUnconfigured() { + assertTrue(PathUtil.remoteAuthHeaders("registry.example.com").isEmpty()); + } + + @Test + void remoteAuthHeadersBearerToken() { + System.setProperty("scs.multiapi.remote.token", "abc123"); + final Map headers = PathUtil.remoteAuthHeaders("registry.example.com"); + assertEquals("Bearer abc123", headers.get("Authorization")); + } + + @Test + void remoteAuthHeadersBasicAuth() { + System.setProperty("scs.multiapi.remote.user", "alice"); + System.setProperty("scs.multiapi.remote.password", "s3cret"); + final String expected = "Basic " + Base64.getEncoder().encodeToString("alice:s3cret".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + assertEquals(expected, PathUtil.remoteAuthHeaders("registry.example.com").get("Authorization")); + } + + @Test + void remoteAuthHeadersArbitraryHeader() { + System.setProperty("scs.multiapi.remote.header.name", "X-Registry-ApiKey"); + System.setProperty("scs.multiapi.remote.header.value", "key-42"); + assertEquals("key-42", PathUtil.remoteAuthHeaders("registry.example.com").get("X-Registry-ApiKey")); + } + + @Test + void remoteAuthHeadersScopedToConfiguredHost() { + System.setProperty("scs.multiapi.remote.token", "abc123"); + System.setProperty("scs.multiapi.remote.host", "registry.example.com"); + // Matching host receives the credentials. + assertEquals("Bearer abc123", PathUtil.remoteAuthHeaders("registry.example.com").get("Authorization")); + // A different host (e.g. reached via an external $ref) must NOT receive them. + assertTrue(PathUtil.remoteAuthHeaders("evil.example.org").isEmpty()); + } } From 6272c5c90824b4b211ad20d95df9360b82906b88 Mon Sep 17 00:00:00 2001 From: joseegarcia Date: Fri, 24 Jul 2026 15:08:57 +0200 Subject: [PATCH 4/5] Fix Codacy CodeStyle issues in remote-spec loading - Replace deprecated new URL(String) with URI.create(...).toURL() (ApiTool, SchemaUtil) - Remove fully-qualified names (java.net.URL, StandardCharsets in test) - Route AsyncApi remote read through PathUtil.openUrlStream (timeouts + shorter line) - Wrap long lines Co-Authored-By: Claude Opus 4.8 (1M context) --- .../api/generator/plugin/asyncapi/AsyncApiGenerator.java | 3 ++- .../sngular/api/generator/plugin/common/tools/ApiTool.java | 3 ++- .../sngular/api/generator/plugin/common/tools/PathUtil.java | 6 ++++-- .../api/generator/plugin/common/tools/SchemaUtil.java | 2 +- .../api/generator/plugin/common/tools/PathUtilTest.java | 3 ++- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java index 0d87631a..b04c2ba1 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java @@ -90,7 +90,8 @@ private static Pair resolveYmlLocation(final String y log.debug("Loading spec from remote URL"); try { final URI uri = URI.create(ymlFilePath); - return new ImmutablePair<>(uri.toURL().openStream(), new RemoteFileLocation(uri.resolve("."))); + final InputStream remoteStream = PathUtil.openUrlStream(uri.toURL()); + return new ImmutablePair<>(remoteStream, new RemoteFileLocation(uri.resolve("."))); } catch (final IOException e) { throw new FileNotFoundException("Could not open remote YAML file: " + ymlFilePath); } diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java index 2ad5a691..4397ce00 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/ApiTool.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -455,7 +456,7 @@ public static JsonNode nodeFromFile(final FileLocation ymlParent, final String f final InputStream file; // Remote references (http/https/ftp/file URLs) are fetched directly from their URL. if (PathUtil.isRemoteUri(filePath)) { - file = PathUtil.openUrlStream(new java.net.URL(filePath)); + file = PathUtil.openUrlStream(URI.create(filePath).toURL()); } else if (PathUtil.isAbsolutePath(filePath)) { // For absolute paths, open directly file = new FileInputStream(filePath); diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java index ae64437f..3e4395d8 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/PathUtil.java @@ -119,8 +119,10 @@ public static Map remoteAuthHeaders(final String host) { if (StringUtils.isNotBlank(token)) { headers.put("Authorization", "Bearer " + token); } else if (StringUtils.isNotBlank(user)) { - final String password = StringUtils.defaultString(config("scs.multiapi.remote.password", "SCS_MULTIAPI_REMOTE_PASSWORD")); - final String basic = Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8)); + final String password = StringUtils.defaultString( + config("scs.multiapi.remote.password", "SCS_MULTIAPI_REMOTE_PASSWORD")); + final String credentials = user + ":" + password; + final String basic = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); headers.put("Authorization", "Basic " + basic); } diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java index 067b0dcd..7447bf50 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/common/tools/SchemaUtil.java @@ -140,7 +140,7 @@ private static String readFile(final URI rootFilePath, final String filePath) th // Remote specifications (http/https/ftp/file URLs, e.g. an Apicurio Registry artifact) are // fetched directly from their URL, bypassing classpath and filesystem resolution. if (PathUtil.isRemoteUri(filePath)) { - return readFromUrl(new URL(filePath)); + return readFromUrl(URI.create(filePath).toURL()); } // Normalize the incoming filePath: remove leading './' and replace backslashes with forward slashes diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java index 0770684d..b1adc2fe 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/common/tools/PathUtilTest.java @@ -10,6 +10,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Map; @@ -65,7 +66,7 @@ void remoteAuthHeadersBearerToken() { void remoteAuthHeadersBasicAuth() { System.setProperty("scs.multiapi.remote.user", "alice"); System.setProperty("scs.multiapi.remote.password", "s3cret"); - final String expected = "Basic " + Base64.getEncoder().encodeToString("alice:s3cret".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + final String expected = "Basic " + Base64.getEncoder().encodeToString("alice:s3cret".getBytes(StandardCharsets.UTF_8)); assertEquals(expected, PathUtil.remoteAuthHeaders("registry.example.com").get("Authorization")); } From 8ab28595671d4ffdce75cae28b912d82194d7d47 Mon Sep 17 00:00:00 2001 From: joseegarcia Date: Fri, 24 Jul 2026 15:15:59 +0200 Subject: [PATCH 5/5] Fix Codacy markdownlint issues in README (MD013 line length, MD032 list spacing) The 17 Codacy issues were all markdownlint on the new README section: lines over 80 chars and a list not surrounded by blank lines. Rewrap prose to <=80, replace the wide auth table with a wrapped bullet list, and add blank lines around lists. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 63 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4a2627d4..8597787b 100644 --- a/README.md +++ b/README.md @@ -837,13 +837,15 @@ from a local JAR containing `contracts/event-api.yml` in its resources: ## Loading specifications from a remote URL (Apicurio Registry, HTTP) -`filePath` also accepts a remote URL (`http`, `https` or `file` scheme). When it does, -the specification is downloaded at generation time instead of being read from the filesystem or -the classpath. This works for both OpenAPI and AsyncAPI specs. Remote fetches use bounded -connect/read timeouts so an unresponsive server cannot hang the build. +`filePath` also accepts a remote URL (`http`, `https` or `file` scheme). When +it does, the spec is downloaded at generation time instead of being read from +the filesystem or the classpath. This works for both OpenAPI and AsyncAPI +specs. Remote fetches use bounded connect/read timeouts so an unresponsive +server cannot hang the build. -This is the mechanism to consume a spec published in an **Apicurio Registry**, whose artifacts are -served over HTTP. Point `filePath` at the artifact's content endpoint, for example: +This is the mechanism to consume a spec published in an **Apicurio Registry**, +whose artifacts are served over HTTP. Point `filePath` at the artifact's +content endpoint, for example: ```xml @@ -861,20 +863,23 @@ filePath = 'https://my-apicurio-host/apis/registry/v2/groups/default/artifacts/m ### Authenticated registries -For a protected registry (e.g. an Apicurio Registry with security enabled), credentials are read -from **system properties** (preferred) or **environment variables** — never from the build files — -and sent as request headers on `http`/`https` fetches. Supported schemes: - -| Purpose | System property | Environment variable | -| --- | --- | --- | -| Bearer token | `scs.multiapi.remote.token` | `SCS_MULTIAPI_REMOTE_TOKEN` | -| Basic user | `scs.multiapi.remote.user` | `SCS_MULTIAPI_REMOTE_USER` | -| Basic password | `scs.multiapi.remote.password` | `SCS_MULTIAPI_REMOTE_PASSWORD` | -| Custom header name | `scs.multiapi.remote.header.name` | `SCS_MULTIAPI_REMOTE_HEADER_NAME` | -| Custom header value | `scs.multiapi.remote.header.value` | `SCS_MULTIAPI_REMOTE_HEADER_VALUE` | -| Restrict creds to host | `scs.multiapi.remote.host` | `SCS_MULTIAPI_REMOTE_HOST` | - -A bearer token takes precedence over basic auth; the custom header (e.g. `X-Registry-ApiKey`) is +For a protected registry (for example an Apicurio Registry with security +enabled), credentials are read from **system properties** (preferred) or +**environment variables** — never from the build files — and sent as request +headers on `http`/`https` fetches. Each mechanism has a system property and an +equivalent environment variable: + +- **Bearer token**: `scs.multiapi.remote.token` / + `SCS_MULTIAPI_REMOTE_TOKEN`. +- **Basic auth**: `scs.multiapi.remote.user` + `scs.multiapi.remote.password` + (env `SCS_MULTIAPI_REMOTE_USER` / `SCS_MULTIAPI_REMOTE_PASSWORD`). +- **Custom header**: `scs.multiapi.remote.header.name` + + `scs.multiapi.remote.header.value` (env `SCS_MULTIAPI_REMOTE_HEADER_NAME` / + `SCS_MULTIAPI_REMOTE_HEADER_VALUE`), e.g. an `X-Registry-ApiKey`. +- **Restrict credentials to a host**: `scs.multiapi.remote.host` + (env `SCS_MULTIAPI_REMOTE_HOST`). + +A bearer token takes precedence over basic auth; the custom header is additive. Example (bearer token from the CI environment): ```bash @@ -884,10 +889,14 @@ mvn generate-sources ``` Notes: -- Set `scs.multiapi.remote.host` to the registry host so the token is sent **only** to that host - and never leaked to a different host reached through an external `$ref` or a cross-host redirect. -- Provide credentials via CI secrets / environment variables; they are never logged. -- External `$ref`s are resolved relative to the spec's URL when the spec itself is remote. -- For an `https` registry using an internally-issued/self-signed certificate, the certificate - must be trusted by the JVM running the build (e.g. imported into its truststore); certificate - validation is not disabled. + +- Set `scs.multiapi.remote.host` to the registry host so the token is sent + **only** to that host and never leaked to a different host reached through + an external `$ref` or a cross-host redirect. +- Provide credentials via CI secrets / environment variables; they are never + logged. +- External `$ref`s are resolved relative to the spec's URL when the spec is + remote. +- For an `https` registry using an internally-issued or self-signed + certificate, the certificate must be trusted by the JVM running the build + (for example imported into its truststore); validation is not disabled.