+
+
+
diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index 58eccd8df1bb8..8a5bcadedecfd 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -17755,6 +17755,18 @@
"index" : 9,
"group" : "advanced",
"label" : "advanced"
+ }, {
+ "name" : "externalReferences",
+ "type" : "enum",
+ "enum" : [ "all", "local", "none" ],
+ "description" : "Which external references ($ref) the OpenAPI specification may use: all allows any reference, local allows references to the classpath and the file system only, and none allows only references inside the document. A reference the option does not allow fails the start.",
+ "title" : "External References",
+ "default" : "all",
+ "displayName" : "External References",
+ "kind" : "attribute",
+ "index" : 10,
+ "group" : "security",
+ "label" : "security"
} ]
},
"openIdConnect" : {
diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiExternalReferences.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiExternalReferences.java
new file mode 100644
index 0000000000000..908bd3f10d641
--- /dev/null
+++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/OpenApiExternalReferences.java
@@ -0,0 +1,316 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.openapi;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.processors.ExternalRefProcessor;
+import io.swagger.v3.parser.reference.ReferenceUtils;
+import io.swagger.v3.parser.util.ClasspathHelper;
+import io.swagger.v3.parser.util.DeserializationUtils;
+import io.swagger.v3.parser.util.RefUtils;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification itself comes from a URL.
+ *
+ * The check works on the tree the parser resolves (YAML aliases expanded), resolves each reference the way the parser
+ * does, that is against the document that contains it and, for OpenAPI 3.1, against the enclosing {@code $id}s, which
+ * the parser also carries into the documents it reaches through a reference, and reads the local documents it allows to
+ * check their references in turn. A document it cannot read fails the check.
+ *
+ * This is a copy of the class of the same name in camel-rest-openapi, which neither module can depend on the other for;
+ * keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+ static final String ALL = "all";
+ static final String LOCAL = "local";
+ static final String NONE = "none";
+
+ /**
+ * The URI the parser gives an OpenAPI 3.1 specification that has no location (OpenAPIDereferencer31).
+ */
+ private static final String LOCAL_URI = "local";
+
+ private OpenApiExternalReferences() {
+ }
+
+ /**
+ * The value of the option to apply: {@link #ALL} when none is set.
+ *
+ * @throws IllegalArgumentException for a value other than all, local or none
+ */
+ static String policy(String externalReferences) {
+ if (externalReferences == null || externalReferences.isBlank()) {
+ return ALL;
+ }
+ String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+ if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) {
+ return policy;
+ }
+ throw new IllegalArgumentException(
+ "Unknown externalReferences value: " + externalReferences + ", expected all, local or none");
+ }
+
+ /**
+ * Checks every {@code $ref} of the specification, and of the local documents it references, against the option.
+ *
+ * @param externalReferences the option: all, local or none
+ * @param specification the specification, as JSON or YAML text
+ * @param specificationUri where the specification comes from, for the error message
+ * @param remote whether the specification comes from a URL: the parser then resolves its
+ * external references against that URL
+ * @throws IllegalArgumentException for the first reference the option does not allow, and for a referenced document
+ * that cannot be read
+ */
+ static void check(String externalReferences, String specification, String specificationUri, boolean remote) {
+ String policy = policy(externalReferences);
+ if (ALL.equals(policy)) {
+ return;
+ }
+ new Check(policy, specificationUri, remote).root(specification);
+ }
+
+ private static boolean isHttp(String target) {
+ String lower = target.toLowerCase(Locale.ROOT);
+ return lower.startsWith("http:") || lower.startsWith("https:");
+ }
+
+ /**
+ * A document a reference leads to: where the reference is, and the OpenAPI 3.1 {@code $id}s the parser carries into
+ * the document, which decide what the references inside it resolve to.
+ */
+ private static final class Target {
+
+ private final String location;
+ private final String origin;
+ private final List inheritedIds;
+
+ Target(String location, String origin, List inheritedIds) {
+ this.location = location;
+ this.origin = origin;
+ this.inheritedIds = inheritedIds;
+ }
+ }
+
+ /**
+ * The check of one specification: the root document and the documents reached from it.
+ */
+ private static final class Check {
+
+ private final String policy;
+ private final String specificationUri;
+ private final boolean remote;
+ private final Set checked = new HashSet<>();
+ private boolean openApi31;
+
+ Check(String policy, String specificationUri, boolean remote) {
+ this.policy = policy;
+ this.specificationUri = specificationUri;
+ this.remote = remote;
+ }
+
+ void root(String specification) {
+ JsonNode tree = tree(specification, null, null);
+ // the parser resolves an OpenAPI 3.1 document with its own dereferencer, which has other rules
+ openApi31 = tree.path("openapi").asText("").startsWith("3.1");
+ follow(tree, null, openApi31 ? LOCAL_URI : null, List.of());
+ }
+
+ /**
+ * Checks the references of one document and then the documents they lead to.
+ *
+ * @param tree the document
+ * @param document the resolved location of the document, as the parser uses it, or null for the root
+ * @param base what the references of the document resolve against
+ * @param inheritedIds the {@code $id}s of the schemas enclosing the reference that led here (OpenAPI 3.1)
+ */
+ private void follow(JsonNode tree, String document, String base, List inheritedIds) {
+ // the external references of this document, with where they are, in the order they are met
+ Map targets = new LinkedHashMap<>();
+ scan(tree, "", document, base, inheritedIds, targets);
+ for (Target target : targets.values()) {
+ String targetBase = target.location;
+ if (openApi31) {
+ // OpenAPI31Traverser.traverseSchema: the inherited $ids resolved, in turn, against the document
+ for (String id : target.inheritedIds) {
+ targetBase = resolveBase(id, targetBase, "", target.location);
+ }
+ }
+ // a document resolves its references the same way whenever it is reached with the same base, whatever
+ // chain of $ids led to it; this also ends the visit of a schema that references itself
+ if (checked.add(target.location + " under " + targetBase)) {
+ JsonNode targetTree = tree(read(target.location, target.origin), target.location, target.origin);
+ follow(targetTree, target.location, targetBase, target.inheritedIds);
+ }
+ }
+ }
+
+ private JsonNode tree(String content, String document, String origin) {
+ try {
+ // the tree the parser resolves: the same YAML reader, with the aliases expanded and its limits applied
+ return DeserializationUtils.deserializeIntoTree(content, document != null ? document : specificationUri);
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException(
+ (document != null ? where(document, origin) : "The OpenAPI specification " + specificationUri)
+ + " cannot be read to check its references: " + e.getMessage(),
+ e);
+ }
+ }
+
+ private void scan(
+ JsonNode node, String path, String document, String base, List ids,
+ Map targets) {
+ if (node.isObject()) {
+ String nodeBase = base;
+ List nodeIds = ids;
+ if (openApi31) {
+ JsonNode id = node.get("$id");
+ if (id != null && id.isTextual() && !id.asText().isBlank()) {
+ nodeBase = resolveBase(id.asText(), base, path, document);
+ nodeIds = new ArrayList<>(ids);
+ nodeIds.add(id.asText());
+ }
+ }
+ for (Map.Entry field : node.properties()) {
+ String fieldPath = path.isEmpty() ? field.getKey() : path + "." + field.getKey();
+ if ("$ref".equals(field.getKey()) && field.getValue().isTextual()) {
+ reference(field.getValue().asText(), fieldPath, document, nodeBase, nodeIds, targets);
+ } else {
+ scan(field.getValue(), fieldPath, document, nodeBase, nodeIds, targets);
+ }
+ }
+ } else if (node.isArray()) {
+ int i = 0;
+ for (JsonNode item : node) {
+ scan(item, path + "[" + i++ + "]", document, base, ids, targets);
+ }
+ }
+ }
+
+ /**
+ * The base the references below an OpenAPI 3.1 {@code $id} resolve against: the id resolved against the
+ * enclosing base, as OpenAPI31Traverser.traverseSchema does.
+ */
+ private String resolveBase(String id, String base, String path, String document) {
+ try {
+ return ReferenceUtils.toBaseURI(ReferenceUtils.resolve(ReferenceUtils.toBaseURI(id), base));
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ where(id, path, document) + " cannot be resolved as $id: " + e.getMessage(), e);
+ }
+ }
+
+ private void reference(
+ String ref, String path, String document, String base, List ids, Map targets) {
+ if (ref.isEmpty() || ref.startsWith("#")) {
+ // inside the document: the parser evaluates the fragment in the document, or in the schema whose $id
+ // is the base, without loading anything
+ return;
+ }
+ String target;
+ try {
+ if (openApi31) {
+ // ReferenceVisitor.resolveSchemaRef: the reference resolved against the enclosing $ids and the
+ // document, without its fragment
+ target = ReferenceUtils.toBaseURI(ReferenceUtils.resolve(ref, base));
+ } else {
+ // ExternalRefProcessor: a reference met in a referenced document is joined with the path of that
+ // document; the parser then reads the part before the fragment
+ String joined = document == null ? ref : ExternalRefProcessor.join(document, ref);
+ int fragment = joined.indexOf("#/");
+ target = fragment < 0 ? joined : joined.substring(0, fragment);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ where(ref, path, document) + " cannot be resolved: " + e.getMessage(), e);
+ }
+ String reason = null;
+ if (NONE.equals(policy)) {
+ reason = "externalReferences=none allows only references inside the document";
+ } else if (remote) {
+ reason = "externalReferences=local allows no external reference in a specification loaded from a URL,"
+ + " since the parser resolves it against that URL";
+ } else if (isHttp(target)) {
+ reason = "externalReferences=local allows no http(s) reference"
+ + (target.equals(ref) ? "" : " (" + ref + " resolves to " + target + ")");
+ }
+ if (reason != null) {
+ throw new IllegalArgumentException(where(ref, path, document) + ", but " + reason);
+ }
+ // a local document: file: and classpath: URIs, and paths the parser reads from the working directory or
+ // the classpath; the parser carries the enclosing $ids into it (ReferenceVisitor.resolveSchemaRef)
+ targets.putIfAbsent(target + " under " + ids, new Target(target, where(ref, path, document), ids));
+ }
+
+ /**
+ * Reads a referenced local document the way the parser does: a {@code file:} or {@code classpath:} URI by its
+ * path (OpenAPI 3.1, Visitor.readURI), anything else from the working directory and then the classpath
+ * (RefUtils.readExternalRef).
+ */
+ private String read(String target, String origin) {
+ try {
+ String scheme = null;
+ String schemePath = null;
+ try {
+ URI uri = new URI(target);
+ scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase(Locale.ROOT) : null;
+ schemePath = uri.getPath();
+ } catch (URISyntaxException e) {
+ // not a URI, so a path
+ }
+ if ("file".equals(scheme)) {
+ return Files.readString(Paths.get(schemePath), StandardCharsets.UTF_8);
+ } else if ("classpath".equals(scheme)) {
+ return ClasspathHelper.loadFileFromClasspath(schemePath);
+ } else {
+ return RefUtils.readExternalRef(target, RefFormat.RELATIVE, null, new File(".").toPath(), null);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ where(target, origin) + " cannot be read to check its references: " + e.getMessage(), e);
+ }
+ }
+
+ private String where(String ref, String path, String document) {
+ return "The OpenAPI specification " + specificationUri + " references " + ref + " at " + path
+ + (document != null ? " in " + document : "");
+ }
+
+ private String where(String document, String origin) {
+ return origin != null
+ ? origin + ", which" : "The OpenAPI specification " + specificationUri + " references "
+ + document + ", which";
+ }
+ }
+}
diff --git a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java
index 9176e41adac48..28dfa6b36365d 100644
--- a/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java
+++ b/components/camel-openapi-java/src/main/java/org/apache/camel/openapi/RestOpenApiReader.java
@@ -67,12 +67,14 @@
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
import org.apache.camel.model.rest.ApiKeyDefinition;
import org.apache.camel.model.rest.BasicAuthDefinition;
import org.apache.camel.model.rest.BearerTokenDefinition;
import org.apache.camel.model.rest.CollectionFormat;
import org.apache.camel.model.rest.MutualTLSDefinition;
import org.apache.camel.model.rest.OAuth2Definition;
+import org.apache.camel.model.rest.OpenApiDefinition;
import org.apache.camel.model.rest.OpenIdConnectDefinition;
import org.apache.camel.model.rest.ParamDefinition;
import org.apache.camel.model.rest.ResponseHeaderDefinition;
@@ -166,6 +168,9 @@ public OpenAPI read(
InputStream is = res.getInputStream();
String data = IOHelper.loadText(is);
IOHelper.close(is);
+ // the parser resolves every external reference of the contract, so check them first
+ OpenApiExternalReferences.check(externalReferences(camelContext, rest.getOpenApi()), data,
+ rest.getOpenApi().getSpecification(), false);
OpenAPIV3Parser parser = new OpenAPIV3Parser();
SwaggerParseResult out = parser.readContents(data);
OpenAPI answer = out.getOpenAPI();
@@ -1347,4 +1352,21 @@ private String fixSchemaReference(String ref, String prefix) {
}
}
+ /**
+ * The externalReferences option of the contract: from its openApi element, or else from the rest-openapi component,
+ * which consumes the contract and whose setting therefore applies to the api-doc as well.
+ */
+ private static String externalReferences(CamelContext camelContext, OpenApiDefinition openApi) {
+ String answer = openApi.getExternalReferences();
+ if (answer == null) {
+ Component component = camelContext.hasComponent("rest-openapi");
+ if (component != null) {
+ Object value = PluginHelper.getBeanIntrospection(camelContext)
+ .getOrElseProperty(component, "externalReferences", null, true);
+ answer = value != null ? value.toString() : null;
+ }
+ }
+ return answer != null ? CamelContextHelper.parseText(camelContext, answer) : null;
+ }
+
}
diff --git a/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiReaderExternalReferencesTest.java b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiReaderExternalReferencesTest.java
new file mode 100644
index 0000000000000..a993dd23efec4
--- /dev/null
+++ b/components/camel-openapi-java/src/test/java/org/apache/camel/openapi/RestOpenApiReaderExternalReferencesTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.openapi;
+
+import java.util.List;
+import java.util.Map;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.model.rest.RestDefinition;
+import org.apache.camel.support.DefaultComponent;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The externalReferences option of a contract-first openApi element, applied to the contract served as api-doc. The
+ * contracts that must be rejected reference a host that does not exist; the check runs before the parser, so nothing is
+ * contacted.
+ */
+class RestOpenApiReaderExternalReferencesTest {
+
+ private final CamelContext context = new DefaultCamelContext();
+
+ @BeforeEach
+ void start() {
+ context.start();
+ }
+
+ @AfterEach
+ void stop() {
+ context.stop();
+ }
+
+ @Test
+ void resolvesTheReferencedDocumentByDefault() throws Exception {
+ RestDefinition rest = new RestDefinition();
+ rest.openApi("external-references.yaml");
+
+ OpenAPI openApi = read(rest);
+
+ assertNotNull(openApi.getPaths().get("/pets/{petId}"));
+ // the parser imported the Pet schema from external-references-pet.yaml
+ assertNotNull(openApi.getComponents().getSchemas().get("Pet"));
+ }
+
+ @Test
+ void failsWhenTheContractAllowsNoExternalReference() {
+ RestDefinition rest = new RestDefinition();
+ rest.openApi().specification("external-references.yaml").externalReferences("none");
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> read(rest));
+
+ assertTrue(e.getMessage().contains("external-references-pet.yaml#/components/schemas/Pet"), e.getMessage());
+ assertTrue(e.getMessage().contains("externalReferences=none"), e.getMessage());
+ }
+
+ @Test
+ void checksTheReferencesOfADocumentInASubdirectory() {
+ RestDefinition rest = new RestDefinition();
+ rest.openApi().specification("external-references-subdir-remote.yaml").externalReferences("local");
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> read(rest));
+
+ // the parser reads the Owner reference of schemas/external-references-pet-remote.yaml next to that document
+ assertTrue(e.getMessage().contains("https://schemas.example.invalid/address.yaml"), e.getMessage());
+ assertTrue(e.getMessage().contains("in schemas/external-references-owner-remote.yaml"), e.getMessage());
+ }
+
+ @Test
+ void checksAnOpenApi31ReferenceAgainstTheIdOfItsSchema() {
+ RestDefinition rest = new RestDefinition();
+ rest.openApi().specification("external-references-31-id.yaml").externalReferences("local");
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> read(rest));
+
+ assertTrue(e.getMessage().contains("owner.yaml resolves to https://schemas.example.invalid/schemas/owner.yaml"),
+ e.getMessage());
+ }
+
+ @Test
+ void keepsTheInheritedIdAcrossAnExternalReference() {
+ RestDefinition rest = new RestDefinition();
+ rest.openApi().specification("external-references-31-inherited-id.yaml").externalReferences("local");
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> read(rest));
+
+ // the parser carries the $id of the Pet schema into the bridge schema it references on the classpath
+ assertTrue(e.getMessage().contains("in classpath:///external-references-31-bridge.yaml"), e.getMessage());
+ assertTrue(e.getMessage().contains("owner.yaml resolves to https://schemas.example.invalid/schemas/owner.yaml"),
+ e.getMessage());
+ }
+
+ @Test
+ void finishesForASchemaThatReferencesItself() throws Exception {
+ RestDefinition rest = new RestDefinition();
+ rest.openApi().specification("external-references-31-recursive.yaml").externalReferences("local");
+
+ // the schema is reached again under its own $id, with the same base, so the check visits it once
+ OpenAPI openApi = read(rest);
+
+ assertNotNull(openApi.getPaths().get("/pets/{petId}"));
+ }
+
+ @Test
+ void checksADocumentReachedAgainUnderAnotherId() {
+ RestDefinition rest = new RestDefinition();
+ rest.openApi().specification("external-references-31-revisit.yaml").externalReferences("local");
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> read(rest));
+
+ // the shared schema passes without an $id and is checked again below the $id of Pet
+ assertTrue(e.getMessage().contains("in classpath:///external-references-31-shared.yaml"), e.getMessage());
+ assertTrue(e.getMessage().contains(
+ "external-references-31-owner.yaml resolves to https://schemas.example.invalid/schemas/external-references-31-owner.yaml"),
+ e.getMessage());
+ }
+
+ @Test
+ void checksAReferenceGivenAsAYamlAlias() {
+ RestDefinition rest = new RestDefinition();
+ rest.openApi().specification("external-references-alias.yaml").externalReferences("none");
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> read(rest));
+
+ // the alias is expanded before the check, as the parser expands it
+ assertTrue(e.getMessage().contains("references https://schemas.example.invalid/schema.yaml"), e.getMessage());
+ }
+
+ @Test
+ void usesTheSettingOfTheRestOpenApiComponent() {
+ context.addComponent("rest-openapi", new RestrictedComponent());
+ RestDefinition rest = new RestDefinition();
+ rest.openApi("external-references.yaml");
+
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> read(rest));
+
+ assertTrue(e.getMessage().contains("externalReferences=none"), e.getMessage());
+ }
+
+ private OpenAPI read(RestDefinition rest) throws Exception {
+ return new RestOpenApiReader().read(context, List.of(rest), new BeanConfig(), context.getName(),
+ context.getClassResolver());
+ }
+
+ /**
+ * Stands in for the rest-openapi component, which is not on the classpath of this module. Public like the real
+ * component, since the reader finds the setting through bean introspection.
+ */
+ public static class RestrictedComponent extends DefaultComponent {
+
+ @SuppressWarnings("unused") // read through bean introspection
+ public String getExternalReferences() {
+ return "none";
+ }
+
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining, Map parameters) {
+ return null;
+ }
+ }
+}
diff --git a/components/camel-openapi-java/src/test/resources/external-references-31-bridge.yaml b/components/camel-openapi-java/src/test/resources/external-references-31-bridge.yaml
new file mode 100644
index 0000000000000..40eb0bd09340f
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-31-bridge.yaml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The owner schema of external-references-31-inherited-id.yaml; under the inherited $id the parser resolves owner.yaml to a URL
+type: object
+properties:
+ owner:
+ $ref: owner.yaml
diff --git a/components/camel-openapi-java/src/test/resources/external-references-31-id.yaml b/components/camel-openapi-java/src/test/resources/external-references-31-id.yaml
new file mode 100644
index 0000000000000..7c6c81b466e50
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-31-id.yaml
@@ -0,0 +1,49 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# An OpenAPI 3.1 contract whose Pet schema has an $id on a server that does not exist, so its owner reference resolves to a URL
+openapi: 3.1.0
+info:
+ title: Pets
+ version: "1.0"
+servers:
+ - url: http://localhost:8080/api
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+components:
+ schemas:
+ Pet:
+ $id: https://schemas.example.invalid/schemas/
+ type: object
+ properties:
+ owner:
+ $ref: owner.yaml
diff --git a/components/camel-openapi-java/src/test/resources/external-references-31-inherited-id.yaml b/components/camel-openapi-java/src/test/resources/external-references-31-inherited-id.yaml
new file mode 100644
index 0000000000000..2cd3543c57162
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-31-inherited-id.yaml
@@ -0,0 +1,49 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# An OpenAPI 3.1 contract whose Pet schema has an $id on a server that does not exist and references a schema on the classpath; the parser keeps the $id when it follows that reference
+openapi: 3.1.0
+info:
+ title: Pets
+ version: "1.0"
+servers:
+ - url: http://localhost:8080/api
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+components:
+ schemas:
+ Pet:
+ $id: https://schemas.example.invalid/schemas/
+ type: object
+ properties:
+ owner:
+ $ref: 'classpath:///external-references-31-bridge.yaml'
diff --git a/components/camel-openapi-java/src/test/resources/external-references-31-node.yaml b/components/camel-openapi-java/src/test/resources/external-references-31-node.yaml
new file mode 100644
index 0000000000000..603fb8274d12c
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-31-node.yaml
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A schema that references itself under its own $id, which the check must visit once
+$id: classpath:///external-references-31-node.yaml
+type: object
+properties:
+ next:
+ $ref: classpath:///external-references-31-node.yaml
diff --git a/components/camel-openapi-java/src/test/resources/external-references-31-owner.yaml b/components/camel-openapi-java/src/test/resources/external-references-31-owner.yaml
new file mode 100644
index 0000000000000..1cab86db9d817
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-31-owner.yaml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The owner schema of external-references-31-shared.yaml
+type: object
+properties:
+ name:
+ type: string
diff --git a/components/camel-openapi-java/src/test/resources/external-references-31-recursive.yaml b/components/camel-openapi-java/src/test/resources/external-references-31-recursive.yaml
new file mode 100644
index 0000000000000..8096bb541b722
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-31-recursive.yaml
@@ -0,0 +1,48 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# An OpenAPI 3.1 contract whose Pet schema references a schema on the classpath that references itself
+openapi: 3.1.0
+info:
+ title: Pets
+ version: "1.0"
+servers:
+ - url: http://localhost:8080/api
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ owner:
+ $ref: 'classpath:///external-references-31-node.yaml'
diff --git a/components/camel-openapi-java/src/test/resources/external-references-31-revisit.yaml b/components/camel-openapi-java/src/test/resources/external-references-31-revisit.yaml
new file mode 100644
index 0000000000000..7018caebf83b1
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-31-revisit.yaml
@@ -0,0 +1,54 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# An OpenAPI 3.1 contract that references the same classpath schema twice: once without an $id, so its owner reference is a classpath document, and once below an $id on a server that does not exist
+openapi: 3.1.0
+info:
+ title: Pets
+ version: "1.0"
+servers:
+ - url: http://localhost:8080/api
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+components:
+ schemas:
+ Safe:
+ type: object
+ properties:
+ shared:
+ $ref: 'classpath:///external-references-31-shared.yaml'
+ Pet:
+ $id: https://schemas.example.invalid/schemas/
+ type: object
+ properties:
+ shared:
+ $ref: 'classpath:///external-references-31-shared.yaml'
diff --git a/components/camel-openapi-java/src/test/resources/external-references-31-shared.yaml b/components/camel-openapi-java/src/test/resources/external-references-31-shared.yaml
new file mode 100644
index 0000000000000..895989f480d93
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-31-shared.yaml
@@ -0,0 +1,22 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The shared schema of external-references-31-revisit.yaml; its owner reference resolves against the $id the parser carries into it
+type: object
+properties:
+ owner:
+ $ref: external-references-31-owner.yaml
diff --git a/components/camel-openapi-java/src/test/resources/external-references-alias.yaml b/components/camel-openapi-java/src/test/resources/external-references-alias.yaml
new file mode 100644
index 0000000000000..53e922e2179a0
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-alias.yaml
@@ -0,0 +1,42 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A contract whose Pet schema reference is a YAML alias of a URL on a server that does not exist
+openapi: 3.0.3
+info:
+ title: Pets
+ version: "1.0"
+servers:
+ - url: http://localhost:8080/api
+x-schema: remote 'https://schemas.example.invalid/schema.yaml'
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: *#remote
diff --git a/components/camel-openapi-java/src/test/resources/external-references-pet.yaml b/components/camel-openapi-java/src/test/resources/external-references-pet.yaml
new file mode 100644
index 0000000000000..a567d1ebc0b06
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-pet.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The schemas of external-references.yaml
+components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
diff --git a/components/camel-openapi-java/src/test/resources/external-references-subdir-remote.yaml b/components/camel-openapi-java/src/test/resources/external-references-subdir-remote.yaml
new file mode 100644
index 0000000000000..8213e05d09fbc
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references-subdir-remote.yaml
@@ -0,0 +1,39 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A contract whose Pet schema lives in schemas/external-references-pet-remote.yaml, which references a document next to it that references a URL
+openapi: 3.0.3
+info:
+ title: Pets
+ version: "1.0"
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: 'schemas/external-references-pet-remote.yaml#/components/schemas/Pet'
diff --git a/components/camel-openapi-java/src/test/resources/external-references.yaml b/components/camel-openapi-java/src/test/resources/external-references.yaml
new file mode 100644
index 0000000000000..27d2143a32e06
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/external-references.yaml
@@ -0,0 +1,41 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A contract whose Pet schema lives in external-references-pet.yaml, next to it on the classpath
+openapi: 3.0.3
+info:
+ title: Pets
+ version: "1.0"
+servers:
+ - url: http://localhost:8080/api
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: 'external-references-pet.yaml#/components/schemas/Pet'
diff --git a/components/camel-openapi-java/src/test/resources/schemas/external-references-owner-remote.yaml b/components/camel-openapi-java/src/test/resources/schemas/external-references-owner-remote.yaml
new file mode 100644
index 0000000000000..f7d42247ddcce
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/schemas/external-references-owner-remote.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The Owner schema of schemas/external-references-pet-remote.yaml; the address schema is on a server that does not exist
+components:
+ schemas:
+ Owner:
+ type: object
+ properties:
+ name:
+ type: string
+ address:
+ $ref: 'https://schemas.example.invalid/address.yaml#/components/schemas/Address'
diff --git a/components/camel-openapi-java/src/test/resources/schemas/external-references-pet-remote.yaml b/components/camel-openapi-java/src/test/resources/schemas/external-references-pet-remote.yaml
new file mode 100644
index 0000000000000..1f9daa5bbbdc7
--- /dev/null
+++ b/components/camel-openapi-java/src/test/resources/schemas/external-references-pet-remote.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The Pet schema of external-references-subdir-remote.yaml; the parser reads the Owner reference as schemas/external-references-owner-remote.yaml
+components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ id:
+ type: integer
+ owner:
+ $ref: 'external-references-owner-remote.yaml#/components/schemas/Owner'
diff --git a/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiComponentConfigurer.java b/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiComponentConfigurer.java
index 6d7dada1a40c1..dcebcdecf8ab5 100644
--- a/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiComponentConfigurer.java
+++ b/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiComponentConfigurer.java
@@ -42,6 +42,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj
case "consumercomponentname":
case "consumerComponentName": target.setConsumerComponentName(property(camelContext, java.lang.String.class, value)); return true;
case "consumes": target.setConsumes(property(camelContext, java.lang.String.class, value)); return true;
+ case "externalreferences":
+ case "externalReferences": target.setExternalReferences(property(camelContext, java.lang.String.class, value)); return true;
case "host": target.setHost(property(camelContext, java.lang.String.class, value)); return true;
case "lazystartproducer":
case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true;
@@ -88,6 +90,8 @@ public Class> getOptionType(String name, boolean ignoreCase) {
case "consumercomponentname":
case "consumerComponentName": return java.lang.String.class;
case "consumes": return java.lang.String.class;
+ case "externalreferences":
+ case "externalReferences": return java.lang.String.class;
case "host": return java.lang.String.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
@@ -135,6 +139,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
case "consumercomponentname":
case "consumerComponentName": return target.getConsumerComponentName();
case "consumes": return target.getConsumes();
+ case "externalreferences":
+ case "externalReferences": return target.getExternalReferences();
case "host": return target.getHost();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
diff --git a/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpointConfigurer.java b/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpointConfigurer.java
index a61ef46239fd1..6392dab18c071 100644
--- a/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpointConfigurer.java
+++ b/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpointConfigurer.java
@@ -42,6 +42,8 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj
case "exceptionHandler": target.setExceptionHandler(property(camelContext, org.apache.camel.spi.ExceptionHandler.class, value)); return true;
case "exchangepattern":
case "exchangePattern": target.setExchangePattern(property(camelContext, org.apache.camel.ExchangePattern.class, value)); return true;
+ case "externalreferences":
+ case "externalReferences": target.setExternalReferences(property(camelContext, java.lang.String.class, value)); return true;
case "host": target.setHost(property(camelContext, java.lang.String.class, value)); return true;
case "lazystartproducer":
case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true;
@@ -84,6 +86,8 @@ public Class> getOptionType(String name, boolean ignoreCase) {
case "exceptionHandler": return org.apache.camel.spi.ExceptionHandler.class;
case "exchangepattern":
case "exchangePattern": return org.apache.camel.ExchangePattern.class;
+ case "externalreferences":
+ case "externalReferences": return java.lang.String.class;
case "host": return java.lang.String.class;
case "lazystartproducer":
case "lazyStartProducer": return boolean.class;
@@ -127,6 +131,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
case "exceptionHandler": return target.getExceptionHandler();
case "exchangepattern":
case "exchangePattern": return target.getExchangePattern();
+ case "externalreferences":
+ case "externalReferences": return target.getExternalReferences();
case "host": return target.getHost();
case "lazystartproducer":
case "lazyStartProducer": return target.isLazyStartProducer();
diff --git a/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpointUriFactory.java b/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpointUriFactory.java
index db68455cb90f6..6376949209c7b 100644
--- a/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpointUriFactory.java
+++ b/components/camel-rest-openapi/src/generated/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpointUriFactory.java
@@ -24,7 +24,7 @@ public class RestOpenApiEndpointUriFactory extends org.apache.camel.support.comp
private static final Set ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map MULTI_VALUE_PREFIXES;
static {
- Set props = new HashSet<>(21);
+ Set props = new HashSet<>(22);
props.add("apiContextPath");
props.add("basePath");
props.add("bridgeErrorHandler");
@@ -35,6 +35,7 @@ public class RestOpenApiEndpointUriFactory extends org.apache.camel.support.comp
props.add("consumes");
props.add("exceptionHandler");
props.add("exchangePattern");
+ props.add("externalReferences");
props.add("host");
props.add("lazyStartProducer");
props.add("missingOperation");
diff --git a/components/camel-rest-openapi/src/generated/resources/META-INF/org/apache/camel/component/rest/openapi/rest-openapi.json b/components/camel-rest-openapi/src/generated/resources/META-INF/org/apache/camel/component/rest/openapi/rest-openapi.json
index d649162cff1c1..5e6f53bd28b3b 100644
--- a/components/camel-rest-openapi/src/generated/resources/META-INF/org/apache/camel/component/rest/openapi/rest-openapi.json
+++ b/components/camel-rest-openapi/src/generated/resources/META-INF/org/apache/camel/component/rest/openapi/rest-openapi.json
@@ -43,8 +43,9 @@
"consumes": { "index": 16, "kind": "property", "displayName": "Consumes", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "What payload type this component capable of consuming. Could be one type, like application\/json or multiple types as application\/json, application\/xml; q=0.5 according to the RFC7231. This equates to the value of Accept HTTP header. If set overrides any value found in the OpenApi specification. Can be overridden in endpoint configuration" },
"produces": { "index": 17, "kind": "property", "displayName": "Produces", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "What payload type this component is producing. For example application\/json according to the RFC7231. This equates to the value of Content-Type HTTP header. If set overrides any value present in the OpenApi specification. Can be overridden in endpoint configuration." },
"autowiredEnabled": { "index": 18, "kind": "property", "displayName": "Autowired Enabled", "group": "advanced", "label": "advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "Whether autowiring is enabled. This is used for automatic autowiring options (the option must be marked as autowired) by looking up in the registry to find if there is a single instance of matching type, which then gets configured on the component. This can be used for automatic configuring JDBC data sources, JMS connection factories, AWS Clients, etc." },
- "sslContextParameters": { "index": 19, "kind": "property", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "autowired": false, "secret": false, "description": "Customize TLS parameters used by the component. If not set defaults to the TLS parameters set in the Camel context" },
- "useGlobalSslContextParameters": { "index": 20, "kind": "property", "displayName": "Use Global Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Enable usage of global SSL context parameters." }
+ "externalReferences": { "index": 19, "kind": "property", "displayName": "External References", "group": "security", "label": "security", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "all", "local", "none" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "all", "description": "Which external references ($ref) the OpenAPI specification may use: all allows any reference, local allows references to the classpath and the file system only, and none allows only references inside the document. A reference the option does not allow fails the start. Can be overridden in endpoint configuration." },
+ "sslContextParameters": { "index": 20, "kind": "property", "displayName": "Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "object", "javaType": "org.apache.camel.support.jsse.SSLContextParameters", "deprecated": false, "autowired": false, "secret": false, "description": "Customize TLS parameters used by the component. If not set defaults to the TLS parameters set in the Camel context" },
+ "useGlobalSslContextParameters": { "index": 21, "kind": "property", "displayName": "Use Global Ssl Context Parameters", "group": "security", "label": "security", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Enable usage of global SSL context parameters." }
},
"properties": {
"specificationUri": { "index": 0, "kind": "path", "displayName": "Specification Uri", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "openapi.json", "description": "Path to the OpenApi specification file. The scheme, host base path are taken from this specification, but these can be overridden with properties on the component or endpoint level. If not given the component tries to load openapi.json resource from the classpath. Note that the host defined on the component and endpoint of this Component should contain the scheme, hostname and optionally the port in the URI syntax (i.e. http:\/\/api.example.com:8080). Overrides component configuration. The OpenApi specification can be loaded from different sources by prefixing with file: classpath: http: https:. Support for https is limited to using the JDK installed UrlHandler, and as such it can be cumbersome to setup TLS\/SSL certificates for https (such as setting a number of javax.net.ssl JVM system properties). How to do that consult the JDK documentation for UrlHandler. Default value notice: By default loads openapi.json file" },
@@ -67,6 +68,7 @@
"requestValidationEnabled": { "index": 17, "kind": "parameter", "displayName": "Request Validation Enabled", "group": "producer", "label": "producer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Enable validation of requests against the configured OpenAPI specification" },
"componentName": { "index": 18, "kind": "parameter", "displayName": "Component Name", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Name of the Camel component that will perform the requests. The component must be present in Camel registry and it must implement RestProducerFactory service provider interface. If not set CLASSPATH is searched for single component that implements RestProducerFactory SPI. Overrides component configuration." },
"lazyStartProducer": { "index": 19, "kind": "parameter", "displayName": "Lazy Start Producer", "group": "producer (advanced)", "label": "producer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether the producer should be started lazy (on the first message). By starting lazy you can use this to allow CamelContext and routes to startup in situations where a producer may otherwise fail during starting and cause the route to fail being started. By deferring this startup to be lazy then the startup failure can be handled during routing messages via Camel's routing error handlers. Beware that when the first message is processed then creating and starting the producer may take a little time and prolong the total processing time of the processing." },
- "oauthProfile": { "index": 20, "kind": "parameter", "displayName": "OAuth Profile", "group": "security", "label": "consumer,security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "OAuth profile name passed to the HTTP consumer delegate for validating incoming Authorization: Bearer tokens. The selected consumer component must support the oauthProfile option; delegates that ignore unknown options will start without endpoint protection." }
+ "externalReferences": { "index": 20, "kind": "parameter", "displayName": "External References", "group": "security", "label": "security", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "all", "local", "none" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "all", "description": "Which external references ($ref) the OpenAPI specification may use. The parser loads every external reference when the consumer or producer is created, from the classpath, the file system or over http(s). all allows any reference, local allows references to the classpath and the file system only, and none allows only references inside the document. A reference the option does not allow fails the start, before the parser loads anything. Overrides component configuration." },
+ "oauthProfile": { "index": 21, "kind": "parameter", "displayName": "OAuth Profile", "group": "security", "label": "consumer,security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "OAuth profile name passed to the HTTP consumer delegate for validating incoming Authorization: Bearer tokens. The selected consumer component must support the oauthProfile option; delegates that ignore unknown options will start without endpoint protection." }
}
}
diff --git a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc
index b7138eab3a7dc..837486545657d 100644
--- a/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc
+++ b/components/camel-rest-openapi/src/main/docs/rest-openapi-component.adoc
@@ -330,6 +330,35 @@ so a misconfigured delegate cannot start without the expected protection. Custom
enforce the option must override `RestOpenApiConsumerFactory.supportsOAuthProfile()` to return
`true`.
+=== Restricting the external references of the specification
+
+An OpenAPI specification can reference other documents with `$ref`: files on the classpath or the file system,
+and documents on an http(s) server. The parser loads every such document when the consumer or producer is
+created, and a document it loads can reference further documents. The `externalReferences` option, on the
+endpoint or the component, restricts this:
+
+* `all` (default) allows any reference.
+* `local` allows references to the classpath and the file system only. A specification loaded from a URL may
+ then reference no other document at all, because the parser resolves its references against that URL.
+* `none` allows only references inside the document, such as `#/components/schemas/Pet`.
+
+The check runs before the parser resolves anything, on the document tree the parser uses (YAML aliases expanded).
+It resolves every `$ref` the way the parser does: against the document that contains it and, for OpenAPI 3.1,
+against the enclosing `$id`. `file:` and `classpath:` references count as local. The local documents the option
+allows are read and checked in turn, and a document that cannot be read fails the check. A reference the option
+does not allow fails the start, with a message that names the reference and where it is.
+
+[source,java]
+----
+from("direct:getPetById")
+ .to("rest-openapi:petstore-v3.json#getPetById?externalReferences=local");
+----
+
+The option can also be set once on the component, for example with
+`camel.component.rest-openapi.external-references=none`. For the contract-first Rest DSL, set it on the `openApi`
+element as shown in xref:manual::rest-dsl-openapi.adoc[REST DSL with contract-first OpenAPI]; the component setting
+applies there as well, also to the specification served as api-doc.
+
== Request validation
API requests can be validated against the configured OpenAPI specification before they are sent by setting the `requestValidationEnabled` option to `true`.
diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java
new file mode 100644
index 0000000000000..2e6bda6c80069
--- /dev/null
+++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java
@@ -0,0 +1,315 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.rest.openapi;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.processors.ExternalRefProcessor;
+import io.swagger.v3.parser.reference.ReferenceUtils;
+import io.swagger.v3.parser.util.ClasspathHelper;
+import io.swagger.v3.parser.util.DeserializationUtils;
+import io.swagger.v3.parser.util.RefUtils;
+
+/**
+ * Checks the references ({@code $ref}) of an OpenAPI specification against the {@code externalReferences} option before
+ * the parser resolves them. The parser loads every external reference it meets: from the working directory and the
+ * classpath, or over http(s) when the reference is a URL or the specification itself comes from a URL.
+ *
+ * The check works on the tree the parser resolves (YAML aliases expanded), resolves each reference the way the parser
+ * does, that is against the document that contains it and, for OpenAPI 3.1, against the enclosing {@code $id}s, which
+ * the parser also carries into the documents it reaches through a reference, and reads the local documents it allows to
+ * check their references in turn. A document it cannot read fails the check.
+ *
+ * camel-openapi-java has a copy of this class for the specification it serves as api-doc; keep the two in sync.
+ */
+final class OpenApiExternalReferences {
+
+ static final String ALL = "all";
+ static final String LOCAL = "local";
+ static final String NONE = "none";
+
+ /**
+ * The URI the parser gives an OpenAPI 3.1 specification that has no location (OpenAPIDereferencer31).
+ */
+ private static final String LOCAL_URI = "local";
+
+ private OpenApiExternalReferences() {
+ }
+
+ /**
+ * The value of the option to apply: {@link #ALL} when none is set.
+ *
+ * @throws IllegalArgumentException for a value other than all, local or none
+ */
+ static String policy(String externalReferences) {
+ if (externalReferences == null || externalReferences.isBlank()) {
+ return ALL;
+ }
+ String policy = externalReferences.trim().toLowerCase(Locale.ROOT);
+ if (ALL.equals(policy) || LOCAL.equals(policy) || NONE.equals(policy)) {
+ return policy;
+ }
+ throw new IllegalArgumentException(
+ "Unknown externalReferences value: " + externalReferences + ", expected all, local or none");
+ }
+
+ /**
+ * Checks every {@code $ref} of the specification, and of the local documents it references, against the option.
+ *
+ * @param externalReferences the option: all, local or none
+ * @param specification the specification, as JSON or YAML text
+ * @param specificationUri where the specification comes from, for the error message
+ * @param remote whether the specification comes from a URL: the parser then resolves its
+ * external references against that URL
+ * @throws IllegalArgumentException for the first reference the option does not allow, and for a referenced document
+ * that cannot be read
+ */
+ static void check(String externalReferences, String specification, String specificationUri, boolean remote) {
+ String policy = policy(externalReferences);
+ if (ALL.equals(policy)) {
+ return;
+ }
+ new Check(policy, specificationUri, remote).root(specification);
+ }
+
+ private static boolean isHttp(String target) {
+ String lower = target.toLowerCase(Locale.ROOT);
+ return lower.startsWith("http:") || lower.startsWith("https:");
+ }
+
+ /**
+ * A document a reference leads to: where the reference is, and the OpenAPI 3.1 {@code $id}s the parser carries into
+ * the document, which decide what the references inside it resolve to.
+ */
+ private static final class Target {
+
+ private final String location;
+ private final String origin;
+ private final List inheritedIds;
+
+ Target(String location, String origin, List inheritedIds) {
+ this.location = location;
+ this.origin = origin;
+ this.inheritedIds = inheritedIds;
+ }
+ }
+
+ /**
+ * The check of one specification: the root document and the documents reached from it.
+ */
+ private static final class Check {
+
+ private final String policy;
+ private final String specificationUri;
+ private final boolean remote;
+ private final Set checked = new HashSet<>();
+ private boolean openApi31;
+
+ Check(String policy, String specificationUri, boolean remote) {
+ this.policy = policy;
+ this.specificationUri = specificationUri;
+ this.remote = remote;
+ }
+
+ void root(String specification) {
+ JsonNode tree = tree(specification, null, null);
+ // the parser resolves an OpenAPI 3.1 document with its own dereferencer, which has other rules
+ openApi31 = tree.path("openapi").asText("").startsWith("3.1");
+ follow(tree, null, openApi31 ? LOCAL_URI : null, List.of());
+ }
+
+ /**
+ * Checks the references of one document and then the documents they lead to.
+ *
+ * @param tree the document
+ * @param document the resolved location of the document, as the parser uses it, or null for the root
+ * @param base what the references of the document resolve against
+ * @param inheritedIds the {@code $id}s of the schemas enclosing the reference that led here (OpenAPI 3.1)
+ */
+ private void follow(JsonNode tree, String document, String base, List inheritedIds) {
+ // the external references of this document, with where they are, in the order they are met
+ Map targets = new LinkedHashMap<>();
+ scan(tree, "", document, base, inheritedIds, targets);
+ for (Target target : targets.values()) {
+ String targetBase = target.location;
+ if (openApi31) {
+ // OpenAPI31Traverser.traverseSchema: the inherited $ids resolved, in turn, against the document
+ for (String id : target.inheritedIds) {
+ targetBase = resolveBase(id, targetBase, "", target.location);
+ }
+ }
+ // a document resolves its references the same way whenever it is reached with the same base, whatever
+ // chain of $ids led to it; this also ends the visit of a schema that references itself
+ if (checked.add(target.location + " under " + targetBase)) {
+ JsonNode targetTree = tree(read(target.location, target.origin), target.location, target.origin);
+ follow(targetTree, target.location, targetBase, target.inheritedIds);
+ }
+ }
+ }
+
+ private JsonNode tree(String content, String document, String origin) {
+ try {
+ // the tree the parser resolves: the same YAML reader, with the aliases expanded and its limits applied
+ return DeserializationUtils.deserializeIntoTree(content, document != null ? document : specificationUri);
+ } catch (RuntimeException e) {
+ throw new IllegalArgumentException(
+ (document != null ? where(document, origin) : "The OpenAPI specification " + specificationUri)
+ + " cannot be read to check its references: " + e.getMessage(),
+ e);
+ }
+ }
+
+ private void scan(
+ JsonNode node, String path, String document, String base, List ids,
+ Map targets) {
+ if (node.isObject()) {
+ String nodeBase = base;
+ List nodeIds = ids;
+ if (openApi31) {
+ JsonNode id = node.get("$id");
+ if (id != null && id.isTextual() && !id.asText().isBlank()) {
+ nodeBase = resolveBase(id.asText(), base, path, document);
+ nodeIds = new ArrayList<>(ids);
+ nodeIds.add(id.asText());
+ }
+ }
+ for (Map.Entry field : node.properties()) {
+ String fieldPath = path.isEmpty() ? field.getKey() : path + "." + field.getKey();
+ if ("$ref".equals(field.getKey()) && field.getValue().isTextual()) {
+ reference(field.getValue().asText(), fieldPath, document, nodeBase, nodeIds, targets);
+ } else {
+ scan(field.getValue(), fieldPath, document, nodeBase, nodeIds, targets);
+ }
+ }
+ } else if (node.isArray()) {
+ int i = 0;
+ for (JsonNode item : node) {
+ scan(item, path + "[" + i++ + "]", document, base, ids, targets);
+ }
+ }
+ }
+
+ /**
+ * The base the references below an OpenAPI 3.1 {@code $id} resolve against: the id resolved against the
+ * enclosing base, as OpenAPI31Traverser.traverseSchema does.
+ */
+ private String resolveBase(String id, String base, String path, String document) {
+ try {
+ return ReferenceUtils.toBaseURI(ReferenceUtils.resolve(ReferenceUtils.toBaseURI(id), base));
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ where(id, path, document) + " cannot be resolved as $id: " + e.getMessage(), e);
+ }
+ }
+
+ private void reference(
+ String ref, String path, String document, String base, List ids, Map targets) {
+ if (ref.isEmpty() || ref.startsWith("#")) {
+ // inside the document: the parser evaluates the fragment in the document, or in the schema whose $id
+ // is the base, without loading anything
+ return;
+ }
+ String target;
+ try {
+ if (openApi31) {
+ // ReferenceVisitor.resolveSchemaRef: the reference resolved against the enclosing $ids and the
+ // document, without its fragment
+ target = ReferenceUtils.toBaseURI(ReferenceUtils.resolve(ref, base));
+ } else {
+ // ExternalRefProcessor: a reference met in a referenced document is joined with the path of that
+ // document; the parser then reads the part before the fragment
+ String joined = document == null ? ref : ExternalRefProcessor.join(document, ref);
+ int fragment = joined.indexOf("#/");
+ target = fragment < 0 ? joined : joined.substring(0, fragment);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ where(ref, path, document) + " cannot be resolved: " + e.getMessage(), e);
+ }
+ String reason = null;
+ if (NONE.equals(policy)) {
+ reason = "externalReferences=none allows only references inside the document";
+ } else if (remote) {
+ reason = "externalReferences=local allows no external reference in a specification loaded from a URL,"
+ + " since the parser resolves it against that URL";
+ } else if (isHttp(target)) {
+ reason = "externalReferences=local allows no http(s) reference"
+ + (target.equals(ref) ? "" : " (" + ref + " resolves to " + target + ")");
+ }
+ if (reason != null) {
+ throw new IllegalArgumentException(where(ref, path, document) + ", but " + reason);
+ }
+ // a local document: file: and classpath: URIs, and paths the parser reads from the working directory or
+ // the classpath; the parser carries the enclosing $ids into it (ReferenceVisitor.resolveSchemaRef)
+ targets.putIfAbsent(target + " under " + ids, new Target(target, where(ref, path, document), ids));
+ }
+
+ /**
+ * Reads a referenced local document the way the parser does: a {@code file:} or {@code classpath:} URI by its
+ * path (OpenAPI 3.1, Visitor.readURI), anything else from the working directory and then the classpath
+ * (RefUtils.readExternalRef).
+ */
+ private String read(String target, String origin) {
+ try {
+ String scheme = null;
+ String schemePath = null;
+ try {
+ URI uri = new URI(target);
+ scheme = uri.getScheme() != null ? uri.getScheme().toLowerCase(Locale.ROOT) : null;
+ schemePath = uri.getPath();
+ } catch (URISyntaxException e) {
+ // not a URI, so a path
+ }
+ if ("file".equals(scheme)) {
+ return Files.readString(Paths.get(schemePath), StandardCharsets.UTF_8);
+ } else if ("classpath".equals(scheme)) {
+ return ClasspathHelper.loadFileFromClasspath(schemePath);
+ } else {
+ return RefUtils.readExternalRef(target, RefFormat.RELATIVE, null, new File(".").toPath(), null);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ where(target, origin) + " cannot be read to check its references: " + e.getMessage(), e);
+ }
+ }
+
+ private String where(String ref, String path, String document) {
+ return "The OpenAPI specification " + specificationUri + " references " + ref + " at " + path
+ + (document != null ? " in " + document : "");
+ }
+
+ private String where(String document, String origin) {
+ return origin != null
+ ? origin + ", which" : "The OpenAPI specification " + specificationUri + " references "
+ + document + ", which";
+ }
+ }
+}
diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiComponent.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiComponent.java
index 011e2cea78e15..cf4371c17c991 100644
--- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiComponent.java
+++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiComponent.java
@@ -147,6 +147,12 @@ public final class RestOpenApiComponent extends DefaultComponent implements SSLC
+ " (platform) or Camel via the unmatched request handler (camel). Can be overridden in endpoint"
+ " configuration.")
private String unmatchedRequestHandling = "platform";
+ @Metadata(label = "security", defaultValue = "all", enums = "all,local,none",
+ description = "Which external references ($ref) the OpenAPI specification may use: all allows any reference,"
+ + " local allows references to the classpath and the file system only, and none allows only"
+ + " references inside the document. A reference the option does not allow fails the start."
+ + " Can be overridden in endpoint configuration.")
+ private String externalReferences = "all";
@Metadata(description = "To use a custom strategy for how to process Rest DSL requests", label = "consumer,advanced")
private RestOpenapiProcessorStrategy restOpenapiProcessorStrategy;
@Metadata(description = "Enable usage of global SSL context parameters.", label = "security")
@@ -185,6 +191,7 @@ protected Endpoint createEndpoint(final String uri, final String remaining, fina
endpoint.setMissingOperation(getMissingOperation());
endpoint.setMockIncludePattern(getMockIncludePattern());
endpoint.setUnmatchedRequestHandling(getUnmatchedRequestHandling());
+ endpoint.setExternalReferences(getExternalReferences());
if (restOpenapiProcessorStrategy != null) {
endpoint.setRestOpenapiProcessorStrategy(restOpenapiProcessorStrategy);
} else {
@@ -294,6 +301,14 @@ public void setUnmatchedRequestHandling(String unmatchedRequestHandling) {
this.unmatchedRequestHandling = unmatchedRequestHandling;
}
+ public String getExternalReferences() {
+ return externalReferences;
+ }
+
+ public void setExternalReferences(String externalReferences) {
+ this.externalReferences = externalReferences;
+ }
+
public void setBasePath(final String basePath) {
this.basePath = basePath;
}
diff --git a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpoint.java b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpoint.java
index d4c39cf24d1c4..b5e031c614ec5 100644
--- a/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpoint.java
+++ b/components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/RestOpenApiEndpoint.java
@@ -46,6 +46,7 @@
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
+import io.swagger.v3.parser.util.RemoteUrl;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelContextAware;
import org.apache.camel.Category;
@@ -181,6 +182,14 @@ public final class RestOpenApiEndpoint extends DefaultEndpoint {
description = "Who answers requests that match no operation in the OpenAPI specification: the HTTP layer"
+ " (platform) or Camel via the unmatched request handler (camel).")
private String unmatchedRequestHandling = "platform";
+ @UriParam(label = "security", defaultValue = "all", enums = "all,local,none",
+ description = "Which external references ($ref) the OpenAPI specification may use. The parser loads every"
+ + " external reference when the consumer or producer is created, from the classpath, the file"
+ + " system or over http(s). all allows any reference, local allows references to the classpath"
+ + " and the file system only, and none allows only references inside the document. A reference"
+ + " the option does not allow fails the start, before the parser loads anything. Overrides"
+ + " component configuration.")
+ private String externalReferences = "all";
@UriParam(label = "consumer,security", displayName = "OAuth Profile",
description = "OAuth profile name passed to the HTTP consumer delegate for validating incoming"
+ " Authorization: Bearer tokens. The selected consumer component must support the"
@@ -227,7 +236,7 @@ public RestOpenApiComponent getComponent() {
@Override
public Consumer createConsumer(final Processor processor) throws Exception {
- OpenAPI doc = loadSpecificationFrom(getCamelContext(), specificationUri);
+ OpenAPI doc = loadSpecificationFrom(getCamelContext(), specificationUri, externalReferences);
String path = determineBasePath(doc);
RestOpenApiProcessor openApiProcessor
@@ -377,7 +386,7 @@ private Consumer createConsumerFor(String basePath, RestOpenApiProcessor openApi
@Override
public Producer createProducer() throws Exception {
final CamelContext camelContext = getCamelContext();
- final OpenAPI openapiDoc = loadSpecificationFrom(camelContext, specificationUri);
+ final OpenAPI openapiDoc = loadSpecificationFrom(camelContext, specificationUri, externalReferences);
final Paths paths = openapiDoc.getPaths();
for (final Entry pathEntry : paths.entrySet()) {
@@ -554,6 +563,14 @@ public void setUnmatchedRequestHandling(String unmatchedRequestHandling) {
this.unmatchedRequestHandling = unmatchedRequestHandling;
}
+ public String getExternalReferences() {
+ return externalReferences;
+ }
+
+ public void setExternalReferences(String externalReferences) {
+ this.externalReferences = externalReferences;
+ }
+
public String getOauthProfile() {
return oauthProfile;
}
@@ -1006,26 +1023,59 @@ static String hostFrom(final RestConfiguration restConfiguration) {
* @return the specification
*/
static OpenAPI loadSpecificationFrom(final CamelContext camelContext, final String uri) {
- final OpenAPIV3Parser openApiParser = new OpenAPIV3Parser();
- final ParseOptions options = new ParseOptions();
- options.setResolveFully(true);
+ return loadSpecificationFrom(camelContext, uri, OpenApiExternalReferences.ALL);
+ }
+ /**
+ * Loads the OpenApi definition model from the given path, after checking its references against the
+ * externalReferences option. The parser resolves every external reference of the specification, so a reference the
+ * option does not allow fails the load before the parser runs.
+ *
+ * @param camelContext context to use
+ * @param uri the specification uri
+ * @param externalReferences the externalReferences option: all, local or none
+ * @return the specification
+ */
+ static OpenAPI loadSpecificationFrom(
+ final CamelContext camelContext, final String uri, final String externalReferences) {
+ final String policy = OpenApiExternalReferences.policy(externalReferences);
+
+ String location = null;
+ String content = null;
InputStream is = null;
- List messages = null;
try {
- String location = null;
- String content = null;
Resource resource = ResourceHelper.resolveMandatoryResource(camelContext, uri);
if (resource.getScheme().startsWith("http")) {
location = resource.getURI().toString();
+ if (!OpenApiExternalReferences.ALL.equals(policy)) {
+ // read the document as the parser would, to check its references before the parser resolves them
+ content = RemoteUrl.urlToString(location, Collections.emptyList());
+ }
} else {
is = resource.getInputStream();
if (is != null) {
content = IOHelper.loadText(is);
}
}
+ } catch (Exception e) {
+ throw new IllegalArgumentException(
+ "Failed to load OpenAPI specification from: " + uri, e);
+ } finally {
+ IOHelper.close(is);
+ }
+ if (content != null) {
+ OpenApiExternalReferences.check(policy, content, uri, location != null);
+ }
+
+ final OpenAPIV3Parser openApiParser = new OpenAPIV3Parser();
+ final ParseOptions options = new ParseOptions();
+ options.setResolveFully(true);
+ List messages = null;
+ try {
SwaggerParseResult openApi = null;
- if (location != null) {
+ if (location != null && content != null) {
+ openApi = openApiParser.readContents(content, null, options, location);
+ } else if (location != null) {
openApi = openApiParser.readLocation(location, null, options);
} else if (content != null) {
openApi = openApiParser.readContents(content, null, options);
@@ -1044,8 +1094,6 @@ static OpenAPI loadSpecificationFrom(final CamelContext camelContext, final Stri
} catch (Exception e) {
throw new IllegalArgumentException(
"Failed to load OpenAPI specification from: " + uri, e);
- } finally {
- IOHelper.close(is);
}
throw new IllegalArgumentException(
diff --git a/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiExternalReferencesTest.java b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiExternalReferencesTest.java
new file mode 100644
index 0000000000000..8408b3db09e92
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/java/org/apache/camel/component/rest/openapi/RestOpenApiExternalReferencesTest.java
@@ -0,0 +1,580 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.rest.openapi;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.ResponseDefinitionBuilder;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.media.Schema;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * The externalReferences option: which $ref the specification may use, checked before the parser resolves them. The
+ * server stands in for every URL a specification could reach, so that a test can assert that nothing was fetched.
+ */
+class RestOpenApiExternalReferencesTest {
+
+ private static final WireMockServer SERVER = new WireMockServer(wireMockConfig().dynamicPort());
+
+ private static final String PET_SCHEMAS = """
+ components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
+ """;
+
+ private static final String OWNER_SCHEMA = """
+ type: object
+ properties:
+ name:
+ type: string
+ """;
+
+ private final CamelContext camelContext = new DefaultCamelContext();
+
+ @BeforeAll
+ static void startServer() {
+ SERVER.start();
+ SERVER.stubFor(get(urlEqualTo("/petstore.yaml")).willReturn(yaml(contract("pet.yaml#/components/schemas/Pet"))));
+ SERVER.stubFor(get(urlEqualTo("/pet.yaml")).willReturn(yaml(PET_SCHEMAS)));
+ SERVER.stubFor(get(urlEqualTo("/self-contained.yaml"))
+ .willReturn(yaml(contract("'#/components/schemas/Pet'") + PET_SCHEMAS)));
+ SERVER.stubFor(get(urlEqualTo("/schema.yaml")).willReturn(yaml(OWNER_SCHEMA)));
+ SERVER.stubFor(get(urlEqualTo("/schemas/owner.yaml")).willReturn(yaml(OWNER_SCHEMA)));
+ }
+
+ @AfterAll
+ static void stopServer() {
+ SERVER.stop();
+ }
+
+ @BeforeEach
+ void resetRequests() {
+ SERVER.resetRequests();
+ }
+
+ @Test
+ void allowsEveryReferenceByDefault() {
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "external-references-root.yaml");
+
+ assertThat(petSchema(openApi).getProperties()).containsKeys("id", "name");
+ }
+
+ @Test
+ void localAllowsAReferenceToTheClasspath() {
+ OpenAPI openApi
+ = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "external-references-root.yaml", "local");
+
+ assertThat(petSchema(openApi).getProperties()).containsKeys("id", "name");
+ }
+
+ @Test
+ void noneRejectsAReferenceToAnotherDocument() {
+ assertThatThrownBy(
+ () -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "external-references-root.yaml", "none"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("external-references-root.yaml")
+ .hasMessageContaining("external-references-pet.yaml#/components/schemas/Pet")
+ .hasMessageContaining("at paths./pets/{petId}.get.responses.200.content.application/json.schema.$ref")
+ .hasMessageContaining("externalReferences=none");
+ }
+
+ @Test
+ void noneAllowsASelfContainedSpecification() {
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "openapi-v3.yaml", "none");
+
+ assertThat(openApi.getPaths()).isNotEmpty();
+ }
+
+ @Test
+ void noneRejectsTheExternalReferencesOfAnOpenApi31Document() {
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "petstore-3.1.yaml", "none"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("/components/schemas/petdetails")
+ .hasMessageContaining("externalReferences=none");
+ }
+
+ @Test
+ void localRejectsAnHttpReferenceWithoutFetchingIt(@TempDir Path dir) throws Exception {
+ Path file = Files.writeString(dir.resolve("contract.yaml"),
+ contract(SERVER.baseUrl() + "/pet.yaml#/components/schemas/Pet"));
+
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + file, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(SERVER.baseUrl() + "/pet.yaml#/components/schemas/Pet")
+ .hasMessageContaining("externalReferences=local allows no http(s) reference");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void localChecksTheDocumentsTheSpecificationReferences() {
+ assertThatThrownBy(
+ () -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "external-references-nested.yaml", "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("https://schemas.example.invalid/owner.yaml#/components/schemas/Owner")
+ .hasMessageContaining("at components.schemas.Pet.properties.owner.$ref in external-references-pet-remote.yaml")
+ .hasMessageContaining("externalReferences=local");
+ }
+
+ @Test
+ void localResolvesTheReferencesOfADocumentInASubdirectory() {
+ OpenAPI openApi
+ = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "external-references-subdir.yaml", "local");
+
+ // the parser read the Owner reference of schemas/external-references-pet.yaml next to that document
+ Schema> owner = (Schema>) petSchema(openApi).getProperties().get("owner");
+ assertThat(owner.getProperties()).containsKey("name");
+ }
+
+ @Test
+ void localChecksTheReferencesOfADocumentInASubdirectory() {
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext,
+ "external-references-subdir-remote.yaml", "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("https://schemas.example.invalid/address.yaml#/components/schemas/Address")
+ .hasMessageContaining(
+ "at components.schemas.Owner.properties.address.$ref in schemas/external-references-owner-remote.yaml")
+ .hasMessageContaining("externalReferences=local");
+ }
+
+ @Test
+ void localChecksADocumentReferencedByAbsolutePath(@TempDir Path dir) throws Exception {
+ Path specs = Files.createDirectory(dir.resolve("specs"));
+ Files.writeString(specs.resolve("first.yaml"), """
+ components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ owner:
+ $ref: 'second.yaml#/components/schemas/Owner'
+ """);
+ Files.writeString(specs.resolve("second.yaml"), """
+ components:
+ schemas:
+ Owner:
+ $ref: '%s/schema.yaml'
+ """.formatted(SERVER.baseUrl()));
+ Path root = Files.writeString(dir.resolve("root.yaml"),
+ contract(specs.resolve("first.yaml") + "#/components/schemas/Pet"));
+
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining(SERVER.baseUrl() + "/schema.yaml")
+ // the parser reads second.yaml next to first.yaml, not from the working directory
+ .hasMessageContaining("in " + specs.resolve("second.yaml"))
+ .hasMessageContaining("externalReferences=local");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void localFailsWhenAReferencedDocumentCannotBeRead(@TempDir Path dir) throws Exception {
+ Path root = Files.writeString(dir.resolve("root.yaml"), contract("missing.yaml#/components/schemas/Pet"));
+
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("references missing.yaml#/components/schemas/Pet")
+ .hasMessageContaining("cannot be read to check its references");
+ // the default lets the parser report the missing document and keep the rest
+ assertThat(RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root).getPaths())
+ .containsKey("/pets/{petId}");
+ }
+
+ @Test
+ void noneRejectsAReferenceGivenAsAYamlAlias(@TempDir Path dir) throws Exception {
+ // an anchor whose name starts with # looks like a reference inside the document before the alias is expanded
+ Path root = Files.writeString(dir.resolve("root.yaml"), "x-schema: remote '" + SERVER.baseUrl() + "/schema.yaml'\n"
+ + contract("*#remote"));
+
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "none"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("references " + SERVER.baseUrl() + "/schema.yaml")
+ .hasMessageContaining("externalReferences=none");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void localRejectsAReferenceGivenAsAYamlAlias(@TempDir Path dir) throws Exception {
+ Path root = Files.writeString(dir.resolve("root.yaml"), "x-schema: &remote '" + SERVER.baseUrl() + "/schema.yaml'\n"
+ + contract("*remote"));
+
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("references " + SERVER.baseUrl() + "/schema.yaml")
+ .hasMessageContaining("externalReferences=local allows no http(s) reference");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void allResolvesAReferenceGivenAsAYamlAlias(@TempDir Path dir) throws Exception {
+ Path root = Files.writeString(dir.resolve("root.yaml"), "x-schema: &remote '" + SERVER.baseUrl() + "/schema.yaml'\n"
+ + contract("*remote"));
+
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root);
+
+ // the parser expands the alias and fetches the schema
+ assertThat(petSchema(openApi).getProperties()).containsKey("name");
+ SERVER.verify(1, getRequestedFor(urlEqualTo("/schema.yaml")));
+ }
+
+ @Test
+ void localRejectsAnOpenApi31ReferenceResolvedAgainstAnHttpId(@TempDir Path dir) throws Exception {
+ Path root = Files.writeString(dir.resolve("root.yaml"), contract31(SERVER.baseUrl() + "/schemas/", "owner.yaml"));
+
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("references owner.yaml at components.schemas.Pet.properties.owner.$ref")
+ .hasMessageContaining("(owner.yaml resolves to " + SERVER.baseUrl() + "/schemas/owner.yaml)")
+ .hasMessageContaining("externalReferences=local");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void allResolvesAnOpenApi31ReferenceAgainstItsId(@TempDir Path dir) throws Exception {
+ Path root = Files.writeString(dir.resolve("root.yaml"), contract31(SERVER.baseUrl() + "/schemas/", "owner.yaml"));
+
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root);
+
+ // the parser resolves the reference against the $id of the schema and fetches it
+ Schema> owner = (Schema>) petSchema(openApi).getProperties().get("owner");
+ assertThat(owner.getProperties()).containsKey("name");
+ SERVER.verify(1, getRequestedFor(urlEqualTo("/schemas/owner.yaml")));
+ }
+
+ @Test
+ void localKeepsTheInheritedIdAcrossAnExternalReference(@TempDir Path dir) throws Exception {
+ Path bridge = Files.writeString(dir.resolve("bridge.yaml"), """
+ type: object
+ properties:
+ owner:
+ $ref: owner.yaml
+ """);
+ Files.writeString(dir.resolve("owner.yaml"), OWNER_SCHEMA);
+ Path root = Files.writeString(dir.resolve("root.yaml"),
+ contract31(SERVER.baseUrl() + "/schemas/", bridge.toUri().toString()));
+
+ // the parser carries the $id of the Pet schema into bridge.yaml, so owner.yaml is not the file next to it
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("references owner.yaml at properties.owner.$ref in " + bridge.toUri())
+ .hasMessageContaining("(owner.yaml resolves to " + SERVER.baseUrl() + "/schemas/owner.yaml)")
+ .hasMessageContaining("externalReferences=local");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void allResolvesAnOpenApi31ReferenceAgainstTheInheritedId(@TempDir Path dir) throws Exception {
+ Path bridge = Files.writeString(dir.resolve("bridge.yaml"), """
+ type: object
+ properties:
+ owner:
+ $ref: owner.yaml
+ """);
+ Files.writeString(dir.resolve("owner.yaml"), OWNER_SCHEMA);
+ Path root = Files.writeString(dir.resolve("root.yaml"),
+ contract31(SERVER.baseUrl() + "/schemas/", bridge.toUri().toString()));
+
+ RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root);
+
+ // the parser fetches owner.yaml from the server of the inherited $id, not from the directory of bridge.yaml
+ SERVER.verify(1, getRequestedFor(urlEqualTo("/schemas/owner.yaml")));
+ }
+
+ @Test
+ void localFinishesForASchemaThatReferencesItself(@TempDir Path dir) throws Exception {
+ Path node = dir.resolve("node.yaml");
+ Files.writeString(node, """
+ $id: %s
+ type: object
+ properties:
+ next:
+ $ref: %s
+ """.formatted(node.toUri(), node.toUri()));
+ Path root = Files.writeString(dir.resolve("root.yaml"), contract31(null, node.toUri().toString()));
+
+ // the schema is reached again under its own $id, with the same base, so the check visits it once
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local");
+
+ assertThat(openApi.getPaths()).containsKey("/pets/{petId}");
+ }
+
+ @Test
+ void localChecksADocumentReachedAgainUnderAnotherId(@TempDir Path dir) throws Exception {
+ Path shared = Files.writeString(dir.resolve("shared.yaml"), """
+ type: object
+ properties:
+ owner:
+ $ref: owner.yaml
+ """);
+ Files.writeString(dir.resolve("owner.yaml"), OWNER_SCHEMA);
+ Path root = Files.writeString(dir.resolve("root.yaml"), """
+ openapi: 3.1.0
+ info:
+ title: Pets
+ version: "1.0"
+ paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ components:
+ schemas:
+ Safe:
+ type: object
+ properties:
+ shared:
+ $ref: '%s'
+ Pet:
+ $id: %s/schemas/
+ type: object
+ properties:
+ shared:
+ $ref: '%s'
+ """.formatted(shared.toUri(), SERVER.baseUrl(), shared.toUri()));
+
+ // shared.yaml is first reached without an $id, where owner.yaml is the file next to it, then below the $id of
+ // Pet, where the parser resolves owner.yaml against the server: the second visit must not be skipped
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("references owner.yaml at properties.owner.$ref in " + shared.toUri())
+ .hasMessageContaining("(owner.yaml resolves to " + SERVER.baseUrl() + "/schemas/owner.yaml)");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void noneAllowsAReferenceInsideTheDocumentBelowAnHttpId(@TempDir Path dir) throws Exception {
+ Path root = Files.writeString(dir.resolve("root.yaml"),
+ contract31(SERVER.baseUrl() + "/schemas/", "#/components/schemas/Owner") + """
+ Owner:
+ type: object
+ properties:
+ name:
+ type: string
+ """);
+
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "none");
+
+ // the parser evaluates the fragment in the document, whatever the $id above it
+ Schema> owner = (Schema>) petSchema(openApi).getProperties().get("owner");
+ assertThat(owner.getProperties()).containsKey("name");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void localChecksAnOpenApi31FileReference(@TempDir Path dir) throws Exception {
+ Path schema = Files.writeString(dir.resolve("schema.yaml"), """
+ components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ owner:
+ $ref: '%s/schema.yaml'
+ """.formatted(SERVER.baseUrl()));
+ Path root = Files.writeString(dir.resolve("root.yaml"),
+ contract31(null, schema.toUri() + "#/components/schemas/Pet"));
+
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("references " + SERVER.baseUrl() + "/schema.yaml")
+ .hasMessageContaining("in " + schema.toUri())
+ .hasMessageContaining("externalReferences=local");
+ SERVER.verify(0, anyRequestedFor(anyUrl()));
+ }
+
+ @Test
+ void localAllowsAnOpenApi31FileReference(@TempDir Path dir) throws Exception {
+ Path schema = Files.writeString(dir.resolve("schema.yaml"), PET_SCHEMAS);
+ Path root = Files.writeString(dir.resolve("root.yaml"),
+ contract31(null, schema.toUri() + "#/components/schemas/Pet"));
+
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "file:" + root, "local");
+
+ Schema> owner = (Schema>) petSchema(openApi).getProperties().get("owner");
+ assertThat(owner.getProperties()).containsKeys("id", "name");
+ }
+
+ @Test
+ void localRejectsEveryExternalReferenceOfARemoteSpecification() {
+ String uri = SERVER.baseUrl() + "/petstore.yaml";
+
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, uri, "local"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("pet.yaml#/components/schemas/Pet")
+ .hasMessageContaining("loaded from a URL");
+ SERVER.verify(1, getRequestedFor(urlEqualTo("/petstore.yaml")));
+ SERVER.verify(0, getRequestedFor(urlEqualTo("/pet.yaml")));
+ }
+
+ @Test
+ void allResolvesTheReferencesOfARemoteSpecification() {
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext, SERVER.baseUrl() + "/petstore.yaml");
+
+ assertThat(petSchema(openApi).getProperties()).containsKeys("id", "name");
+ SERVER.verify(1, getRequestedFor(urlEqualTo("/petstore.yaml")));
+ SERVER.verify(1, getRequestedFor(urlEqualTo("/pet.yaml")));
+ }
+
+ @Test
+ void noneAllowsASelfContainedRemoteSpecification() {
+ OpenAPI openApi = RestOpenApiEndpoint.loadSpecificationFrom(camelContext,
+ SERVER.baseUrl() + "/self-contained.yaml", "none");
+
+ assertThat(petSchema(openApi).getProperties()).containsKeys("id", "name");
+ SERVER.verify(1, getRequestedFor(urlEqualTo("/self-contained.yaml")));
+ }
+
+ @Test
+ void rejectsAnUnknownValue() {
+ assertThatThrownBy(() -> RestOpenApiEndpoint.loadSpecificationFrom(camelContext, "openapi-v3.yaml", "remote"))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Unknown externalReferences value: remote, expected all, local or none");
+ }
+
+ @Test
+ void endpointOptionAppliesToTheProducer() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ Endpoint endpoint = context
+ .getEndpoint("rest-openapi:external-references-root.yaml#getPetById?externalReferences=none");
+
+ assertThatThrownBy(endpoint::createProducer)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("externalReferences=none");
+ }
+ }
+
+ @Test
+ void componentOptionAppliesToEveryEndpoint() throws Exception {
+ try (CamelContext context = new DefaultCamelContext()) {
+ context.start();
+ context.getComponent("rest-openapi", RestOpenApiComponent.class).setExternalReferences("none");
+ Endpoint endpoint = context.getEndpoint("rest-openapi:external-references-root.yaml#getPetById");
+
+ assertThatThrownBy(endpoint::createProducer)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessageContaining("externalReferences=none");
+ }
+ }
+
+ private static ResponseDefinitionBuilder yaml(String body) {
+ return aResponse().withHeader("Content-Type", "application/yaml").withBody(body);
+ }
+
+ private static Schema> petSchema(OpenAPI openApi) {
+ return openApi.getPaths().get("/pets/{petId}").getGet().getResponses().get("200").getContent()
+ .get("application/json").getSchema();
+ }
+
+ /**
+ * An OpenAPI 3.0 contract whose response schema is the given reference, written as given (so a YAML alias stays an
+ * alias; a reference that starts with # must be quoted).
+ */
+ private static String contract(String petSchemaRef) {
+ return """
+ openapi: 3.0.3
+ info:
+ title: Pets
+ version: "1.0"
+ paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: %s
+ """.formatted(petSchemaRef);
+ }
+
+ /**
+ * An OpenAPI 3.1 contract whose Pet schema has the given $id (none when null) and whose owner property is the given
+ * reference. Further schemas can be appended under components.schemas.
+ */
+ private static String contract31(String petId, String ownerRef) {
+ return """
+ openapi: 3.1.0
+ info:
+ title: Pets
+ version: "1.0"
+ paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Pet'
+ components:
+ schemas:
+ Pet:
+ %s type: object
+ properties:
+ owner:
+ $ref: '%s'
+ """.formatted(petId != null ? " $id: " + petId + "\n" : "", ownerRef);
+ }
+}
diff --git a/components/camel-rest-openapi/src/test/resources/external-references-nested.yaml b/components/camel-rest-openapi/src/test/resources/external-references-nested.yaml
new file mode 100644
index 0000000000000..a7366e9453054
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/external-references-nested.yaml
@@ -0,0 +1,39 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A contract whose Pet schema lives in external-references-pet-remote.yaml, which in turn references a URL
+openapi: 3.0.3
+info:
+ title: Pets
+ version: "1.0"
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: 'external-references-pet-remote.yaml#/components/schemas/Pet'
diff --git a/components/camel-rest-openapi/src/test/resources/external-references-pet-remote.yaml b/components/camel-rest-openapi/src/test/resources/external-references-pet-remote.yaml
new file mode 100644
index 0000000000000..9685156a2f08b
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/external-references-pet-remote.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The schemas of external-references-nested.yaml; the owner schema is on a server that does not exist
+components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ id:
+ type: integer
+ owner:
+ $ref: 'https://schemas.example.invalid/owner.yaml#/components/schemas/Owner'
diff --git a/components/camel-rest-openapi/src/test/resources/external-references-pet.yaml b/components/camel-rest-openapi/src/test/resources/external-references-pet.yaml
new file mode 100644
index 0000000000000..82a3854f7a0e9
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/external-references-pet.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The schemas of external-references-root.yaml
+components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ id:
+ type: integer
+ name:
+ type: string
diff --git a/components/camel-rest-openapi/src/test/resources/external-references-root.yaml b/components/camel-rest-openapi/src/test/resources/external-references-root.yaml
new file mode 100644
index 0000000000000..c8d171fb4e616
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/external-references-root.yaml
@@ -0,0 +1,39 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A contract whose Pet schema lives in external-references-pet.yaml, next to it on the classpath
+openapi: 3.0.3
+info:
+ title: Pets
+ version: "1.0"
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: 'external-references-pet.yaml#/components/schemas/Pet'
diff --git a/components/camel-rest-openapi/src/test/resources/external-references-subdir-remote.yaml b/components/camel-rest-openapi/src/test/resources/external-references-subdir-remote.yaml
new file mode 100644
index 0000000000000..8213e05d09fbc
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/external-references-subdir-remote.yaml
@@ -0,0 +1,39 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A contract whose Pet schema lives in schemas/external-references-pet-remote.yaml, which references a document next to it that references a URL
+openapi: 3.0.3
+info:
+ title: Pets
+ version: "1.0"
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: 'schemas/external-references-pet-remote.yaml#/components/schemas/Pet'
diff --git a/components/camel-rest-openapi/src/test/resources/external-references-subdir.yaml b/components/camel-rest-openapi/src/test/resources/external-references-subdir.yaml
new file mode 100644
index 0000000000000..4b5b1e52c6703
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/external-references-subdir.yaml
@@ -0,0 +1,39 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# A contract whose Pet schema lives in schemas/external-references-pet.yaml, which references a document next to it
+openapi: 3.0.3
+info:
+ title: Pets
+ version: "1.0"
+paths:
+ /pets/{petId}:
+ get:
+ operationId: getPetById
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ schema:
+ type: integer
+ responses:
+ "200":
+ description: The pet
+ content:
+ application/json:
+ schema:
+ $ref: 'schemas/external-references-pet.yaml#/components/schemas/Pet'
diff --git a/components/camel-rest-openapi/src/test/resources/schemas/external-references-owner-remote.yaml b/components/camel-rest-openapi/src/test/resources/schemas/external-references-owner-remote.yaml
new file mode 100644
index 0000000000000..f7d42247ddcce
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/schemas/external-references-owner-remote.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The Owner schema of schemas/external-references-pet-remote.yaml; the address schema is on a server that does not exist
+components:
+ schemas:
+ Owner:
+ type: object
+ properties:
+ name:
+ type: string
+ address:
+ $ref: 'https://schemas.example.invalid/address.yaml#/components/schemas/Address'
diff --git a/components/camel-rest-openapi/src/test/resources/schemas/external-references-owner.yaml b/components/camel-rest-openapi/src/test/resources/schemas/external-references-owner.yaml
new file mode 100644
index 0000000000000..28734baab68aa
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/schemas/external-references-owner.yaml
@@ -0,0 +1,25 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The Owner schema of schemas/external-references-pet.yaml
+components:
+ schemas:
+ Owner:
+ type: object
+ properties:
+ name:
+ type: string
diff --git a/components/camel-rest-openapi/src/test/resources/schemas/external-references-pet-remote.yaml b/components/camel-rest-openapi/src/test/resources/schemas/external-references-pet-remote.yaml
new file mode 100644
index 0000000000000..1f9daa5bbbdc7
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/schemas/external-references-pet-remote.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The Pet schema of external-references-subdir-remote.yaml; the parser reads the Owner reference as schemas/external-references-owner-remote.yaml
+components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ id:
+ type: integer
+ owner:
+ $ref: 'external-references-owner-remote.yaml#/components/schemas/Owner'
diff --git a/components/camel-rest-openapi/src/test/resources/schemas/external-references-pet.yaml b/components/camel-rest-openapi/src/test/resources/schemas/external-references-pet.yaml
new file mode 100644
index 0000000000000..d4ef280f0d114
--- /dev/null
+++ b/components/camel-rest-openapi/src/test/resources/schemas/external-references-pet.yaml
@@ -0,0 +1,27 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# The Pet schema of external-references-subdir.yaml; the parser reads the Owner reference as schemas/external-references-owner.yaml
+components:
+ schemas:
+ Pet:
+ type: object
+ properties:
+ id:
+ type: integer
+ owner:
+ $ref: 'external-references-owner.yaml#/components/schemas/Owner'
diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/openApi.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/openApi.json
index 6925078501fa4..7cb3798e889bc 100644
--- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/openApi.json
+++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/rest/openApi.json
@@ -21,6 +21,7 @@
"routeId": { "index": 6, "kind": "attribute", "displayName": "Route Id", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The route id." },
"missingOperation": { "index": 7, "kind": "attribute", "displayName": "Missing Operation", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "ignore", "mock" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "fail", "description": "Whether to fail, ignore or return a mock response for OpenAPI operations that are not mapped to a corresponding route." },
"unmatchedRequestHandling": { "index": 8, "kind": "attribute", "displayName": "Unmatched Request Handling", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "platform", "camel" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "platform", "description": "Who answers requests that match no operation in the OpenAPI specification: the HTTP layer (platform) or Camel via the unmatched request handler (camel)." },
- "mockIncludePattern": { "index": 9, "kind": "attribute", "displayName": "Mock Include Pattern", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "classpath:camel-mock\/**", "description": "Used for inclusive filtering of mock data from directories. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma." }
+ "mockIncludePattern": { "index": 9, "kind": "attribute", "displayName": "Mock Include Pattern", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "classpath:camel-mock\/**", "description": "Used for inclusive filtering of mock data from directories. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma." },
+ "externalReferences": { "index": 10, "kind": "attribute", "displayName": "External References", "group": "security", "label": "security", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "all", "local", "none" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "all", "description": "Which external references ($ref) the OpenAPI specification may use: all allows any reference, local allows references to the classpath and the file system only, and none allows only references inside the document. A reference the option does not allow fails the start." }
}
}
diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/OpenApiDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/OpenApiDefinition.java
index 4993c41d894cf..ab98ec353d0ca 100644
--- a/core/camel-core-model/src/main/java/org/apache/camel/model/rest/OpenApiDefinition.java
+++ b/core/camel-core-model/src/main/java/org/apache/camel/model/rest/OpenApiDefinition.java
@@ -62,6 +62,10 @@ public class OpenApiDefinition extends OptionalIdentifiedDefinition switch (key) {
case "apiContextPath": def.setApiContextPath(val); yield true;
case "disabled": def.setDisabled(val); yield true;
+ case "externalReferences": def.setExternalReferences(val); yield true;
case "missingOperation": def.setMissingOperation(val); yield true;
case "mockIncludePattern": def.setMockIncludePattern(val); yield true;
case "routeId": def.setRouteId(val); yield true;
diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
index c18aec4ad8d63..505c2ac69619e 100644
--- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
+++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java
@@ -3289,6 +3289,7 @@ protected void doWriteOpenApiDefinition(String name, OpenApiDefinition def) thro
doWriteAttribute("missingOperation", def.getMissingOperation(), "fail");
doWriteAttribute("unmatchedRequestHandling", def.getUnmatchedRequestHandling(), "platform");
doWriteAttribute("mockIncludePattern", def.getMockIncludePattern(), "classpath:camel-mock/**");
+ doWriteAttribute("externalReferences", def.getExternalReferences(), "all");
endElement(name);
}
protected void doWriteOpenIdConnectDefinition(String name, OpenIdConnectDefinition def) throws IOException {
diff --git a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
index 614e3877de5e7..d019af156f520 100644
--- a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
+++ b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java
@@ -3290,6 +3290,7 @@ protected JsonObject doWriteOpenApiDefinition(OpenApiDefinition def) {
doWriteAttribute(jo, "missingOperation", def.getMissingOperation(), "fail");
doWriteAttribute(jo, "unmatchedRequestHandling", def.getUnmatchedRequestHandling(), "platform");
doWriteAttribute(jo, "mockIncludePattern", def.getMockIncludePattern(), "classpath:camel-mock/**");
+ doWriteAttribute(jo, "externalReferences", def.getExternalReferences(), "all");
return jo;
}
protected JsonObject doWriteOpenIdConnectDefinition(OpenIdConnectDefinition def) {
diff --git a/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc b/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc
index 9bd9ecc4cc64f..601967f0d0632 100644
--- a/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc
+++ b/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc
@@ -661,6 +661,49 @@ YAML::
----
====
+=== Restricting external references
+
+A contract can reference other documents with `$ref`: files on the classpath or the file system, and documents on an
+http(s) server. Camel loads every such document when the route starts. The `externalReferences` option restricts this:
+`all` (default) allows any reference, `local` allows references to the classpath and the file system only, and `none`
+allows only references inside the contract. The check follows the references into the local documents it allows, and
+a document it cannot read fails the start too. A reference the option does not allow fails the start, with a message
+that names it.
+
+[tabs]
+====
+
+Java::
++
+[source,java]
+----
+ rest().openApi().specification("petstore-v3.json").externalReferences("none");
+----
+
+XML::
++
+[source,xml]
+----
+
+
+
+----
+
+YAML::
++
+[source,yaml]
+----
+- rest:
+ openApi:
+ specification: petstore-v3.json
+ externalReferences: none
+----
+====
+
+The option can also be set once on the `rest-openapi` component, for example with
+`camel.component.rest-openapi.external-references=none`, which covers every contract and the specification served as
+api-doc. See the xref:components::rest-openapi-component.adoc[REST OpenAPI component] for the details.
+
== Examples
You can find a few examples such as:
diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/RestOpenapiComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/RestOpenapiComponentBuilderFactory.java
index 9168bc5c13ece..b2d65b555c009 100644
--- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/RestOpenapiComponentBuilderFactory.java
+++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/RestOpenapiComponentBuilderFactory.java
@@ -421,6 +421,27 @@ default RestOpenapiComponentBuilder autowiredEnabled(boolean autowiredEnabled) {
return this;
}
+
+ /**
+ * Which external references ($ref) the OpenAPI specification may use:
+ * all allows any reference, local allows references to the classpath
+ * and the file system only, and none allows only references inside the
+ * document. A reference the option does not allow fails the start. Can
+ * be overridden in endpoint configuration.
+ *
+ * The option is a: <code>java.lang.String</code> type.
+ *
+ * Default: all
+ * Group: security
+ *
+ * @param externalReferences the value to set
+ * @return the dsl builder
+ */
+ default RestOpenapiComponentBuilder externalReferences(java.lang.String externalReferences) {
+ doSetProperty("externalReferences", externalReferences);
+ return this;
+ }
+
/**
* Customize TLS parameters used by the component. If not set defaults
* to the TLS parameters set in the Camel context.
@@ -488,6 +509,7 @@ protected boolean setPropertyOnComponent(
case "consumes": ((RestOpenApiComponent) component).setConsumes((java.lang.String) value); return true;
case "produces": ((RestOpenApiComponent) component).setProduces((java.lang.String) value); return true;
case "autowiredEnabled": ((RestOpenApiComponent) component).setAutowiredEnabled((boolean) value); return true;
+ case "externalReferences": ((RestOpenApiComponent) component).setExternalReferences((java.lang.String) value); return true;
case "sslContextParameters": ((RestOpenApiComponent) component).setSslContextParameters((org.apache.camel.support.jsse.SSLContextParameters) value); return true;
case "useGlobalSslContextParameters": ((RestOpenApiComponent) component).setUseGlobalSslContextParameters((boolean) value); return true;
default: return false;
diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/RestOpenApiEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/RestOpenApiEndpointBuilderFactory.java
index 8830a4f34e75d..dbd18b17df37e 100644
--- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/RestOpenApiEndpointBuilderFactory.java
+++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/RestOpenApiEndpointBuilderFactory.java
@@ -175,6 +175,28 @@ default RestOpenApiEndpointConsumerBuilder missingOperation(String missingOperat
doSetProperty("missingOperation", missingOperation);
return this;
}
+ /**
+ * Which external references ($ref) the OpenAPI specification may use.
+ * The parser loads every external reference when the consumer or
+ * producer is created, from the classpath, the file system or over
+ * http(s). all allows any reference, local allows references to the
+ * classpath and the file system only, and none allows only references
+ * inside the document. A reference the option does not allow fails the
+ * start, before the parser loads anything. Overrides component
+ * configuration.
+ *
+ * The option is a: java.lang.String type.
+ *
+ * Default: all
+ * Group: security
+ *
+ * @param externalReferences the value to set
+ * @return the dsl builder
+ */
+ default RestOpenApiEndpointConsumerBuilder externalReferences(String externalReferences) {
+ doSetProperty("externalReferences", externalReferences);
+ return this;
+ }
/**
* OAuth profile name passed to the HTTP consumer delegate for
* validating incoming Authorization: Bearer tokens. The selected
@@ -499,6 +521,28 @@ default RestOpenApiEndpointProducerBuilder requestValidationEnabled(String reque
doSetProperty("requestValidationEnabled", requestValidationEnabled);
return this;
}
+ /**
+ * Which external references ($ref) the OpenAPI specification may use.
+ * The parser loads every external reference when the consumer or
+ * producer is created, from the classpath, the file system or over
+ * http(s). all allows any reference, local allows references to the
+ * classpath and the file system only, and none allows only references
+ * inside the document. A reference the option does not allow fails the
+ * start, before the parser loads anything. Overrides component
+ * configuration.
+ *
+ * The option is a: java.lang.String type.
+ *
+ * Default: all
+ * Group: security
+ *
+ * @param externalReferences the value to set
+ * @return the dsl builder
+ */
+ default RestOpenApiEndpointProducerBuilder externalReferences(String externalReferences) {
+ doSetProperty("externalReferences", externalReferences);
+ return this;
+ }
}
/**
@@ -602,6 +646,28 @@ default RestOpenApiEndpointBuilder basePath(String basePath) {
doSetProperty("basePath", basePath);
return this;
}
+ /**
+ * Which external references ($ref) the OpenAPI specification may use.
+ * The parser loads every external reference when the consumer or
+ * producer is created, from the classpath, the file system or over
+ * http(s). all allows any reference, local allows references to the
+ * classpath and the file system only, and none allows only references
+ * inside the document. A reference the option does not allow fails the
+ * start, before the parser loads anything. Overrides component
+ * configuration.
+ *
+ * The option is a: java.lang.String type.
+ *
+ * Default: all
+ * Group: security
+ *
+ * @param externalReferences the value to set
+ * @return the dsl builder
+ */
+ default RestOpenApiEndpointBuilder externalReferences(String externalReferences) {
+ doSetProperty("externalReferences", externalReferences);
+ return this;
+ }
}
/**
diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
index e97a971eb6581..20dd9a2296c67 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
+++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
@@ -1102,6 +1102,10 @@
"source": "rest-dsl.adoc",
"yaml": "- restConfiguration:\n inlineRoutes: true\n- rest:\n get:\n - path: \"\/customers\/{id}\"\n to: \"direct:customerDetails\"\n - path: \"\/customers\/{id}\/orders\"\n to: \"direct:customerOrders\"\n post:\n - path: \"\/customers\/neworder\"\n to: \"direct:customerNewOrder\"\n"
},
+ {
+ "source": "rest-dsl-openapi.adoc",
+ "yaml": "- rest:\n openApi:\n specification: petstore-v3.json\n externalReferences: none\n"
+ },
{
"source": "rest-dsl.adoc",
"yaml": "- route:\n from:\n uri: kafka:new-order\n steps:\n - to:\n uri: direct:customerNewOrder\n"
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
index 162ff134f2397..c33284efb4372 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/generated/java/org/apache/camel/dsl/yaml/deserializers/ModelDeserializers.java
@@ -10936,6 +10936,7 @@ protected boolean setProperty(OnWhenDefinition target, String propertyKey,
@YamlProperty(name = "apiContextPath", type = "string", description = "Context path for exposing the OpenAPI specification file as a REST endpoint, allowing clients to obtain the specification from the running application.", displayName = "Api Context Path"),
@YamlProperty(name = "description", type = "string", description = "The description for this node", displayName = "Description"),
@YamlProperty(name = "disabled", type = "boolean", defaultValue = "false", description = "Whether to disable all the REST services from the OpenAPI contract from the route during build time. Once an REST service has been disabled then it cannot be enabled later at runtime.", displayName = "Disabled"),
+ @YamlProperty(name = "externalReferences", type = "enum:all,local,none", defaultValue = "all", description = "Which external references ($ref) the OpenAPI specification may use: all allows any reference, local allows references to the classpath and the file system only, and none allows only references inside the document. A reference the option does not allow fails the start.", displayName = "External References"),
@YamlProperty(name = "id", type = "string", description = "The id of this node", displayName = "Id"),
@YamlProperty(name = "missingOperation", type = "enum:fail,ignore,mock", defaultValue = "fail", description = "Whether to fail, ignore or return a mock response for OpenAPI operations that are not mapped to a corresponding route.", displayName = "Missing Operation"),
@YamlProperty(name = "mockIncludePattern", type = "string", defaultValue = "classpath:camel-mock/**", description = "Used for inclusive filtering of mock data from directories. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma.", displayName = "Mock Include Pattern"),
@@ -10970,6 +10971,11 @@ protected boolean setProperty(OpenApiDefinition target, String propertyKey,
target.setDisabled(val);
break;
}
+ case "externalReferences": {
+ String val = asText(node);
+ target.setExternalReferences(val);
+ break;
+ }
case "missingOperation": {
String val = asText(node);
target.setMissingOperation(val);
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
index dcfc26e8c91fa..0c4702cceabf3 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
@@ -11741,6 +11741,13 @@
"description" : "Whether to disable all the REST services from the OpenAPI contract from the route during build time. Once an REST service has been disabled then it cannot be enabled later at runtime.",
"default" : false
},
+ "externalReferences" : {
+ "type" : "string",
+ "title" : "External References",
+ "description" : "Which external references ($ref) the OpenAPI specification may use: all allows any reference, local allows references to the classpath and the file system only, and none allows only references inside the document. A reference the option does not allow fails the start.",
+ "default" : "all",
+ "enum" : [ "all", "local", "none" ]
+ },
"id" : {
"type" : "string",
"title" : "Id",
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index 58eccd8df1bb8..8a5bcadedecfd 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -17755,6 +17755,18 @@
"index" : 9,
"group" : "advanced",
"label" : "advanced"
+ }, {
+ "name" : "externalReferences",
+ "type" : "enum",
+ "enum" : [ "all", "local", "none" ],
+ "description" : "Which external references ($ref) the OpenAPI specification may use: all allows any reference, local allows references to the classpath and the file system only, and none allows only references inside the document. A reference the option does not allow fails the start.",
+ "title" : "External References",
+ "default" : "all",
+ "displayName" : "External References",
+ "kind" : "attribute",
+ "index" : 10,
+ "group" : "security",
+ "label" : "security"
} ]
},
"openIdConnect" : {
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
index d580432221c3e..cd593cb418e60 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
@@ -14181,6 +14181,13 @@
"description" : "Whether to disable all the REST services from the OpenAPI contract from the route during build time. Once an REST service has been disabled then it cannot be enabled later at runtime.",
"default" : false
},
+ "externalReferences" : {
+ "type" : "string",
+ "title" : "External References",
+ "description" : "Which external references ($ref) the OpenAPI specification may use: all allows any reference, local allows references to the classpath and the file system only, and none allows only references inside the document. A reference the option does not allow fails the start.",
+ "default" : "all",
+ "enum" : [ "all", "local", "none" ]
+ },
"id" : {
"type" : "string",
"title" : "Id",