luigidemasi commented on code in PR #28183:
URL: https://github.com/apache/camel/pull/28183#discussion_r4326839751


##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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.
+ * <p>
+ * 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";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    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
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot read the OpenAPI specification " + specificationUri
+                                               + (document != null ? " (" + 
document + ")" : "")
+                                               + " to check its references: " 
+ e.getMessage(),
+                    e);
+        }
+    }
+
+    private static String readLocalDocument(String file) {
+        try {
+            // how the parser reads a relative reference of a specification 
that has no location: from the working
+            // directory, then from the classpath
+            return RefUtils.readExternalRef(file, RefFormat.RELATIVE, null, 
new File(".").toPath());
+        } catch (RuntimeException e) {
+            LOG.debug("Cannot read {} to check its references, the parser 
reports it when it resolves the specification: {}",
+                    file, e.getMessage());
+            return null;

Review Comment:
   **P1 — Preserve the containing document's location and fail closed on an 
unreadable validation target.**
   
   Every nested reference is read relative to `.` here, and a read failure 
returns null so validation continues. Swagger-parser instead resolves a nested 
relative reference against the containing document. A root referencing 
`/tmp/specs/first.yaml`, whose `$ref` points to `second.yaml`, therefore skips 
checking `/tmp/specs/second.yaml`; if that document references HTTP, `local` 
still permits the request. I reproduced this through both endpoint loading and 
api-doc generation.
   
   The forced `RefFormat.RELATIVE` lookup also misses OpenAPI 3.1 `file:` URIs 
that the parser successfully reads. A referenced 
`file:///tmp/specs/schema.yaml` containing an HTTP `$ref` likewise passed 
`local` and fetched the URL on both call paths.
   
   Please resolve each target using its containing document's base and the 
parser's supported URI semantics, track resolved document identities, and 
reject a restricted parse when a target cannot be checked. Both copies need 
these regressions.



##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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.
+ * <p>
+ * 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";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    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
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);

Review Comment:
   **P1 — Validate expanded YAML aliases before accepting the policy.**
   
   `Yaml.mapper().readTree()` leaves an alias as its anchor name, whereas 
swagger-parser expands it before resolving references. For example, put this 
extension before `paths`:
   
   ```yaml
   x-url: &#remote 'http://127.0.0.1:PORT/schema.yaml'
   ```
   
   and use `$ref: *#remote` as a response schema. This checker sees `#remote`, 
considers it an internal reference, and accepts `externalReferences=none`. The 
parser then expands the alias to the HTTP URL and fetches it. An ordinary 
`&remote` / `*remote` alias similarly bypasses `local`.
   
   I reproduced both cases through endpoint loading and api-doc generation: 
each completed and made one request to the referenced loopback URL. Please 
validate the same expanded YAML representation the parser consumes, and add 
zero-request regressions for both policies. Apply this to the 
camel-openapi-java copy too.



##########
dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json:
##########
@@ -17755,6 +17755,17 @@
         "index" : 9,
         "group" : "advanced",
         "label" : "advanced"
+      }, {
+        "name" : "externalReferences",

Review Comment:
   **P2 — Regenerate the catalog's YAML model schema mirror.**
   
   This new `externalReferences` field is absent from 
`catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json`.
 Comparing the two files shows this 11-line field block is their only 
difference. `PrepareCatalogMojo.executeXmlSchemas()` copies this source into 
the catalog, so regeneration will leave an uncommitted change and the catalog 
currently lacks the new attribute. Please regenerate and commit the mirror.



##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,194 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import io.swagger.v3.core.util.Json;
+import io.swagger.v3.core.util.Yaml;
+import io.swagger.v3.parser.models.RefFormat;
+import io.swagger.v3.parser.util.RefUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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.
+ * <p>
+ * 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";
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(OpenApiExternalReferences.class);
+
+    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
+     */
+    static void check(String externalReferences, String specification, String 
specificationUri, boolean remote) {
+        String policy = policy(externalReferences);
+        if (ALL.equals(policy)) {
+            return;
+        }
+        checkDocument(policy, specification, specificationUri, null, remote, 
new HashSet<>());
+    }
+
+    private static void checkDocument(
+            String policy, String content, String specificationUri, String 
document, boolean remote,
+            Set<String> checked) {
+        DocumentCheck check = new DocumentCheck(policy, specificationUri, 
document, remote);
+        check.scan(readTree(content, specificationUri, document), "");
+        // a local document can reference further documents, which the parser 
loads as well
+        for (String file : check.localDocuments) {
+            if (checked.add(file)) {
+                String text = readLocalDocument(file);
+                if (text != null) {
+                    checkDocument(policy, text, specificationUri, file, false, 
checked);
+                }
+            }
+        }
+    }
+
+    private static JsonNode readTree(String content, String specificationUri, 
String document) {
+        try {
+            return (content.trim().startsWith("{") ? Json.mapper() : 
Yaml.mapper()).readTree(content);
+        } catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(
+                    "Cannot read the OpenAPI specification " + specificationUri
+                                               + (document != null ? " (" + 
document + ")" : "")
+                                               + " to check its references: " 
+ e.getMessage(),
+                    e);
+        }
+    }
+
+    private static String readLocalDocument(String file) {
+        try {
+            // how the parser reads a relative reference of a specification 
that has no location: from the working
+            // directory, then from the classpath
+            return RefUtils.readExternalRef(file, RefFormat.RELATIVE, null, 
new File(".").toPath());
+        } catch (RuntimeException e) {
+            LOG.debug("Cannot read {} to check its references, the parser 
reports it when it resolves the specification: {}",
+                    file, e.getMessage());
+            return null;
+        }
+    }
+
+    private static boolean isHttp(String file) {
+        String lower = file.toLowerCase(Locale.ROOT);
+        return lower.startsWith("http:") || lower.startsWith("https:");
+    }
+
+    /**
+     * The check of one document.
+     */
+    private static final class DocumentCheck {
+
+        private final String policy;
+        private final String specificationUri;
+        private final String document;
+        private final boolean remote;
+        private final List<String> localDocuments = new ArrayList<>();
+
+        DocumentCheck(String policy, String specificationUri, String document, 
boolean remote) {
+            this.policy = policy;
+            this.specificationUri = specificationUri;
+            this.document = document;
+            this.remote = remote;
+        }
+
+        void scan(JsonNode node, String path) {
+            if (node.isObject()) {
+                for (Map.Entry<String, JsonNode> 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);
+                    } else {
+                        scan(field.getValue(), fieldPath);
+                    }
+                }
+            } else if (node.isArray()) {
+                int i = 0;
+                for (JsonNode item : node) {
+                    scan(item, path + "[" + i++ + "]");
+                }
+            }
+        }
+
+        private void reference(String ref, String path) {
+            if (ref.isEmpty() || ref.startsWith("#")) {
+                // inside the document
+                return;
+            }
+            int fragment = ref.indexOf('#');
+            String file = fragment < 0 ? ref : ref.substring(0, fragment);
+            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(file)) {
+                reason = "externalReferences=local allows no http(s) 
reference";

Review Comment:
   **P1 — Apply the policy to the effective URI after OpenAPI 3.1 `$id` 
resolution.**
   
   Checking only the literal `$ref` misses inherited schema bases. For example, 
a response schema with `$id: http://127.0.0.1:PORT/schemas/` and `$ref: 
pet.yaml` passes this check as a local reference. Swagger-parser resolves it 
against `$id` and requests `/schemas/pet.yaml`.
   
   I reproduced successful parsing and one HTTP request under 
`externalReferences=local` through both the endpoint and api-doc reader. Carry 
the effective schema base through traversal and classify the resolved reference 
before allowing parser resolution. Add a regression asserting rejection and 
zero HTTP requests, including for the camel-openapi-java copy.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to