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


##########
components/camel-rest-openapi/src/main/java/org/apache/camel/component/rest/openapi/OpenApiExternalReferences.java:
##########
@@ -0,0 +1,275 @@
+/*
+ * 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.HashSet;
+import java.util.LinkedHashMap;
+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.
+ * <p>
+ * 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, and
+ * reads the local documents it allows to check their references in turn. A 
document it cannot read fails the check.
+ * <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";
+
+    /**
+     * 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).document(specification, 
null, null);
+    }
+
+    private static boolean isHttp(String target) {
+        String lower = target.toLowerCase(Locale.ROOT);
+        return lower.startsWith("http:") || lower.startsWith("https:");
+    }
+
+    /**
+     * 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<String> checked = new HashSet<>();
+        private boolean openApi31;
+
+        Check(String policy, String specificationUri, boolean remote) {
+            this.policy = policy;
+            this.specificationUri = specificationUri;
+            this.remote = remote;
+        }
+
+        /**
+         * Checks one document.
+         *
+         * @param content  the document
+         * @param document the resolved location of the document, as the 
parser uses it, or null for the root
+         * @param origin   where the root references this document, for the 
error message, or null for the root
+         */
+        void document(String content, String document, String origin) {
+            JsonNode tree;
+            try {
+                // the tree the parser resolves: the same YAML reader, with 
the aliases expanded and its limits applied
+                tree = 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);
+            }
+            if (document == 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");
+            }
+            // the external references of this document, with where they are, 
in the order they are met
+            Map<String, String> targets = new LinkedHashMap<>();
+            String base = openApi31 && document == null ? LOCAL_URI : document;
+            scan(tree, "", document, base, targets);
+            for (Map.Entry<String, String> target : targets.entrySet()) {
+                if (checked.add(target.getKey())) {
+                    // a referenced local document can reference further 
documents, which the parser loads as well
+                    document(read(target.getKey(), target.getValue()), 
target.getKey(), target.getValue());

Review Comment:
   [P1] Preserve the inherited schema base across external references
   
   `document(...)` resets the base to the referenced file URI and passes no 
inherited `$id` context to the next scan. Swagger-parser 2.1.48 does carry 
`inheritedIds` into `traverseSchema` when `ReferenceVisitor.resolveSchemaRef` 
follows that reference, so the checker and parser can resolve the next relative 
reference to different resources.
   
   Reproduced with an OpenAPI 3.1 response schema containing:
   
   ```yaml
   $id: http://127.0.0.1:PORT/schemas/
   $ref: file:///tmp/example/bridge.yaml
   ```
   
   where `bridge.yaml` contains:
   
   ```yaml
   type: object
   properties:
     owner:
       $ref: owner.yaml
   ```
   
   and a benign `/tmp/example/owner.yaml` also exists. With 
`externalReferences=local`, the checker validates that local sibling, but the 
parser retains the HTTP `$id` and fetches `/schemas/owner.yaml`. Both 
`RestOpenApiEndpoint.loadSpecificationFrom` and `RestOpenApiReader.read` 
completed successfully and made one HTTP request in the loopback regression.
   
   Please preserve the parser's inherited schema-base context across reference 
edges in both copies of this helper, account for that context when 
deduplicating visits, and add a regression in both modules requiring rejection 
with zero HTTP requests. The existing direct-`$id` test does not cross an 
external-file boundary.
   
   _AI-generated review by Codex on behalf of [Luigi De Masi 
(@luigidemasi)](https://github.com/luigidemasi)._



-- 
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