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 2015/09/30 20:34:46 UTC

svn commit: r1706119 [1/2] - in /jackrabbit/oak/branches/1.0: oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/ oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/ex...

Author: tripod
Date: Wed Sep 30 18:34:45 2015
New Revision: 1706119

URL: http://svn.apache.org/viewvc?rev=1706119&view=rev
Log:
OAK-2948 Expose DefaultSyncHandler

Added:
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncConfig.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncResultImpl.java
      - copied, changed from r1706040, jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncedIdentity.java
      - copied, changed from r1706040, jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncedIdentityImpl.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/package-info.java
      - copied, changed from r1706040, jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfigImpl.java
Removed:
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfig.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncedIdentityImpl.java
Modified:
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncHandler.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/jmx/SyncMBeanImpl.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleFactoryTest.java
    jackrabbit/oak/branches/1.0/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/ExternalLoginModuleTestBase.java
    jackrabbit/oak/branches/1.0/oak-auth-ldap/src/test/java/org/apache/jackrabbit/oak/security/authentication/ldap/LdapLoginTestBase.java

Added: jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncConfig.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncConfig.java?rev=1706119&view=auto
==============================================================================
--- jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncConfig.java (added)
+++ jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncConfig.java Wed Sep 30 18:34:45 2015
@@ -0,0 +1,259 @@
+/*
+ * 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.basic;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+/**
+ * {@code DefaultSyncConfig} defines how users and groups from an external source are synced into the repository using
+ * the {@link org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncHandler}.
+ */
+public class DefaultSyncConfig {
+
+    private final User user = new User();
+
+    private final Group group = new Group();
+
+    private String name = "default";
+
+    /**
+     * 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.
+     */
+    @Nonnull
+    public User user() {
+        return user;
+    }
+
+    /**
+     * Returns the sync configuration for groups.
+     * @return the group sync configuration.
+     */
+    @Nonnull
+    public Group group() {
+        return group;
+    }
+
+    /**
+     * 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 == null ? Collections.<String>emptySet() : 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>();
+            for (String groupName: autoMembership) {
+                if (!groupName.trim().isEmpty()) {
+                    this.autoMembership.add(groupName.trim());
+                }
+            }
+            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 == null ? Collections.<String, String>emptyMap() : 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 == null ? "" : pathPrefix;
+        }
+
+        /**
+         * Sets the path prefix.
+         * @param pathPrefix the path prefix.
+         * @return {@code this}
+         * @see #getPathPrefix()
+         */
+        @Nonnull
+        public Authorizable setPathPrefix(String pathPrefix) {
+            this.pathPrefix = 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 {
+
+    }
+}
\ No newline at end of file

Added: jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java?rev=1706119&view=auto
==============================================================================
--- jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java (added)
+++ jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncContext.java Wed Sep 30 18:34:45 2015
@@ -0,0 +1,716 @@
+/*
+ * 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.basic;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigDecimal;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.jcr.Binary;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+
+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.spi.security.authentication.external.ExternalGroup;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentity;
+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.ExternalIdentityRef;
+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.SyncResult;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DebugTimer;
+import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Internal implementation of the sync context
+ */
+public class DefaultSyncContext implements SyncContext {
+
+    /**
+     * default logger
+     */
+    private static final Logger log = LoggerFactory.getLogger(DefaultSyncContext.class);
+
+    /**
+     * Name of the {@link ExternalIdentity#getExternalId()} property of a synchronized identity.
+     */
+    public static final String REP_EXTERNAL_ID = "rep:externalId";
+
+    /**
+     * Name of the property that stores the time when an identity was synced.
+     */
+    public static final String REP_LAST_SYNCED = "rep:lastSynced";
+
+    protected final DefaultSyncConfig config;
+
+    protected final ExternalIdentityProvider idp;
+
+    protected final UserManager userManager;
+
+    protected final ValueFactory valueFactory;
+
+    protected boolean keepMissing;
+
+    protected boolean forceUserSync;
+
+    protected boolean forceGroupSync;
+
+    // we use the same wall clock for the entire context
+    protected final long now;
+
+    protected final Value nowValue;
+
+    public DefaultSyncContext(DefaultSyncConfig config, ExternalIdentityProvider idp, UserManager userManager, ValueFactory valueFactory) {
+        this.config = config;
+        this.idp = idp;
+        this.userManager = userManager;
+        this.valueFactory = valueFactory;
+
+        // initialize 'now'
+        final Calendar nowCal = Calendar.getInstance();
+        this.nowValue = valueFactory.createValue(nowCal);
+        this.now = nowCal.getTimeInMillis();
+    }
+
+    /**
+     * Creates a synced identity from the given authorizable.
+     * @param auth the authorizable
+     * @return the id
+     * @throws RepositoryException if an error occurs
+     */
+    @CheckForNull
+    public static DefaultSyncedIdentity createSyncedIdentity(@Nullable Authorizable auth) throws RepositoryException {
+        ExternalIdentityRef ref = (auth == null) ? null : getIdentityRef(auth);
+        if (ref == null) {
+            return null;
+        } else {
+            Value[] lmValues = auth.getProperty(REP_LAST_SYNCED);
+            long lastModified = -1;
+            if (lmValues != null && lmValues.length > 0) {
+                lastModified = lmValues[0].getLong();
+            }
+            return new DefaultSyncedIdentity(auth.getID(), ref, auth.isGroup(), lastModified);
+        }
+    }
+
+    /**
+     * Retrieves the external identity ref from the authorizable
+     * @param auth the authorizable
+     * @return the ref
+     * @throws RepositoryException if an error occurs
+     */
+    @CheckForNull
+    public static ExternalIdentityRef getIdentityRef(@Nullable Authorizable auth) throws RepositoryException {
+        if (auth == null) {
+            return null;
+        }
+        Value[] v = auth.getProperty(REP_EXTERNAL_ID);
+        if (v == null || v.length == 0) {
+            return null;
+        }
+        return ExternalIdentityRef.fromString(v[0].getString());
+    }
+
+    /**
+     * Robust relative path concatenation.
+     * @param paths relative paths
+     * @return the concatenated path
+     */
+    public static String joinPaths(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();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void close() {
+        // nothing to do
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isKeepMissing() {
+        return keepMissing;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Nonnull
+    @Override
+    public SyncContext setKeepMissing(boolean keepMissing) {
+        this.keepMissing = keepMissing;
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isForceUserSync() {
+        return forceUserSync;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Nonnull
+    @Override
+    public SyncContext setForceUserSync(boolean forceUserSync) {
+        this.forceUserSync = forceUserSync;
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isForceGroupSync() {
+        return forceGroupSync;
+    }
+
+    @Nonnull
+    public SyncContext setForceGroupSync(boolean forceGroupSync) {
+        this.forceGroupSync = forceGroupSync;
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Nonnull
+    @Override
+    public SyncResult sync(@Nonnull ExternalIdentity identity) throws SyncException {
+        try {
+            DebugTimer timer = new DebugTimer();
+            DefaultSyncResultImpl ret;
+            boolean created = false;
+            if (identity instanceof ExternalUser) {
+                User user = getAuthorizable(identity, User.class);
+                timer.mark("find");
+                if (user == null) {
+                    user = createUser((ExternalUser) identity);
+                    timer.mark("create");
+                    created = true;
+                }
+                ret = syncUser((ExternalUser) identity, user);
+                timer.mark("sync");
+            } else if (identity instanceof ExternalGroup) {
+                Group group = getAuthorizable(identity, Group.class);
+                timer.mark("find");
+                if (group == null) {
+                    group = createGroup((ExternalGroup) identity);
+                    timer.mark("create");
+                    created = true;
+                }
+                ret = syncGroup((ExternalGroup) identity, group);
+                timer.mark("sync");
+            } else {
+                throw new IllegalArgumentException("identity must be user or group but was: " + identity);
+            }
+            if (log.isDebugEnabled()) {
+                log.debug("sync({}) -> {} {}", identity.getExternalId().getString(), identity.getId(), timer.getString());
+            }
+            if (created) {
+                ret.setStatus(SyncResult.Status.ADD);
+            }
+            return ret;
+        } catch (RepositoryException e) {
+            throw new SyncException(e);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Nonnull
+    @Override
+    public SyncResult sync(@Nonnull String id) throws SyncException {
+        try {
+            DebugTimer timer = new DebugTimer();
+            DefaultSyncResultImpl ret;
+            // find authorizable
+            Authorizable auth = userManager.getAuthorizable(id);
+            if (auth == null) {
+                return new DefaultSyncResultImpl(new DefaultSyncedIdentity(id, null, false, -1), SyncResult.Status.NO_SUCH_AUTHORIZABLE);
+            }
+            // check if we need to deal with this authorizable
+            ExternalIdentityRef ref = DefaultSyncContext.getIdentityRef(auth);
+            if (ref == null || !idp.getName().equals(ref.getProviderName())) {
+                return new DefaultSyncResultImpl(new DefaultSyncedIdentity(id, null, false, -1), SyncResult.Status.FOREIGN);
+            }
+
+            if (auth instanceof Group) {
+                Group group = (Group) auth;
+                ExternalGroup external = idp.getGroup(id);
+                timer.mark("retrieve");
+                if (external == null) {
+                    DefaultSyncedIdentity syncId = DefaultSyncContext.createSyncedIdentity(auth);
+                    if (group.getDeclaredMembers().hasNext()) {
+                        log.info("won't remove local group with members: {}", id);
+                        ret = new DefaultSyncResultImpl(syncId, SyncResult.Status.NOP);
+                    } else if (!keepMissing) {
+                        auth.remove();
+                        log.debug("removing authorizable '{}' that no longer exists on IDP {}", id, idp.getName());
+                        timer.mark("remove");
+                        ret = new DefaultSyncResultImpl(syncId, SyncResult.Status.DELETE);
+                    } else {
+                        ret = new DefaultSyncResultImpl(syncId, SyncResult.Status.MISSING);
+                        log.info("external identity missing for {}, but purge == false.", id);
+                    }
+                } else {
+                    ret = syncGroup(external, group);
+                    timer.mark("sync");
+                }
+            } else {
+                ExternalUser external = idp.getUser(id);
+                timer.mark("retrieve");
+                if (external == null) {
+                    DefaultSyncedIdentity syncId = DefaultSyncContext.createSyncedIdentity(auth);
+                    if (!keepMissing) {
+                        auth.remove();
+                        log.debug("removing authorizable '{}' that no longer exists on IDP {}", id, idp.getName());
+                        timer.mark("remove");
+                        ret = new DefaultSyncResultImpl(syncId, SyncResult.Status.DELETE);
+                    } else {
+                        ret = new DefaultSyncResultImpl(syncId, SyncResult.Status.MISSING);
+                        log.info("external identity missing for {}, but purge == false.", id);
+                    }
+                } else {
+                    ret = syncUser(external, (User) auth);
+                    timer.mark("sync");
+                }
+            }
+            if (log.isDebugEnabled()) {
+                log.debug("sync({}) -> {} {}", id, ref.getString(), timer.getString());
+            }
+            return ret;
+        } catch (RepositoryException e) {
+            throw new SyncException(e);
+        } catch (ExternalIdentityException e) {
+            throw new SyncException(e);
+        }
+    }
+
+    /**
+     * Retrieves the repository authorizable that corresponds to the given external identity
+     * @param external the external identity
+     * @param type the authorizable type
+     * @return the repository authorizable or {@code null} if not found.
+     * @throws RepositoryException if an error occurs.
+     * @throws SyncException if the repository contains a colliding authorizable with the same name.
+     */
+    @CheckForNull
+    protected <T extends Authorizable> T getAuthorizable(@Nonnull ExternalIdentity external, Class<T> type)
+            throws RepositoryException, SyncException {
+        Authorizable authorizable = userManager.getAuthorizable(external.getId());
+        if (authorizable == null) {
+            authorizable = userManager.getAuthorizable(external.getPrincipalName());
+        }
+        if (authorizable == null) {
+            return null;
+        } else if (type.isInstance(authorizable)) {
+            //noinspection unchecked
+            return (T) authorizable;
+        } else {
+            log.error("Unable to process external {}: {}. Colliding authorizable exists in repository.", type.getSimpleName(), external.getId());
+            throw new SyncException("Unexpected authorizable: " + authorizable);
+        }
+    }
+
+    /**
+     * Creates a new repository user for the given external one.
+     * Note that this method only creates the authorizable but does not perform any synchronization.
+     *
+     * @param externalUser the external user
+     * @return the repository user
+     * @throws RepositoryException if an error occurs
+     */
+    @Nonnull
+    protected User createUser(@Nonnull ExternalUser externalUser) throws RepositoryException {
+        Principal principal = new PrincipalImpl(externalUser.getPrincipalName());
+        User user = userManager.createUser(
+                externalUser.getId(),
+                null,
+                principal,
+                joinPaths(config.user().getPathPrefix(), externalUser.getIntermediatePath())
+        );
+        user.setProperty(REP_EXTERNAL_ID, valueFactory.createValue(externalUser.getExternalId().getString()));
+        return user;
+    }
+
+    /**
+     * Creates a new repository group for the given external one.
+     * Note that this method only creates the authorizable but does not perform any synchronization.
+     *
+     * @param externalGroup the external group
+     * @return the repository group
+     * @throws RepositoryException if an error occurs
+     */
+    @Nonnull
+    protected Group createGroup(@Nonnull ExternalGroup externalGroup) throws RepositoryException {
+        Principal principal = new PrincipalImpl(externalGroup.getPrincipalName());
+        Group group = userManager.createGroup(
+                externalGroup.getId(),
+                principal,
+                joinPaths(config.group().getPathPrefix(), externalGroup.getIntermediatePath())
+        );
+        group.setProperty(REP_EXTERNAL_ID, valueFactory.createValue(externalGroup.getExternalId().getString()));
+        return group;
+    }
+
+    @Nonnull
+    protected DefaultSyncResultImpl syncUser(@Nonnull ExternalUser external, @Nonnull User user) throws RepositoryException {
+        // first check if user is expired
+        if (!forceUserSync && !isExpired(user, config.user().getExpirationTime(), "Properties")) {
+            DefaultSyncedIdentity syncId = DefaultSyncContext.createSyncedIdentity(user);
+            return new DefaultSyncResultImpl(syncId, SyncResult.Status.NOP);
+        }
+
+        // synchronize the properties
+        syncProperties(external, user, config.user().getPropertyMapping());
+
+        // synchronize auto-group membership
+        applyMembership(user, config.user().getAutoMembership());
+
+        if (isExpired(user, config.user().getMembershipExpirationTime(), "Membership")) {
+            // synchronize external memberships
+            syncMembership(external, user, config.user().getMembershipNestingDepth());
+        }
+
+        // finally "touch" the sync property
+        user.setProperty(REP_LAST_SYNCED, nowValue);
+        DefaultSyncedIdentity syncId = DefaultSyncContext.createSyncedIdentity(user);
+        return new DefaultSyncResultImpl(syncId, SyncResult.Status.UPDATE);
+    }
+
+    @Nonnull
+    protected DefaultSyncResultImpl syncGroup(@Nonnull ExternalGroup external, @Nonnull Group group) throws RepositoryException {
+        // first check if user is expired
+        if (!forceGroupSync && !isExpired(group, config.group().getExpirationTime(), "Properties")) {
+            DefaultSyncedIdentity syncId = DefaultSyncContext.createSyncedIdentity(group);
+            return new DefaultSyncResultImpl(syncId, SyncResult.Status.NOP);
+        }
+
+        // synchronize the properties
+        syncProperties(external, group, config.group().getPropertyMapping());
+
+        // synchronize auto-group membership
+        applyMembership(group, config.group().getAutoMembership());
+
+        // finally "touch" the sync property
+        group.setProperty(REP_LAST_SYNCED, nowValue);
+        DefaultSyncedIdentity syncId = DefaultSyncContext.createSyncedIdentity(group);
+        return new DefaultSyncResultImpl(syncId, SyncResult.Status.UPDATE);
+    }
+
+    /**
+     * Recursively sync the memberships of an authorizable up-to the specified depth. If the given depth
+     * is equal or less than 0, no syncing is performed.
+     *
+     * @param external the external identity
+     * @param auth the authorizable
+     * @param depth recursion depth.
+     * @throws RepositoryException
+     */
+    protected void syncMembership(ExternalIdentity external, Authorizable auth, long depth)
+            throws RepositoryException {
+        if (depth <= 0) {
+            return;
+        }
+        if (log.isDebugEnabled()) {
+            log.debug("Syncing membership '{}' -> '{}'", external.getExternalId().getString(), auth.getID());
+        }
+
+        final DebugTimer timer = new DebugTimer();
+        Iterable<ExternalIdentityRef> externalGroups;
+        try {
+            externalGroups = external.getDeclaredGroups();
+        } catch (ExternalIdentityException e) {
+            log.error("Error while retrieving external declared groups for '{}'", external.getId(), e);
+            return;
+        }
+        timer.mark("fetching");
+
+        // first get the set of the existing groups that are synced ones
+        Map<String, Group> declaredExternalGroups = new HashMap<String, Group>();
+        Iterator<Group> grpIter = auth.declaredMemberOf();
+        while (grpIter.hasNext()) {
+            Group grp = grpIter.next();
+            if (isSameIDP(grp)) {
+                declaredExternalGroups.put(grp.getID(), grp);
+            }
+        }
+        timer.mark("reading");
+
+        for (ExternalIdentityRef ref : externalGroups) {
+            log.debug("- processing membership {}", ref.getId());
+            // get group
+            ExternalGroup extGroup;
+            try {
+                extGroup = (ExternalGroup) idp.getIdentity(ref);
+            } catch (ClassCastException e) {
+                // this should really not be the case, so catching the CCE is ok here.
+                log.warn("External identity '{}' is not a group, but should be one.", ref.getString());
+                continue;
+            } catch (ExternalIdentityException e) {
+                log.warn("Unable to retrieve external group '{}' from provider.", ref.getString(), e);
+                continue;
+            }
+            if (extGroup == null) {
+                log.warn("External group for ref '{}' could not be retrieved from provider.", ref);
+                continue;
+            }
+            log.debug("- idp returned '{}'", extGroup.getId());
+
+            Group grp;
+            try {
+                grp = (Group) userManager.getAuthorizable(extGroup.getId());
+            } catch (ClassCastException e) {
+                // this should really not be the case, so catching the CCE is ok here.
+                log.warn("Authorizable '{}' is not a group, but should be one.", extGroup.getId());
+                continue;
+            }
+            log.debug("- user manager returned '{}'", grp);
+
+            if (grp == null) {
+                grp = createGroup(extGroup);
+                log.debug("- created new group");
+            }
+            syncGroup(extGroup, grp);
+
+            // ensure membership
+            grp.addMember(auth);
+            log.debug("- added '{}' as member to '{}'", auth, grp);
+
+            // remember the declared group
+            declaredExternalGroups.remove(grp.getID());
+
+            // recursively apply further membership
+            if (depth > 1) {
+                log.debug("- recursively sync group membership of '{}' (depth = {}).", grp.getID(), depth);
+                syncMembership(extGroup, grp, depth - 1);
+            } else {
+                log.debug("- group nesting level for '{}' reached", grp.getID());
+            }
+        }
+        timer.mark("adding");
+        // remove us from the lost membership groups
+        for (Group grp: declaredExternalGroups.values()) {
+            grp.removeMember(auth);
+            log.debug("- removing member '{}' for group '{}'", auth.getID(), grp.getID());
+        }
+        if (log.isDebugEnabled()) {
+            timer.mark("removing");
+            log.debug("syncMembership({}) {}", external.getId(), timer.getString());
+        }
+    }
+
+    /**
+     * Ensures that the given authorizable is member of the specific groups. Note that it does not create groups
+     * if missing, nor remove memberships of groups not in the given set.
+     * @param member the authorizable
+     * @param groups set of groups.
+     */
+    protected void applyMembership(Authorizable member, Set<String> groups) throws RepositoryException {
+        for (String groupName: groups) {
+            Authorizable group = userManager.getAuthorizable(groupName);
+            if (group == null) {
+                log.warn("Unable to apply auto-membership to {}. No such group: {}", member.getID(), groupName);
+            } else if (group instanceof Group) {
+                ((Group) group).addMember(member);
+            } else {
+                log.warn("Unable to apply auto-membership to {}. Authorizable '{}' is not a group.", member.getID(), groupName);
+            }
+        }
+    }
+
+    /**
+     * Syncs the properties specified in the {@code mapping} from the external identity to the given authorizable.
+     * Note that this method does not check for value equality and just blindly copies or deletes the properties.
+     *
+     * @param ext external identity
+     * @param auth the authorizable
+     * @param mapping the property mapping
+     * @throws RepositoryException if an error occurs
+     */
+    protected void syncProperties(ExternalIdentity ext, Authorizable auth, Map<String, String> mapping)
+            throws RepositoryException {
+        Map<String, ?> properties = ext.getProperties();
+        for (Map.Entry<String, String> entry: mapping.entrySet()) {
+            String relPath = entry.getKey();
+            String name = entry.getValue();
+            Object obj = properties.get(name);
+            if (obj == null) {
+                int nameLen = name.length();
+                if (nameLen > 1 && name.charAt(0) == '"' && name.charAt(nameLen-1) == '"') {
+                    auth.setProperty(relPath, valueFactory.createValue(name.substring(1, nameLen - 1)));
+                } else {
+                    auth.removeProperty(relPath);
+                }
+            } else {
+                if (obj instanceof Collection) {
+                    auth.setProperty(relPath, createValues((Collection) obj));
+                } else if (obj instanceof byte[] || obj instanceof char[]) {
+                    auth.setProperty(relPath, createValue(obj));
+                } else if (obj instanceof Object[]) {
+                    auth.setProperty(relPath, createValues(Arrays.asList((Object[]) obj)));
+                } else {
+                    auth.setProperty(relPath, createValue(obj));
+                }
+            }
+        }
+    }
+
+    /**
+     * Checks if the given authorizable needs syncing based on the {@link #REP_LAST_SYNCED} property.
+     * @param auth the authorizable to check
+     * @param expirationTime the expiration time to compare to.
+     * @param type debug message type
+     * @return {@code true} if the authorizable needs sync
+     */
+    protected boolean isExpired(Authorizable auth, long expirationTime, String type) throws RepositoryException {
+        Value[] values = auth.getProperty(REP_LAST_SYNCED);
+        if (values == null || values.length == 0) {
+            if (log.isDebugEnabled()) {
+                log.debug("{} of {} '{}' need sync. " + REP_LAST_SYNCED + " not set.",
+                        type, auth.isGroup() ? "group" : "user", auth.getID());
+            }
+            return true;
+        } else if (now - values[0].getLong() > expirationTime) {
+            if (log.isDebugEnabled()) {
+                log.debug("{} of {} '{}' need sync. " + REP_LAST_SYNCED + " expired ({} > {})",
+                        type, auth.isGroup() ? "group" : "user", auth.getID(), now - values[0].getLong(), expirationTime);
+            }
+            return true;
+        } else {
+            if (log.isDebugEnabled()) {
+                log.debug("{} of {} '{}' do not need sync.", type, auth.isGroup() ? "group" : "user", auth.getID());
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Creates a new JCR value of the given object, checking the internal type.
+     * @param v the value
+     * @return the JCR value or null
+     * @throws RepositoryException if an error occurs
+     */
+    @CheckForNull
+    protected Value createValue(@Nullable Object v) throws RepositoryException {
+        if (v == null) {
+            return null;
+        } else if (v instanceof Boolean) {
+            return valueFactory.createValue((Boolean) v);
+        } else if (v instanceof Byte || v instanceof Short || v instanceof Integer || v instanceof Long) {
+            return valueFactory.createValue(((Number) v).longValue());
+        } else if (v instanceof Float || v instanceof Double) {
+            return valueFactory.createValue(((Number) v).doubleValue());
+        } else if (v instanceof BigDecimal) {
+            return valueFactory.createValue((BigDecimal) v);
+        } else if (v instanceof Calendar) {
+            return valueFactory.createValue((Calendar) v);
+        } else if (v instanceof Date) {
+            Calendar cal = Calendar.getInstance();
+            cal.setTime((Date) v);
+            return valueFactory.createValue(cal);
+        } else if (v instanceof byte[]) {
+            Binary bin = valueFactory.createBinary(new ByteArrayInputStream((byte[])v));
+            return valueFactory.createValue(bin);
+        } else if (v instanceof char[]) {
+            return valueFactory.createValue(new String((char[]) v));
+        } else {
+            return valueFactory.createValue(String.valueOf(v));
+        }
+    }
+
+    /**
+     * Creates an array of JCR values based on the type.
+     * @param propValues the given values
+     * @return and array of JCR values
+     * @throws RepositoryException if an error occurs
+     */
+    @CheckForNull
+    protected Value[] createValues(Collection<?> propValues) throws RepositoryException {
+        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()]);
+    }
+
+    /**
+     * Checks if the given authorizable was synced from the same IDP by comparing the IDP name of the
+     * {@value #REP_EXTERNAL_ID} property.
+     *
+     * todo: allow multiple IDPs on 1 authorizable
+     *
+     * @param auth the authorizable.
+     * @return {@code true} if same IDP.
+     */
+    protected boolean isSameIDP(@Nullable Authorizable auth) throws RepositoryException {
+        ExternalIdentityRef ref = DefaultSyncContext.getIdentityRef(auth);
+        return ref != null && idp.getName().equals(ref.getProviderName());
+    }
+}
\ No newline at end of file

Copied: jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncResultImpl.java (from r1706040, jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java)
URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncResultImpl.java?p2=jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncResultImpl.java&p1=jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java&r1=1706040&r2=1706119&rev=1706119&view=diff
==============================================================================
--- jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java (original)
+++ jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncResultImpl.java Wed Sep 30 18:34:45 2015
@@ -14,25 +14,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
+package org.apache.jackrabbit.oak.spi.security.authentication.external.basic;
+
+import javax.annotation.CheckForNull;
 
 import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncResult;
 
 /**
-* {@code SyncResultImpl}...
-*/
-public class SyncResultImpl implements SyncResult {
+ * Implements a simple sync result with and id and a status.
+ */
+public class DefaultSyncResultImpl implements SyncResult {
 
-    private final SyncedIdentityImpl id;
+    private final DefaultSyncedIdentity id;
 
     private Status status = Status.NOP;
 
-    public SyncResultImpl(SyncedIdentityImpl id, Status status) {
+    public DefaultSyncResultImpl(DefaultSyncedIdentity id, Status status) {
         this.id = id;
         this.status = status;
     }
 
-    public SyncedIdentityImpl getIdentity() {
+    @CheckForNull
+    public DefaultSyncedIdentity getIdentity() {
         return id;
     }
 

Copied: jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncedIdentity.java (from r1706040, jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncedIdentityImpl.java)
URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncedIdentity.java?p2=jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncedIdentity.java&p1=jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncedIdentityImpl.java&r1=1706040&r2=1706119&rev=1706119&view=diff
==============================================================================
--- jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncedIdentityImpl.java (original)
+++ jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/DefaultSyncedIdentity.java Wed Sep 30 18:34:45 2015
@@ -14,17 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
+package org.apache.jackrabbit.oak.spi.security.authentication.external.basic;
 
+import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
 import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncedIdentity;
 
 /**
-* {@code SyncedIdentityImpl}...
-*/
-public class SyncedIdentityImpl implements SyncedIdentity {
+ * Implements a simple synced identity that maps an authorizable id to an external ref.
+ */
+public class DefaultSyncedIdentity implements SyncedIdentity {
 
     private final String id;
 
@@ -34,7 +36,7 @@ public class SyncedIdentityImpl implemen
 
     private final long lastSynced;
 
-    public SyncedIdentityImpl(String id, ExternalIdentityRef ref, boolean isGroup, long lastSynced) {
+    public DefaultSyncedIdentity(@Nonnull String id, @Nullable ExternalIdentityRef ref, boolean isGroup, long lastSynced) {
         this.id = id;
         this.ref = ref;
         this.isGroup = isGroup;
@@ -47,6 +49,7 @@ public class SyncedIdentityImpl implemen
         return id;
     }
 
+    @CheckForNull
     @Override
     public ExternalIdentityRef getExternalIdRef() {
         return ref;

Copied: jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/package-info.java (from r1706040, jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java)
URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/package-info.java?p2=jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/package-info.java&p1=jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java&r1=1706040&r2=1706119&rev=1706119&view=diff
==============================================================================
--- jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/SyncResultImpl.java (original)
+++ jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/basic/package-info.java Wed Sep 30 18:34:45 2015
@@ -14,33 +14,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.jackrabbit.oak.spi.security.authentication.external.impl;
+@Version("1.0.0")
+@Export
+package org.apache.jackrabbit.oak.spi.security.authentication.external.basic;
 
-import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncResult;
-
-/**
-* {@code SyncResultImpl}...
-*/
-public class SyncResultImpl implements SyncResult {
-
-    private final SyncedIdentityImpl id;
-
-    private Status status = Status.NOP;
-
-    public SyncResultImpl(SyncedIdentityImpl id, Status status) {
-        this.id = id;
-        this.status = status;
-    }
-
-    public SyncedIdentityImpl getIdentity() {
-        return id;
-    }
-
-    public Status getStatus() {
-        return status;
-    }
-
-    public void setStatus(Status status) {
-        this.status = status;
-    }
-}
\ No newline at end of file
+import aQute.bnd.annotation.Version;
+import aQute.bnd.annotation.Export;
\ No newline at end of file

Added: jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfigImpl.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfigImpl.java?rev=1706119&view=auto
==============================================================================
--- jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfigImpl.java (added)
+++ jackrabbit/oak/branches/1.0/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/DefaultSyncConfigImpl.java Wed Sep 30 18:34:45 2015
@@ -0,0 +1,273 @@
+/*
+ * 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.HashMap;
+import java.util.Map;
+
+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;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig;
+
+/**
+ * {@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 DefaultSyncConfigImpl extends 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 DefaultSyncConfigImpl.User#getExpirationTime()
+     */
+    public static final String PARAM_USER_EXPIRATION_TIME_DEFAULT = "1h";
+
+    /**
+     * @see DefaultSyncConfigImpl.User#getExpirationTime()
+     */
+    @Property(
+            label = "User Expiration Time",
+            description = "Duration until a synced user gets expired (eg. '1h 30m' or '1d').",
+            value = PARAM_USER_EXPIRATION_TIME_DEFAULT
+    )
+    public static final String PARAM_USER_EXPIRATION_TIME = "user.expirationTime";
+
+    /**
+     * @see DefaultSyncConfigImpl.User#getAutoMembership()
+     */
+    public static final String[] PARAM_USER_AUTO_MEMBERSHIP_DEFAULT = {};
+
+    /**
+     * @see DefaultSyncConfigImpl.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 DefaultSyncConfigImpl.User#getPropertyMapping()
+     */
+    public static final String[] PARAM_USER_PROPERTY_MAPPING_DEFAULT = {"rep:fullname=cn"};
+
+    /**
+     * @see DefaultSyncConfigImpl.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 DefaultSyncConfigImpl.User#getPathPrefix()
+     */
+    public static final String PARAM_USER_PATH_PREFIX_DEFAULT = "";
+
+    /**
+     * @see DefaultSyncConfigImpl.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 DefaultSyncConfigImpl.User#getMembershipExpirationTime()
+     */
+    public static final String PARAM_USER_MEMBERSHIP_EXPIRATION_TIME_DEFAULT = "1h";
+
+    /**
+     * @see DefaultSyncConfigImpl.User#getMembershipExpirationTime()
+     */
+    @Property(
+            label = "User Membership Expiration",
+            description = "Time after which membership expires (eg. '1h 30m' or '1d').",
+            value = 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 DefaultSyncConfigImpl.Group#getExpirationTime()
+     */
+    public static final String PARAM_GROUP_EXPIRATION_TIME_DEFAULT = "1d";
+
+    /**
+     * @see DefaultSyncConfigImpl.Group#getExpirationTime()
+     */
+    @Property(
+            label = "Group Expiration Time",
+            description = "Duration until a synced group expires (eg. '1h 30m' or '1d').",
+            value = PARAM_GROUP_EXPIRATION_TIME_DEFAULT
+    )
+    public static final String PARAM_GROUP_EXPIRATION_TIME = "group.expirationTime";
+
+    /**
+     * @see DefaultSyncConfigImpl.Group#getAutoMembership()
+     */
+    public static final String[] PARAM_GROUP_AUTO_MEMBERSHIP_DEFAULT = {};
+
+    /**
+     * @see DefaultSyncConfigImpl.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 DefaultSyncConfigImpl.Group#getPropertyMapping()
+     */
+    public static final String[] PARAM_GROUP_PROPERTY_MAPPING_DEFAULT = {};
+
+    /**
+     * @see DefaultSyncConfigImpl.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 DefaultSyncConfigImpl.Group#getPathPrefix()
+     */
+    public static final String PARAM_GROUP_PATH_PREFIX_DEFAULT = "";
+
+    /**
+     * @see DefaultSyncConfigImpl.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";
+
+    private static final long MILLIS_PER_HOUR = 60 * 60 * 1000;
+    private static final ConfigurationParameters.Milliseconds ONE_HOUR = ConfigurationParameters.Milliseconds.of(MILLIS_PER_HOUR);
+    private static final ConfigurationParameters.Milliseconds ONE_DAY = ConfigurationParameters.Milliseconds.of(24 * MILLIS_PER_HOUR);
+
+    /**
+     * 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 DefaultSyncConfigImpl()
+                .setName(params.getConfigValue(PARAM_NAME, PARAM_NAME_DEFAULT));
+
+        cfg.user()
+                .setMembershipExpirationTime(getMilliSeconds(params, PARAM_USER_MEMBERSHIP_EXPIRATION_TIME, PARAM_USER_MEMBERSHIP_EXPIRATION_TIME_DEFAULT, ONE_HOUR))
+                .setMembershipNestingDepth(params.getConfigValue(PARAM_USER_MEMBERSHIP_NESTING_DEPTH, PARAM_USER_MEMBERSHIP_NESTING_DEPTH_DEFAULT))
+                .setExpirationTime(getMilliSeconds(params, PARAM_USER_EXPIRATION_TIME, PARAM_USER_EXPIRATION_TIME_DEFAULT, ONE_HOUR))
+                .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(getMilliSeconds(params, PARAM_GROUP_EXPIRATION_TIME, PARAM_GROUP_EXPIRATION_TIME_DEFAULT, ONE_DAY))
+                .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;
+    }
+
+    private static long getMilliSeconds(@Nonnull ConfigurationParameters params, @Nonnull String paramName,
+                                        @Nonnull String defaultParamValue,
+                                        @Nonnull ConfigurationParameters.Milliseconds defaultMillis) {
+        ConfigurationParameters.Milliseconds ms = ConfigurationParameters.Milliseconds.of(params.getConfigValue(paramName, defaultParamValue));
+        return ms == null ? defaultMillis.value : ms.value;
+    }
+
+    /**
+     * 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(@Nonnull 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;
+    }
+
+}
\ No newline at end of file