You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by js...@apache.org on 2026/07/17 09:46:34 UTC
(sling-org-apache-sling-resourceresolver) 01/01: SLING-13265: Wrong ResourceMetadata for alias resolution with unreadable ancestor
This is an automated email from the ASF dual-hosted git repository.
jsedding pushed a commit to branch jsedding/SLING-13265-resolution-with-inaccessible-ancestors
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-resourceresolver.git
commit 2097e793731e8b56da4e2f6a615633db1c6aaf06
Author: Julian Sedding <js...@apache.org>
AuthorDate: Fri Jul 17 11:39:54 2026 +0200
SLING-13265: Wrong ResourceMetadata for alias resolution with unreadable ancestor
---
pom.xml | 6 +-
.../impl/ResourceResolverImpl.java | 86 ++++++------
.../ResolutionWithInaccessiblePathsTest.java | 145 +++++++++++++++++++++
3 files changed, 190 insertions(+), 47 deletions(-)
diff --git a/pom.xml b/pom.xml
index 429a650d..cde5ff73 100644
--- a/pom.xml
+++ b/pom.xml
@@ -154,12 +154,12 @@
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-engine</artifactId>
+ <artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
- <groupId>org.junit.vintage</groupId>
- <artifactId>junit-vintage-engine</artifactId>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
index ff04067f..99830ecc 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
@@ -809,59 +809,57 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso
* the {@link ResourcePathIterator} to resolve the resource.
*/
public Resource resolveInternal(final String absPath, final Map<String, String> parameters) {
- Resource resource = null;
if (absPath != null && !absPath.isEmpty() && !absPath.startsWith("/")) {
logger.debug("resolveInternal: absolute path expected {} ", absPath);
- return resource; // resource is null at this point
+ return null;
}
String curPath = absPath;
try {
final ResourcePathIterator it = new ResourcePathIterator(absPath);
+ Resource resource = null;
while (it.hasNext() && resource == null) {
curPath = it.next();
resource = getAbsoluteResourceInternal(null, curPath, parameters, true);
}
+ // SLING-627: set the part cut off from the uriPath as
+ // sling.resolutionPathInfo property such that
+ // uriPath = curPath + sling.resolutionPathInfo
+ if (resource != null) {
+
+ final String rpi = absPath.substring(curPath.length());
+ resource.getResourceMetadata().setResolutionPath(absPath.substring(0, curPath.length()));
+ resource.getResourceMetadata().setResolutionPathInfo(rpi);
+ resource.getResourceMetadata().setParameterMap(parameters);
+
+ logger.debug(
+ "resolveInternal: Found resource {} with path info {} for {}",
+ new Object[] {resource, rpi, absPath});
+ return resource;
+ }
} catch (final Exception ex) {
throw new SlingException("Problem trying " + curPath + " for request path " + absPath, ex);
}
- // SLING-627: set the part cut off from the uriPath as
- // sling.resolutionPathInfo property such that
- // uriPath = curPath + sling.resolutionPathInfo
+ // no direct resource found, so we have to drill down into the
+ // resource tree to find a match
+ Resource resource = getAbsoluteResourceInternal(absPath, parameters, true);
if (resource != null) {
-
- final String rpi = absPath.substring(curPath.length());
- resource.getResourceMetadata().setResolutionPath(absPath.substring(0, curPath.length()));
- resource.getResourceMetadata().setResolutionPathInfo(rpi);
- resource.getResourceMetadata().setParameterMap(parameters);
-
- logger.debug(
- "resolveInternal: Found resource {} with path info {} for {}",
- new Object[] {resource, rpi, absPath});
-
- } else {
-
- String tokenizedPath = absPath;
-
- // no direct resource found, so we have to drill down into the
- // resource tree to find a match
- resource = getAbsoluteResourceInternal(null, "/", parameters, true);
-
- // no read access on / drilling further down
- // SLING-5638
- if (resource == null) {
- resource = getAbsoluteResourceInternal(absPath, parameters, true);
- if (resource != null) {
- tokenizedPath = tokenizedPath.substring(resource.getPath().length());
- }
- }
-
final StringBuilder resolutionPath = new StringBuilder();
+ String tokenizedPath = Objects.equals(resource.getPath(), "/")
+ ? absPath
+ : absPath.substring(resource.getPath().length());
final StringTokenizer tokener = new StringTokenizer(tokenizedPath, "/");
final int delimCount = StringUtils.countMatches(tokenizedPath, '/');
final int redundantDelimCount = delimCount - tokener.countTokens();
- while (resource != null && tokener.hasMoreTokens()) {
+ // we found an ancestor resource, so we need to prefix the resolutionPath with it
+ // unless it is the root path (that would result in a double slash prefix "//")
+ String resolutionPathPrefix = resource.getResourceMetadata().getResolutionPath();
+ if (!Objects.equals(resolutionPathPrefix, "/")) {
+ resolutionPath.append(resolutionPathPrefix);
+ }
+
+ while (tokener.hasMoreTokens()) {
final String childNameRaw = tokener.nextToken();
Resource nextResource = getChildInternal(resource, childNameRaw);
@@ -1000,20 +998,20 @@ public class ResourceResolverImpl extends SlingAdaptable implements ResourceReso
return null;
}
- absPath = absPath.substring(absPath.indexOf("/"));
- Resource resource = getAbsoluteResourceInternal(null, absPath, parameters, isResolved);
-
- absPath = absPath.substring(0, absPath.lastIndexOf("/"));
-
- while (!absPath.equals("")) {
- Resource r = getAbsoluteResourceInternal(null, absPath, parameters, true);
-
+ String candidatePath = absPath.substring(absPath.indexOf("/"));
+ while (candidatePath != null) {
+ // If this was a "getResource" call, i.e. isResolved = false,
+ // then the first iteration will return a Resource; in case the
+ // resource doesn't exist a SyntheticResource. I.e. for
+ // "getResource" calls this loop should always return in during
+ // the first iteration.
+ Resource r = getAbsoluteResourceInternal(null, candidatePath, parameters, isResolved);
if (r != null) {
- resource = r;
+ return r;
}
- absPath = absPath.substring(0, absPath.lastIndexOf("/"));
+ candidatePath = ResourceUtil.getParent(candidatePath);
}
- return resource;
+ return null;
}
/**
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResolutionWithInaccessiblePathsTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResolutionWithInaccessiblePathsTest.java
new file mode 100644
index 00000000..0c2322ca
--- /dev/null
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/mapping/ResolutionWithInaccessiblePathsTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.sling.resourceresolver.impl.mapping;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.resourceresolver.impl.ResourceAccessSecurityTracker;
+import org.apache.sling.resourceresolver.impl.ResourceResolverFactoryActivator;
+import org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContext;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.osgi.util.tracker.ServiceTracker;
+
+import static org.apache.sling.spi.resource.provider.ResourceProvider.PROPERTY_NAME;
+import static org.apache.sling.spi.resource.provider.ResourceProvider.PROPERTY_ROOT;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class ResolutionWithInaccessiblePathsTest {
+
+ @RegisterExtension
+ OsgiContextExtension osgiContextExtension = new OsgiContextExtension();
+
+ @ParameterizedTest(name = "[{index}] inaccessible path: {0}")
+ @ValueSource(strings = {"none (equivalent to admin access)", "/", "/content", "/content/en"})
+ void simpleResolution(String inaccessiblePath, OsgiContext ctx) throws InterruptedException, LoginException {
+ ResourceResolverFactory resourceResolverFactory = registerResourceResolverfactory(ctx, p -> {
+ Stream.of("/", "/content", "/content/en")
+ .filter(path -> !Objects.equals(path, inaccessiblePath))
+ .forEach(p::putResource);
+ p.putResource("/content/en/solutions");
+ p.putResource("/content/en/solutions/airlines");
+ p.putResource("/content/en/solutions/airlines/products");
+ p.putResource("/content/en/solutions/airlines/products/wings");
+ });
+
+ ResourceResolver resolver = resourceResolverFactory.getResourceResolver(Collections.emptyMap());
+ Resource resource = resolver.resolve("/content/en/solutions/airlines/products/wings.html");
+ assertEquals(
+ "/content/en/solutions/airlines/products/wings",
+ resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".html", resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/en/solutions/airlines/products/wings", resource.getPath());
+ }
+
+ @ParameterizedTest(name = "[{index}] inaccessible path: {0}")
+ @ValueSource(strings = {"none (equivalent to admin access)", "/", "/content", "/content/es"})
+ void aliasResolution(String inaccessiblePath, OsgiContext ctx) throws InterruptedException, LoginException {
+ ResourceResolverFactory resourceResolverFactory = registerResourceResolverfactory(ctx, p -> {
+ Stream.of("/", "/content", "/content/es")
+ .filter(path -> !Objects.equals(path, inaccessiblePath))
+ .forEach(p::putResource);
+ p.putResource("/content/es/solutions");
+ p.putResource("/content/es/solutions/airlines", "sling:alias", "aerolineas");
+ p.putResource("/content/es/solutions/airlines/products", "sling:alias", "productos");
+ p.putResource("/content/es/solutions/airlines/products/wings", "sling:alias", "alas");
+ });
+
+ ResourceResolver resolver = resourceResolverFactory.getResourceResolver(Collections.emptyMap());
+ Resource resource;
+
+ resource = resolver.resolve("/content/es/solutions/aerolineas/productos/alas.html");
+ assertEquals(
+ "/content/es/solutions/aerolineas/productos/alas",
+ resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".html", resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/es/solutions/airlines/products/wings", resource.getPath());
+
+ resource = resolver.resolve("/content/es/solutions/aerolineas/productos/wings.mobile.html");
+ assertEquals(
+ "/content/es/solutions/aerolineas/productos/wings",
+ resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".mobile.html", resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/es/solutions/airlines/products/wings", resource.getPath());
+
+ resource = resolver.resolve("/content/es/solutions/airlines/productos/alas.json");
+ assertEquals(
+ "/content/es/solutions/airlines/productos/alas",
+ resource.getResourceMetadata().getResolutionPath());
+ assertEquals(".json", resource.getResourceMetadata().getResolutionPathInfo());
+ assertEquals("/content/es/solutions/airlines/products/wings", resource.getPath());
+ }
+
+ private static ResourceResolverFactory registerResourceResolverfactory(
+ OsgiContext ctx, Consumer<InMemoryResourceProvider> resourceInitializer) throws InterruptedException {
+ ctx.registerInjectActivateService(new ServiceUserMapperImpl());
+ ctx.registerInjectActivateService(new ResourceAccessSecurityTracker());
+ ctx.registerInjectActivateService(new StringInterpolationProviderImpl());
+
+ InMemoryResourceProvider provider = new InMemoryResourceProvider(false);
+ resourceInitializer.accept(provider);
+
+ // we fake the fact that we are the JCR resource provider since it's the required one
+ ctx.registerService(ResourceProvider.class, provider, PROPERTY_ROOT, "/", PROPERTY_NAME, "JCR");
+
+ Map<String, Object> properties = new HashMap<>();
+ properties.put("resource.resolver.optimize.alias.resolution", true);
+ properties.put("resource.resolver.alias.cache.in.background", false);
+ properties.put("resource.resolver.mapping", new String[] {"/:/"});
+ ctx.registerInjectActivateService(ResourceResolverFactoryActivator.class, properties);
+
+ final ResourceResolverFactory factory;
+ final ServiceTracker<ResourceResolverFactory, ResourceResolverFactory> tracker =
+ new ServiceTracker<>(ctx.bundleContext(), ResourceResolverFactory.class, null);
+ try {
+ tracker.open();
+ factory = tracker.waitForService(TimeUnit.SECONDS.toMillis(300));
+ } finally {
+ tracker.close();
+ }
+
+ assertNotNull(factory);
+ return factory;
+ }
+}