This is an automated email from the ASF dual-hosted git repository.
bdelacretaz pushed a commit to branch resolver-2.x
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-servlets-resolver.git
The following commit(s) were added to refs/heads/resolver-2.x by this push:
new 720d215 SLING-12739 - selectively hide scripts and servlets
720d215 is described below
commit 720d215a9253eb92d530b563214c424a28aa0351
Author: Bertrand Delacretaz <[email protected]>
AuthorDate: Thu Jul 10 16:12:44 2025 +0200
SLING-12739 - selectively hide scripts and servlets
---
.../resolver/internal/SlingServletResolver.java | 23 +++-
.../internal/resourcehiding/ServletHidingTest.java | 125 +++++++++++++++++++++
.../sling/servlets/resolver/it/TestServlet.java | 4 +-
.../it/resourcehiding/BasicResourceHidingIT.java | 49 ++++++++
.../it/resourcehiding/HiddenServletFallbackIT.java | 47 ++++++++
.../resolver/it/resourcehiding/NoHidingIT.java | 53 +++++++++
.../it/resourcehiding/ResourceHidingITBase.java | 82 ++++++++++++++
7 files changed, 380 insertions(+), 3 deletions(-)
diff --git
a/src/main/java/org/apache/sling/servlets/resolver/internal/SlingServletResolver.java
b/src/main/java/org/apache/sling/servlets/resolver/internal/SlingServletResolver.java
index 2eecb28..a54d8bd 100644
---
a/src/main/java/org/apache/sling/servlets/resolver/internal/SlingServletResolver.java
+++
b/src/main/java/org/apache/sling/servlets/resolver/internal/SlingServletResolver.java
@@ -28,6 +28,7 @@ import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
@@ -63,6 +64,7 @@ import
org.apache.sling.servlets.resolver.internal.helper.ResourceCollector;
import org.apache.sling.servlets.resolver.internal.resolution.ResolutionCache;
import
org.apache.sling.servlets.resolver.internal.resource.MergingServletResourceProvider;
import org.apache.sling.servlets.resolver.internal.resource.SlingServletConfig;
+import org.jetbrains.annotations.NotNull;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
@@ -70,6 +72,8 @@ import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
@@ -129,6 +133,13 @@ public class SlingServletResolver
private final ThreadLocal<ResourceResolver> perThreadScriptResolver = new
ThreadLocal<>();
+ @Reference(
+ target = "(name=sling.servlet.resolver.resource.hiding)",
+ policy = ReferencePolicy.DYNAMIC,
+ cardinality = ReferenceCardinality.OPTIONAL
+ )
+ private volatile Predicate<String> resourceHidingPredicate;
+
/**
* The allowed execution paths.
*/
@@ -442,6 +453,15 @@ public class SlingServletResolver
return res;
}
+ /** @return true if the given Resource is hidden by our
resourceHidingPredicate */
+ private boolean isHidden(@NotNull Resource r) {
+ final boolean result = r != null && resourceHidingPredicate != null &&
resourceHidingPredicate.test(r.getPath());
+ if(result && LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Resource hidden by resource hiding predicate: {}",
r.getPath());
+ }
+ return result;
+ }
+
/**
* Resolve an appropriate servlet for a given request and resource type
* using the provided ResourceResolver
@@ -468,7 +488,7 @@ public class SlingServletResolver
final String scriptPath =
ResourceUtil.normalize(scriptNameOrResourceType);
if (scriptPath != null && isPathAllowed(scriptPath,
this.executionPaths.get()) ) {
final Resource res =
AbstractResourceCollector.getResourceOrNull(resolver,scriptPath,useResourceCaching);
- servlet = this.getServlet(res);
+ servlet = isHidden(res) ? null : this.getServlet(res);
if (servlet != null &&
!pathBasedServletAcceptor.accept(request, servlet)) {
if(LOGGER.isDebugEnabled()) {
LOGGER.debug("Servlet {} rejected by {} returning
FORBIDDEN status", RequestUtil.getServletName(servlet),
@@ -536,6 +556,7 @@ public class SlingServletResolver
}
final Collection<Resource> candidates =
locationUtil.getServlets(resolver, localCache.getScriptEngineExtensions());
+ candidates.removeIf(r -> isHidden(r));
if (LOGGER.isDebugEnabled()) {
if (candidates.isEmpty()) {
diff --git
a/src/test/java/org/apache/sling/servlets/resolver/internal/resourcehiding/ServletHidingTest.java
b/src/test/java/org/apache/sling/servlets/resolver/internal/resourcehiding/ServletHidingTest.java
new file mode 100644
index 0000000..55577da
--- /dev/null
+++
b/src/test/java/org/apache/sling/servlets/resolver/internal/resourcehiding/ServletHidingTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.servlets.resolver.internal.resourcehiding;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
+
+import javax.servlet.Servlet;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
+import org.apache.sling.commons.testing.sling.MockSlingHttpServletRequest;
+import
org.apache.sling.servlets.resolver.internal.SlingServletResolverTestBase;
+import org.apache.sling.servlets.resolver.internal.helper.HelperTestBase;
+import
org.apache.sling.servlets.resolver.internal.resource.MockServletResource;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+
+public class ServletHidingTest extends SlingServletResolverTestBase {
+
+ private static final String TEST_ID = UUID.randomUUID().toString();
+
+ protected static class TestServlet extends SlingSafeMethodsServlet {
+ private final String id;
+
+ public TestServlet(String id) {
+ this.id = id;
+ }
+
+ public String toString() {
+ return id;
+ }
+ }
+
+ private void setServletHidingFilter(Predicate<String> predicate) throws
Exception {
+ final Field predicateField =
servletResolver.getClass().getDeclaredField("resourceHidingPredicate");
+ predicateField.setAccessible(true);
+ predicateField.set(servletResolver, predicate);
+ }
+
+ private void registerServlet(String id, String resourceType) {
+ final String path = "/" + resourceType + "/" +
ResourceUtil.getName(resourceType) + ".servlet";
+ Map<String, Object> props = new HashMap<>();
+ props.put(MockServletResource.PROP_SERVLET, new TestServlet(id));
+ HelperTestBase.addOrReplaceResource(mockResourceResolver, path, props);
+ try {
+ mockResourceResolver.commit();
+ } catch (PersistenceException e) {
+ fail(e.toString());
+ }
+ }
+
+ private Servlet resolveServlet() {
+ MockSlingHttpServletRequest req = new MockSlingHttpServletRequest(
+ MockSlingHttpServletRequest.RESOURCE_TYPE, null, "html", null,
null);
+ req.setResourceResolver(mockResourceResolver);
+ return servletResolver.resolveServlet(req);
+ }
+
+ @Override
+ protected void defineTestServlets(Bundle bundle) {
+ registerServlet(TEST_ID, MockSlingHttpServletRequest.RESOURCE_TYPE);
+ }
+
+ private void assertResolvesToTestServletId(String info, boolean
expectMatch) {
+ final Servlet s = resolveServlet();
+ assertNotNull("Expecting non-null Servlet", s);
+ if(expectMatch) {
+ assertEquals("Expecting our test servlet (" + info + ")", TEST_ID,
s.toString());
+ } else {
+ assertNotEquals("NOT expecting our test servlet (" + info + ")",
TEST_ID, s.toString());
+ }
+ }
+
+ @Test
+ public void testHideAndSeek() throws Exception {
+ final AtomicBoolean hide = new AtomicBoolean();
+ final Predicate<String> pred = (ignoredPath) -> hide.get();
+
+ // No filtering
+ setServletHidingFilter(null);
+ assertResolvesToTestServletId("before hiding", true);
+
+ // Filter with our predicate
+ setServletHidingFilter(pred);
+ hide.set(true);
+ assertResolvesToTestServletId("hidden by our Predicate", false);
+ hide.set(false);
+ assertResolvesToTestServletId("Predicate active but returns false",
true);
+
+ // Back to no filtering, (paranoid) check that it's really gone
+ setServletHidingFilter(null);
+ hide.set(false);
+ assertResolvesToTestServletId("No Predicate set, hide=false", true);
+ hide.set(true);
+ assertResolvesToTestServletId("No Predicate set, hide=true", true);
+ }
+
+}
diff --git
a/src/test/java/org/apache/sling/servlets/resolver/it/TestServlet.java
b/src/test/java/org/apache/sling/servlets/resolver/it/TestServlet.java
index 39e1d08..181307f 100644
--- a/src/test/java/org/apache/sling/servlets/resolver/it/TestServlet.java
+++ b/src/test/java/org/apache/sling/servlets/resolver/it/TestServlet.java
@@ -56,12 +56,12 @@ public class TestServlet extends HttpServlet {
doGet(req, resp);
}
- TestServlet with(String key, Object value) {
+ public TestServlet with(String key, Object value) {
properties.put(key, value);
return this;
}
- void register(BundleContext context) {
+ public void register(BundleContext context) {
context.registerService(Servlet.class.getName(), this, properties);
}
}
\ No newline at end of file
diff --git
a/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/BasicResourceHidingIT.java
b/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/BasicResourceHidingIT.java
new file mode 100644
index 0000000..10afb48
--- /dev/null
+++
b/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/BasicResourceHidingIT.java
@@ -0,0 +1,49 @@
+/*
+ * 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.servlets.resolver.it.resourcehiding;
+
+import static org.junit.Assert.assertEquals;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class BasicResourceHidingIT extends ResourceHidingITBase {
+
+ @Before
+ public void setupPredicate() {
+ registerPredicate((path) -> path.contains(EXT_B));
+ }
+
+ @Test
+ public void testOnlyApresent() throws Exception {
+ assertEquals(0, hiddenResourcesCount);
+ assertTestServlet("/." + EXT_A, EXT_A);
+ assertEquals(0, hiddenResourcesCount);
+ assertTestServlet("/." + EXT_B, HttpServletResponse.SC_NOT_FOUND);
+ assertEquals(1, hiddenResourcesCount);
+ }
+}
\ No newline at end of file
diff --git
a/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/HiddenServletFallbackIT.java
b/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/HiddenServletFallbackIT.java
new file mode 100644
index 0000000..c891e7c
--- /dev/null
+++
b/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/HiddenServletFallbackIT.java
@@ -0,0 +1,47 @@
+/*
+ * 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.servlets.resolver.it.resourcehiding;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class HiddenServletFallbackIT extends ResourceHidingITBase {
+
+ @Before
+ public void setupPredicate() {
+ registerPredicate((path) -> path.contains(SEL_A));
+ }
+
+ @Test
+ public void testFallbackToExtA() throws Exception {
+ assertEquals(0, hiddenResourcesCount);
+ assertTestServlet("/." + SEL_A + "." + EXT_A, EXT_A);
+ assertEquals(1, hiddenResourcesCount);
+ assertTestServlet("/." + EXT_A, EXT_A);
+ assertEquals(1, hiddenResourcesCount);
+ }
+}
\ No newline at end of file
diff --git
a/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/NoHidingIT.java
b/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/NoHidingIT.java
new file mode 100644
index 0000000..4fae143
--- /dev/null
+++
b/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/NoHidingIT.java
@@ -0,0 +1,53 @@
+/*
+ * 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.servlets.resolver.it.resourcehiding;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class NoHidingIT extends ResourceHidingITBase {
+
+ @After
+ public void checkNothingHidden() {
+ assertEquals(0, hiddenResourcesCount);
+ }
+
+ @Test
+ public void testExtApresent() throws Exception {
+ assertTestServlet("/." + EXT_A, EXT_A);
+ }
+
+ @Test
+ public void testExtBpresent() throws Exception {
+ assertTestServlet("/." + EXT_B, EXT_B);
+ }
+
+ @Test
+ public void testSelApresent() throws Exception {
+ assertTestServlet("/." + SEL_A + "." + EXT_A, SEL_A);
+ }
+}
diff --git
a/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/ResourceHidingITBase.java
b/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/ResourceHidingITBase.java
new file mode 100644
index 0000000..86cd3bb
--- /dev/null
+++
b/src/test/java/org/apache/sling/servlets/resolver/it/resourcehiding/ResourceHidingITBase.java
@@ -0,0 +1,82 @@
+/*
+ * 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.servlets.resolver.it.resourcehiding;
+
+import java.util.Hashtable;
+import java.util.UUID;
+import java.util.function.Predicate;
+
+import org.apache.sling.servlets.resolver.it.ServletResolverTestSupport;
+import org.apache.sling.servlets.resolver.it.TestServlet;
+import org.junit.Before;
+
+/** Base for all our hiding tests, so that they all use the same set of
servlets */
+public class ResourceHidingITBase extends ServletResolverTestSupport {
+
+ protected final static String EXT_A = "EXT_A" + UUID.randomUUID();
+ protected final static String EXT_B = "EXT_B" + UUID.randomUUID();
+ protected final static String SEL_A = "SEL_A" + UUID.randomUUID();
+ protected int hiddenResourcesCount = 0;
+
+ @Before
+ public void reset() {
+ hiddenResourcesCount = 0;
+ }
+
+ @Before
+ public void setupTestServletsAndResourceHiding() throws Exception {
+ // Register two servlets differing only in extensions
+ new TestServlet(EXT_A)
+ .with(P_RESOURCE_TYPES, RT_DEFAULT)
+ .with(P_METHODS, M_GET)
+ .with(P_EXTENSIONS, EXT_A)
+ .register(bundleContext);
+
+ new TestServlet(EXT_B)
+ .with(P_RESOURCE_TYPES, RT_DEFAULT)
+ .with(P_METHODS, M_GET)
+ .with(P_EXTENSIONS, EXT_B)
+ .register(bundleContext);
+
+ // And one more specific servlet, that will fall back
+ // to EXT_A when hidden
+ new TestServlet(SEL_A)
+ .with(P_RESOURCE_TYPES, RT_DEFAULT)
+ .with(P_METHODS, M_GET)
+ .with(P_EXTENSIONS, EXT_A)
+ .with(P_SELECTORS, SEL_A)
+ .register(bundleContext);
+ }
+
+ protected void registerPredicate(Predicate<String> p) {
+ final Predicate<String> wrappedPredicate = new Predicate<String>() {
+ @Override
+ public boolean test(String path) {
+ final boolean result = p.test(path);
+ if(result) {
+ hiddenResourcesCount++;
+ }
+ return result;
+ }
+ };
+ final Hashtable<String,String> props = new Hashtable<>();
+ props.put("name","sling.servlet.resolver.resource.hiding");
+ bundleContext.registerService(Predicate.class.getName(),
wrappedPredicate, props);
+ }
+}
\ No newline at end of file