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