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

Reply via email to