You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by da...@apache.org on 2018/11/07 11:45:21 UTC

[sling-org-apache-sling-feature-apiregions] 01/19: Rename feature whitelist component to feature apiregions

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

davidb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-apiregions.git

commit f8b59082f5dc4bf4be75ba75c64b976317cb9ae8
Author: David Bosschaert <bo...@adobe.com>
AuthorDate: Sat Nov 3 10:11:24 2018 +0000

    Rename feature whitelist component to feature apiregions
---
 pom.xml                                            | 125 ++++++++++++
 .../sling/feature/apiregions/impl/Activator.java   |  35 ++++
 .../feature/apiregions/impl/RegionEnforcer.java    | 134 +++++++++++++
 .../feature/apiregions/impl/ResolverHookImpl.java  | 186 ++++++++++++++++++
 .../apiregions/impl/ResolverHookImplTest.java      | 209 +++++++++++++++++++++
 .../apiregions/impl/WhitelistEnforcerTest.java     |  62 ++++++
 .../impl/WhitelistServiceFactoryImplTest.java      |  64 +++++++
 .../apiregions/impl/WhitelistServiceImplTest.java  |  52 +++++
 8 files changed, 867 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..5602b63
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+    <!--
+        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.
+    -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>34</version>
+        <relativePath />
+    </parent>
+
+    <artifactId>org.apache.sling.feature.apiregions</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling Feature API Regions Runtime</name>
+    <description>
+        A runtime component to enforce API Regions
+    </description>
+
+    <properties>
+        <sling.java.version>8</sling.java.version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <ExtensionBundle-Activator>org.apache.sling.feature.apiregions.impl.Activator</ExtensionBundle-Activator>
+                        <Fragment-Host>system.bundle;extension:=framework</Fragment-Host>
+                    </instructions>
+                
+                    <!--  Skip baselining for 0.x version -->
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>*.md</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.service</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.annotation.versioning</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+            <scope>provided</scope>
+        </dependency>        
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.geronimo.specs</groupId>
+            <artifactId>geronimo-json_1.1_spec</artifactId>
+            <version>1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature</artifactId>
+            <version>0.1.3-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.launcher</artifactId>
+            <version>0.1.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        
+
+        <!-- Testing -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>2.8.9</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.johnzon</groupId>
+            <artifactId>johnzon-core</artifactId>
+            <version>1.0.0</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java b/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java
new file mode 100644
index 0000000..7d77d5e
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java
@@ -0,0 +1,35 @@
+/*
+ * 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.feature.apiregions.impl;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.hooks.resolver.ResolverHookFactory;
+
+public class Activator implements BundleActivator {
+    @Override
+    public synchronized void start(BundleContext context) throws Exception {
+        RegionEnforcer enforcer = new RegionEnforcer();
+        context.registerService(ResolverHookFactory.class, enforcer, null);
+    }
+
+    @Override
+    public synchronized void stop(BundleContext context) throws Exception {
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/apiregions/impl/RegionEnforcer.java b/src/main/java/org/apache/sling/feature/apiregions/impl/RegionEnforcer.java
new file mode 100644
index 0000000..26807ef
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/apiregions/impl/RegionEnforcer.java
@@ -0,0 +1,134 @@
+/*
+ * 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.feature.apiregions.impl;
+
+import org.osgi.framework.Version;
+import org.osgi.framework.hooks.resolver.ResolverHook;
+import org.osgi.framework.hooks.resolver.ResolverHookFactory;
+import org.osgi.framework.wiring.BundleRevision;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+class RegionEnforcer implements ResolverHookFactory {
+    private static final String idbsnverFileName = "idbsnver.properties";
+    private static final String bundleFeatureFileName = "bundles.properties";
+    private static final String regionPackageFileName = "regions.properties";
+    private static final String featureRegionFileName = "features.properties";
+
+    private final Map<Map.Entry<String, Version>, List<String>> bsnVerMap;
+    private final Map<String, Set<String>> bundleFeatureMap;
+    private final Map<String, Set<String>> featureRegionMap;
+    private final Map<String, Set<String>> regionPackageMap;
+
+    public RegionEnforcer() throws IOException {
+        bsnVerMap = populateBSNVerMap();
+        bundleFeatureMap = populateBundleFeatureMap();
+        featureRegionMap = populateFeatureRegionMap();
+        regionPackageMap = populateRegionPackageMap();
+    }
+
+    private Map<Map.Entry<String, Version>, List<String>> populateBSNVerMap() throws IOException {
+        File idbsnverFile = getDataFile(idbsnverFileName);
+        if (idbsnverFile != null && idbsnverFile.exists()) {
+            Map<Map.Entry<String, Version>, List<String>> m = new HashMap<>();
+
+            Properties p = new Properties();
+            try (InputStream is = new FileInputStream(idbsnverFile)) {
+                p.load(is);
+            }
+
+            for (String n : p.stringPropertyNames()) {
+                String[] bsnver = p.getProperty(n).split("~");
+                Map.Entry<String, Version> key = new AbstractMap.SimpleEntry<String, Version>(bsnver[0], Version.valueOf(bsnver[1]));
+                List<String> l = m.get(key);
+                if (l == null) {
+                    l = new ArrayList<>();
+                    m.put(key, l);
+                }
+            }
+
+            Map<Map.Entry<String, Version>, List<String>> m2 = new HashMap<>();
+
+            for (Map.Entry<Map.Entry<String, Version>, List<String>> entry : m.entrySet()) {
+                m2.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
+            }
+
+            return Collections.unmodifiableMap(m2);
+        } else {
+            return Collections.emptyMap();
+        }
+    }
+
+    private Map<String, Set<String>> populateBundleFeatureMap() throws IOException {
+        return loadMap(bundleFeatureFileName);
+    }
+
+    private Map<String, Set<String>> populateFeatureRegionMap() throws IOException {
+        return loadMap(featureRegionFileName);
+    }
+
+    private Map<String, Set<String>> populateRegionPackageMap() throws IOException {
+        return loadMap(regionPackageFileName);
+    }
+
+    private Map<String, Set<String>> loadMap(String fileName) throws IOException {
+        Map<String, Set<String>> m = new HashMap<>();
+
+        File propsFile = getDataFile(fileName);
+        if (propsFile != null && propsFile.exists()) {
+            Properties p = new Properties();
+            try (InputStream is = new FileInputStream(propsFile)) {
+                p.load(is);
+            }
+
+            for (String n : p.stringPropertyNames()) {
+                String[] features = p.getProperty(n).split(",");
+                m.put(n, Collections.unmodifiableSet(new HashSet<>(Arrays.asList(features))));
+            }
+        }
+
+        return Collections.unmodifiableMap(m);
+    }
+
+    private File getDataFile(String name) throws IOException {
+        String fn = System.getProperty("whitelisting." + name);
+        if (fn == null)
+            return null;
+        return new File(fn);
+    }
+
+    @Override
+    public ResolverHook begin(Collection<BundleRevision> triggers) {
+        return new ResolverHookImpl(bsnVerMap, bundleFeatureMap, featureRegionMap, regionPackageMap);
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/apiregions/impl/ResolverHookImpl.java b/src/main/java/org/apache/sling/feature/apiregions/impl/ResolverHookImpl.java
new file mode 100644
index 0000000..5642d47
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/apiregions/impl/ResolverHookImpl.java
@@ -0,0 +1,186 @@
+/*
+ * 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.feature.apiregions.impl;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Version;
+import org.osgi.framework.hooks.resolver.ResolverHook;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleRequirement;
+import org.osgi.framework.wiring.BundleRevision;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+class ResolverHookImpl implements ResolverHook {
+    private static final Logger LOG = Logger.getLogger(ResolverHookImpl.class.getName());
+
+    private final Map<Map.Entry<String, Version>, List<String>> bsnVerMap;
+    private final Map<String, Set<String>> bundleFeatureMap;
+    private final Map<String, Set<String>> featureRegionMap;
+    private final Map<String, Set<String>> regionPackageMap;
+
+    public ResolverHookImpl(Map<Entry<String, Version>, List<String>> bsnVerMap, Map<String, Set<String>> bundleFeatureMap,
+            Map<String, Set<String>> featureRegionMap, Map<String, Set<String>> regionPackageMap) {
+        this.bsnVerMap = bsnVerMap;
+        this.bundleFeatureMap = bundleFeatureMap;
+        this.featureRegionMap = featureRegionMap;
+        this.regionPackageMap = regionPackageMap;
+    }
+
+    @Override
+    public void filterResolvable(Collection<BundleRevision> candidates) {
+        // Nothing to do
+    }
+
+    @Override
+    public void filterSingletonCollisions(BundleCapability singleton, Collection<BundleCapability> collisionCandidates) {
+        // Nothing to do
+    }
+
+    @Override
+    public void filterMatches(BundleRequirement requirement, Collection<BundleCapability> candidates) {
+        // Filtering is only on package resolution. Any other kind of resolution is not limited
+        if (!PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace()))
+            return;
+
+        System.out.println("*** Filter Matches: " + requirement);
+        Bundle reqBundle = requirement.getRevision().getBundle();
+        long reqBundleID = reqBundle.getBundleId();
+        String reqBundleName = reqBundle.getSymbolicName();
+        Version reqBundleVersion = reqBundle.getVersion();
+
+        List<String> aids = bsnVerMap.get(new AbstractMap.SimpleEntry<String, Version>(reqBundleName, reqBundleVersion));
+        if (aids == null)
+            return; // TODO what to do?
+        List<String> reqFeatures = new ArrayList<>();
+        for (String aid : aids) {
+            Set<String> fid = bundleFeatureMap.get(aid);
+            if (fid != null)
+                reqFeatures.addAll(fid);
+        }
+
+        Set<String> regions = new HashSet<>();
+        for (String feature : reqFeatures) {
+            Set<String> fr = featureRegionMap.get(feature);
+            if (fr != null) {
+                regions.addAll(fr);
+            }
+        }
+
+        Set<BundleCapability> coveredCaps = new HashSet<>();
+
+        nextCapability:
+        for (BundleCapability bc : candidates) {
+            BundleRevision rev = bc.getRevision();
+
+            Bundle capBundle = rev.getBundle();
+            long capBundleID = capBundle.getBundleId();
+            if (capBundleID == 0) {
+                // always allow capability from the system bundle
+                coveredCaps.add(bc);
+                continue nextCapability;
+            }
+
+            if (capBundleID == reqBundleID) {
+                // always allow capability from same bundle
+                coveredCaps.add(bc);
+                continue nextCapability;
+            }
+
+            String capBundleName = capBundle.getSymbolicName();
+            Version capBundleVersion = capBundle.getVersion();
+
+            List<String> capBundleArtifacts = bsnVerMap.get(new AbstractMap.SimpleEntry<String, Version>(capBundleName, capBundleVersion));
+            if (capBundleArtifacts == null)
+                return; // TODO what to do?
+            List<String> capFeatures = new ArrayList<>();
+            for (String ba : capBundleArtifacts) {
+                Set<String> capfeats = bundleFeatureMap.get(ba);
+                if (capfeats != null)
+                    capFeatures.addAll(capfeats);
+            }
+
+            if (capFeatures.isEmpty())
+                capFeatures = Collections.singletonList(null);
+
+            for (String capFeat : capFeatures) {
+                if (capFeat == null) {
+                    // always allow capability not coming from a feature
+                    coveredCaps.add(bc);
+                    continue nextCapability;
+                }
+
+                if (reqFeatures.contains(capFeat)) {
+                    // Within a single feature everything can wire to everything else
+                    coveredCaps.add(bc);
+                    continue nextCapability;
+                }
+
+                if (featureRegionMap.get(capFeat) == null) {
+                    // If the feature hosting the capability has no regions defined, everyone can access
+                    coveredCaps.add(bc);
+                    continue nextCapability;
+                }
+
+                Object pkg = bc.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE);
+                if (pkg instanceof String) {
+                    String packageName = (String) pkg;
+
+                    Set<String> globalPackages = regionPackageMap.get("global");
+                    if (globalPackages != null && globalPackages.contains(packageName)) {
+                        // If the export is in the global region everyone can access
+                        coveredCaps.add(bc);
+                        continue nextCapability;
+                    }
+
+                    for (String region : regions) {
+                        Set<String> regionPackages = regionPackageMap.get(region);
+                        if (regionPackages != null && regionPackages.contains(packageName)) {
+                            // If the export is in a region that the feature is also in, then allow
+                            coveredCaps.add(bc);
+                            continue nextCapability;
+                        }
+                    }
+                }
+            }
+        }
+
+        // Remove any capabilities that are not covered
+        if (candidates.retainAll(coveredCaps)) {
+            LOG.log(Level.INFO,
+                    "Removed one ore more candidates for requirement {0} as they are not in the correct region", requirement);
+        }
+    }
+
+    @Override
+    public void end() {
+        // Nothing to do
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/apiregions/impl/ResolverHookImplTest.java b/src/test/java/org/apache/sling/feature/apiregions/impl/ResolverHookImplTest.java
new file mode 100644
index 0000000..5bc60a1
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/apiregions/impl/ResolverHookImplTest.java
@@ -0,0 +1,209 @@
+/*
+ * 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.feature.apiregions.impl;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Version;
+import org.osgi.framework.namespace.PackageNamespace;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleRequirement;
+import org.osgi.framework.wiring.BundleRevision;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class ResolverHookImplTest {
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test @Ignore
+    public void testFilterMatches() throws Exception {
+        String f = "gid:aid:0.0.9";
+        String f2 = "gid2:aid2:1.0.0-SNAPSHOT";
+        String f3 = "gid3:aid3:1.2.3";
+        String f4 = "gid4:aid4:1.2.3";
+        String f5 = "gid5:aid5:1.2.3";
+
+        /*
+        Features fs = Mockito.mock(Features.class);
+        Mockito.when(fs.getFeaturesForBundle("a.b.c", new Version(0,0,0)))
+            .thenReturn(Collections.singleton(f)); // b7
+        Mockito.when(fs.getFeaturesForBundle("some.other.bundle", new Version(9,9,9,"suffix")))
+            .thenReturn(Collections.singleton(f2)); // b9
+        Mockito.when(fs.getFeaturesForBundle("a-bundle", new Version(1,0,0,"SNAPSHOT")))
+            .thenReturn(Collections.singleton(f2)); // b10
+        Mockito.when(fs.getFeaturesForBundle("a.b.c", new Version(1,2,3)))
+            .thenReturn(Collections.singleton(f3)); // b17
+        Mockito.when(fs.getFeaturesForBundle("z.z.z", new Version(3,2,1)))
+            .thenReturn(new HashSet<>(Arrays.asList(f, f3))); // b18
+        Mockito.when(fs.getFeaturesForBundle("x.y.z", new Version(9,9,9)))
+            .thenReturn(Collections.singleton(f3)); // b19
+        Mockito.when(fs.getFeaturesForBundle("zzz", new Version(1,0,0)))
+            .thenReturn(Collections.singleton(f4)); // b20
+        Mockito.when(fs.getFeaturesForBundle("www", new Version(1,0,0)))
+        .thenReturn(Collections.singleton(f5)); // b20
+
+        ServiceTracker st = Mockito.mock(ServiceTracker.class);
+        Mockito.when(st.waitForService(Mockito.anyLong())).thenReturn(fs);
+
+        Map<String, Set<String>> rpm = new HashMap<>();
+        rpm.put("r0", Collections.singleton("org.bar"));
+        rpm.put("r1", new HashSet<>(Arrays.asList("org.blah", "org.foo")));
+        rpm.put(WhitelistService.GLOBAL_REGION, Collections.singleton("org.bar.tar"));
+        rpm.put("r3", Collections.singleton("xyz"));
+
+        Map<String, Set<String>> frm = new HashMap<>();
+        frm.put("gid:aid:0.0.9",
+                new HashSet<>(Arrays.asList("r1", "r2", WhitelistService.GLOBAL_REGION)));
+        frm.put("gid2:aid2:1.0.0-SNAPSHOT", Collections.singleton("r2"));
+        frm.put("gid3:aid3:1.2.3", Collections.singleton("r3"));
+        frm.put("gid4:aid4:1.2.3", Collections.singleton("r3"));
+        frm.put("gid5:aid5:1.2.3", Collections.emptySet());
+
+        WhitelistService wls = new WhitelistServiceImpl(rpm, frm);
+        ResolverHookImpl rh = new ResolverHookImpl(st, wls);
+
+        // Check that we can get the capability from another bundle in the same region
+        // Bundle 7 is in feature f with regions r1, r2. Bundle 9 is in feature f2 with regions r2
+        BundleRequirement req = mockRequirement(7, "a.b.c", new Version(0,0,0));
+        BundleCapability bc1 = mockCapability("org.foo", 9, "some.other.bundle", new Version(9,9,9,"suffix"));
+        List<BundleCapability> candidates = new ArrayList<>(Arrays.asList(bc1));
+        rh.filterMatches(req, candidates);
+        assertEquals(Collections.singletonList(bc1), candidates);
+
+        // Check that we cannot get the capability from another bundle in a different region
+        // Bundle 9 is in feature f2 with region r2
+        BundleRequirement req2 = mockRequirement(9, "some.other.bundle", new Version(9,9,9,"suffix"));
+        BundleCapability bc2 = mockCapability("org.bar", 17, "a.b.c", new Version(1,2,3));
+        Collection<BundleCapability> c2 = new ArrayList<>(Arrays.asList(bc2));
+        rh.filterMatches(req2, c2);
+        assertEquals(0, c2.size());
+
+        // Check that we can get the capability from the same bundle
+        BundleRequirement req3 = mockRequirement(9, "some.other.bundle", new Version(9,9,9,"suffix"));
+        BundleCapability bc3 = mockCapability("org.bar", 9, "some.other.bundle", new Version(9,9,9,"suffix"));
+        Collection<BundleCapability> c3 = new ArrayList<>(Arrays.asList(bc3));
+        rh.filterMatches(req3, c3);
+        assertEquals(Collections.singletonList(bc3), c3);
+
+        // Check that we can get the capability from the another bundle in the same feature
+        BundleRequirement req4 = mockRequirement(9, "some.other.bundle", new Version(9,9,9,"suffix"));
+        BundleCapability bc4 = mockCapability("org.bar", 10, "a-bundle", new Version(1,0,0,"SNAPSHOT"));
+        Collection<BundleCapability> c4 = new ArrayList<>(Arrays.asList(bc4));
+        rh.filterMatches(req4, c4);
+        assertEquals(Collections.singletonList(bc4), c4);
+
+        // Check that we can get the capability from another bundle where the capability
+        // is globally visible (from bundle 9, f2)
+        BundleRequirement req5 = mockRequirement(17, "a.b.c", new Version(1,2,3));
+        BundleCapability bc5 = mockCapability("org.bar.tar", 9, "some.other.bundle", new Version(9,9,9,"suffix"));
+        Collection<BundleCapability> c5 = new ArrayList<>(Arrays.asList(bc5));
+        rh.filterMatches(req5, c5);
+        assertEquals(Collections.singletonList(bc5), c5);
+
+        // Check that we can get the capability from another bundle where the capability
+        // is globally visible (from bundle 7, f)
+        BundleRequirement req6 = mockRequirement(7, "a.b.c", new Version(0,0,0));
+        BundleCapability bc6 = mockCapability("org.bar.tar", 17, "a.b.c", new Version(1,2,3));
+        Collection<BundleCapability> c6 = new ArrayList<>(Arrays.asList(bc6));
+        rh.filterMatches(req6, c6);
+        assertEquals(Collections.singletonList(bc6), c6);
+
+        // Check that capabilities in non-package namespaces are ignored
+        BundleRequirement req7 = Mockito.mock(BundleRequirement.class);
+        Mockito.when(req7.getNamespace()).thenReturn("some.other.namespace");
+        BundleCapability bc7 = mockCapability("org.bar", 17, "a.b.c", new Version(1,2,3));
+        Collection<BundleCapability> c7 = new ArrayList<>(Arrays.asList(bc7));
+        rh.filterMatches(req7, c7);
+        assertEquals(Collections.singletonList(bc7), c7);
+
+        // Check that we can get the capability from another provider in the same region
+        BundleRequirement req8 = mockRequirement(20, "zzz", new Version(1,0,0));
+        BundleCapability bc8 = mockCapability("xyz", 19, "x.y.z", new Version(9,9,9));
+        Collection<BundleCapability> c8 = new ArrayList<>(Arrays.asList(bc8));
+        rh.filterMatches(req8, c8);
+
+        assertEquals(Collections.singletonList(bc8), c8);
+        // A requirement from a bundle that has no feature cannot access one in a region
+        BundleRequirement req9 = mockRequirement(11, "qqq", new Version(6,6,6));
+        BundleCapability bc9 = mockCapability("org.bar", 17, "a.b.c", new Version(1,2,3));
+        ArrayList c9 = new ArrayList<>(Arrays.asList(bc9));
+        rh.filterMatches(req9, c9);
+        assertEquals(0, c9.size());
+
+        // A requirement from a bundle that has no feature can still access on in the global region
+        BundleRequirement req10 = mockRequirement(11, "qqq", new Version(6,6,6));
+        BundleCapability bc10 = mockCapability("org.bar.tar", 18, "z.z.z", new Version(3,2,1));
+        ArrayList c10 = new ArrayList<>(Arrays.asList(bc10));
+        rh.filterMatches(req10, c10);
+        assertEquals(Collections.singletonList(bc10), c10);
+
+        // A requirement from a bundle that has no feature can be satisfied by a capability
+        // from a bundle that has no feature
+        BundleRequirement req11 = mockRequirement(11, "qqq", new Version(6,6,6));
+        BundleCapability bc11 = mockCapability("org.bar.tar", 20, "www", new Version(1,0,0));
+        ArrayList c11 = new ArrayList<>(Arrays.asList(bc11));
+        rh.filterMatches(req11, c11);
+        assertEquals(Collections.singletonList(bc11), c11);
+
+        // A capability from the system bundle is always accessible
+        BundleRequirement req12 = mockRequirement(11, "qqq", new Version(6,6,6));
+        BundleCapability bc12 = mockCapability("ping.pong", 0, "system.bundle", new Version(3,2,1));
+        ArrayList c12 = new ArrayList<>(Arrays.asList(bc12));
+        rh.filterMatches(req12, c12);
+        assertEquals(Collections.singletonList(bc12), c12);
+        */
+    }
+
+    private BundleCapability mockCapability(String pkg, long bundleID, String bsn, Version version) {
+        Map<String, Object> attrs =
+                Collections.singletonMap(PackageNamespace.PACKAGE_NAMESPACE, pkg);
+
+        Bundle bundle = Mockito.mock(Bundle.class);
+        Mockito.when(bundle.getBundleId()).thenReturn(bundleID);
+        Mockito.when(bundle.getSymbolicName()).thenReturn(bsn);
+        Mockito.when(bundle.getVersion()).thenReturn(version);
+
+        BundleRevision br = Mockito.mock(BundleRevision.class);
+        Mockito.when(br.getBundle()).thenReturn(bundle);
+
+
+        BundleCapability cap = Mockito.mock(BundleCapability.class);
+        Mockito.when(cap.getAttributes()).thenReturn(attrs);
+        Mockito.when(cap.getRevision()).thenReturn(br);
+        return cap;
+    }
+
+    private BundleRequirement mockRequirement(long bundleID, String bsn, Version version) {
+        Bundle bundle = Mockito.mock(Bundle.class);
+        Mockito.when(bundle.getBundleId()).thenReturn(bundleID);
+        Mockito.when(bundle.getSymbolicName()).thenReturn(bsn);
+        Mockito.when(bundle.getVersion()).thenReturn(version);
+
+        BundleRevision br = Mockito.mock(BundleRevision.class);
+        Mockito.when(br.getBundle()).thenReturn(bundle);
+
+        BundleRequirement req = Mockito.mock(BundleRequirement.class);
+        Mockito.when(req.getNamespace()).thenReturn(PackageNamespace.PACKAGE_NAMESPACE);
+        Mockito.when(req.getRevision()).thenReturn(br);
+
+        return req;
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistEnforcerTest.java b/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistEnforcerTest.java
new file mode 100644
index 0000000..e83c27f
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistEnforcerTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.feature.apiregions.impl;
+
+public class WhitelistEnforcerTest {
+    /*
+    @Test
+    public void testWhitelistEnforcerConfigUpdate() throws ConfigurationException {
+        BundleContext bc = Mockito.mock(BundleContext.class);
+        WhitelistEnforcer enf = new WhitelistEnforcer(bc, null);
+
+        assertNull("Precondition", enf.whitelistService);
+
+        Dictionary<String, Object> props = new Hashtable<>();
+        props.put("ignored", "ignored-too");
+        props.put("whitelist.region.region1", "org.foo.pkg1");
+        props.put("whitelist.region.region2", new String[] {"pkga", "pkgb"});
+        props.put("whitelist.region.region.3", Arrays.asList("a.b.c", "d.e.f", "g.h.i"));
+        props.put("whitelist.feature.gid:aid:slingfeature:testfeature:1.0.0", WhitelistService.GLOBAL_REGION);
+        props.put("whitelist.feature.gid:myfeature:1.0.0", new String [] {"region1", "region2"});
+        enf.updated(props);
+
+        assertNotNull(enf.whitelistService);
+        Mockito.verify(bc, Mockito.times(1)).registerService(
+                Mockito.eq(WhitelistService.class), Mockito.isA(WhitelistService.class), Mockito.any());
+
+        // check that the configuration parsing worked
+        assertTrue(enf.whitelistService.regionWhitelistsPackage("region1", "org.foo.pkg1"));
+        assertTrue(enf.whitelistService.regionWhitelistsPackage("region2", "pkga"));
+        assertTrue(enf.whitelistService.regionWhitelistsPackage("region2", "pkgb"));
+        assertFalse(enf.whitelistService.regionWhitelistsPackage("region1", "pkg1"));
+        assertTrue(enf.whitelistService.regionWhitelistsPackage("region.3", "d.e.f"));
+        assertFalse(enf.whitelistService.regionWhitelistsPackage("region.3", "d.e.f.g"));
+        assertNull(enf.whitelistService.regionWhitelistsPackage("unknown", "pkga"));
+
+        Set<String> regions = enf.whitelistService.listRegions("gid:myfeature:1.0.0");
+        assertEquals(new HashSet<String>(Arrays.asList("region1", "region2")), regions);
+        assertEquals(Collections.singleton("global"), enf.whitelistService
+                .listRegions("gid:aid:slingfeature:testfeature:1.0.0"));
+        assertNull(enf.whitelistService.listRegions("unknown"));
+
+        enf.updated(null);
+        assertNull("A null configuration should put back the null whitelist service", enf.whitelistService);
+    }
+    */
+}
diff --git a/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistServiceFactoryImplTest.java b/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistServiceFactoryImplTest.java
new file mode 100644
index 0000000..f9120d3
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistServiceFactoryImplTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.feature.apiregions.impl;
+
+public class WhitelistServiceFactoryImplTest {
+    /*
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test
+    public void testWhitelistServiceFactory() {
+        List<ResolverHookFactory> resolverHookFactory = new ArrayList<>();
+        Map<String, Map<String, Set<String>>> wlsCfg = new HashMap<>();
+
+        ServiceTracker st = Mockito.mock(ServiceTracker.class);
+        BundleContext bc = Mockito.mock(BundleContext.class);
+        Mockito.when(bc.registerService(Mockito.isA(Class.class), Mockito.isA(Object.class), Mockito.isA(Dictionary.class)))
+            .then(i -> { resolverHookFactory.add(i.getArgument(1)); return null; });
+
+        WhitelistServiceFactory wsf = new WhitelistServiceFactoryImpl(bc, st) {
+            @Override
+            WhitelistService createWhitelistService(Map<String, Set<String>> packages, Map<String, Set<String>> regions) {
+                wlsCfg.put("packages", packages);
+                wlsCfg.put("regions", regions);
+                return super.createWhitelistService(packages, regions);
+            }
+        };
+
+        Map<String, Map<String, Set<String>>> m = new HashMap<>();
+        Map<String, Set<String>> packages = new HashMap<>();
+        packages.put("region1", new HashSet<>(Arrays.asList("org.foo", "org.bar")));
+        packages.put("region2", Collections.singleton("org.foo.bar"));
+        m.put("packages", packages);
+
+        Map<String, Set<String>> regions = new HashMap<>();
+        regions.put("f1", new HashSet<String>(Arrays.asList("region1", "region3")));
+        regions.put("f2", Collections.singleton("region2"));
+        regions.put("f3", Collections.singleton("region4"));
+        regions.put("f4", Collections.singleton("region2"));
+        m.put("regions", regions);
+
+        wsf.initialize(m);
+
+        assertEquals(wlsCfg, m);
+
+        ResolverHookFactory rhf = resolverHookFactory.get(0);
+        assertTrue(rhf instanceof WhitelistEnforcer);
+    }
+    */
+}
diff --git a/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistServiceImplTest.java b/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistServiceImplTest.java
new file mode 100644
index 0000000..832e199
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/apiregions/impl/WhitelistServiceImplTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.feature.apiregions.impl;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class WhitelistServiceImplTest {
+    @Test
+    @Ignore
+    public void testListRegions() {
+        /*
+        Map<String, Set<String>> frm = new HashMap<>();
+        frm.put("myfeature", new HashSet<>(Arrays.asList("rega", "regb", "regc")));
+        frm.put("myotherfeature", Collections.emptySet());
+        WhitelistService wls = new WhitelistServiceImpl(Collections.emptyMap(), frm);
+
+        assertEquals(new HashSet<>(Arrays.asList("rega", "regb", "regc")),
+                wls.listRegions("myfeature"));
+        assertEquals(0, wls.listRegions("myotherfeature").size());
+        assertNull(wls.listRegions("nonexistent"));
+    }
+
+    @Test
+    public void testRegionContainsPackage() {
+        Map<String, Set<String>> rpm = new HashMap<>();
+        rpm.put("region1", new HashSet<>(Arrays.asList("org.foo", "org.bar", "org.foo.bar")));
+        WhitelistService wls = new WhitelistServiceImpl(rpm, Collections.emptyMap());
+
+        assertTrue(wls.regionWhitelistsPackage("region1", "org.foo"));
+        assertTrue(wls.regionWhitelistsPackage("region1", "org.foo.bar"));
+        assertFalse(wls.regionWhitelistsPackage("region1", "org.bar.foo"));
+        assertNull(wls.regionWhitelistsPackage("nonexitent", "org.foo"));
+        */
+    }
+}