You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by tr...@apache.org on 2014/02/10 23:57:59 UTC

svn commit: r1566806 - in /jackrabbit/oak/trunk: oak-auth-ldap/ oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/ oak-core/src/main/java/org/apache/jackrabbit/oak/security/ oak-core/src/main/java/org/apache/jackra...

Author: tripod
Date: Mon Feb 10 22:57:58 2014
New Revision: 1566806

URL: http://svn.apache.org/r1566806
Log:
OAK-516 Create LdapLoginModule based on ExternalLoginModule (wip)

Added:
    jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/PoolableUnboundConnectionFactory.java
    jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/UnboundLdapConnectionPool.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncManagerImpl.java
Removed:
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/DefaultSyncHandler.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncConfig.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncMode.java
Modified:
    jackrabbit/oak/trunk/oak-auth-ldap/pom.xml
    jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java
    jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProvider.java
    jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapProviderConfig.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/SecurityProviderImpl.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentity.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncHandler.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
    jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleFactory.java
    jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestLoginModule.java

Modified: jackrabbit/oak/trunk/oak-auth-ldap/pom.xml
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-ldap/pom.xml?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-auth-ldap/pom.xml (original)
+++ jackrabbit/oak/trunk/oak-auth-ldap/pom.xml Mon Feb 10 22:57:58 2014
@@ -42,12 +42,11 @@
                         <Import-Package>
                             !antlr.*,
                             !org.dom4j.*,
-                            !org.apache.mina.*,
                             !org.xmlpull.v1,
                             *
                         </Import-Package>
                         <Embed-Dependency>
-                            api-all,commons-pool,commons-lang
+                            api-all,commons-pool,commons-lang,mina-core
                         </Embed-Dependency>
                     </instructions>
                 </configuration>
@@ -90,6 +89,13 @@
             <version>2.6</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.mina</groupId>
+            <artifactId>mina-core</artifactId>
+            <version>2.0.7</version>
+            <scope>provided</scope>
+        </dependency>
+
 
         <!-- Optional OSGi dependencies, used only when running within OSGi -->
         <dependency>

Modified: jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java (original)
+++ jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentity.java Mon Feb 10 22:57:58 2014
@@ -16,7 +16,7 @@
  */
 package org.apache.jackrabbit.oak.security.authentication.ldap.impl;
 
-import java.security.Principal;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -25,7 +25,6 @@ import javax.annotation.Nonnull;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalGroup;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
-import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
 
 /**
  * Implements an identity that is provided by the {@link LdapIdentityProvider}.
@@ -65,6 +64,16 @@ public abstract class LdapIdentity imple
     }
 
     /**
+     * Returns the DN as principal name.
+     * @return the DN
+     */
+    @Nonnull
+    @Override
+    public String getPrincipalName() {
+        return ref.getId();
+    }
+
+    /**
      * {@inheritDoc}
      */
     @Override
@@ -78,7 +87,7 @@ public abstract class LdapIdentity imple
     @Nonnull
     @Override
     public Iterable<? extends ExternalGroup> getGroups() {
-        return null;
+        return Collections.emptyList();
     }
 
     /**
@@ -90,13 +99,12 @@ public abstract class LdapIdentity imple
         return properties;
     }
 
-    private Principal principal;
     @Override
-    public Principal getPrincipal() {
-        if (principal == null) {
-            principal = new PrincipalImpl(getId());
-        }
-        return principal;
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("LdapIdentity{");
+        sb.append("ref=").append(ref);
+        sb.append(", id='").append(id).append('\'');
+        sb.append('}');
+        return sb.toString();
     }
-
 }

Modified: jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProvider.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProvider.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProvider.java (original)
+++ jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapIdentityProvider.java Mon Feb 10 22:57:58 2014
@@ -16,17 +16,20 @@
  */
 package org.apache.jackrabbit.oak.security.authentication.ldap.impl;
 
-import java.io.IOException;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
 import javax.jcr.Credentials;
+import javax.jcr.SimpleCredentials;
 import javax.security.auth.login.LoginException;
 
+import org.apache.commons.pool.impl.GenericObjectPool;
+import org.apache.directory.api.ldap.model.constants.SchemaConstants;
 import org.apache.directory.api.ldap.model.cursor.CursorException;
 import org.apache.directory.api.ldap.model.cursor.SearchCursor;
 import org.apache.directory.api.ldap.model.entry.Attribute;
 import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
 import org.apache.directory.api.ldap.model.exception.LdapException;
 import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
 import org.apache.directory.api.ldap.model.message.Response;
@@ -36,10 +39,13 @@ import org.apache.directory.api.ldap.mod
 import org.apache.directory.api.ldap.model.message.SearchScope;
 import org.apache.directory.api.ldap.model.name.Dn;
 import org.apache.directory.ldap.client.api.LdapConnection;
-import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapConnectionPool;
+import org.apache.directory.ldap.client.api.PoolableLdapConnectionFactory;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.ConfigurationPolicy;
+import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Service;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalGroup;
@@ -75,6 +81,16 @@ public class LdapIdentityProvider implem
     private LdapProviderConfig config;
 
     /**
+     * the connection pool with connections authenticated with the bind DN
+     */
+    private LdapConnectionPool adminPool;
+
+    /**
+     * the connection pool with unbound connections
+     */
+    private UnboundLdapConnectionPool userPool;
+
+    /**
      * Default constructor for OSGi
      */
     public LdapIdentityProvider() {
@@ -86,14 +102,77 @@ public class LdapIdentityProvider implem
      */
     public LdapIdentityProvider(LdapProviderConfig config) {
         this.config = config;
+        init();
     }
 
     @Activate
     private void activate(Map<String, Object> properties) {
         ConfigurationParameters cfg = ConfigurationParameters.of(properties);
         config = LdapProviderConfig.of(cfg);
+        init();
+    }
+
+    @Deactivate
+    private void deactivate() {
+        close();
+    }
+
+    /**
+     * Initializes the ldap identity provider.
+     */
+    private void init() {
+        if (adminPool != null) {
+            throw new IllegalStateException("Provider already initialized.");
+        }
+
+        // setup admin connection pool
+        LdapConnectionConfig cc = new LdapConnectionConfig();
+        cc.setLdapHost(config.getHostname());
+        cc.setLdapPort(config.getPort());
+        if (!config.getBindDN().isEmpty()) {
+            cc.setName(config.getBindDN());
+            cc.setCredentials(config.getBindPassword());
+        }
+        cc.setUseSsl(config.useSSL());
+        PoolableLdapConnectionFactory factory = new PoolableLdapConnectionFactory(cc);
+        adminPool = new LdapConnectionPool(factory);
+        adminPool.setTestOnBorrow(true);
+        adminPool.setWhenExhaustedAction(GenericObjectPool.WHEN_EXHAUSTED_GROW);
+
+        // setup unbound connection pool. let's create a new version of the config
+        cc = new LdapConnectionConfig();
+        cc.setLdapHost(config.getHostname());
+        cc.setLdapPort(config.getPort());
+        cc.setUseSsl(config.useSSL());
+        userPool = new UnboundLdapConnectionPool(new PoolableUnboundConnectionFactory(cc));
+        userPool.setTestOnBorrow(true);
+        userPool.setWhenExhaustedAction(GenericObjectPool.WHEN_EXHAUSTED_GROW);
+    }
+
+    /**
+     * Closes this provider and releases the internal pool. This should be called by Non-OSGi users of this provider.
+     */
+    public void close() {
+        if (adminPool != null) {
+            try {
+                adminPool.close();
+            } catch (Exception e) {
+                log.warn("Error while closing LDAP connection pool", e);
+            }
+            adminPool = null;
+        }
+        if (userPool != null) {
+            try {
+                userPool.close();
+            } catch (Exception e) {
+                log.warn("Error while closing LDAP connection pool", e);
+            }
+            userPool = null;
+        }
     }
 
+
+
     @Nonnull
     @Override
     public String getName() {
@@ -127,9 +206,15 @@ public class LdapIdentityProvider implem
 
     @Override
     public ExternalUser getUser(@Nonnull String userId) throws ExternalIdentityException {
+        long t0 = System.nanoTime();
         LdapConnection connection = connect();
+        long t1 = System.nanoTime();
         try {
             Entry entry = getEntry(connection, config.getUserConfig(), userId);
+            long t2 = System.nanoTime();
+            if (log.isDebugEnabled()) {
+                log.debug("getUser({}) connect: {}us, lookup: {}us", new Object[]{userId, (t1-t0)/1000, (t2-t1)/1000});
+            }
             if (entry != null) {
                 return createUser(entry, userId);
             } else {
@@ -174,13 +259,11 @@ public class LdapIdentityProvider implem
         // Create the SearchRequest object
         SearchRequest req = new SearchRequestImpl();
         req.setScope(SearchScope.SUBTREE);
-        req.addAttributes("*");
+        req.addAttributes(SchemaConstants.ALL_USER_ATTRIBUTES);
         req.setTimeLimit(config.getSearchTimeout());
         req.setBase(new Dn(idConfig.getBaseDN()));
         req.setFilter(searchFilter);
 
-        log.debug("Searching entries below {} with {}", idConfig.getBaseDN(), searchFilter);
-
         // Process the request
         SearchCursor searchCursor = connection.search(req);
         while (searchCursor.next()) {
@@ -192,9 +275,16 @@ public class LdapIdentityProvider implem
                 if (searchCursor.next()) {
                     log.warn("search for {} returned more than one entry. discarding additional ones.", searchFilter);
                 }
+                if (log.isDebugEnabled()) {
+                    log.debug("search below {} with {} found {}",
+                            new Object[]{idConfig.getBaseDN(), searchFilter, resultEntry.getDn()});
+                }
                 return resultEntry;
             }
         }
+        if (log.isDebugEnabled()) {
+            log.debug("search below {} with {} found 0 entries.", idConfig.getBaseDN(), searchFilter);
+        }
         return null;
     }
 
@@ -233,14 +323,8 @@ public class LdapIdentityProvider implem
 
     private LdapConnection connect() throws ExternalIdentityException {
         try {
-            LdapConnection connection = new LdapNetworkConnection(config.getHostname(), config.getPort(), config.useSSL());
-            if (config.getBindDN().length() > 0) {
-                connection.bind(config.getBindDN(), config.getBindPassword());
-            } else {
-                connection.bind();
-            }
-            return connection;
-        } catch (LdapException e) {
+            return adminPool.getConnection();
+        } catch (Throwable e) {
             log.error("Error while connecting to the ldap server.", e);
             throw new ExternalIdentityException("Error while connecting and binding to the ldap server", e);
         }
@@ -248,20 +332,47 @@ public class LdapIdentityProvider implem
 
     private void disconnect(LdapConnection connection) throws ExternalIdentityException {
         try {
-            connection.unBind();
-        } catch (LdapException e) {
-            log.error("Error while unbinding from the ldap server.", e);
-        }
-        try {
-            connection.close();
-        } catch (IOException e) {
-            log.error("Error while disconnecting from the ldap server.", e);
+            adminPool.releaseConnection(connection);
+        } catch (Exception e) {
+            log.warn("Error while disconnecting from the ldap server.", e);
         }
     }
 
     @Override
     public ExternalUser authenticate(@Nonnull Credentials credentials) throws ExternalIdentityException, LoginException {
-        return null;
+        if (!(credentials instanceof SimpleCredentials)) {
+            log.debug("LDAP IDP can only authenticate SimpleCredentials.");
+            return null;
+        }
+        final SimpleCredentials creds = (SimpleCredentials) credentials;
+        final ExternalUser user = getUser(creds.getUserID());
+        if (user != null) {
+            // authenticate
+            LdapConnection connection = null;
+            try {
+                long t0 = System.nanoTime();
+                connection = userPool.getConnection();
+                long t1 = System.nanoTime();
+                connection.bind(user.getExternalId().getId(), new String(creds.getPassword()));
+                long t2 = System.nanoTime();
+                if (log.isDebugEnabled()) {
+                    log.debug("authenticate({}) connect: {}us, bind: {}us", new Object[]{user.getId(), (t1-t0)/1000, (t2-t1)/1000});
+                }
+            } catch (LdapAuthenticationException e) {
+                throw new LoginException("Unable to authenticate against LDAP server: " + e.getMessage());
+            } catch (Exception e) {
+                throw new ExternalIdentityException("Error while binding user credentials", e);
+            } finally {
+                if (connection != null) {
+                    try {
+                        userPool.releaseConnection(connection);
+                    } catch (Exception e) {
+                        // ignore
+                    }
+                }
+            }
+        }
+        return user;
     }
 
     private boolean isMyRef(ExternalIdentityRef ref) {

Modified: jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapProviderConfig.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapProviderConfig.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapProviderConfig.java (original)
+++ jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/LdapProviderConfig.java Mon Feb 10 22:57:58 2014
@@ -204,6 +204,21 @@ public class LdapProviderConfig {
     public static final String PARAM_USER_EXTRA_FILTER = "user.extraFilter";
 
     /**
+     * @see Identity#makeDnPath()
+     */
+    public static final boolean PARAM_USER_MAKE_DN_PATH_DEFAULT = false;
+
+    /**
+     * @see Identity#makeDnPath()
+     */
+    @Property(
+            label = "User DN paths",
+            description = "Controls if the DN should be used for calculating a portion of the intermediate path.",
+            boolValue = PARAM_USER_MAKE_DN_PATH_DEFAULT
+    )
+    public static final String PARAM_USER_MAKE_DN_PATH = "user.makeDnPath";
+
+    /**
      * @see Identity#getBaseDN()
      */
     public static final String PARAM_GROUP_BASE_DN_DEFAULT = "ou=groups,o=example,dc=com";
@@ -266,6 +281,21 @@ public class LdapProviderConfig {
     public static final String PARAM_GROUP_EXTRA_FILTER = "group.extraFilter";
 
     /**
+     * @see Identity#makeDnPath()
+     */
+    public static final boolean PARAM_GROUP_MAKE_DN_PATH_DEFAULT = false;
+
+    /**
+     * @see Identity#makeDnPath()
+     */
+    @Property(
+            label = "Group DN paths",
+            description = "Controls if the DN should be used for calculating a portion of the intermediate path.",
+            boolValue = PARAM_GROUP_MAKE_DN_PATH_DEFAULT
+    )
+    public static final String PARAM_GROUP_MAKE_DN_PATH = "group.makeDnPath";
+
+    /**
      * @see #getGroupMemberAttribute()
      */
     public static final String PARAM_GROUP_MEMBER_ATTRIBUTE_DEFAULT = "uniquemember";
@@ -295,6 +325,8 @@ public class LdapProviderConfig {
 
         private String filterTemplate;
 
+        private boolean makeDnPath;
+
         /**
          * Configures the base DN for searches of this kind of identity
          * @return the base DN
@@ -388,6 +420,27 @@ public class LdapProviderConfig {
             return this;
         }
 
+
+        /**
+         * Configures if the identities DN should be used to generate a portion of the authorizables intermediate path.
+         * @return {@code true} if the DN is used a intermediate path.
+         */
+        public boolean makeDnPath() {
+            return makeDnPath;
+        }
+
+        /**
+         * Sets the intermediate path flag.
+         * @param makeDnPath {@code true} to use the DN as intermediate path
+         * @return {@code this}
+         * @see #makeDnPath()
+         */
+        @Nonnull
+        public Identity setMakeDnPath(boolean makeDnPath) {
+            this.makeDnPath = makeDnPath;
+            return this;
+        }
+
         /**
          * Returns the LDAP filter that is used when searching this type of identity. The filter is based on the
          * configuration and has the following format:
@@ -491,13 +544,15 @@ public class LdapProviderConfig {
                 .setBaseDN(params.getConfigValue(PARAM_USER_BASE_DN, PARAM_USER_BASE_DN))
                 .setIdAttribute(params.getConfigValue(PARAM_USER_ID_ATTRIBUTE, PARAM_USER_ID_ATTRIBUTE_DEFAULT))
                 .setExtraFilter(params.getConfigValue(PARAM_USER_EXTRA_FILTER, PARAM_USER_EXTRA_FILTER_DEFAULT))
-                .setObjectClasses(params.getConfigValue(PARAM_USER_OBJECTCLASS, PARAM_USER_OBJECTCLASS_DEFAULT));
+                .setObjectClasses(params.getConfigValue(PARAM_USER_OBJECTCLASS, PARAM_USER_OBJECTCLASS_DEFAULT))
+                .setMakeDnPath(params.getConfigValue(PARAM_USER_MAKE_DN_PATH, PARAM_USER_MAKE_DN_PATH_DEFAULT));
 
         cfg.getGroupConfig()
                 .setBaseDN(params.getConfigValue(PARAM_GROUP_BASE_DN, PARAM_GROUP_BASE_DN))
                 .setIdAttribute(params.getConfigValue(PARAM_GROUP_NAME_ATTRIBUTE, PARAM_GROUP_NAME_ATTRIBUTE_DEFAULT))
                 .setExtraFilter(params.getConfigValue(PARAM_GROUP_EXTRA_FILTER, PARAM_GROUP_EXTRA_FILTER_DEFAULT))
-                .setObjectClasses(params.getConfigValue(PARAM_GROUP_OBJECTCLASS, PARAM_GROUP_OBJECTCLASS_DEFAULT));
+                .setObjectClasses(params.getConfigValue(PARAM_GROUP_OBJECTCLASS, PARAM_GROUP_OBJECTCLASS_DEFAULT))
+                .setMakeDnPath(params.getConfigValue(PARAM_GROUP_MAKE_DN_PATH, PARAM_GROUP_MAKE_DN_PATH_DEFAULT));
 
         return cfg;
     }

Added: jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/PoolableUnboundConnectionFactory.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/PoolableUnboundConnectionFactory.java?rev=1566806&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/PoolableUnboundConnectionFactory.java (added)
+++ jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/PoolableUnboundConnectionFactory.java Mon Feb 10 22:57:58 2014
@@ -0,0 +1,97 @@
+/*
+ * 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.jackrabbit.oak.security.authentication.ldap.impl;
+
+
+import org.apache.commons.pool.PoolableObjectFactory;
+import org.apache.directory.ldap.client.api.LdapConnection;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * A factory for creating unbound LdapConnection objects managed by LdapConnectionPool.
+ */
+public class PoolableUnboundConnectionFactory implements PoolableObjectFactory<LdapConnection> {
+
+    /**
+     * the logger
+     */
+    private static final Logger log = LoggerFactory.getLogger(PoolableUnboundConnectionFactory.class);
+
+    /**
+     * configuration object for the connection
+     */
+    private LdapConnectionConfig config;
+
+    /**
+     * Creates a new instance of PoolableUnboundConnectionFactory
+     *
+     * @param config the configuration for creating LdapConnections
+     */
+    public PoolableUnboundConnectionFactory(LdapConnectionConfig config) {
+        this.config = config;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public void activateObject(LdapConnection connection) throws Exception {
+        log.debug("Activating {}", connection);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public void destroyObject(LdapConnection connection) throws Exception {
+        log.debug("Destroying {}", connection);
+        connection.close();
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public LdapConnection makeObject() throws Exception {
+        log.debug("Creating a LDAP connection");
+        LdapNetworkConnection connection = new LdapNetworkConnection(config);
+        connection.connect();
+        return connection;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public void passivateObject(LdapConnection connection) throws Exception {
+        log.debug("Passivating {}", connection);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean validateObject(LdapConnection connection) {
+        log.debug("Validating {}", connection);
+        return connection.isConnected();
+    }
+}

Added: jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/UnboundLdapConnectionPool.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/UnboundLdapConnectionPool.java?rev=1566806&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/UnboundLdapConnectionPool.java (added)
+++ jackrabbit/oak/trunk/oak-auth-ldap/src/main/java/org/apache/jackrabbit/oak/security/authentication/ldap/impl/UnboundLdapConnectionPool.java Mon Feb 10 22:57:58 2014
@@ -0,0 +1,59 @@
+/*
+ * 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.jackrabbit.oak.security.authentication.ldap.impl;
+
+import org.apache.commons.pool.impl.GenericObjectPool;
+import org.apache.directory.ldap.client.api.LdapConnection;
+
+/**
+ * A pool implementation for LdapConnection objects.
+ * <p/>
+ * This class is just a wrapper around the commons GenericObjectPool, and has
+ * a more meaningful name to represent the pool type.
+ */
+public class UnboundLdapConnectionPool extends GenericObjectPool<LdapConnection> {
+    /**
+     * Instantiates a new LDAP connection pool.
+     *
+     * @param factory the LDAP connection factory
+     */
+    public UnboundLdapConnectionPool(PoolableUnboundConnectionFactory factory) {
+        super(factory);
+    }
+
+
+    /**
+     * Gives a Unbound LdapConnection fetched from the pool.
+     *
+     * @return an LdapConnection object from pool
+     * @throws Exception if an error occurs while obtaining a connection from the factory
+     */
+    public LdapConnection getConnection() throws Exception {
+        return super.borrowObject();
+    }
+
+
+    /**
+     * Places the given LdapConnection back in the pool.
+     *
+     * @param connection the LdapConnection to be released
+     * @throws Exception if an error occurs while releasing the connection
+     */
+    public void releaseConnection(LdapConnection connection) throws Exception {
+        super.returnObject(connection);
+    }
+}

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/SecurityProviderImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/SecurityProviderImpl.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/SecurityProviderImpl.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/SecurityProviderImpl.java Mon Feb 10 22:57:58 2014
@@ -43,8 +43,10 @@ import org.apache.jackrabbit.oak.spi.sec
 import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration;
 import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
 import org.apache.jackrabbit.oak.spi.security.authentication.AuthenticationConfiguration;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncManager;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIDPManagerImpl;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProviderManager;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.SyncManagerImpl;
 import org.apache.jackrabbit.oak.spi.security.authentication.token.CompositeTokenConfiguration;
 import org.apache.jackrabbit.oak.spi.security.authentication.token.TokenConfiguration;
 import org.apache.jackrabbit.oak.spi.security.authorization.AuthorizationConfiguration;
@@ -115,6 +117,9 @@ public class SecurityProviderImpl implem
     @Reference
     private ExternalIdentityProviderManager identityProviderManager;
 
+    @Reference
+    private SyncManager syncManager;
+
     /**
      * Default constructor used in OSGi environments.
      */
@@ -136,6 +141,7 @@ public class SecurityProviderImpl implem
         privilegeConfiguration = new PrivilegeConfigurationImpl();
         tokenConfiguration = new TokenConfigurationImpl(this);
         identityProviderManager = new ExternalIDPManagerImpl();
+        syncManager = new SyncManagerImpl();
     }
 
     @Nonnull
@@ -184,6 +190,8 @@ public class SecurityProviderImpl implem
             return (T) tokenConfiguration;
         } else if (ExternalIdentityProviderManager.class == configClass) {
             return (T) identityProviderManager;
+        } else if (SyncManager.class == configClass) {
+            return (T) syncManager;
         } else {
             throw new IllegalArgumentException("Unsupported security configuration class " + configClass);
         }

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentity.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentity.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentity.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentity.java Mon Feb 10 22:57:58 2014
@@ -44,6 +44,14 @@ public interface ExternalIdentity {
     String getId();
 
     /**
+     * Returns the principal name of this identity. This usually corresponds to
+     * {@link org.apache.jackrabbit.api.security.user.Authorizable#getPrincipal()}.{@link java.security.Principal#getName()}
+     * @return the principal name
+     */
+    @Nonnull
+    String getPrincipalName();
+
+    /**
      * Returns the desired intermediate relative path of the authorizable to be created. For example, one could map
      * an external hierarchy into the local users and groups hierarchy.
      *
@@ -66,7 +74,4 @@ public interface ExternalIdentity {
     @Nonnull
     Map<String, ?> getProperties();
 
-    // todo: really?
-    Principal getPrincipal();
-
 }
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalIdentityRef.java Mon Feb 10 22:57:58 2014
@@ -92,4 +92,13 @@ public class ExternalIdentityRef {
             );
         }
     }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("ExternalIdentityRef{");
+        sb.append("id='").append(id).append('\'');
+        sb.append(", providerName='").append(providerName).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
 }
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java?rev=1566806&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncContext.java Mon Feb 10 22:57:58 2014
@@ -0,0 +1,42 @@
+/*
+ * 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.jackrabbit.oak.spi.security.authentication.external;
+
+import javax.annotation.Nonnull;
+
+/**
+ * {@code SyncContext} is used as scope for sync operations. Implementations are free to associate any resources with
+ * the sync context. The sync context must not be accessed concurrently and must be closed after use.
+ */
+public interface SyncContext {
+
+    /**
+     * Synchronizes an external identity with the repository based on the respective configuration.
+     *
+     * @param identity the identity to sync.
+     * @return {@code true} if the given identity was synced; {@code false} if for no change.
+     * @throws SyncException if an error occurrs
+     */
+    boolean sync(@Nonnull ExternalIdentity identity) throws SyncException;
+
+    /**
+     * Closes this context and releases any resources bound to it. Note that an implementation must not commit the
+     * {@link org.apache.jackrabbit.oak.api.Root} passed during the creation call. This is the responsibility of the
+     * application.
+     */
+    void close();
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncHandler.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncHandler.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/SyncHandler.java Mon Feb 10 22:57:58 2014
@@ -20,21 +20,39 @@ import javax.annotation.Nonnull;
 
 import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.oak.api.Root;
-import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 
 /**
- * SyncHandler is used to sync users and groups from an external source an {@link ExternalUserProvider}.
- * One sync task always operates within a {@link SyncContext} and is associated with a {@link SyncConfig}.
+ * SyncHandler is used to sync users and groups from an {@link ExternalIdentityProvider}.
+ * The synchronization performed within the scope of a {@link SyncContext} which is acquired during the
+ * {@link #createContext(ExternalIdentityProvider, org.apache.jackrabbit.api.security.user.UserManager, org.apache.jackrabbit.oak.api.Root)} call.
  *
- * todo:
- * - cleanup expired authorizables ?
+ * The exact configuration is managed by the sync handler instance. The system may contain several sync handler
+ * implementations with different configurations. those are managed by the {@link SyncManager}.
  *
+ * @see org.apache.jackrabbit.oak.spi.security.authentication.external.SyncContext
+ * @see org.apache.jackrabbit.oak.spi.security.authentication.external.SyncManager
  */
 public interface SyncHandler {
 
-    boolean initialize(@Nonnull UserManager userManager, @Nonnull Root root,
-                       @Nonnull SyncMode mode,
-                       @Nonnull ConfigurationParameters options) throws SyncException;
+    /**
+     * Returns the name of this sync handler.
+     * @return sync handler name
+     */
+    @Nonnull
+    String getName();
+
+    /**
+     * Initializes a sync context which is used to start the sync operations.
+     *
+     * @param idp the external identity provider used for syncing
+     * @param userManager user manager for managing authorizables
+     * @param root root of the current tree
+     * @return the sync context
+     * @throws SyncException if an error occurs
+     */
+    @Nonnull
+    SyncContext createContext(@Nonnull ExternalIdentityProvider idp,
+                              @Nonnull UserManager userManager,
+                              @Nonnull Root root) throws SyncException;
 
-    boolean sync(@Nonnull ExternalUser externalUser) throws SyncException;
 }
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java?rev=1566806&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java Mon Feb 10 22:57:58 2014
@@ -0,0 +1,485 @@
+/*
+ * 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.jackrabbit.oak.spi.security.authentication.external.impl;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Property;
+import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
+
+/**
+ * {@code DefaultSyncConfig} defines how users and groups from an external source are synced into the repository using
+ * the {@link DefaultSyncHandler}.
+ */
+@Component(
+        label = "Apache Jackrabbit Oak Default Sync Handler",
+        name = "org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncHandler",
+        configurationFactory = true,
+        metatype = true,
+        ds = false
+)
+public class DefaultSyncConfig {
+
+    /**
+     * @see #getName()
+     */
+    public static final String PARAM_NAME_DEFAULT = "default";
+
+    /**
+     * @see #getName()
+     */
+    @Property(
+            label = "Sync Handler Name",
+            description = "Name of this sync configuration. This is used to reference this handler by the login modules.",
+            value = PARAM_NAME_DEFAULT
+    )
+    public static final String PARAM_NAME = "handler.name";
+
+    /**
+     * @see DefaultSyncConfig.User#getExpirationTime()
+     */
+    public static final long PARAM_USER_EXPIRATION_TIME_DEFAULT = 3600*1000;
+
+    /**
+     * @see DefaultSyncConfig.User#getExpirationTime()
+     */
+    @Property(
+            label = "User Expiration Time",
+            description = "Duration in milliseconds until a synced user gets expired.",
+            longValue = PARAM_USER_EXPIRATION_TIME_DEFAULT
+    )
+    public static final String PARAM_USER_EXPIRATION_TIME = "user.expirationTime";
+
+    /**
+     * @see DefaultSyncConfig.User#getAutoMembership()
+     */
+    public static final String[] PARAM_USER_AUTO_MEMBERSHIP_DEFAULT = {};
+
+    /**
+     * @see DefaultSyncConfig.User#getAutoMembership()
+     */
+    @Property(
+            label = "User auto membership",
+            description = "List of groups that a synced user is added to automatically",
+            value = {},
+            cardinality = Integer.MAX_VALUE
+    )
+    public static final String PARAM_USER_AUTO_MEMBERSHIP = "user.autoMembership";
+
+    /**
+     * @see DefaultSyncConfig.User#getPropertyMapping()
+     */
+    public static final String[] PARAM_USER_PROPERTY_MAPPING_DEFAULT = {"rep:fullname=cn"};
+
+    /**
+     * @see DefaultSyncConfig.User#getPropertyMapping()
+     */
+    @Property(
+            label = "User property mapping",
+            description = "List mapping definition of local properties from external ones. eg: 'profile/email=mail'." +
+                    "Use double quotes for fixed values. eg: 'profile/nt:primaryType=\"nt:unstructured\"",
+            value = {"rep:fullname=cn"},
+            cardinality = Integer.MAX_VALUE
+    )
+    public static final String PARAM_USER_PROPERTY_MAPPING = "user.propertyMapping";
+
+    /**
+     * @see DefaultSyncConfig.User#getPathPrefix()
+     */
+    public static final String PARAM_USER_PATH_PREFIX_DEFAULT = "";
+
+    /**
+     * @see DefaultSyncConfig.User#getPathPrefix()
+     */
+    @Property(
+            label = "User Path Prefix",
+            description = "The path prefix used when creating new users.",
+            value = PARAM_USER_PATH_PREFIX_DEFAULT
+    )
+    public static final String PARAM_USER_PATH_PREFIX = "user.pathPrefix";
+
+    /**
+     * @see DefaultSyncConfig.User#getMembershipExpirationTime()
+     */
+    public static final long PARAM_USER_MEMBERSHIP_EXPIRATION_TIME_DEFAULT = 3600*1000;
+
+    /**
+     * @see DefaultSyncConfig.User#getMembershipExpirationTime()
+     */
+    @Property(
+            label = "User Membership Expiration",
+            description = "Time in milliseconds after which membership expires.",
+            longValue = PARAM_USER_MEMBERSHIP_EXPIRATION_TIME_DEFAULT
+    )
+    public static final String PARAM_USER_MEMBERSHIP_EXPIRATION_TIME = "user.membershipExpTime";
+
+    /**
+     * @see User#getMembershipNestingDepth()
+     */
+    public static final int PARAM_USER_MEMBERSHIP_NESTING_DEPTH_DEFAULT = 0;
+
+    /**
+     * @see User#getMembershipNestingDepth()
+     */
+    @Property(
+            label = "User membership nesting depth",
+            description = "Returns the maximum depth of group nesting when membership relations are synced. " +
+                    "A value of 0 effectively disables group membership lookup. A value of 1 only adds the direct " +
+                    "groups of a user. This value has no effect when syncing individual groups only when syncing a " +
+                    "users membership ancestry.",
+            intValue = PARAM_USER_MEMBERSHIP_NESTING_DEPTH_DEFAULT
+    )
+    public static final String PARAM_USER_MEMBERSHIP_NESTING_DEPTH = "user.membershipNestingDepth";
+
+    /**
+     * @see DefaultSyncConfig.Group#getExpirationTime()
+     */
+    public static final long PARAM_GROUP_EXPIRATION_TIME_DEFAULT = 24*3600*1000;
+
+    /**
+     * @see DefaultSyncConfig.Group#getExpirationTime()
+     */
+    @Property(
+            label = "Group Expiration Time",
+            description = "Duration in milliseconds until a synced group expires.",
+            longValue = PARAM_GROUP_EXPIRATION_TIME_DEFAULT
+    )
+    public static final String PARAM_GROUP_EXPIRATION_TIME = "group.expirationTime";
+
+    /**
+     * @see DefaultSyncConfig.Group#getAutoMembership()
+     */
+    public static final String[] PARAM_GROUP_AUTO_MEMBERSHIP_DEFAULT = {};
+
+    /**
+     * @see DefaultSyncConfig.Group#getAutoMembership()
+     */
+    @Property(
+            label = "Group auto membership",
+            description = "List of groups that a synced group is added to automatically",
+            value = {},
+            cardinality = Integer.MAX_VALUE
+    )
+    public static final String PARAM_GROUP_AUTO_MEMBERSHIP = "group.autoMembership";
+
+    /**
+     * @see DefaultSyncConfig.Group#getPropertyMapping()
+     */
+    public static final String[] PARAM_GROUP_PROPERTY_MAPPING_DEFAULT = {};
+
+    /**
+     * @see DefaultSyncConfig.Group#getPropertyMapping()
+     */
+    @Property(
+            label = "Group property mapping",
+            description = "List mapping definition of local properties from external ones.",
+            value = {},
+            cardinality = Integer.MAX_VALUE
+    )
+    public static final String PARAM_GROUP_PROPERTY_MAPPING = "group.propertyMapping";
+
+    /**
+     * @see DefaultSyncConfig.Group#getPathPrefix()
+     */
+    public static final String PARAM_GROUP_PATH_PREFIX_DEFAULT = "";
+
+    /**
+     * @see DefaultSyncConfig.Group#getPathPrefix()
+     */
+    @Property(
+            label = "Group Path Prefix",
+            description = "The path prefix used when creating new groups.",
+            value = PARAM_GROUP_PATH_PREFIX_DEFAULT
+    )
+    public static final String PARAM_GROUP_PATH_PREFIX = "group.pathPrefix";
+
+    /**
+     * Base config class for users and groups
+     */
+    public abstract static class Authorizable {
+
+        private long expirationTime;
+
+        private Set<String> autoMembership;
+
+        private Map<String, String> propertyMapping;
+
+        private String pathPrefix;
+
+        /**
+         * Returns the duration in milliseconds until a synced authorizable gets expired. An expired authorizable will
+         * be re-synced.
+         * @return the expiration time in milliseconds.
+         */
+        public long getExpirationTime() {
+            return expirationTime;
+        }
+
+        /**
+         * Sets the expiration time.
+         * @param expirationTime time in milliseconds.
+         * @return {@code this}
+         * @see #getExpirationTime()
+         */
+        @Nonnull
+        public Authorizable setExpirationTime(long expirationTime) {
+            this.expirationTime = expirationTime;
+            return this;
+        }
+
+        /**
+         * Defines the set of group names that are automatically added to synced authorizable.
+         * @return set of group names.
+         */
+        @Nonnull
+        public Set<String> getAutoMembership() {
+            return autoMembership;
+        }
+
+        /**
+         * Sets the auto membership
+         * @param autoMembership the membership
+         * @return {@code this}
+         * @see #getAutoMembership()
+         */
+        @Nonnull
+        public Authorizable setAutoMembership(String ... autoMembership) {
+            this.autoMembership = new HashSet<String>(Arrays.asList(autoMembership));
+            return this;
+        }
+
+        /**
+         * Defines the mapping of internal property names from external values. Only the external properties defined as
+         * keys of this map are synced with the mapped internal properties. note that the property names can be relative
+         * paths. the intermediate nodes will be created accordingly.
+         *
+         * Example:
+         * <xmp>
+         *     {
+         *         "rep:fullname": "cn",
+         *         "country", "c",
+         *         "profile/email": "mail",
+         *         "profile/givenName": "cn"
+         *     }
+         * </xmp>
+         *
+         * The implicit properties like userid, groupname, password must not be mapped.
+         *
+         * @return the property mapping where the keys are the local property names and the values the external ones.
+         */
+        @Nonnull
+        public Map<String, String> getPropertyMapping() {
+            return propertyMapping;
+        }
+
+        /**
+         * Sets the property mapping.
+         * @param propertyMapping the mapping
+         * @return {@code this}
+         * @see #getPropertyMapping()
+         */
+        @Nonnull
+        public Authorizable setPropertyMapping(Map<String, String> propertyMapping) {
+            this.propertyMapping = propertyMapping;
+            return this;
+        }
+
+        /**
+         * Defines the authorizables intermediate path prefix that is used when creating new authorizables. This prefix
+         * is always prepended to the path provided by the {@link org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity}.
+         * @return the intermediate path prefix.
+         */
+        @Nonnull
+        public String getPathPrefix() {
+            return pathPrefix;
+        }
+
+        /**
+         * Sets the path prefix.
+         * @param pathPrefix the path prefix.
+         * @return {@code this}
+         * @see #getPathPrefix()
+         */
+        @Nonnull
+        public Authorizable setPathPrefix(String pathPrefix) {
+            this.pathPrefix = pathPrefix == null ? "" : pathPrefix;
+            return this;
+        }
+    }
+
+    /**
+     * User specific config.
+     */
+    public static class User extends Authorizable {
+
+        private long membershipExpirationTime;
+
+        private long membershipNestingDepth;
+
+        /**
+         * Returns the duration in milliseconds until the group membership of a user is expired. If the
+         * membership information is expired it is re-synced according to the maximum nesting depth.
+         * Note that the membership is the groups an authorizable is member of, not the list of members of a group.
+         * Also note, that the group membership expiration time can be higher than the user expiration time itself and
+         * that value has no effect when syncing individual groups only when syncing a users membership ancestry.
+         *
+         * @return the expiration time in milliseconds.
+         */
+        public long getMembershipExpirationTime() {
+            return membershipExpirationTime;
+        }
+
+        /**
+         * Sets the membership expiration time
+         * @param membershipExpirationTime the time in milliseconds.
+         * @return {@code this}
+         * @see #getMembershipExpirationTime()
+         */
+        @Nonnull
+        public User setMembershipExpirationTime(long membershipExpirationTime) {
+            this.membershipExpirationTime = membershipExpirationTime;
+            return this;
+        }
+
+        /**
+         * Returns the maximum depth of group nesting when membership relations are synced. A value of 0 effectively
+         * disables group membership lookup. A value of 1 only adds the direct groups of a user. This value has no effect
+         * when syncing individual groups only when syncing a users membership ancestry.
+         * @return the group nesting depth
+         */
+        public long getMembershipNestingDepth() {
+            return membershipNestingDepth;
+        }
+
+        /**
+         * Sets the group nesting depth.
+         * @param membershipNestingDepth the depth.
+         * @return {@code this}
+         * @see #getMembershipNestingDepth()
+         */
+        @Nonnull
+        public User setMembershipNestingDepth(long membershipNestingDepth) {
+            this.membershipNestingDepth = membershipNestingDepth;
+            return this;
+        }
+
+    }
+
+    /**
+     * Group specific config
+     */
+    public static class Group extends Authorizable {
+
+    }
+
+    /**
+     * Creates a new LDAP provider configuration based on the properties store in the given parameters.
+     * @param params the configuration parameters.
+     * @return the config
+     */
+    public static DefaultSyncConfig of(ConfigurationParameters params) {
+        DefaultSyncConfig cfg = new DefaultSyncConfig()
+                .setName(params.getConfigValue(PARAM_NAME, PARAM_NAME_DEFAULT));
+
+        cfg.user()
+                .setMembershipExpirationTime(params.getConfigValue(PARAM_USER_MEMBERSHIP_EXPIRATION_TIME, PARAM_USER_MEMBERSHIP_EXPIRATION_TIME_DEFAULT))
+                .setMembershipNestingDepth(params.getConfigValue(PARAM_USER_MEMBERSHIP_NESTING_DEPTH, PARAM_USER_MEMBERSHIP_NESTING_DEPTH_DEFAULT))
+                .setExpirationTime(params.getConfigValue(PARAM_USER_EXPIRATION_TIME, PARAM_USER_EXPIRATION_TIME_DEFAULT))
+                .setPathPrefix(params.getConfigValue(PARAM_USER_PATH_PREFIX, PARAM_USER_PATH_PREFIX_DEFAULT))
+                .setAutoMembership(params.getConfigValue(PARAM_USER_AUTO_MEMBERSHIP, PARAM_USER_AUTO_MEMBERSHIP_DEFAULT))
+                .setPropertyMapping(createMapping(
+                        params.getConfigValue(PARAM_USER_PROPERTY_MAPPING, PARAM_USER_PROPERTY_MAPPING_DEFAULT)));
+
+        cfg.group()
+                .setExpirationTime(params.getConfigValue(PARAM_GROUP_EXPIRATION_TIME, PARAM_GROUP_EXPIRATION_TIME_DEFAULT))
+                .setPathPrefix(params.getConfigValue(PARAM_GROUP_PATH_PREFIX, PARAM_GROUP_PATH_PREFIX_DEFAULT))
+                .setAutoMembership(params.getConfigValue(PARAM_GROUP_AUTO_MEMBERSHIP, PARAM_GROUP_AUTO_MEMBERSHIP_DEFAULT))
+                .setPropertyMapping(createMapping(
+                        params.getConfigValue(PARAM_GROUP_PROPERTY_MAPPING, PARAM_GROUP_PROPERTY_MAPPING_DEFAULT)));
+
+        return cfg;
+    }
+
+    /**
+     * Creates a new property mapping map from a list of patterns.
+     * @param patterns the patterns
+     * @return the mapping map
+     */
+    private static Map<String, String> createMapping(String[] patterns) {
+        Map<String, String> mapping = new HashMap<String, String>();
+        for (String pattern: patterns) {
+            int idx = pattern.indexOf('=');
+            if (idx > 0) {
+                String relPath = pattern.substring(0, idx).trim();
+                String value = pattern.substring(idx+1).trim();
+                mapping.put(relPath, value);
+            }
+        }
+        return mapping;
+    }
+
+    private String name = PARAM_NAME_DEFAULT;
+
+    private final User user = new User();
+
+    private final Group group = new Group();
+
+    /**
+     * Configures the name of this configuration
+     * @return the name
+     */
+    @Nonnull
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name
+     * @param name the name
+     * @return {@code this}
+     * @see #getName()
+     */
+    public DefaultSyncConfig setName(@Nonnull String name) {
+        this.name = name;
+        return this;
+    }
+
+    /**
+     * Returns the sync configuration for users.
+     * @return the user sync configuration.
+     */
+    public User user() {
+        return user;
+    }
+
+    /**
+     * Returns the sync configuration for groups.
+     * @return the group sync configuration.
+     */
+    public Group group() {
+        return group;
+    }
+
+}
\ No newline at end of file

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java?rev=1566806&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java Mon Feb 10 22:57:58 2014
@@ -0,0 +1,298 @@
+/*
+ * 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.jackrabbit.oak.spi.security.authentication.external.impl;
+
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+import javax.jcr.ValueFormatException;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.ConfigurationPolicy;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.namepath.NamePathMapper;
+import org.apache.jackrabbit.oak.plugins.value.ValueFactoryImpl;
+import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalGroup;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncContext;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncException;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
+import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@code DefaultSyncHandler} implements an sync handler that synchronizes users and groups from an external identity
+ * provider with the repository users.
+ * <p/>
+ * Please refer to {@link DefaultSyncConfig} for configuration options.
+ */
+@Component(
+        // note that the metatype information is generated from DefaultSyncConfig
+        policy = ConfigurationPolicy.REQUIRE
+)
+@Service
+public class DefaultSyncHandler implements SyncHandler {
+
+    /**
+     * logger instance
+     */
+    private static final Logger log = LoggerFactory.getLogger(DefaultSyncHandler.class);
+
+    /**
+     * internal configuration
+     */
+    private DefaultSyncConfig config;
+
+    /**
+     * Default constructor for OSGi
+     */
+    public DefaultSyncHandler() {
+    }
+
+    /**
+     * Constructor for non-OSGi cases.
+     *
+     * @param config the configuration
+     */
+    public DefaultSyncHandler(DefaultSyncConfig config) {
+        this.config = config;
+    }
+
+    @Activate
+    private void activate(Map<String, Object> properties) {
+        ConfigurationParameters cfg = ConfigurationParameters.of(properties);
+        config = DefaultSyncConfig.of(cfg);
+    }
+
+    @Nonnull
+    @Override
+    public String getName() {
+        return config.getName();
+    }
+
+    @Nonnull
+    @Override
+    public SyncContext createContext(@Nonnull ExternalIdentityProvider idp, @Nonnull UserManager userManager, @Nonnull Root root) throws SyncException {
+        return new ContextImpl(idp, userManager, root);
+    }
+
+    private class ContextImpl implements SyncContext {
+
+        private final ExternalIdentityProvider idp;
+
+        private final UserManager userManager;
+
+        private final Root root;
+
+        private final ValueFactory valueFactory;
+
+        private ContextImpl(ExternalIdentityProvider idp, UserManager userManager, Root root) {
+            this.idp = idp;
+            this.userManager = userManager;
+            this.root = root;
+            valueFactory = new ValueFactoryImpl(root, NamePathMapper.DEFAULT);
+        }
+
+        @Override
+        public void close() {
+            // nothing to do
+        }
+
+        @Override
+        public boolean sync(@Nonnull ExternalIdentity identity) throws SyncException {
+            try {
+                if (identity instanceof ExternalUser) {
+                    User user = getUser(identity);
+                    if (user == null) {
+                        createUser((ExternalUser) identity);
+                    } else {
+                        updateUser((ExternalUser) identity, user);
+                    }
+                    return true;
+                } else if (identity instanceof ExternalGroup) {
+                    // todo
+                    return false;
+
+                } else {
+                    throw new IllegalArgumentException("identity must be user or group but was: " + identity);
+                }
+            } catch (RepositoryException e) {
+                throw new SyncException(e);
+            }
+        }
+
+        @CheckForNull
+        private User getUser(@Nonnull ExternalIdentity externalUser) throws RepositoryException {
+            Authorizable authorizable = userManager.getAuthorizable(externalUser.getId());
+            if (authorizable == null) {
+                authorizable = userManager.getAuthorizable(externalUser.getPrincipalName());
+            }
+            if (authorizable == null) {
+                return null;
+            } else if (authorizable instanceof User) {
+                return (User) authorizable;
+            } else {
+                // TODO: deal with colliding authorizable that is group.
+                log.warn("unexpected authorizable: {}", authorizable);
+                return null;
+            }
+        }
+
+        @CheckForNull
+        private User createUser(ExternalUser externalUser) throws RepositoryException, SyncException {
+            String password = externalUser.getPassword(); // todo: make configurable
+            Principal principal = new PrincipalImpl(externalUser.getPrincipalName());
+            User user = userManager.createUser(
+                    externalUser.getId(),
+                    password,
+                    principal,
+                    concatPaths(config.user().getPathPrefix(), externalUser.getIntermediatePath())
+            );
+            syncAuthorizable(externalUser, user);
+            return user;
+        }
+
+        @CheckForNull
+        private Group createGroup(ExternalGroup externalGroup) throws RepositoryException, SyncException {
+            Principal principal = new PrincipalImpl(externalGroup.getPrincipalName());
+            Group group = userManager.createGroup(
+                    externalGroup.getId(),
+                    principal,
+                    concatPaths(config.user().getPathPrefix(), externalGroup.getIntermediatePath()));
+            syncAuthorizable(externalGroup, group);
+            return group;
+        }
+
+        private void updateUser(ExternalUser externalUser, User user) throws RepositoryException, SyncException {
+            syncAuthorizable(externalUser, user);
+        }
+
+        private void syncAuthorizable(ExternalIdentity externalUser, Authorizable authorizable) throws RepositoryException, SyncException {
+            for (ExternalGroup externalGroup : externalUser.getGroups()) {
+                String groupId = externalGroup.getId();
+                Group group;
+                Authorizable a = userManager.getAuthorizable(groupId);
+                if (a == null) {
+                    group = createGroup(externalGroup);
+                } else {
+                    group = (a.isGroup()) ? (Group) a : null;
+                }
+
+                if (group != null) {
+                    group.addMember(authorizable);
+                } else {
+                    log.debug("No such group " + groupId + "; Ignoring group membership.");
+                }
+            }
+
+            Map<String, ?> properties = externalUser.getProperties();
+            for (String key : properties.keySet()) {
+                Object prop = properties.get(key);
+                if (prop instanceof Collection) {
+                    Value[] values = createValues((Collection) prop);
+                    if (values != null) {
+                        authorizable.setProperty(key, values);
+                    }
+                } else {
+                    Value value = createValue(prop);
+                    if (value != null) {
+                        authorizable.setProperty(key, value);
+                    }
+                }
+            }
+        }
+
+        @CheckForNull
+        private Value createValue(Object propValue) throws ValueFormatException {
+            int type = getType(propValue);
+            if (type == PropertyType.UNDEFINED) {
+                return null;
+            } else {
+                return valueFactory.createValue(propValue.toString(), type);
+            }
+        }
+
+        @CheckForNull
+        private Value[] createValues(Collection<?> propValues) throws ValueFormatException {
+            List<Value> values = new ArrayList<Value>();
+            for (Object obj : propValues) {
+                Value v = createValue(obj);
+                if (v != null) {
+                    values.add(v);
+                }
+            }
+            return values.toArray(new Value[values.size()]);
+        }
+
+        private int getType(Object propValue) {
+            // TODO: add proper type detection
+            if (propValue == null) {
+                return PropertyType.UNDEFINED;
+            } else {
+                return PropertyType.STRING;
+            }
+        }
+
+    }
+
+    /**
+     * Robust relative path concatenation.
+     * @param paths relative paths
+     * @return the concatenated path
+     */
+    private static String concatPaths(String ... paths) {
+        StringBuilder result = new StringBuilder();
+        for (String path: paths) {
+            if (path != null && !path.isEmpty()) {
+                int i0 = 0;
+                int i1 = path.length();
+                while (i0 < i1 && path.charAt(i0) == '/') {
+                    i0++;
+                }
+                while (i1 > i0 && path.charAt(i1-1) == '/') {
+                    i1--;
+                }
+                if (i1 > i0) {
+                    if (result.length() > 0) {
+                        result.append('/');
+                    }
+                    result.append(path.substring(i0, i1));
+                }
+            }
+        }
+        return result.length() == 0 ? null : result.toString();
+    }
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModule.java Mon Feb 10 22:57:58 2014
@@ -20,7 +20,6 @@ import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
-import javax.annotation.Nonnull;
 import javax.jcr.Credentials;
 import javax.jcr.SimpleCredentials;
 import javax.security.auth.Subject;
@@ -32,14 +31,14 @@ import org.apache.jackrabbit.oak.api.Com
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.authentication.AbstractLoginModule;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.DefaultSyncHandler;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityException;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProvider;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityProviderManager;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalUser;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncContext;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncException;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
-import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncMode;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -50,11 +49,6 @@ public class ExternalLoginModule extends
 
     private static final Logger log = LoggerFactory.getLogger(ExternalLoginModule.class);
 
-    public static final SyncMode DEFAULT_SYNC_MODE = SyncMode.DEFAULT_SYNC;
-
-    private static final String PARAM_SYNC_HANDLER = "syncHandler";
-    private static final String DEFAULT_SYNC_HANDLER = DefaultSyncHandler.class.getName();
-
     /**
      * Name of the parameter that configures the name of the external identity provider.
      */
@@ -66,11 +60,6 @@ public class ExternalLoginModule extends
     public static final String PARAM_SYNC_HANDLER_NAME = "sync.handlerName";
 
     /**
-     * Name of the parameter that configures the synchronization mode.
-     */
-    public static final String PARAM_SYNC_MODE = "sync.mode";
-
-    /**
      * internal configuration when invoked from a factory rather than jaas
      */
     private ConfigurationParameters osgiConfig;
@@ -81,6 +70,11 @@ public class ExternalLoginModule extends
     private ExternalIdentityProvider idp;
 
     /**
+     * The configured sync handler as specified by the {@link #PARAM_SYNC_HANDLER_NAME}
+     */
+    private SyncHandler syncHandler;
+
+    /**
      * The external user as resolved in the login call.
      */
     private ExternalUser externalUser;
@@ -118,23 +112,38 @@ public class ExternalLoginModule extends
                 log.error("No IDP found with name {}. Will not be used for login.", idpName);
             }
         }
+
+        String syncHandlerName = options.getConfigValue(PARAM_SYNC_HANDLER_NAME, "");
+        if (syncHandlerName.length() == 0) {
+            log.error("External login module needs SyncHandler name. Will not be used for login.");
+        } else {
+            SyncManager syncMgr = getSecurityProvider().getConfiguration(SyncManager.class);
+            syncHandler = syncMgr.getSyncHandler(syncHandlerName);
+            if (syncHandler == null) {
+                log.error("No SyncHandler found with name {}. Will not be used for login.", idpName);
+            }
+        }
     }
 
     @Override
     public boolean login() throws LoginException {
-        if (idp == null) {
+        if (idp == null || syncHandler == null) {
             return false;
         }
 
         Credentials credentials = getCredentials();
         if (credentials == null) {
-            log.info("No credentials found for external login module. ignoring.");
+            log.debug("No credentials found for external login module. ignoring.");
             return false;
         }
 
         try {
             externalUser = idp.authenticate(credentials);
             if (externalUser != null) {
+                if (log.isDebugEnabled()) {
+                    log.debug("IDP {} returned valid user {}", idp.getName(), externalUser);
+                }
+
                 //noinspection unchecked
                 sharedState.put(SHARED_KEY_CREDENTIALS, credentials);
 
@@ -142,11 +151,24 @@ public class ExternalLoginModule extends
                 sharedState.put(SHARED_KEY_LOGIN_NAME, externalUser.getId());
 
                 return true;
+            } else {
+                if (log.isDebugEnabled()) {
+                    if (credentials instanceof SimpleCredentials) {
+                        log.debug("IDP {} returned null for simple creds of {}", idp.getName(), ((SimpleCredentials) credentials).getUserID());
+                    } else {
+                        log.debug("IDP {} returned null for {}", idp.getName(), credentials);
+                    }
+                }
             }
         } catch (ExternalIdentityException e) {
             log.error("Error while authenticating credentials {} with {}: {}", new Object[]{
                     credentials, idp.getName(), e.toString()});
             return false;
+        } catch (LoginException e) {
+            if (log.isDebugEnabled()) {
+                log.debug("IDP {} throws login exception for {}", idp.getName(), credentials);
+            }
+            throw e;
         }
         return false;
     }
@@ -165,56 +187,28 @@ public class ExternalLoginModule extends
 
     @Override
     public boolean commit() throws LoginException {
-        if (externalUser == null) {
+        if (externalUser == null || syncHandler == null) {
             return false;
         }
 
+        SyncContext context = null;
         try {
-            SyncHandler handler = getSyncHandler();
             Root root = getRoot();
             UserManager userManager = getUserManager();
             if (root == null || userManager == null) {
                 throw new LoginException("Cannot synchronize user.");
             }
-            Object smValue = options.getConfigValue(PARAM_SYNC_MODE, null, null);
-            SyncMode syncMode;
-            if (smValue == null) {
-                syncMode = DEFAULT_SYNC_MODE;
-            } else {
-                syncMode = SyncMode.fromObject(smValue);
-            }
-            if (externalUser != null && handler.initialize(userManager, root, syncMode, options)) {
-                handler.sync(externalUser);
-                root.commit();
-                return true;
-            } else {
-                log.warn("Failed to initialize sync handler.");
-                return false;
-            }
+            context = syncHandler.createContext(idp, userManager, root);
+            context.sync(externalUser);
+            root.commit();
+            return true;
         } catch (SyncException e) {
             throw new LoginException("User synchronization failed: " + e);
         } catch (CommitFailedException e) {
             throw new LoginException("User synchronization failed: " + e);
-        }
-    }
-
-    @Nonnull
-    protected SyncHandler getSyncHandler() throws SyncException {
-        Object syncHandler = options.getConfigValue(PARAM_SYNC_HANDLER, null, null);
-        if (syncHandler == null) {
-            return new DefaultSyncHandler();
-        } else if (syncHandler instanceof SyncHandler) {
-            return (SyncHandler) syncHandler;
-        } else {
-            try {
-                Object sh = Class.forName(syncHandler.toString()).newInstance();
-                if (sh instanceof SyncHandler) {
-                    return (SyncHandler) sh;
-                } else {
-                    throw new SyncException("Invalid SyncHandler configuration: " + sh);
-                }
-            } catch (Exception e) {
-                throw new SyncException("Error while getting SyncHandler:", e);
+        } finally {
+            if (context != null) {
+                context.close();
             }
         }
     }

Modified: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleFactory.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleFactory.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleFactory.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/ExternalLoginModuleFactory.java Mon Feb 10 22:57:58 2014
@@ -79,14 +79,6 @@ public class ExternalLoginModuleFactory 
     )
     public static final String PARAM_SYNC_HANDLER_NAME = ExternalLoginModule.PARAM_SYNC_HANDLER_NAME;
 
-    @Property(
-            value = "default",
-            label = "Sync Mode",
-            // TODO!!!
-            description = "a comma separated list of sync modes. allowed values: 'user','group','default','update'"
-    )
-    public static final String PARAM_SYNC_MODE = ExternalLoginModule.PARAM_SYNC_MODE;
-
     /**
      * default configuration for the login modules
      */

Added: jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncManagerImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncManagerImpl.java?rev=1566806&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncManagerImpl.java (added)
+++ jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncManagerImpl.java Mon Feb 10 22:57:58 2014
@@ -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.jackrabbit.oak.spi.security.authentication.external.impl;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.annotation.Nonnull;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.ReferencePolicy;
+import org.apache.felix.scr.annotations.Service;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncManager;
+
+/**
+ * {@code SyncManagerImpl} is used to manage registered sync handlers. This class automatically
+ * tracks the SyncHandlers that are registered via OSGi but can also be used in non-OSGi environments by manually
+ * adding and removing the handlers.
+ */
+@Component
+@Service
+public class SyncManagerImpl implements SyncManager {
+
+    @Reference(
+            name = "syncHandler",
+            bind = "addHandler",
+            unbind = "removeHandler",
+            referenceInterface = SyncHandler.class,
+            cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE,
+            policy = ReferencePolicy.DYNAMIC
+    )
+    final private Map<String, SyncHandler> handlers = new ConcurrentHashMap<String, SyncHandler>();
+
+    public void addHandler(SyncHandler handler, final Map<String, Object> props) {
+        handlers.put(handler.getName(), handler);
+    }
+
+    public void removeHandler(SyncHandler handler, final Map<String, Object> props) {
+        handlers.remove(handler.getName());
+    }
+
+    @Override
+    public SyncHandler getSyncHandler(@Nonnull String name) {
+        return handlers.get(name);
+    }
+}
\ No newline at end of file

Modified: jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestLoginModule.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestLoginModule.java?rev=1566806&r1=1566805&r2=1566806&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestLoginModule.java (original)
+++ jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/TestLoginModule.java Mon Feb 10 22:57:58 2014
@@ -102,7 +102,7 @@ public class TestLoginModule extends Ext
         }
 
         @Override
-        public Principal getPrincipal() {
+        public Principal getPrincipalName() {
             return new PrincipalImpl(userId);
         }