You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 10:15:05 UTC

[sling-org-apache-sling-serviceusermapper] 02/04: SLING-6963: Add Service user declaration based on principal names - patch provided by Angela Schreiber.

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

rombert pushed a commit to annotated tag org.apache.sling.serviceusermapper-1.3.4
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-serviceusermapper.git

commit 341899c59b23e25db5a10eb4c6bd0d31069f6109
Author: Karl Pauls <pa...@apache.org>
AuthorDate: Mon Jul 10 15:40:52 2017 +0000

    SLING-6963: Add Service user declaration based on principal names - patch provided by Angela Schreiber.
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/serviceusermapper@1801482 13f79535-47bb-0310-9956-ffa450edef68
---
 .../ServicePrincipalsValidator.java                |  37 +++++
 .../serviceusermapping/ServiceUserMapper.java      |  15 ++
 .../sling/serviceusermapping/impl/Mapping.java     |  63 +++++++-
 .../impl/MappingConfigAmendment.java               |  10 +-
 .../impl/MappingInventoryPrinter.java              |  92 ++++++++++--
 .../impl/ServiceUserMapperImpl.java                | 110 +++++++++++++-
 .../sling/serviceusermapping/package-info.java     |   2 +-
 .../sling/serviceusermapping/impl/MappingTest.java | 161 ++++++++++++++-------
 .../impl/ServiceUserMapperImplTest.java            | 141 ++++++++++++++++++
 9 files changed, 551 insertions(+), 80 deletions(-)

diff --git a/src/main/java/org/apache/sling/serviceusermapping/ServicePrincipalsValidator.java b/src/main/java/org/apache/sling/serviceusermapping/ServicePrincipalsValidator.java
new file mode 100644
index 0000000..4fa384b
--- /dev/null
+++ b/src/main/java/org/apache/sling/serviceusermapping/ServicePrincipalsValidator.java
@@ -0,0 +1,37 @@
+/*
+ * 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.serviceusermapping;
+
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * The {@code ServicePrincipalsValidator} allows to implement validation of configured
+ * service user mappings.
+ */
+@ConsumerType
+public interface ServicePrincipalsValidator {
+
+    /**
+     * Validates the configured service principal names.
+     *
+     * @param serviceUserId The principal names associated with the service.
+     * @param serviceName The name of the service
+     * @param subServiceName The optional sub service name.
+     * @return {@code true} if all configured service principal names are valid; {@code false} otherwise.
+     */
+    boolean isValid(Iterable<String> servicePrincipalNames, String serviceName, String subServiceName);
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapper.java b/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapper.java
index d8e6701..8431d7e 100644
--- a/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapper.java
+++ b/src/main/java/org/apache/sling/serviceusermapping/ServiceUserMapper.java
@@ -71,4 +71,19 @@ public interface ServiceUserMapper {
      *         optional {@code serviceInfo}.
      */
     String getServiceUserID(Bundle bundle, String subServiceName);
+
+    /**
+     * Returns the principal names to access the data store on behalf of the
+     * service.
+     *
+     * @param bundle The bundle implementing the service request access to resources.
+     * @param subServiceName Name of the sub service. This parameter is optional
+     *                       and may be an empty string or {@code null}.
+     * @return The principal names to use to provide access to the resources for
+     *         the service. This may be {@code null} if no mapping has been defined
+     *         for the service identified by the bundle and the optional {@code serviceInfo}
+     *         or if no principal names have been specified with the mapping.
+     *         In this case {@link #getServiceUserID(Bundle, String)} should be used instead.
+     */
+    Iterable<String> getServicePrincipalNames(Bundle bundle, String subServiceName);
 }
diff --git a/src/main/java/org/apache/sling/serviceusermapping/impl/Mapping.java b/src/main/java/org/apache/sling/serviceusermapping/impl/Mapping.java
index b4650c2..503a985 100644
--- a/src/main/java/org/apache/sling/serviceusermapping/impl/Mapping.java
+++ b/src/main/java/org/apache/sling/serviceusermapping/impl/Mapping.java
@@ -18,9 +18,12 @@
  */
 package org.apache.sling.serviceusermapping.impl;
 
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * The <code>Mapping</code> class defines the mapping of a service's name and
- * optional service information to a user name.
+ * optional service information to a user name and optionally to a set of principal names.
  */
 class Mapping implements Comparable<Mapping> {
 
@@ -36,11 +39,14 @@ class Mapping implements Comparable<Mapping> {
 
     private final String userName;
 
+    private final Set<String> principalNames;
+
     /**
      * Creates a mapping entry for the entry specification of the form:
      *
      * <pre>
-     * spec = serviceName [ ":" subServiceName ] "=" userName .
+     * spec = serviceName [ ":" subServiceName ] "=" userName | "[" principalNames "]"
+     * principalNames = principalName ["," principalNames]
      * </pre>
      *
      * @param spec The mapping specification.
@@ -56,7 +62,7 @@ class Mapping implements Comparable<Mapping> {
         if (colon == 0 || equals <= 0) {
             throw new IllegalArgumentException("serviceName is required");
         } else if (equals == spec.length() - 1) {
-            throw new IllegalArgumentException("userName is required");
+            throw new IllegalArgumentException("userName or principalNames is required");
         } else if (colon + 1 == equals) {
             throw new IllegalArgumentException("serviceInfo must not be empty");
         }
@@ -69,18 +75,38 @@ class Mapping implements Comparable<Mapping> {
             this.subServiceName = spec.substring(colon + 1, equals);
         }
 
-        this.userName = spec.substring(equals + 1);
+        String s = spec.substring(equals + 1);
+        if (s.charAt(0) == '[' && s.charAt(s.length()-1) == ']') {
+            this.userName = null;
+            this.principalNames = extractPrincipalNames(s);
+        } else {
+            this.userName = s;
+            this.principalNames = null;
+        }
+    }
+
+    static Set<String> extractPrincipalNames(String s) {
+        String[] sArr = s.substring(1, s.length() - 1).split(",");
+        Set<String> set = new HashSet<>();
+        for (String name : sArr) {
+            String n = name.trim();
+            if (!n.isEmpty()) {
+                set.add(n);
+            }
+        }
+        return set;
     }
 
     /**
      * Returns the user name if the {@code serviceName} and the
-     * {@code serviceInfo} match. Otherwise {@code null} is returned.
+     * {@code serviceInfo} match and a single user name is configured (in contrast
+     * to a set of principal names). Otherwise {@code null} is returned.
      *
      * @param serviceName The name of the service to match. If this is
      *            {@code null} this mapping will not match.
      * @param subServiceName The Subservice Name to match. This may be
      *            {@code null}.
-     * @return The user name if this mapping matches or {@code null} otherwise.
+     * @return The user name if this mapping matches and the configuration doesn't specify a set of principal names; {@code null} otherwise.
      */
     String map(final String serviceName, final String subServiceName) {
         if (this.serviceName.equals(serviceName) && equals(this.subServiceName, subServiceName)) {
@@ -90,14 +116,37 @@ class Mapping implements Comparable<Mapping> {
         return null;
     }
 
+    /**
+     * Returns the principal names if the {@code serviceName} and the
+     * {@code serviceInfo} match and principal names have been configured.
+     * Otherwise {@code null} is returned. If no principal names are configured
+     * {@link #map(String, String)} needs to be used instead.
+     *
+     * @param serviceName The name of the service to match. If this is
+     *            {@code null} this mapping will not match.
+     * @param subServiceName The Subservice Name to match. This may be
+     *            {@code null}.
+     * @return An iterable of principals names this mapping matches and the configuration
+     * does specify a set of principal names (intstead of a single user name); {@code null}
+     * otherwise.
+     */
+    Iterable<String> mapPrincipals(final String serviceName, final String subServiceName) {
+        if (this.serviceName.equals(serviceName) && equals(this.subServiceName, subServiceName)) {
+            return principalNames;
+        }
+
+        return null;
+    }
+
     private boolean equals(String str1, String str2) {
         return ((str1 == null) ? str2 == null : str1.equals(str2));
     }
 
     @Override
     public String toString() {
+        String name = (userName != null) ? "userName=" + userName : "principleNames" + principalNames.toString();
         return "Mapping [serviceName=" + serviceName + ", subServiceName="
-                + subServiceName + ", userName=" + userName + "]";
+                + subServiceName + ", " + name;
     }
 
     public String getServiceName() {
diff --git a/src/main/java/org/apache/sling/serviceusermapping/impl/MappingConfigAmendment.java b/src/main/java/org/apache/sling/serviceusermapping/impl/MappingConfigAmendment.java
index c0fe863..60a425d 100644
--- a/src/main/java/org/apache/sling/serviceusermapping/impl/MappingConfigAmendment.java
+++ b/src/main/java/org/apache/sling/serviceusermapping/impl/MappingConfigAmendment.java
@@ -46,10 +46,12 @@ public class MappingConfigAmendment implements Comparable<MappingConfigAmendment
         int service_ranking() default 0;
 
         @AttributeDefinition(name = "Service Mappings",
-            description = "Provides mappings from service name to user names. "
-                + "Each entry is of the form 'bundleId [ \":\" subServiceName ] \"=\" userName' "
-                + "where bundleId and subServiceName identify the service and userName "
-                + "defines the name of the user to provide to the service. Invalid entries are logged and ignored.")
+            description = "Provides mappings from service name to user (and optionally principal) names. "
+                + "Each entry is of the form 'bundleId [ \":\" subServiceName ] \"=\" userName' | \"[\" principalNames \"]\" "
+                + "where bundleId and subServiceName identify the service and userName/principalNames "
+                + "defines the name(s) of the user/principals to provide to the service. "
+                + "'principalNames is defined to be a comma separated list of principal names. "
+                + "Invalid entries are logged and ignored.")
         String[] user_mapping() default {};
 
         // Internal Name hint for web console.
diff --git a/src/main/java/org/apache/sling/serviceusermapping/impl/MappingInventoryPrinter.java b/src/main/java/org/apache/sling/serviceusermapping/impl/MappingInventoryPrinter.java
index e3488f5..8f32898 100644
--- a/src/main/java/org/apache/sling/serviceusermapping/impl/MappingInventoryPrinter.java
+++ b/src/main/java/org/apache/sling/serviceusermapping/impl/MappingInventoryPrinter.java
@@ -21,6 +21,7 @@ package org.apache.sling.serviceusermapping.impl;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.SortedMap;
@@ -62,16 +63,49 @@ public class MappingInventoryPrinter implements InventoryPrinter {
         return m.map(m.getServiceName(), m.getSubServiceName());
     }
 
+    private String[] getMappedPrincipalNames(Mapping m) {
+        Iterable<String> principalNames = m.mapPrincipals(m.getServiceName(), m.getSubServiceName());
+        if (principalNames == null) {
+            return null;
+        } else {
+            List<String> l = new ArrayList<>();
+            for (String pName : principalNames) {
+                l.add(pName);
+            }
+            return l.toArray(new String[l.size()]);
+        }
+    }
+
     private SortedMap<String, List<Mapping>> getMappingsByUser(List<Mapping> mappings) {
         SortedMap<String, List<Mapping>> result = new TreeMap<String, List<Mapping>>();
         for(Mapping m : mappings) {
             final String user = getMappedUser(m);
-            List<Mapping> list = result.get(user);
-            if(list == null) {
-                list = new ArrayList<Mapping>();
-                result.put(user, list);
+            if (user != null) {
+                List<Mapping> list = result.get(user);
+                if (list == null) {
+                    list = new ArrayList<Mapping>();
+                    result.put(user, list);
+                }
+                list.add(m);
+            }
+        }
+        return result;
+    }
+
+    private SortedMap<String, List<Mapping>> getMappingsByPrincipalName(List<Mapping> mappings) {
+        SortedMap<String, List<Mapping>> result = new TreeMap<String, List<Mapping>>();
+        for(Mapping m : mappings) {
+            final String[] principalNames = getMappedPrincipalNames(m);
+            if (principalNames != null) {
+                for (String pName : principalNames) {
+                    List<Mapping> list = result.get(pName);
+                    if (list == null) {
+                        list = new ArrayList<Mapping>();
+                        result.put(pName, list);
+                    }
+                    list.add(m);
+                }
             }
-            list.add(m);
         }
         return result;
     }
@@ -80,19 +114,26 @@ public class MappingInventoryPrinter implements InventoryPrinter {
         w.object();
         w.key("serviceName").value(m.getServiceName());
         w.key("subServiceName").value(m.getSubServiceName());
-        w.key("user").value(getMappedUser(m));
+        String[] pNames = getMappedPrincipalNames(m);
+        if (pNames != null) {
+            w.key("principals").value(pNames);
+        } else {
+            w.key("user").value(getMappedUser(m));
+        }
         w.endObject();
     }
 
     private void renderJson(PrintWriter out) throws IOException {
         final List<Mapping> data = mapper.getActiveMappings();
         final Map<String, List<Mapping>> byUser = getMappingsByUser(data);
+        final Map<String, List<Mapping>> byPrincipalName = getMappingsByPrincipalName(data);
 
         final JSONWriter w = new JSONWriter(out);
         w.object();
         w.key("title").value("Service User Mappings");
         w.key("mappingsCount").value(data.size());
         w.key("uniqueUsersCount").value(byUser.keySet().size());
+        w.key("uniquePrincipalsCount").value(byPrincipalName.keySet().size());
 
         w.key("mappingsByUser");
         w.object();
@@ -106,6 +147,18 @@ public class MappingInventoryPrinter implements InventoryPrinter {
         }
         w.endObject();
 
+        w.key("mappingsByPrincipal");
+        w.object();
+        for(Map.Entry<String, List<Mapping>> e : byPrincipalName.entrySet()) {
+            w.key(e.getKey());
+            w.array();
+            for(Mapping m : e.getValue()) {
+                asJSON(w,m);
+            }
+            w.endArray();
+        }
+        w.endObject();
+
         w.endObject();
     }
 
@@ -117,19 +170,23 @@ public class MappingInventoryPrinter implements InventoryPrinter {
         final String sub = m.getSubServiceName();
         w.print(sub == null ? "" : sub);
         w.print(SEP);
-        w.println(getMappedUser(m));
+        String[] principalNames = getMappedPrincipalNames(m);
+        if (principalNames != null) {
+            w.println(Arrays.toString(principalNames));
+        } else {
+            w.println(getMappedUser(m));
+        }
     }
 
     private void renderText(PrintWriter out) {
         final List<Mapping> data = mapper.getActiveMappings();
-        final Map<String, List<Mapping>> byUser = getMappingsByUser(data);
 
-        final String formatInfo = " (format: service name / sub service name / user)";
+        final Map<String, List<Mapping>> byUser = getMappingsByUser(data);
 
         out.print("*** Mappings by user (");
         out.print(byUser.keySet().size());
         out.print(" users):");
-        out.println(formatInfo);
+        out.println(" (format: service name / sub service name / user)");
 
         for(Map.Entry<String, List<Mapping>> e : byUser.entrySet()) {
             out.print("  ");
@@ -138,5 +195,20 @@ public class MappingInventoryPrinter implements InventoryPrinter {
                 asText(out, m, "    ");
             }
         }
+
+        final Map<String, List<Mapping>> byPrincipalName = getMappingsByPrincipalName(data);
+
+        out.print("*** Mappings by principals (");
+        out.print(byPrincipalName.keySet().size());
+        out.print(" principals):");
+        out.println(" (format: service name / sub service name / principal names)");
+
+        for(Map.Entry<String, List<Mapping>> e : byPrincipalName.entrySet()) {
+            out.print("  ");
+            out.println(e.getKey());
+            for(Mapping m : e.getValue()) {
+                asText(out, m, "    ");
+            }
+        }
    }
 }
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImpl.java b/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImpl.java
index a7b8c48..904dcef 100644
--- a/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImpl.java
+++ b/src/main/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImpl.java
@@ -35,6 +35,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import org.apache.sling.serviceusermapping.ServicePrincipalsValidator;
 import org.apache.sling.serviceusermapping.ServiceUserMapped;
 import org.apache.sling.serviceusermapping.ServiceUserMapper;
 import org.apache.sling.serviceusermapping.ServiceUserValidator;
@@ -65,9 +66,10 @@ public class ServiceUserMapperImpl implements ServiceUserMapper {
 
         @AttributeDefinition(name = "Service Mappings",
             description = "Provides mappings from service name to user names. "
-                + "Each entry is of the form 'bundleId [ \":\" subServiceName ] \"=\" userName' "
+                + "Each entry is of the form 'bundleId [ \":\" subServiceName ] \"=\" userName' | \"[\" principalNames \"]\" "
                 + "where bundleId and subServiceName identify the service and userName "
-                + "defines the name of the user to provide to the service. Invalid entries are logged and ignored.")
+                + "defines the name of the user to provide to the service; alternative the the mapping"
+                + "can define a comma separated set of principalNames instead of the userName. Invalid entries are logged and ignored.")
         String[] user_mapping() default {};
 
         @AttributeDefinition(name = "Default User",
@@ -95,7 +97,9 @@ public class ServiceUserMapperImpl implements ServiceUserMapper {
 
     private Mapping[] activeMappings = new Mapping[0];
 
-    private final List<ServiceUserValidator> validators = new CopyOnWriteArrayList<>();
+    private final List<ServiceUserValidator> userValidators = new CopyOnWriteArrayList<>();
+
+    private final List<ServicePrincipalsValidator> principalsValidators = new CopyOnWriteArrayList<>();
 
     private SortedMap<Mapping, Registration> activeRegistrations = new TreeMap<>();
 
@@ -166,7 +170,7 @@ public class ServiceUserMapperImpl implements ServiceUserMapper {
      */
     @Reference(cardinality=ReferenceCardinality.MULTIPLE, policy= ReferencePolicy.DYNAMIC)
     protected synchronized void bindServiceUserValidator(final ServiceUserValidator serviceUserValidator) {
-        validators.add(serviceUserValidator);
+        userValidators.add(serviceUserValidator);
         restartAllActiveServiceUserMappedServices();
     }
 
@@ -175,7 +179,26 @@ public class ServiceUserMapperImpl implements ServiceUserMapper {
      * @param serviceUserValidator
      */
     protected synchronized void unbindServiceUserValidator(final ServiceUserValidator serviceUserValidator) {
-        validators.remove(serviceUserValidator);
+        userValidators.remove(serviceUserValidator);
+        restartAllActiveServiceUserMappedServices();
+    }
+
+    /**
+     * bind the servicePrincipalsValidator
+     * @param servicePrincipalsValidator
+     */
+    @Reference(cardinality=ReferenceCardinality.MULTIPLE, policy= ReferencePolicy.DYNAMIC)
+    protected synchronized void bindServicePrincipalsValidator(final ServicePrincipalsValidator servicePrincipalsValidator) {
+        principalsValidators.add(servicePrincipalsValidator);
+        restartAllActiveServiceUserMappedServices();
+    }
+
+    /**
+     * unbind the servicePrincipalsValidator
+     * @param servicePrincipalsValidator
+     */
+    protected synchronized void unbindServicePrincipalsValidator(final ServicePrincipalsValidator servicePrincipalsValidator) {
+        principalsValidators.remove(servicePrincipalsValidator);
         restartAllActiveServiceUserMappedServices();
     }
 
@@ -194,6 +217,21 @@ public class ServiceUserMapperImpl implements ServiceUserMapper {
         return result;
     }
 
+    /**
+     * @see org.apache.sling.serviceusermapping.ServiceUserMapper#getServicePrincipalNames(org.osgi.framework.Bundle, java.lang.String)
+     */
+    @Override
+    public Iterable<String> getServicePrincipalNames(Bundle bundle, String subServiceName) {
+        final String serviceName = getServiceName(bundle);
+        final Iterable<String> names = internalGetPrincipalNames(serviceName, subServiceName);
+        final boolean valid = areValidPrincipals(names, serviceName, subServiceName);
+        final Iterable<String> result = valid ? names : null;
+        log.debug(
+                "getServicePrincipalNames(bundle {}, subServiceName {}) returns [{}] (raw principalNames={}, valid={})",
+                new Object[] { bundle, subServiceName, result, names, valid});
+        return result;
+    }
+
     @Reference(cardinality=ReferenceCardinality.MULTIPLE,policy=ReferencePolicy.DYNAMIC,updated="updateAmendment")
     protected synchronized void bindAmendment(final MappingConfigAmendment amendment, final Map<String, Object> props) {
         final Long key = (Long) props.get(Constants.SERVICE_ID);
@@ -393,8 +431,8 @@ public class ServiceUserMapperImpl implements ServiceUserMapper {
             log.debug("isValidUser: userId is null -> invalid");
             return false;
         }
-        if ( !validators.isEmpty() ) {
-            for (final ServiceUserValidator validator : validators) {
+        if ( !userValidators.isEmpty() ) {
+            for (final ServiceUserValidator validator : userValidators) {
                 if ( validator.isValid(userId, serviceName, subServiceName) ) {
                     log.debug("isValidUser: Validator {} accepts userId [{}] -> valid", validator, userId);
                     return true;
@@ -408,6 +446,64 @@ public class ServiceUserMapperImpl implements ServiceUserMapper {
         }
     }
 
+    private boolean areValidPrincipals(final Iterable<String> principalNames, final String serviceName, final String subServiceName) {
+        if (principalNames == null) {
+            log.debug("areValidPrincipals: principalNames are null -> invalid");
+            return false;
+        }
+        if ( !principalsValidators.isEmpty() ) {
+            for (final ServicePrincipalsValidator validator : principalsValidators) {
+                if ( validator.isValid(principalNames, serviceName, subServiceName) ) {
+                    log.debug("areValidPrincipals: Validator {} accepts principal names [{}] -> valid", validator, principalNames);
+                    return true;
+                }
+            }
+            log.debug("areValidPrincipals: No validator accepted principal names [{}] -> invalid", principalNames);
+            return false;
+        } else {
+            log.debug("areValidPrincipals: No active validators for principal names [{}] -> valid", principalNames);
+            return true;
+        }
+    }
+
+    private Iterable<String> internalGetPrincipalNames(final String serviceName, final String subServiceName) {
+        log.debug(
+                "internalGetPrincipalNames: {} active mappings, looking for mapping for {}/{}",
+                new Object[] { this.activeMappings.length, serviceName, subServiceName });
+
+        for (final Mapping mapping : this.activeMappings) {
+            final Iterable<String> principalNames = mapping.mapPrincipals(serviceName, subServiceName);
+            if (principalNames != null) {
+                log.debug("Got principalNames [{}] from {}/{}", new Object[] {principalNames, serviceName, subServiceName });
+                return principalNames;
+            }
+        }
+
+        for (Mapping mapping : this.activeMappings) {
+            final Iterable<String> principalNames = mapping.mapPrincipals(serviceName, null);
+            if (principalNames != null) {
+                log.debug("Got principalNames [{}] from {}/{}", new Object[] {principalNames, serviceName });
+                return principalNames;
+            }
+        }
+
+        // second round without serviceInfo
+        log.debug(
+                "internalGetPrincipalNames: {} active mappings, looking for mapping for {}/<no subServiceName>",
+                this.activeMappings.length, serviceName);
+
+        for (Mapping mapping : this.activeMappings) {
+            final Iterable<String> principalNames = mapping.mapPrincipals(serviceName, null);
+            if (principalNames != null) {
+                log.debug("Got principalNames [{}] from {}/<no subServiceName>", principalNames, serviceName);
+                return principalNames;
+            }
+        }
+
+        log.debug("internalGetPrincipalNames: no mapping found.");
+        return null;
+    }
+
     static String getServiceName(final Bundle bundle) {
         return bundle.getSymbolicName();
     }
diff --git a/src/main/java/org/apache/sling/serviceusermapping/package-info.java b/src/main/java/org/apache/sling/serviceusermapping/package-info.java
index 69a2cdd..1ee3078 100644
--- a/src/main/java/org/apache/sling/serviceusermapping/package-info.java
+++ b/src/main/java/org/apache/sling/serviceusermapping/package-info.java
@@ -17,6 +17,6 @@
  * under the License.
  */
 
-@org.osgi.annotation.versioning.Version("1.2.1")
+@org.osgi.annotation.versioning.Version("1.3.0")
 package org.apache.sling.serviceusermapping;
 
diff --git a/src/test/java/org/apache/sling/serviceusermapping/impl/MappingTest.java b/src/test/java/org/apache/sling/serviceusermapping/impl/MappingTest.java
index 7764d79..2ade43e 100644
--- a/src/test/java/org/apache/sling/serviceusermapping/impl/MappingTest.java
+++ b/src/test/java/org/apache/sling/serviceusermapping/impl/MappingTest.java
@@ -19,69 +19,43 @@
 package org.apache.sling.serviceusermapping.impl;
 
 import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.Set;
 
 import junit.framework.TestCase;
-
-import org.apache.sling.serviceusermapping.impl.Mapping;
 import org.junit.Test;
 
 public class MappingTest {
 
-    @Test
+    @Test(expected = NullPointerException.class)
     public void test_constructor_null() {
-        try {
-            new Mapping(null);
-            TestCase.fail("NullPointerException expected");
-        } catch (NullPointerException npe) {
-            // expected
-        }
+        new Mapping(null);
     }
 
-    @Test
+    @Test(expected = IllegalArgumentException.class)
     public void test_constructor_empty() {
-        try {
-            new Mapping("");
-            TestCase.fail("IllegalArgumentException expected");
-        } catch (IllegalArgumentException iae) {
-            // expected
-        }
+        new Mapping("");
     }
 
-    @Test
+    @Test(expected = IllegalArgumentException.class)
     public void test_constructor_missing_user_name() {
-        try {
-            new Mapping("serviceName");
-            TestCase.fail("IllegalArgumentException expected");
-        } catch (IllegalArgumentException iae) {
-            // expected
-        }
+        new Mapping("serviceName");
 
-        try {
-            new Mapping("serviceName=");
-            TestCase.fail("IllegalArgumentException expected");
-        } catch (IllegalArgumentException iae) {
-            // expected
-        }
     }
 
-    @Test
+    @Test(expected = IllegalArgumentException.class)
+    public void test_constructor_missing_user_name2() {
+        new Mapping("serviceName=");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
     public void test_constructor_missing_service_name() {
-        try {
-            new Mapping("=user");
-            TestCase.fail("IllegalArgumentException expected");
-        } catch (IllegalArgumentException iae) {
-            // expected
-        }
+        new Mapping("=user");
     }
 
-    @Test
+    @Test(expected = IllegalArgumentException.class)
     public void test_constructor_empty_service_info() {
-        try {
-            new Mapping("srv:=user");
-            TestCase.fail("IllegalArgumentException expected");
-        } catch (IllegalArgumentException iae) {
-            // expected
-        }
+        new Mapping("srv:=user");
     }
 
     @Test
@@ -91,41 +65,115 @@ public class MappingTest {
 
     @Test
     public void test_constructor_and_map() {
-        assertMapping("service", null, "user");
-        assertMapping("service", "subServiceName", "user");
+        assertMapping("service", null, "user", (String[]) null);
+        assertMapping("service", "subServiceName", "user", (String[]) null);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void test_constructor_and_null_principals() {
+        assertMapping("service", "subServiceName", null, (String[]) null);
+    }
+
+    @Test
+    public void test_constructor_and_map_empty_principals() {
+        assertMapping("service", "subServiceName", null);
+    }
+
+    @Test
+    public void test_constructor_and_map_with_empty_principal() {
+        assertMapping("service", "subServiceName", null, "principal", "", "principal1");
     }
 
-    private void assertMapping(final String serviceName, final String subServiceName, final String userName) {
+    @Test
+    public void test_constructor_and_map_with_null_principal() {
+        assertMapping("service", "subServiceName", null, "principal", null, "principal1");
+    }
+
+    @Test
+    public void test_constructor_and_map_single_principal() {
+        assertMapping("service", "subServiceName", null, "principal");
+    }
+
+    @Test
+    public void test_constructor_and_map_duplicate_principals() {
+        assertMapping("service", "subServiceName", null, "principal", "principal");
+    }
+
+    @Test
+    public void test_constructor_and_map_principals() {
+        assertMapping("service", "subServiceName", null, "principal1", "principal2", "principal3");
+    }
+
+    @Test
+    public void test_constructor_and_map_user_and_principals() {
+        assertMapping("service", "subServiceName", "user", "principal1", "principal2", "principal3");
+    }
+
+    private void assertMapping(final String serviceName, final String subServiceName, final String userName, final String... principalNames) {
         StringBuilder spec = new StringBuilder();
         spec.append(serviceName);
         if (subServiceName != null) {
             spec.append(':').append(subServiceName);
         }
-        spec.append('=').append(userName);
+        spec.append('=');
+
+        String expectedUserName = null;
+        Set<String> expectedPrincipalsNames = null;
+        if (principalNames != null) {
+            spec.append(Arrays.toString(principalNames));
+            expectedPrincipalsNames = Mapping.extractPrincipalNames(Arrays.toString(principalNames));
+        } else if (userName != null) {
+            spec.append(userName);
+            expectedUserName = userName;
+        }
 
         // spec analysis
         final Mapping mapping = new Mapping(spec.toString());
-        TestCase.assertEquals(getField(mapping, "serviceName"), serviceName);
-        TestCase.assertEquals(getField(mapping, "subServiceName"), subServiceName);
-        TestCase.assertEquals(getField(mapping, "userName"), userName);
+        TestCase.assertEquals(serviceName, getField(mapping, "serviceName"));
+        TestCase.assertEquals(subServiceName, getField(mapping, "subServiceName"));
+        if (expectedUserName == null) {
+            TestCase.assertNull(getField(mapping, "userName"));
+        } else {
+            TestCase.assertEquals(expectedUserName, getField(mapping, "userName"));
+        }
+        if (principalNames == null) {
+            TestCase.assertNull(getSetField(mapping, "principalNames"));
+        } else {
+            TestCase.assertEquals(expectedPrincipalsNames, getSetField(mapping, "principalNames"));
+        }
 
         // mapping
-        TestCase.assertEquals(userName, mapping.map(serviceName, subServiceName));
+        if (expectedUserName == null) {
+            TestCase.assertNull(mapping.map(serviceName, subServiceName));
+        } else {
+            TestCase.assertEquals(userName, mapping.map(serviceName, subServiceName));
+        }
+
+        if (expectedPrincipalsNames == null) {
+            TestCase.assertNull(mapping.mapPrincipals(serviceName, subServiceName));
+        } else {
+            TestCase.assertEquals(expectedPrincipalsNames, mapping.mapPrincipals(serviceName, subServiceName));
+        }
+
         if (subServiceName == null) {
             // Mapping without subServiceName must not match request with any
             // subServiceName
             TestCase.assertNull(mapping.map(serviceName, subServiceName + "-garbage"));
+            TestCase.assertNull(mapping.mapPrincipals(serviceName, subServiceName + "-garbage"));
         } else {
             // Mapping with subServiceName must not match request without
             // subServiceName
             TestCase.assertNull(mapping.map(serviceName, null));
+            TestCase.assertNull(mapping.mapPrincipals(serviceName, null));
         }
 
         // no match for different service name
         TestCase.assertNull(mapping.map(serviceName + "-garbage", subServiceName));
+        TestCase.assertNull(mapping.mapPrincipals(serviceName + "-garbage", subServiceName));
 
         // no match for null service name
         TestCase.assertNull(mapping.map(null, subServiceName));
+        TestCase.assertNull(mapping.mapPrincipals(null, subServiceName));
     }
 
     private String getField(final Object object, final String fieldName) {
@@ -138,4 +186,15 @@ public class MappingTest {
             return null; // will not get here, quiesce compiler
         }
     }
+
+    private Set<String> getSetField(final Object object, final String fieldName) {
+        try {
+            Field f = object.getClass().getDeclaredField(fieldName);
+            f.setAccessible(true);
+            return (Set<String>) f.get(object);
+        } catch (Exception e) {
+            TestCase.fail("Cannot get field " + fieldName + ": " + e.toString());
+            return null; // will not get here, quiesce compiler
+        }
+    }
 }
diff --git a/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImplTest.java b/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImplTest.java
index 8d0b719..41da0f6 100644
--- a/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImplTest.java
+++ b/src/test/java/org/apache/sling/serviceusermapping/impl/ServiceUserMapperImplTest.java
@@ -18,14 +18,22 @@
  */
 package org.apache.sling.serviceusermapping.impl;
 
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNull;
+import static org.junit.Assert.assertEquals;
 import static org.mockito.Matchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
+import java.util.Arrays;
 import java.util.Dictionary;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
 
+import org.apache.sling.serviceusermapping.ServicePrincipalsValidator;
 import org.apache.sling.serviceusermapping.ServiceUserValidator;
 import org.junit.Test;
 import org.mockito.invocation.InvocationOnMock;
@@ -44,6 +52,10 @@ public class ServiceUserMapperImplTest {
 
     private static final String BUNDLE_SYMBOLIC3 = "bundle3";
 
+    private static final String BUNDLE_SYMBOLIC4 = "bundle4";
+
+    private static final String BUNDLE_SYMBOLIC5 = "bundle5";
+
     private static final String SUB = "sub";
 
     private static final String NONE = "none";
@@ -62,6 +74,10 @@ public class ServiceUserMapperImplTest {
 
     private static final Bundle BUNDLE3;
 
+    private static final Bundle BUNDLE4;
+
+    private static final Bundle BUNDLE5;
+
     static {
         BUNDLE1 = mock(Bundle.class);
         when(BUNDLE1.getSymbolicName()).thenReturn(BUNDLE_SYMBOLIC1);
@@ -71,6 +87,12 @@ public class ServiceUserMapperImplTest {
 
         BUNDLE3 = mock(Bundle.class);
         when(BUNDLE3.getSymbolicName()).thenReturn(BUNDLE_SYMBOLIC3);
+
+        BUNDLE4 = mock(Bundle.class);
+        when(BUNDLE4.getSymbolicName()).thenReturn(BUNDLE_SYMBOLIC4);
+
+        BUNDLE5 = mock(Bundle.class);
+        when(BUNDLE5.getSymbolicName()).thenReturn(BUNDLE_SYMBOLIC5);
     }
 
     @Test
@@ -184,6 +206,125 @@ public class ServiceUserMapperImplTest {
     }
 
     @Test
+    public void test_getServicePrincipalNames() {
+        ServiceUserMapperImpl.Config config = mock(ServiceUserMapperImpl.Config.class);
+        when(config.user_mapping()).thenReturn(new String[] {
+                BUNDLE_SYMBOLIC1 + "=[" + SAMPLE + "]", //
+                BUNDLE_SYMBOLIC2 + "=[ " + ANOTHER + " ]", //
+                BUNDLE_SYMBOLIC3 + "=[" + SAMPLE + "," + ANOTHER + "]", //
+                BUNDLE_SYMBOLIC4 + "=[ " + SAMPLE + ", " + ANOTHER + " ]", //
+                BUNDLE_SYMBOLIC5 + "=[]", //
+                BUNDLE_SYMBOLIC1 + ":" + SUB + "=[" + SAMPLE_SUB + "]", //
+                BUNDLE_SYMBOLIC2 + ":" + SUB + "=[" + SAMPLE_SUB + "," + ANOTHER_SUB + "]" //
+        });
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE1, null), SAMPLE);
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE2, null), ANOTHER);
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE3, null), SAMPLE, ANOTHER);
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE4, null), SAMPLE, ANOTHER);
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE5, null));
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE1, SUB), SAMPLE_SUB);
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE2, SUB), SAMPLE_SUB, ANOTHER_SUB);
+    }
+
+    @Test
+    public void test_getServicePrincipalNames_EmptySubService() {
+        ServiceUserMapperImpl.Config config = mock(ServiceUserMapperImpl.Config.class);
+        when(config.user_mapping()).thenReturn(new String[] {
+                BUNDLE_SYMBOLIC1 + "=[" + SAMPLE + "]", //
+                BUNDLE_SYMBOLIC2 + "=[ " + ANOTHER + " ]", //
+        });
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE1, ""), SAMPLE);
+        assertEqualPrincipalNames(sum.getServicePrincipalNames(BUNDLE2, ""), ANOTHER);
+    }
+
+    @Test
+    public void test_getServicePrincipalNames_WithUserNameConfig() {
+        ServiceUserMapperImpl.Config config = mock(ServiceUserMapperImpl.Config.class);
+        when(config.user_mapping()).thenReturn(new String[] {
+                BUNDLE_SYMBOLIC1 + "=" + SAMPLE, //
+                BUNDLE_SYMBOLIC1 + ":" + SUB + "=" + SAMPLE_SUB, //
+        });
+        when(config.user_default()).thenReturn(NONE);
+        when(config.user_enable_default_mapping()).thenReturn(false);
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+
+        assertNull(sum.getServicePrincipalNames(BUNDLE1, null));
+        assertNull(SAMPLE_SUB, sum.getServicePrincipalNames(BUNDLE1, SUB));
+    }
+
+    @Test
+    public void test_getServicePrincipalNames_IgnoresDefaultUser() {
+        ServiceUserMapperImpl.Config config = mock(ServiceUserMapperImpl.Config.class);
+        when(config.user_default()).thenReturn(NONE);
+        when(config.user_enable_default_mapping()).thenReturn(true);
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+
+        assertNull(sum.getServicePrincipalNames(BUNDLE1, null));
+        assertNull(sum.getServicePrincipalNames(BUNDLE1, SUB));
+    }
+
+    @Test
+    public void test_getServicePrincipalnames_WithServicePrincipalsValidator() {
+        ServiceUserMapperImpl.Config config = mock(ServiceUserMapperImpl.Config.class);
+        when(config.user_mapping()).thenReturn(new String[] {
+                BUNDLE_SYMBOLIC1 + "=[" + SAMPLE + "]", //
+                BUNDLE_SYMBOLIC2 + "=[" + SAMPLE + "," + ANOTHER + "]", //
+                BUNDLE_SYMBOLIC1 + ":" + SUB + "=[" + SAMPLE + "," + SAMPLE_SUB + "]", //
+                BUNDLE_SYMBOLIC2 + ":" + SUB + "=[" + ANOTHER_SUB + "," + SAMPLE_SUB + "," + SAMPLE + "]"//
+        });
+
+        final ServiceUserMapperImpl sum = new ServiceUserMapperImpl();
+        sum.configure(null, config);
+        ServicePrincipalsValidator validator = new ServicePrincipalsValidator() {
+            @Override
+            public boolean isValid(Iterable<String> servicePrincipalNames, String serviceName, String subServiceName) {
+                for (String pName : servicePrincipalNames) {
+                    if (SAMPLE.equals(pName)) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+        };
+        sum.bindServicePrincipalsValidator(validator);
+
+        assertNull(sum.getServicePrincipalNames(BUNDLE1, null));
+        assertNull(sum.getServicePrincipalNames(BUNDLE2, null));
+        assertNull(sum.getServicePrincipalNames(BUNDLE1, SUB));
+        assertNull(sum.getServicePrincipalNames(BUNDLE2, SUB));
+    }
+
+    private static void assertEqualPrincipalNames(Iterable<String> result, String... expected) {
+        if (expected == null) {
+            assertNull(result);
+        } else if (expected.length == 0) {
+            assertFalse(result.iterator().hasNext());
+        } else {
+            Set<String> resultSet = new HashSet<>();
+            Iterator<String> it = result.iterator();
+            while (it.hasNext()) {
+                resultSet.add(it.next());
+            }
+            Set<String> expectedSet = new HashSet<>();
+            expectedSet.addAll(Arrays.asList(expected));
+            assertEquals(expectedSet, resultSet);
+        }
+    }
+
+
+    @Test
     public void test_amendment() {
         ServiceUserMapperImpl.Config config = mock(ServiceUserMapperImpl.Config.class);
         when(config.user_mapping()).thenReturn(new String[] {

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.