This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 8e8f2e2b92 Use `org.apache.sis.io.Authorization` for the GML access 
control of "xlink:href" attributes.
8e8f2e2b92 is described below

commit 8e8f2e2b92215dc5e4bee268da508dfcb735b61a
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Sep 23 21:32:22 2026 +0900

    Use `org.apache.sis.io.Authorization` for the GML access control of 
"xlink:href" attributes.
---
 .../main/org/apache/sis/xml/ReferenceResolver.java | 59 +++++++++++++---------
 .../main/org/apache/sis/xml/XML.java               |  4 +-
 .../xml/internal/shared/ExternalLinkHandler.java   | 31 +++---------
 .../apache/sis/xml/internal/shared/URISource.java  | 36 +++++++++++--
 .../org/apache/sis/metadata/xml/TestUsingFile.java |  5 +-
 .../sis/metadata/xml/extern/UsingExternalXLink.xml | 51 +++++++++++++++++++
 .../org/apache/sis/xml/ReferenceResolverTest.java  | 51 +++++++++++++++----
 .../org/apache/sis/storage/base/URIDataStore.java  |  4 +-
 .../main/org/apache/sis/storage/xml/Store.java     |  4 +-
 .../main/org/apache/sis/io/Authorization.java      | 28 +++++++---
 .../main/org/apache/sis/system/DataURI.java        | 29 ++++++++---
 11 files changed, 219 insertions(+), 83 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
index 566cba471f..bfa3034b2b 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
@@ -24,6 +24,7 @@ import javax.xml.transform.Source;
 import javax.xml.transform.URIResolver;
 import jakarta.xml.bind.Unmarshaller;
 import org.opengis.metadata.Identifier;
+import org.apache.sis.io.Authorization;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Emptiable;
 import org.apache.sis.util.LenientComparable;
@@ -52,24 +53,29 @@ import org.apache.sis.xml.internal.shared.XmlUtilities;
  */
 public class ReferenceResolver {
     /**
-     * The default resolved used at unmarshalling time when no resolver was 
explicitly set.
-     * This instance resolves {@code xlink:href} which are <abbr>URI</abbr> 
fragments relative
-     * to the current document, but does not accept to open references to 
external documents.
+     * The default resolver used at unmarshalling time when no resolver was 
explicitly set.
+     * For security reasons, this instance follows only the following types of 
{@code xlink:href}:
+     *
+     * <ul>
+     *   <li>references to fragments inside the current document, or</li>
+     *   <li>references to files in the same directory or in a sub-directory
+     *       of the file containing the {@code xlink:href}.</li>
+     * </ul>
      *
      * @see XML#RESOLVER
      */
     public static final ReferenceResolver DEFAULT = new ReferenceResolver();
 
     /**
-     * A resolver which accepts to open all external documents referenced by 
{@code xlink:href}.
-     * By {@linkplain #DEFAULT default}, only <abbr>URI</abbr> fragments 
relative to the current document are opened.
+     * A resolver which accepts to open all documents referenced by {@code 
xlink:href}.
+     * By {@linkplain #DEFAULT default}, only references in the same directory 
or sub-directory are followed.
      * But if this resolver is specified as a {@link XML#RESOLVER} property, 
all <abbr>URI</abbr>s will be accepted.
      *
      * <p><b>Historical note:</b> this was the default behavior in Apache 
<abbr>SIS</abbr> 1.5 and 1.6, but
      * <abbr>SIS</abbr> 1.7 reverted to not opening external document by 
default for security reasons.</p>
      *
      * @see XML#RESOLVER
-     * @see #canOpenExternal(URI)
+     * @see #accessControl(URI)
      *
      * @since 1.7
      */
@@ -205,7 +211,7 @@ public class ReferenceResolver {
              * For forward references, see 
https://issues.apache.org/jira/browse/SIS-420
              */
             final String fragment = Strings.trimOrNull(href.getFragment());
-            if (fragment == null) {
+            if (fragment == null || accessControl(href) == 
Authorization.DENIED) {
                 return null;
             }
             object = Context.getObjectForID(c, fragment);
@@ -224,7 +230,7 @@ public class ReferenceResolver {
                     source = externalSourceResolver.resolve(href.toString(), 
base.toString());
                 }
             }
-            if (source == null && (source = handler.openReader(href)) == null) 
{
+            if (source == null && (source = handler.tryResolve(href)) == null) 
{
                 reasonIfNull = Errors.Keys.CanNotResolveAsAbsolutePath_1;
                 object = null;
             } else {
@@ -266,20 +272,19 @@ public class ReferenceResolver {
      * The default implementation loads the file from the given source if it 
is not in the cache,
      * then returns the object identified by the fragment part of the URI.
      *
-     * <p>The {@code source} argument should have been determined by the 
caller has below:</p>
+     * <p>The {@code source} argument is constructed by the caller ({@code 
resolve(…)}) has below:</p>
      * <ul>
      *   <li>If an {@link URIResolver} has been specified at construction 
time, delegates to it.</li>
      *   <li>Otherwise or if the above returned {@code null}, then if the 
source of the current document
      *       is associated to a {@link javax.xml.stream.XMLResolver}, 
delegates to it.</li>
      *   <li>Otherwise, the caller tries to resolve the URI itself.</li>
      * </ul>
-     * The resolved URL, if known, should be available in {@link 
Source#getSystemId()}.
+     * The resolved URL, if known, is available in {@link 
Source#getSystemId()}.
      *
      * <h4>Authorization to resolve {@code xlink:href}</h4>
      * If the given {@code source} argument wraps an {@link URI}, then this 
method asks to
-     * {@link #canOpenExternal(URI)} whether this {@code ReferenceResolver} 
can open that <abbr>URI</abbr>.
-     * If {@code canOpenExternal(…)} returns {@code false}, then an {@link 
AccessDeniedException} is thrown.
-     * For security reasons, the default {@code canOpenExternal(…)} 
implementation returns always {@code false}.
+     * {@link #accessControl(URI)} whether this {@code ReferenceResolver} can 
open that <abbr>URI</abbr>.
+     * If {@code accessControl(…)} returns {@code DENIED}, then an {@link 
AccessDeniedException} is thrown.
      *
      * <h4>Error handling on failure to resolve {@code xlink:href}</h4>
      * The default implementation keeps a cache during the execution of an 
{@code XML.unmarshall(…)} method
@@ -297,15 +302,16 @@ public class ReferenceResolver {
      *
      * @since 1.5
      */
-    @SuppressWarnings("UseSpecificCatch")
+    @SuppressWarnings({"UseSpecificCatch", "fallthrough"})
     protected Object resolveExternal(final MarshalContext context, final 
Source source) throws Exception {
         final Object document;
         final String fragment;
         final URI uri;
         if (source instanceof URISource) {
             final var s = (URISource) source;
-            if (!canOpenExternal(s.document)) {
-                throw new AccessDeniedException(s.document.toString());
+            switch (accessControl(s.document)) {
+                case DEFAULT: if (s.isChildOfBase()) break;     // Else 
fallthrough.
+                case DENIED:  throw new 
AccessDeniedException(s.document.toString());
             }
             uri = s.getReadableURI();
             document = s.document;
@@ -378,20 +384,25 @@ public class ReferenceResolver {
     }
 
     /**
-     * Returns whether the given external document referenced in a {@code 
xlink:href} can be opened.
-     * If this method returns {@code false}, then {@link 
#resolveExternal(MarshalContext, Source)}
-     * while throw an {@link AccessDeniedException}.
-     * The {@linkplain #DEFAULT default} implementation returns {@code false}.
+     * Returns whether the specified document or fragment referenced in a 
{@code xlink:href} can be opened.
+     * The return value control the {@link #resolveExternal(MarshalContext, 
Source)} behavior as below:
+     *
+     * <ul>
+     *   <li>{@code GRANTED}: parse the document or fragment at the given 
<abbr>URI</abbr>.</li>
+     *   <li>{@code DENIED}:  throw an {@link AccessDeniedException}.</li>
+     *   <li>{@code DEFAULT}: behave like {@code GRANTED} if the file is in 
the same directory or in a subdirectory
+     *       of the document containing the {@code xlink:href}, otherwise 
behave like {@code DENIED}.</li>
+     * </ul>
      *
-     * @param  document  the external document referenced in a {@code 
xlink:href}.
-     * @return whether the given document can be opened.
+     * @param  document  the document or fragment referenced in a {@code 
xlink:href}.
+     * @return whether the given document or fragment can be opened.
      *
      * @see #FOLLOW_EXTERNAL_XLINK
      *
      * @since 1.7
      */
-    public boolean canOpenExternal(URI document) {
-        return this == FOLLOW_EXTERNAL_XLINK;
+    public Authorization accessControl(URI document) {
+        return (this == FOLLOW_EXTERNAL_XLINK) ? Authorization.GRANTED : 
Authorization.DEFAULT;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
index a63e299a53..370d26481d 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
@@ -261,7 +261,7 @@ public final class XML {
      *   <li>If the reference is of the form {@code xlink:href="#foo"} and an 
object with the {@code gml:id="foo"}
      *       attribute was previously found in the same <abbr>XML</abbr> 
document, then that object will be used.</li>
      *   <li>Otherwise, if {@code xlink:href} references an external document 
and the resolver is
-     *       {@linkplain ReferenceResolver#canOpenExternal(java.net.URI) 
authorized to open external documents},
+     *       {@linkplain ReferenceResolver#accessControl authorized to open 
external documents},
      *       then that document is unmarshalled.</li>
      *   <li>Otherwise, an empty element containing only the values of the 
above-cited attributes is created.</li>
      * </ul>
@@ -623,7 +623,7 @@ public final class XML {
         ensureNonNull("input", input);
         final Object object;
         try (InputStream in = new 
BufferedInputStream(Files.newInputStream(input, StandardOpenOption.READ))) {
-            object = unmarshal(URISource.create(in, input.toUri()), null);
+            object = unmarshal(URISource.create(in, null, input.toUri()), 
null);
         } catch (URISyntaxException | IOException e) {
             throw new JAXBException(Errors.format(Errors.Keys.CanNotRead_1, 
input), e);
         }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/ExternalLinkHandler.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/ExternalLinkHandler.java
index 5aa850b588..4ed4e850f5 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/ExternalLinkHandler.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/ExternalLinkHandler.java
@@ -188,24 +188,6 @@ public class ExternalLinkHandler {
         return baseURI;
     }
 
-    /**
-     * Resolves the given path as an URI. This method behaves as specified in 
{@link URI#resolve(URI)},
-     * with the URI given at construction-time as the base URI. If the given 
path is relative and there
-     * is no base URI, then the path cannot be resolved and this method 
returns {@code null}.
-     *
-     * @param  path  path to resolve.
-     * @return resolved path, or {@code null} it it cannot be resolved.
-     *
-     * @see URI#resolve(URI)
-     */
-    final URI resolve(final URI path) {
-        final URI baseURI = getURI();
-        if (baseURI != null) {
-            return baseURI.resolve(path);
-        }
-        return path.isAbsolute() ? path : null;
-    }
-
     /**
      * Reports a warning about a URI that cannot be parsed.
      * This method declares {@link ReferenceResolver} as the public source of 
the warning.
@@ -226,9 +208,9 @@ public class ExternalLinkHandler {
      * @return source of the XML document, or {@code null} if the path cannot 
be resolved.
      * @throws Exception if an error occurred while creating the source.
      */
-    public Source openReader(URI path) throws Exception {
-        path = resolve(path);
-        return (path != null) ? new URISource(path) : null;
+    public Source tryResolve(final URI path) throws Exception {
+        final var source = new URISource(getURI(), path);
+        return source.document.isAbsolute() ? source : null;
     }
 
     /*
@@ -269,7 +251,7 @@ public class ExternalLinkHandler {
     }
 
     /**
-     * Creates a link resolver for a XML document reads a StAX stream or event 
reader.
+     * Creates a link resolver for a XML document which is read with a StAX 
stream or event reader.
      *
      * @param  property  value of the {@value XMLInputFactory#RESOLVER} 
property. May be null.
      * @param  location  current location of the reader, or {@code null} if 
unknown.
@@ -285,7 +267,7 @@ public class ExternalLinkHandler {
         }
         final var resolver = (XMLResolver) property;
         return new ExternalLinkHandler(base) {
-            @Override public Source openReader(final URI path) throws 
Exception {
+            @Override public Source tryResolve(final URI path) throws 
Exception {
                 /*
                  * According StAX specification, the return type can be either 
InputStream,
                  * XMLStreamReader or XMLEventReader. We additionally accept 
Source as well.
@@ -301,7 +283,8 @@ public class ExternalLinkHandler {
                 } else if (source instanceof XMLStreamReader) {
                     return new StAXSource((XMLStreamReader) source);
                 } else if (source instanceof InputStream) {
-                    return URISource.create((InputStream) source, 
resolve(path));
+                    // No check for `URISource.document.isAbsolute()` because 
an input stream is provided.
+                    return URISource.create((InputStream) source, getURI(), 
path);
                 } else {
                     throw new 
XMLStreamException(Errors.format(Errors.Keys.UnsupportedType_1, 
source.getClass()));
                 }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/URISource.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/URISource.java
index 51cc351f2a..1ee12c65d5 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/URISource.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/internal/shared/URISource.java
@@ -22,6 +22,7 @@ import java.net.URI;
 import java.net.URISyntaxException;
 import javax.xml.transform.stream.StreamSource;
 import org.apache.sis.util.internal.shared.Strings;
+import org.apache.sis.system.DataURI;
 
 
 /**
@@ -32,6 +33,12 @@ import org.apache.sis.util.internal.shared.Strings;
  * @author  Martin Desruisseaux (Geomatys)
  */
 public final class URISource extends StreamSource {
+    /**
+     * If the URI has been resolved relatively to some base directory or 
sibling file, that base.
+     * Otherwise {@code null}.
+     */
+    private final URI base;
+
     /**
      * Normalized URI of the XML document, without the fragment part if the 
document will be read from this URL.
      * The URI is normalized for making possible to use it as a key in a cache 
of previously loaded documents.
@@ -49,10 +56,17 @@ public final class URISource extends StreamSource {
      * Creates a source from an URI. This constructor separates the fragment 
from the path.
      * The URI stored by this constructor in {@link #document} excludes the 
fragment part.
      *
+     * @param  base    the base directory from which to resolve the source, or 
{@code null} if none.
      * @param  source  URI to the XML document.
      * @throws URISyntaxException if an error occurred while normalizing the 
URI.
      */
-    URISource(URI source) throws URISyntaxException {
+    URISource(URI base, URI source) throws URISyntaxException {
+        if (base != null) {
+            if (source == (source = base.resolve(source))) {
+                base = null;    // The base could not be used.
+            }
+        }
+        this.base = base;
         source = source.normalize();
         fragment = Strings.trimOrNull(source.getFragment());
         // Build a new URI unconditionally because it also decodes escaped 
characters.
@@ -74,15 +88,19 @@ public final class URISource extends StreamSource {
      * Creates a new source from the given input stream.
      * The input should not be null, unless it will be specified later
      * by a call to {@code setInputStream(…)} or {@code setReader(…)}.
+     * This method never returns {@code null} since the input may be set after 
this method call.
      *
      * @param  input   stream of the XML document, or {@code null} if none.
+     * @param  base    the base directory from which to resolve the source, or 
{@code null} if none.
      * @param  source  URL of the XML document, or {@code null} if none.
-     * @return the given input stream as a source.
+     * @return the given input stream as a source, never null even if all 
arguments were null.
      * @throws URISyntaxException if an error occurred while normalizing the 
URI.
      */
-    public static StreamSource create(final InputStream input, final URI 
source) throws URISyntaxException {
+    public static StreamSource create(final InputStream input, final URI base, 
final URI source)
+            throws URISyntaxException
+    {
         if (source != null) {
-            var s = new URISource(source);
+            var s = new URISource(base, source);
             s.setInputStream(input);
             return s;
         } else {
@@ -90,6 +108,16 @@ public final class URISource extends StreamSource {
         }
     }
 
+    /**
+     * Returns whether the document URL is in the same directory or in a 
sub-directory
+     * of the base given at construction time. This is used for access control.
+     *
+     * @return whether the document URL has been resolved relatively to the 
base.
+     */
+    public boolean isChildOfBase() {
+        return (base != null) && DataURI.isPathInDirectory(base, document);
+    }
+
     /**
      * If this source is defined only by URI (no input stream), returns that 
URI.
      * Otherwise returns {@code null}.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/TestUsingFile.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/TestUsingFile.java
index c20081f7b9..6a3274d806 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/TestUsingFile.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/TestUsingFile.java
@@ -39,6 +39,9 @@ public abstract class TestUsingFile extends TestCase {
      * Identification of the data to use for a test.
      */
     protected enum Format {
+        /** A document in the sub-directory of files simulating inputs from 
external users in unspecified format. */
+        EXTERN(null, "extern/"),
+
         /** A document in the sub-directory of XML files encoded according the 
ISO 19115-3:2016 schema. */
         XML2016(VERSION_2014, "2016/"),
 
@@ -65,7 +68,7 @@ public abstract class TestUsingFile extends TestCase {
          * @throws URISyntaxException if the URL to the file is not valid.
          */
         public final Source getSource(final String filename) throws 
URISyntaxException {
-            return URISource.create(null, getURL(filename).toURI());
+            return URISource.create(null, null, getURL(filename).toURI());
         }
 
         /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/extern/UsingExternalXLink.xml
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/extern/UsingExternalXLink.xml
new file mode 100644
index 0000000000..bb31a24c04
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/extern/UsingExternalXLink.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+
+<mri:MD_DataIdentification
+    xmlns:mri   = "http://standards.iso.org/iso/19115/-3/mri/1.0";
+    xmlns:cit   = "http://standards.iso.org/iso/19115/-3/cit/1.0";
+    xmlns:gco   = "http://standards.iso.org/iso/19115/-3/gco/1.0";
+    xmlns:xsi   = "http://www.w3.org/2001/XMLSchema-instance";
+    xmlns:xlink = "http://www.w3.org/1999/xlink";
+    xsi:schemaLocation = "http://standards.iso.org/iso/19115/-3/mri/1.0
+                          
https://schemas.isotc211.org/19115/-3/mri/1.0/mri.xsd";>
+
+  <mri:citation xlink:href="../2016/Citation.xml"/>
+  <mri:abstract>
+    <gco:CharacterString>Test the use of XLink to an external document in 
another directory.</gco:CharacterString>
+  </mri:abstract>
+  <mri:pointOfContact>
+    <cit:CI_Responsibility>
+      <cit:role>
+        <cit:CI_RoleCode 
codeList="http://standards.iso.org/iso/19115/resources/Codelist/cat/codelists.xml#CI_RoleCode";
+                         
codeListValue="pointOfContact">pointOfContact</cit:CI_RoleCode>
+      </cit:role>
+      <cit:party>
+        <cit:CI_Individual>
+          <cit:name>
+            <gco:CharacterString>Little John</gco:CharacterString>
+          </cit:name>
+          <cit:contactInfo xlink:href="../2016/Citation.xml#ip-protocol"/>
+        </cit:CI_Individual>
+      </cit:party>
+    </cit:CI_Responsibility>
+  </mri:pointOfContact>
+
+</mri:MD_DataIdentification>
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverTest.java
index c2a55bbe97..99b54c6751 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverTest.java
@@ -61,27 +61,30 @@ public final class ReferenceResolverTest extends 
TestUsingFile implements Filter
      */
     @Override
     public boolean isLoggable(final LogRecord record) {
-        assertNotEquals(0, expectAccessDenied);
         String file = assertInstanceOf(AccessDeniedException.class, 
record.getThrown()).getFile();
         assertTrue(file.endsWith("Citation.xml"), file);
+        assertNotEquals(0, expectAccessDenied, "Access should not be denied.");
         expectAccessDenied--;
         return false;
     }
 
     /**
-     * Reads the test <abbr>XML</abbr> document.
+     * Parses the test <abbr>XML</abbr> document.
      *
+     * @param  directory     the directory where to look for the {@code 
"UsingExternalXLink.xml"} file.
      * @param  readExternal  whether to allow the reading of external 
documents.
      */
-    private DataIdentification data(final boolean readExternal) throws 
URISyntaxException, JAXBException {
-        final Source source = 
Format.XML2016.getSource("UsingExternalXLink.xml");
+    private DataIdentification data(final Format directory, final boolean 
readExternal)
+            throws URISyntaxException, JAXBException
+    {
+        final Source source = directory.getSource("UsingExternalXLink.xml");
         final var properties = new HashMap<String, Object>(4);
         assertNull(properties.put(XML.WARNING_FILTER, this));
         if (readExternal) {
             assertNull(properties.put(XML.RESOLVER, 
ReferenceResolver.FOLLOW_EXTERNAL_XLINK));
         }
         final var data = assertInstanceOf(DataIdentification.class, 
XML.unmarshal(source, properties));
-        assertEquals("Test the use of XLink to an external document.", 
data.getAbstract().toString());
+        assertTrue(data.getAbstract().toString().startsWith("Test the use of 
XLink to an external document"));
         return data;
     }
 
@@ -95,22 +98,52 @@ public final class ReferenceResolverTest extends 
TestUsingFile implements Filter
     @Test
     public void testAccessDenied() throws URISyntaxException, IOException, 
JAXBException {
         expectAccessDenied = 2;
-        final DataIdentification data = data(false);
+        final DataIdentification data = data(Format.EXTERN, false);
         final Citation citation = data.getCitation();
         assertNull(citation.getTitle());
         assertEquals(0, expectAccessDenied, "Expected a warning.");
     }
 
     /**
-     * Tests loading a document with a {@code xlink:href} to an external 
document.
+     * Tests loading a document with a {@code xlink:href} to an external 
document in a different directory.
+     * This is not allowed by default (verified by {@link #testAccessDenied()},
+     * but this test grants authorization.
+     *
+     * @throws URISyntaxException if an error occurred while getting the URL 
to the test file.
+     * @throws IOException if an error occurred while opening the test file.
+     * @throws JAXBException if an error occurred while parsing the test file.
+     */
+    @Test
+    public void testAccessGranted() throws URISyntaxException, IOException, 
JAXBException {
+        testUsingExternalXLink(Format.EXTERN, true);
+    }
+
+    /**
+     * Tests loading a document with a {@code xlink:href} to an external 
document in the same directory.
+     * The access is granted by default.
      *
      * @throws URISyntaxException if an error occurred while getting the URL 
to the test file.
      * @throws IOException if an error occurred while opening the test file.
      * @throws JAXBException if an error occurred while parsing the test file.
      */
     @Test
-    public void testUsingExternalXLink() throws URISyntaxException, 
IOException, JAXBException {
-        final DataIdentification data = data(true);
+    public void testSameDirectory() throws URISyntaxException, IOException, 
JAXBException {
+        testUsingExternalXLink(Format.XML2016, false);
+    }
+
+    /**
+     * Tests loading a document with a {@code xlink:href} to an external 
document.
+     *
+     * @param  directory     the directory where to look for the {@code 
"UsingExternalXLink.xml"} file.
+     * @param  readExternal  whether to allow the reading of external 
documents.
+     * @throws URISyntaxException if an error occurred while getting the URL 
to the test file.
+     * @throws IOException if an error occurred while opening the test file.
+     * @throws JAXBException if an error occurred while parsing the test file.
+     */
+    private void testUsingExternalXLink(final Format directory, final boolean 
readExternal)
+            throws URISyntaxException, IOException, JAXBException
+    {
+        final DataIdentification data = data(directory, readExternal);
         final Citation citation = data.getCitation();
         DefaultCitationTest.verifyUnmarshalledCitation(citation);
         /*
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
index c2dd9ccce9..3917712e0b 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
@@ -437,7 +437,7 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource {
             throws URISyntaxException, IOException, JAXBException
     {
         try (input) {
-            return readXML(URISource.create(input, source));
+            return readXML(URISource.create(input, location, source));
         }
     }
 
@@ -521,7 +521,7 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource {
          * for giving a chance of `org.apache.sis.xml.XML.unmarshal(Source)` 
to resolve relative links.
          */
         if (acceptXML && stream.markSupported() && 
org.apache.sis.storage.xml.AbstractProvider.isXML(stream)) {
-            return new AuxiliaryContent(source, URISource.create(stream, (path 
!= null) ? path.toUri() : sourceURI));
+            return new AuxiliaryContent(source, URISource.create(stream, 
location, (path != null) ? path.toUri() : sourceURI));
         }
         /*
          * If the auxiliary file is not an XML file, reads it fully as a text 
file with an arbitrary size limit.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/Store.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/Store.java
index 344c2f4c05..beb65769fd 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/Store.java
@@ -90,11 +90,11 @@ final class Store extends URIDataStore implements Filter {
         super(provider, connector);
         final InputStream in = connector.getStorageAs(InputStream.class);
         if (in != null) {
-            source = URISource.create(in, location);
+            source = URISource.create(in, null, location);
         } else {
             final Reader reader = connector.getStorageAs(Reader.class);
             if (reader != null) {
-                var s = URISource.create(null, location);
+                var s = URISource.create(null, null, location);
                 s.setReader(reader);
                 source = s;
             }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/Authorization.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/Authorization.java
index 0349db0905..1f07600604 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/Authorization.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/Authorization.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.io;
 
+import java.net.URI;
 import java.nio.file.AccessDeniedException;
 
 
@@ -23,8 +24,18 @@ import java.nio.file.AccessDeniedException;
  * Indication of whether access to a file or <abbr>URL</abbr> is granted or 
denied.
  * A file may be specified in a {@code xlink:href} attribute of an 
<abbr>XML</abbr> document,
  * or as a parameter in the definition of a coordinate operation (e.g. a datum 
shift grid file).
- * By default, Apache <abbr>SIS</abbr> opens these files only if they are in 
dedicated directories.
- * This enumeration is used when the default behavior is replaced by user 
policy.
+ * By default, Apache <abbr>SIS</abbr> opens these files only if they are 
relative to a directory
+ * inferred by the context (for example {@code $SIS_DATA/DatumChanges} for 
datum shift grid files),
+ * of if they are in the same directory or a sub-directory of the document 
referencing the file,
+ * This enumeration can be used for replacing the default policy by an 
user-specified policy.
+ * See the following methods for more information:
+ *
+ * <ul>
+ *   <li>{@link org.apache.sis.xml.ReferenceResolver#accessControl(URI)} —
+ *       for {@code xlink:href} attributes in <abbr>GML</abbr> document,</li>
+ *   <li>{@link 
org.apache.sis.referencing.operation.transform.MathTransformBuilder#getAccessControl()}
 —
+ *       for datum shift grids or other files used by coordinate 
operations.</li>
+ * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.7
@@ -45,14 +56,15 @@ public enum Authorization {
 
     /**
      * Access to the file or <abbr>URL</abbr> is determined by Apache 
<abbr>SIS</abbr> default policy.
-     * These defaults depend on the type of document containing references by 
<abbr>URL</abbr>s.
-     * Examples:
+     * The access is granted if the reference is an <abbr>URL</abbr> local to 
the <abbr>JSON</abbr> or
+     * <abbr>GML</abbr> document, or if the <abbr>URL</abbr> is a file in the 
same directory or in a
+     * sub-directory of the <abbr>JSON</abbr>, <abbr>GML</abbr>, 
<abbr>WKT</abbr>, <abbr>netCDF</abbr>,
+     * <i>etc.</i> document referencing the file.
+     * In addition, some other special directories are accepted depending on 
the context:
      *
      * <ul>
-     *   <li>In a <abbr>GML</abbr> document, follow {@code xlink:href} only if 
the reference is local to the document.</li>
-     *   <li>In coordinate operations defined in <abbr>JSON</abbr>, 
<abbr>GML</abbr> or <abbr>WKT</abbr> documents,
-     *       read datum shift grid file only if inside the {@code 
$SIS_DATA/DatumChanges} directory or in the same
-     *       directory or server as the document.</li>
+     *   <li>For coordinate operations using datum shift grids,
+     *       grant access to files inside the {@code $SIS_DATA/DatumChanges} 
directory.</li>
      * </ul>
      */
     DEFAULT
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/DataURI.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/DataURI.java
index e78471652c..f4da983f6f 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/DataURI.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/DataURI.java
@@ -108,13 +108,7 @@ public class DataURI {
         }
         final URI result = base.resolve(parameter).normalize();
         if (result != resolved) {
-            if (result == parameter) {
-                isRelative = false;
-            } else {
-                String path = Strings.orEmpty(base.getPath());
-                path = path.substring(0, path.lastIndexOf('/') + 1);
-                isRelative = 
Strings.orEmpty(result.getPath()).startsWith(path);
-            }
+            isRelative = (result != parameter) && isPathInDirectory(base, 
result);
             resolved = result;
             try {
                 asPath = Path.of(result);
@@ -127,6 +121,27 @@ public class DataURI {
         return true;
     }
 
+    /**
+     * Returns {@code true} if the resolved <abbr>URI</abbr> has a path inside 
the directory of the base <abbr>URI</abbr>.
+     * If the given base URI ends with {@code '/'}, it is assumed to be a 
directory and its path will be used as-is.
+     * Otherwise, the base URI is assumed a file and the last path component 
(the filename) will be ignored.
+     * This policy is consistent with the behavior of {@link URI#resolve(URI)}.
+     *
+     * <p>This method can be used for verifying the result of {@code 
base.resolve(parameter)}.
+     * Caller should verify that {@code resolve(parameter)} did not returned 
{@code parameter},
+     * in which case this method can assume that the new URI has the same 
scheme as the base URI
+     * (this is not verified by this method).</p>
+     *
+     * @param  base      the base directory.
+     * @param  resolved  result of {@code base.resolve(parameter)}.
+     * @return whether the resolved path starts with the directory part of the 
base path.
+     */
+    public static boolean isPathInDirectory(final URI base, final URI 
resolved) {
+        String path = Strings.orEmpty(base.getPath());
+        path = path.substring(0, path.lastIndexOf('/') + 1);
+        return Strings.orEmpty(resolved.getPath()).startsWith(path);
+    }
+
     /**
      * Returns {@code true} if the file is inside the expected directory but 
does not exists.
      * In case of doubt, or if the file is outside the expected directory, 
returns {@code false}.

Reply via email to