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 an...@apache.org on 2021/06/24 14:00:46 UTC

[jackrabbit-oak] 01/01: OAK-9462 : Extensible DynamicMembershipProvider

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

angela pushed a commit to branch OAK-9462
in repository https://gitbox.apache.org/repos/asf/jackrabbit-oak.git

commit 91e40e1c51ac4a7d1bfa361bc99cfa984809a17f
Author: angela <an...@adobe.com>
AuthorDate: Thu Jun 24 16:00:32 2021 +0200

    OAK-9462 : Extensible DynamicMembershipProvider
---
 .../impl/principal/AutoMembershipPrincipals.java   |  32 ++
 .../impl/principal/AutoMembershipProvider.java     | 247 +++++++++++++
 .../impl/principal/AutomembershipService.java      |  43 +++
 .../principal/ExternalPrincipalConfiguration.java  |  10 +
 .../principal/AutoMembershipPrincipalsTest.java    |  52 ++-
 .../impl/principal/AutoMembershipProviderTest.java | 386 +++++++++++++++++++++
 .../impl/principal/AutomembershipServiceTest.java  | 114 ++++++
 .../oak/security/user/AuthorizableImpl.java        |  37 +-
 .../oak/security/user/AuthorizableIterator.java    |  44 ++-
 .../security/user/DynamicMembershipTracker.java    | 127 +++++++
 .../security/user/EveryoneMembershipProvider.java  |  75 ++++
 .../jackrabbit/oak/security/user/GroupImpl.java    |  96 ++---
 .../oak/security/user/UserConfigurationImpl.java   |  28 +-
 .../oak/security/user/UserManagerImpl.java         |  19 +-
 .../apache/jackrabbit/oak/security/user/Utils.java |   9 +
 .../security/user/AbstractAddMembersByIdTest.java  |  27 +-
 .../user/AbstractRemoveMembersByIdTest.java        |  24 +-
 .../oak/security/user/AbstractUserTest.java        |  68 ++++
 .../security/user/AuthorizableIteratorTest.java    |  53 +++
 .../user/DynamicMembershipTrackerTest.java         | 136 ++++++++
 .../oak/security/user/GroupImplTest.java           |  14 +-
 .../oak/security/user/MembershipBaseTest.java      |  26 +-
 .../user/UserConfigurationImplOSGiTest.java        |  13 +
 .../security/user/UserManagerImplActionsTest.java  |  11 +-
 .../oak/security/user/UserManagerImplTest.java     |   3 +-
 .../user/query/ResultRowToAuthorizableTest.java    |   7 +-
 .../security/user/query/UserQueryManagerTest.java  |   9 +-
 .../security/user/DynamicMembershipProvider.java   |  89 +++++
 .../security/user/DynamicMembershipService.java    |  36 ++
 .../oak/spi/security/user/package-info.java        |   2 +-
 30 files changed, 1689 insertions(+), 148 deletions(-)

diff --git a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipPrincipals.java b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipPrincipals.java
index 453bcb1..2a9aa35 100644
--- a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipPrincipals.java
+++ b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipPrincipals.java
@@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory;
 import javax.jcr.RepositoryException;
 import java.security.Principal;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -46,6 +47,37 @@ final class AutoMembershipPrincipals {
         this.principalMap = new ConcurrentHashMap<>(autoMembershipMapping.size());
     }
 
+    boolean isConfiguredPrincipal(@NotNull Principal groupPrincipal) {
+        initPrincipalMap();
+        String name = groupPrincipal.getName();
+        for (Set<Principal> principals : principalMap.values()) {
+            if (principals.stream().anyMatch(principal -> name.equals(principal.getName()))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    Set<String> getConfiguredIdpNames(@NotNull Principal groupPrincipal) {
+        initPrincipalMap();
+        String name = groupPrincipal.getName();
+        Set<String> idpNames = new HashSet<>(principalMap.size());
+        principalMap.forEach((idpName, principals) -> {
+            if (principals.stream().anyMatch(principal -> name.equals(principal.getName()))) {
+                idpNames.add(idpName);
+            }
+        });
+        return idpNames;
+    }
+
+    private void initPrincipalMap() {
+        if (principalMap.isEmpty() && !autoMembershipMapping.isEmpty()) {
+            for (String idpName : autoMembershipMapping.keySet()) {
+                getPrincipals(idpName);
+            }
+        }
+    }
+    
     @NotNull
     Collection<Principal> getPrincipals(@Nullable String idpName) {
         if (idpName == null) {
diff --git a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipProvider.java b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipProvider.java
new file mode 100644
index 0000000..cd8cc65
--- /dev/null
+++ b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipProvider.java
@@ -0,0 +1,247 @@
+/*
+ * 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.principal;
+
+import com.google.common.collect.Iterators;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.commons.iterator.AbstractLazyIterator;
+import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
+import org.apache.jackrabbit.oak.api.PropertyValue;
+import org.apache.jackrabbit.oak.api.QueryEngine;
+import org.apache.jackrabbit.oak.api.Result;
+import org.apache.jackrabbit.oak.api.ResultRow;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.namepath.NamePathMapper;
+import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncContext;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.query.Query;
+import java.security.Principal;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+import static org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants.REP_EXTERNAL_ID;
+import static org.apache.jackrabbit.oak.spi.security.user.UserConstants.NT_REP_USER;
+import static org.apache.jackrabbit.oak.spi.security.user.UserConstants.REP_AUTHORIZABLE_ID;
+
+class AutoMembershipProvider implements DynamicMembershipProvider {
+
+    private static final Logger log = LoggerFactory.getLogger(AutoMembershipProvider.class);
+
+    private static final String BINDING_AUTHORIZABLE_IDS = "authorizableIds";
+
+    private final Root root;
+    private final UserManager userManager;
+    private final NamePathMapper namePathMapper;
+    private final AutoMembershipPrincipals autoMembershipPrincipals;
+    
+    AutoMembershipProvider(@NotNull Root root, 
+                           @NotNull UserManager userManager, @NotNull NamePathMapper namePathMapper, 
+                           @NotNull Map<String, String[]> autoMembershipMapping) {
+        this.root = root;
+        this.userManager = userManager;
+        this.namePathMapper = namePathMapper;
+        this.autoMembershipPrincipals = new AutoMembershipPrincipals(userManager, autoMembershipMapping);
+    }
+    
+    @Override
+    public boolean coversAllMembers(@NotNull Group group) {
+        return false;
+    }
+
+    @Override
+    public @NotNull Iterator<Authorizable> getMembers(@NotNull Group group, boolean includeInherited) throws RepositoryException {
+        Principal p = getPrincipalOrNull(group);
+        if (p == null) {
+            return RangeIteratorAdapter.EMPTY;
+        }
+
+        // retrieve all idp-names for which the given group-principal is configured in the auto-membership option
+        // NOTE: while the configuration takes the group-id the cache in 'autoMembershipPrincipals' is built based on the principal
+        Set<String> idpNames = autoMembershipPrincipals.getConfiguredIdpNames(p);
+        if (idpNames.isEmpty()) {
+            return RangeIteratorAdapter.EMPTY;
+        }
+        
+        // since this provider is only enabled for dynamic-automembership only users are expected to be returned by the
+        // query and thus the 'includeInherited' flag can be ignored.
+        List<Iterator<Authorizable>> results = new ArrayList<>(idpNames.size());
+        // TODO: execute a single (more complex) query ?
+        for (String idpName : idpNames) {
+            Map<String, ? extends PropertyValue> bindings = buildBinding(idpName);
+            String statement = "SELECT '" + REP_AUTHORIZABLE_ID + "' FROM ["+NT_REP_USER+"] WHERE PROPERTY(["
+                    + REP_EXTERNAL_ID + "], '" + PropertyType.TYPENAME_STRING + "')"
+                    + " LIKE $" + BINDING_AUTHORIZABLE_IDS + QueryEngine.INTERNAL_SQL2_QUERY;
+            try {
+                Result qResult = root.getQueryEngine().executeQuery(statement, Query.JCR_SQL2, bindings, namePathMapper.getSessionLocalMappings());
+                Iterator<Authorizable> it = StreamSupport.stream(qResult.getRows().spliterator(), false).map((Function<ResultRow, Authorizable>) resultRow -> {
+                    try {
+                        return userManager.getAuthorizableByPath(namePathMapper.getJcrPath(resultRow.getPath()));
+                    } catch (RepositoryException e) {
+                        return null;
+                    }
+                }).filter(Objects::nonNull).iterator();
+                results.add(it);
+            } catch (ParseException e) {
+                throw new RepositoryException("Failed to retrieve members of auto-membership group "+ group);
+            }
+        }
+        return Iterators.concat(results.toArray(new Iterator[0]));
+    }
+
+    @Override
+    public boolean isMember(@NotNull Group group, @NotNull Authorizable authorizable, boolean includeInherited) throws RepositoryException {
+        String idpName = getIdpName(authorizable);
+        if (idpName == null || authorizable.isGroup()) {
+            // not an external user (NOTE: with dynamic membership enabled external groups will not be synced into the repository)
+            return false;
+        }
+
+        // since this provider is only enabled for dynamic-automembership (external groups not synced), the 
+        // 'includeInherited' flag can be ignored.        
+        Collection<Principal> groupPrincipals = autoMembershipPrincipals.getPrincipals(idpName);
+        return groupPrincipals.contains(group.getPrincipal());
+    }
+
+    @Override
+    public @NotNull Iterator<Group> getMembership(@NotNull Authorizable authorizable, boolean includeInherited) throws RepositoryException {
+        String idpName = getIdpName(authorizable);
+        if (idpName == null || authorizable.isGroup()) {
+            // not an external user (NOTE: with dynamic membership enabled external groups will not be synced into the repository)
+            return RangeIteratorAdapter.EMPTY;
+        }
+        Collection<Principal> groupPrincipals = autoMembershipPrincipals.getPrincipals(idpName);
+        Set<Group> groups = groupPrincipals.stream().map(principal -> {
+            try {
+                Authorizable a = userManager.getAuthorizable(principal);
+                if (a != null && a.isGroup()) {
+                    return (Group) a;
+                } else {
+                    return null;
+                }
+            } catch (RepositoryException e) {
+                return null;
+            }
+        }).filter(Objects::nonNull).collect(Collectors.toSet());
+        Iterator<Group> groupIt = new RangeIteratorAdapter(groups);
+        
+        if (!includeInherited) {
+            return groupIt;
+        } else {
+            Set<Group> processed = new HashSet<>();
+            return Iterators.filter(new InheritedMembershipIterator(groupIt), processed::add);
+        }
+    }
+    
+    @Nullable
+    private static Principal getPrincipalOrNull(@NotNull Group group) {
+        try {
+            return group.getPrincipal();
+        } catch (RepositoryException e) {
+            return null;
+        }
+    }
+    
+    @Nullable
+    private static String getIdpName(@NotNull Authorizable authorizable) throws RepositoryException {
+        ExternalIdentityRef ref = DefaultSyncContext.getIdentityRef(authorizable);
+        return (ref == null) ? null : ref.getProviderName();
+    }
+
+    @NotNull
+    private static Map<String, ? extends PropertyValue> buildBinding(@NotNull String idpName) {
+        String val;
+        // idp-name is stored as trailing end after external id followed by ';' => add leading % to the binding
+        StringBuilder sb = new StringBuilder();
+        sb.append("%;");
+        sb.append(idpName.replace("%", "\\%").replace("_", "\\_"));
+        val = sb.toString();
+        return Collections.singletonMap(BINDING_AUTHORIZABLE_IDS, PropertyValues.newString(val));
+    }
+    
+    private static class InheritedMembershipIterator extends AbstractLazyIterator<Group> {
+
+        private final Iterator<Group> groupIterator;
+        private final List<Iterator<Group>> inherited = new ArrayList<>();
+        private Iterator<Group> inheritedIterator = null;
+        
+        private InheritedMembershipIterator(Iterator<Group> groupIterator) {
+            this.groupIterator = groupIterator;
+        }
+        
+        @Override
+        protected Group getNext() {
+            if (groupIterator.hasNext()) {
+                Group gr = groupIterator.next();
+                try {
+                    // call 'memberof' to cover nested inheritance
+                    Iterator<Group> it = gr.memberOf();
+                    if (it.hasNext()) {
+                        inherited.add(it);
+                    }
+                } catch (RepositoryException e) {
+                    log.error("Failed to retrieve membership of group {}", gr, e);
+                }
+                return gr;
+            }
+            
+            if (inheritedIterator == null || !inheritedIterator.hasNext()) {
+                inheritedIterator = getNextInheritedIterator();
+            }
+            
+            if (inheritedIterator.hasNext()) {
+                return inheritedIterator.next();
+            } else {
+                // all inherited groups have been processed
+                return null;
+            }
+        }
+        
+        @NotNull
+        private Iterator<Group> getNextInheritedIterator() {
+            if (inherited.isEmpty()) {
+                // no more inherited groups to retrieve
+                return Iterators.emptyIterator();
+            } else {
+                // no need to verify if the inherited iterator has any elements as this has been asserted before
+                // adding it to the list.
+                return inherited.remove(0);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutomembershipService.java b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutomembershipService.java
new file mode 100644
index 0000000..7950969
--- /dev/null
+++ b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutomembershipService.java
@@ -0,0 +1,43 @@
+/*
+ * 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.principal;
+
+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.spi.security.user.DynamicMembershipProvider;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipService;
+import org.jetbrains.annotations.NotNull;
+
+public class AutomembershipService implements DynamicMembershipService {
+    
+    private final SyncConfigTracker scTracker;
+    
+    public AutomembershipService(@NotNull SyncConfigTracker scTracker) {
+        this.scTracker = scTracker;
+    }
+    
+    @Override
+    @NotNull
+    public DynamicMembershipProvider getDynamicMembershipProvider(@NotNull Root root, @NotNull UserManager userManager, @NotNull NamePathMapper namePathMapper) {
+        if (scTracker.isEnabled()) {
+            return new AutoMembershipProvider(root, userManager, namePathMapper, scTracker.getAutoMembership());
+        } else {
+            return DynamicMembershipProvider.EMPTY;
+        }
+    }
+}
\ No newline at end of file
diff --git a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalPrincipalConfiguration.java b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalPrincipalConfiguration.java
index 0310b53..9eb52f0 100644
--- a/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalPrincipalConfiguration.java
+++ b/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalPrincipalConfiguration.java
@@ -40,12 +40,14 @@ import org.apache.jackrabbit.oak.spi.security.principal.EmptyPrincipalProvider;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalConfiguration;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalManagerImpl;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalProvider;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipService;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter;
 import org.apache.jackrabbit.oak.stats.Monitor;
 import org.apache.jackrabbit.oak.stats.StatisticsProvider;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
 
 import java.security.Principal;
 import java.util.Collections;
@@ -84,6 +86,8 @@ public class ExternalPrincipalConfiguration extends ConfigurationBase implements
     
     private SyncConfigTracker syncConfigTracker;
     private SyncHandlerMappingTracker syncHandlerMappingTracker;
+    
+    private ServiceRegistration automembershipRegistration;
 
     private ExternalIdentityMonitor monitor = ExternalIdentityMonitor.NOOP;
 
@@ -162,6 +166,8 @@ public class ExternalPrincipalConfiguration extends ConfigurationBase implements
 
         syncConfigTracker = new SyncConfigTracker(bundleContext, syncHandlerMappingTracker);
         syncConfigTracker.open();
+        
+        automembershipRegistration = bundleContext.registerService(DynamicMembershipService.class.getName(), new AutomembershipService(syncConfigTracker), null);
     }
 
     @SuppressWarnings("UnusedDeclaration")
@@ -173,6 +179,10 @@ public class ExternalPrincipalConfiguration extends ConfigurationBase implements
         if (syncHandlerMappingTracker != null) {
             syncHandlerMappingTracker.close();
         }
+        
+        if (automembershipRegistration != null) {
+            automembershipRegistration.unregister();
+        }
     }
 
     //------------------------------------------------------------< private >---
diff --git a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipPrincipalsTest.java b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipPrincipalsTest.java
index ad48687..0c23200 100644
--- a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipPrincipalsTest.java
+++ b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipPrincipalsTest.java
@@ -31,9 +31,11 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
@@ -84,7 +86,8 @@ public class AutoMembershipPrincipalsTest extends AbstractAutoMembershipTest {
     @Test
     public void testEmptyMapping() {
         Map<String, String[]> m = spy(new HashMap<>());
-        AutoMembershipPrincipals amprincipals = new AutoMembershipPrincipals(userManager, m);
+        UserManager um = spy(userManager);
+        AutoMembershipPrincipals amprincipals = new AutoMembershipPrincipals(um, m);
         
         assertTrue(amprincipals.getPrincipals(null).isEmpty());
         assertTrue(amprincipals.getPrincipals(IDP_VALID_AM).isEmpty());
@@ -92,5 +95,52 @@ public class AutoMembershipPrincipalsTest extends AbstractAutoMembershipTest {
         verify(m, times(1)).size();
         verify(m, times(1)).get(anyString());
         verifyNoMoreInteractions(m);
+
+        assertFalse(amprincipals.isConfiguredPrincipal(() -> AUTOMEMBERSHIP_GROUP_ID_1));
+        assertTrue(amprincipals.getConfiguredIdpNames(() -> AUTOMEMBERSHIP_GROUP_ID_1).isEmpty());
+
+        verify(m, never()).isEmpty();
+        verifyNoMoreInteractions(m);
+        verifyNoInteractions(um);
+    }
+
+    @Test
+    public void testEmptyMapping2() {
+        Map<String, String[]> m = spy(new HashMap<>());
+        UserManager um = spy(userManager);
+        AutoMembershipPrincipals amprincipals = new AutoMembershipPrincipals(um, m);
+
+        assertFalse(amprincipals.isConfiguredPrincipal(() -> AUTOMEMBERSHIP_GROUP_ID_1));
+        assertFalse(amprincipals.isConfiguredPrincipal(() -> AUTOMEMBERSHIP_GROUP_ID_2));
+        assertFalse(amprincipals.isConfiguredPrincipal(() -> NON_EXISTING_GROUP_ID));
+
+        assertTrue(amprincipals.getConfiguredIdpNames(() -> AUTOMEMBERSHIP_GROUP_ID_1).isEmpty());
+        assertTrue(amprincipals.getConfiguredIdpNames(() -> AUTOMEMBERSHIP_GROUP_ID_2).isEmpty());
+        assertTrue(amprincipals.getConfiguredIdpNames(() -> NON_EXISTING_GROUP_ID).isEmpty());
+
+        verify(m, times(6)).isEmpty();
+        verify(m).size();
+        verifyNoMoreInteractions(m);
+
+        assertTrue(amprincipals.getPrincipals(null).isEmpty());
+        assertTrue(amprincipals.getPrincipals(IDP_VALID_AM).isEmpty());
+
+        verify(m, times(1)).get(anyString());
+        verifyNoMoreInteractions(m);
+        verifyNoInteractions(um);
+    }
+
+    @Test
+    public void testIsConfiguredPrincipal() {
+        assertTrue(amp.isConfiguredPrincipal(() -> AUTOMEMBERSHIP_GROUP_ID_1));
+        assertTrue(amp.isConfiguredPrincipal(() -> AUTOMEMBERSHIP_GROUP_ID_2));
+        assertFalse(amp.isConfiguredPrincipal(() -> NON_EXISTING_GROUP_ID));
+    }
+
+    @Test
+    public void testGetConfiguredIdpNames() {
+        assertEquals(ImmutableSet.of(IDP_VALID_AM, IDP_MIXED_AM), amp.getConfiguredIdpNames(() -> AUTOMEMBERSHIP_GROUP_ID_1));
+        assertEquals(ImmutableSet.of(IDP_VALID_AM), amp.getConfiguredIdpNames(() -> AUTOMEMBERSHIP_GROUP_ID_2));
+        assertTrue(amp.getConfiguredIdpNames(() -> NON_EXISTING_GROUP_ID).isEmpty());
     }
 }
\ No newline at end of file
diff --git a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipProviderTest.java b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipProviderTest.java
new file mode 100644
index 0000000..8d2bcd9
--- /dev/null
+++ b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipProviderTest.java
@@ -0,0 +1,386 @@
+/*
+ * 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.principal;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterators;
+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.QueryEngine;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.jetbrains.annotations.NotNull;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.jcr.RepositoryException;
+import java.security.Principal;
+import java.text.ParseException;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants.REP_EXTERNAL_ID;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+public class AutoMembershipProviderTest extends AbstractAutoMembershipTest {
+
+    private AutoMembershipProvider provider;
+
+    @Before
+    public void before() throws Exception {
+        super.before();
+        provider = new AutoMembershipProvider(root, userManager, getNamePathMapper(), MAPPING);
+    }
+    
+    @After
+    public void after() throws Exception {
+        try {
+            root.refresh();
+            Authorizable a = userManager.getAuthorizable(USER_ID);
+            if (a != null) {
+                a.remove();
+            }
+            root.commit();
+        } finally {
+            super.after();
+        }
+    }
+    
+    private void setExternalId(@NotNull String id, @NotNull String idpName) throws Exception {
+        Root sr = getSystemRoot();
+        sr.refresh();
+        Authorizable a = getUserManager(sr).getAuthorizable(id);
+        a.setProperty(REP_EXTERNAL_ID, getValueFactory(sr).createValue(new ExternalIdentityRef(USER_ID, idpName).getString()));
+        sr.commit();
+        root.refresh();
+    }
+
+    @Test
+    public void testCoversAllMembers() throws RepositoryException {
+        assertFalse(provider.coversAllMembers(automembershipGroup1));
+        assertFalse(provider.coversAllMembers(userManager.createGroup(EveryonePrincipal.getInstance())));
+        assertFalse(provider.coversAllMembers(mock(Group.class)));
+    }
+    
+    @Test
+    public void testGetMembersNoExternalUsers() throws Exception {
+        // no user has rep:externalId set to the configured IPD-names
+        assertFalse(provider.getMembers(automembershipGroup1, true).hasNext());
+        assertFalse(provider.getMembers(automembershipGroup1, false).hasNext());
+    }
+
+    @Test
+    public void testGetMembersExternalUser() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+        
+        Iterator<Authorizable> it = provider.getMembers(automembershipGroup1, false);
+        assertTrue(it.hasNext());
+        assertEquals(getTestUser().getID(), it.next().getID());
+        assertFalse(it.hasNext());
+
+        it = provider.getMembers(automembershipGroup1, true);
+        assertTrue(it.hasNext());
+        assertEquals(getTestUser().getID(), it.next().getID());
+        assertFalse(it.hasNext());
+    }
+
+    @Test
+    public void testGetMembersExternalUserIdpMismatch() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_INVALID_AM);
+
+        Iterator<Authorizable> it = provider.getMembers(automembershipGroup1, false);
+        assertFalse(it.hasNext());
+
+        it = provider.getMembers(automembershipGroup1, true);
+        assertFalse(it.hasNext());
+    }
+
+    @Test
+    public void testGetMembersExternalUserMultipleIdps() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+        User u = null;
+        try {
+            u = userManager.createUser("second", null);
+            root.commit();
+            setExternalId("second", IDP_MIXED_AM);
+
+            Iterator<Authorizable> it = provider.getMembers(automembershipGroup1, false);
+            assertEquals(2, Iterators.size(it));
+
+            it = provider.getMembers(automembershipGroup1, true);
+            assertEquals(2, Iterators.size(it));
+        } finally {
+            if (u != null) {
+                u.remove();
+                root.commit();
+            }
+        }
+    }
+    
+    @Test
+    public void testGetMembersExternalGroup() throws Exception {
+        setExternalId(automembershipGroup1.getID(), IDP_VALID_AM);
+        
+        Iterator<Authorizable> it = provider.getMembers(automembershipGroup1, false);
+        assertFalse(it.hasNext());
+
+        it = provider.getMembers(automembershipGroup1, true);
+        assertFalse(it.hasNext());
+    }
+    
+    @Test
+    public void testGetMembersCannotRetrievePrincipalFromGroup() throws Exception {
+        Group gr = mock(Group.class);
+        when(gr.getPrincipal()).thenThrow(new RepositoryException());
+        
+        assertFalse(provider.getMembers(gr, false).hasNext());
+        assertFalse(provider.getMembers(gr, true).hasNext());
+    }
+    
+    @Test
+    public void testGetMembersGroupNotConfigured() throws Exception {
+        Group gr = mock(Group.class);
+        when(gr.getPrincipal()).thenReturn(EveryonePrincipal.getInstance());
+        
+        assertFalse(provider.getMembers(gr, false).hasNext());
+        assertFalse(provider.getMembers(gr, true).hasNext());
+    }
+
+    @Test
+    public void testGetMembersLookupByPathFails() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+
+        UserManager um = spy(userManager);
+        doThrow(new RepositoryException()).when(um).getAuthorizableByPath(anyString());
+        
+        AutoMembershipProvider amp = new AutoMembershipProvider(root, um, getNamePathMapper(), MAPPING);
+        assertFalse(amp.getMembers(automembershipGroup1, false).hasNext());
+    }
+
+    @Test(expected = RepositoryException.class)
+    public void testGetMembersQueryFails() throws Exception {
+        QueryEngine qe = mock(QueryEngine.class);
+        when(qe.executeQuery(anyString(), anyString(), any(Map.class), any(Map.class))).thenThrow(new ParseException("query failed", 0));
+        Root r = when(mock(Root.class).getQueryEngine()).thenReturn(qe).getMock();
+        
+        AutoMembershipProvider amp = new AutoMembershipProvider(r, userManager, getNamePathMapper(), MAPPING);
+        assertFalse(amp.getMembers(automembershipGroup1, false).hasNext());
+    }
+    
+    @Test
+    public void testIsMemberLocalUser() throws Exception {
+        assertFalse(provider.isMember(automembershipGroup1, getTestUser(), true));
+        assertFalse(provider.isMember(automembershipGroup1, getTestUser(), false));
+    }
+
+    @Test
+    public void testIsMemberSelf() throws Exception {
+        assertFalse(provider.isMember(automembershipGroup1, automembershipGroup1, true));
+        assertFalse(provider.isMember(automembershipGroup1, automembershipGroup1, false));
+    }
+
+    @Test
+    public void testIsMemberExternalUser() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+
+        assertTrue(provider.isMember(automembershipGroup1, getTestUser(), false));
+        assertTrue(provider.isMember(automembershipGroup1, getTestUser(), true));
+    }
+    
+    @Test
+    public void testIsMemberExternalUserIdpMismatch() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_INVALID_AM);
+
+        assertFalse(provider.isMember(automembershipGroup1, getTestUser(), false));
+    }
+
+    @Test
+    public void testIsMemberExternalGroup() throws Exception {
+        setExternalId(automembershipGroup1.getID(), IDP_VALID_AM);
+        
+        assertFalse(provider.isMember(automembershipGroup1, automembershipGroup1, false));
+        assertFalse(provider.isMember(automembershipGroup1, automembershipGroup1, true));
+    }
+    
+    @Test
+    public void testGetMembershipLocalUser() throws Exception {
+        assertFalse(provider.getMembership(getTestUser(), true).hasNext());
+        assertFalse(provider.getMembership(getTestUser(), false).hasNext());
+    }
+
+    @Test
+    public void testGetMembershipSelf() throws Exception {
+        assertFalse(provider.getMembership(automembershipGroup1, true).hasNext());
+        assertFalse(provider.getMembership(automembershipGroup1, false).hasNext());
+    }
+    
+    @Test
+    public void testGetMembershipExternalUser() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+
+        Set<Group> groups = ImmutableSet.copyOf(provider.getMembership(getTestUser(), false));
+        assertEquals(2, groups.size());
+        assertTrue(groups.contains(automembershipGroup1));
+        assertTrue(groups.contains(automembershipGroup2));
+    }
+
+    @Test
+    public void testGetMembershipExternalUserInherited() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+
+        Set<Group> groups = ImmutableSet.copyOf(provider.getMembership(getTestUser(), true));
+        assertEquals(2, groups.size());
+        assertTrue(groups.contains(automembershipGroup1));
+    }
+
+    @Test
+    public void testGetMembershipExternalUserNestedGroups() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+        
+        Group baseGroup = userManager.createGroup("baseGroup");
+        try {
+            baseGroup.addMember(automembershipGroup1);
+            root.commit();
+
+            Set<Group> groups = ImmutableSet.copyOf(provider.getMembership(getTestUser(), false));
+            assertEquals(2, groups.size());
+            assertTrue(groups.contains(automembershipGroup1));
+            assertTrue(groups.contains(automembershipGroup2));
+
+            groups = ImmutableSet.copyOf(provider.getMembership(getTestUser(), true));
+            assertEquals(3, groups.size());
+            assertTrue(groups.contains(automembershipGroup1));
+            assertTrue(groups.contains(automembershipGroup2));
+            assertTrue(groups.contains(baseGroup));
+        } finally {
+            baseGroup.remove();
+            root.commit();
+        }
+    }
+
+    @Test
+    public void testGetMembershipExternalUserEveryoneGroupExists() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+
+        // create dynamic group everyone. both automembershipGroups are members thereof without explicit add-member
+        Group everyone = userManager.createGroup(EveryonePrincipal.getInstance());
+        // in addition establish a 2 level inheritance for automembershipGroup1 
+        automembershipGroup2.addMember(automembershipGroup1);
+        root.commit();
+
+        Set<Group> groups = ImmutableSet.copyOf(provider.getMembership(getTestUser(), false));
+        assertEquals(2, groups.size());
+        assertTrue(groups.contains(automembershipGroup1));
+        assertTrue(groups.contains(automembershipGroup2));
+
+        groups = ImmutableSet.copyOf(provider.getMembership(getTestUser(), true));
+        assertEquals(3, groups.size()); // all duplicates must be properly filtered
+        assertTrue(groups.contains(automembershipGroup1));
+        assertTrue(groups.contains(automembershipGroup2));
+        assertTrue(groups.contains(everyone));
+    }
+
+    @Test
+    public void testGetMembershipExternalUserIdpMismatch() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_INVALID_AM);
+
+        assertFalse(provider.getMembership(getTestUser(), false).hasNext());
+        assertFalse(provider.getMembership(getTestUser(), true).hasNext());
+    }
+
+    @Test
+    public void testGetMembershipExternalGroup() throws Exception {
+        setExternalId(automembershipGroup1.getID(), IDP_VALID_AM);
+
+        assertFalse(provider.getMembership(automembershipGroup1, false).hasNext());
+        assertFalse(provider.getMembership(automembershipGroup1, true).hasNext());
+    }
+    
+    @Test
+    public void testGetMembershipAutogroupIsUser() throws Exception {
+        UserManager um = spy(userManager);
+        
+        User user = mock(User.class);
+        when(user.isGroup()).thenReturn(false);
+        when(um.getAuthorizable(automembershipGroup1.getPrincipal())).thenReturn(user);
+        when(um.getAuthorizable(automembershipGroup2.getPrincipal())).thenReturn(user);
+
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+
+        AutoMembershipProvider amp = new AutoMembershipProvider(root, um, getNamePathMapper(), MAPPING);
+        assertFalse(amp.getMembership(getTestUser(), false).hasNext());
+    }
+
+    @Test
+    public void testGetMembershipAutogroupGroupLookupFails() throws Exception {
+        UserManager um = spy(userManager);
+
+        User user = mock(User.class);
+        when(user.isGroup()).thenReturn(false);
+        when(um.getAuthorizable(any(Principal.class))).thenThrow(new RepositoryException());
+
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+        
+        AutoMembershipProvider amp = new AutoMembershipProvider(root, um, getNamePathMapper(), MAPPING);
+        assertFalse(amp.getMembership(getTestUser(), false).hasNext());
+    }
+
+    @Test
+    public void testGetMembershipAutogroupGroupMemberOfFails() throws Exception {
+        // establish nested groups
+        automembershipGroup2.addMember(automembershipGroup1);
+        root.commit();
+        
+        Group spiedGroup = spy(automembershipGroup1);
+        when(spiedGroup.memberOf()).thenThrow(new RepositoryException());
+        
+        UserManager um = spy(userManager);
+        when(um.getAuthorizable(automembershipGroup1.getPrincipal())).thenReturn(spiedGroup);
+
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+
+        AutoMembershipProvider amp = new AutoMembershipProvider(root, um, getNamePathMapper(), MAPPING);
+        Set<Group> membership = ImmutableSet.copyOf(amp.getMembership(getTestUser(), true));
+        assertEquals(2, membership.size());
+    }
+    
+    @Test
+    public void testGetMembershipAutogroupRemoved() throws Exception {
+        setExternalId(getTestUser().getID(), IDP_VALID_AM);
+        
+        automembershipGroup1.remove();
+        assertEquals(1, Iterators.size(provider.getMembership(getTestUser(), false)));
+        assertEquals(1, Iterators.size(provider.getMembership(getTestUser(), true)));
+        
+        automembershipGroup2.remove();
+        assertFalse(provider.getMembership(getTestUser(), false).hasNext());
+        assertFalse(provider.getMembership(getTestUser(), true).hasNext());
+    }
+}
\ No newline at end of file
diff --git a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutomembershipServiceTest.java b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutomembershipServiceTest.java
new file mode 100644
index 0000000..8d6d3ae
--- /dev/null
+++ b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutomembershipServiceTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.principal;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncHandler;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.SyncHandlerMapping;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipProvider;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncConfigImpl.PARAM_NAME;
+import static org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncConfigImpl.PARAM_USER_AUTO_MEMBERSHIP;
+import static org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncConfigImpl.PARAM_USER_DYNAMIC_MEMBERSHIP;
+import static org.apache.jackrabbit.oak.spi.security.authentication.external.impl.SyncHandlerMapping.PARAM_IDP_NAME;
+import static org.apache.jackrabbit.oak.spi.security.authentication.external.impl.SyncHandlerMapping.PARAM_SYNC_HANDLER_NAME;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class AutomembershipServiceTest extends AbstractAutoMembershipTest {
+    
+    private SyncHandlerMappingTracker mappingTracker;
+    private AutomembershipService service;
+    private SyncConfigTracker scTracker;
+    
+    @Before
+    public void before() throws Exception {
+        super.before();
+
+        mappingTracker = new SyncHandlerMappingTracker(context.bundleContext());
+        mappingTracker.open();
+        
+        scTracker = new SyncConfigTracker(context.bundleContext(), mappingTracker);
+        scTracker.open();
+        
+        service = new AutomembershipService(scTracker);
+        
+        assertFalse(scTracker.isEnabled());
+    }
+    
+    @After
+    public void after() throws Exception {
+        try {
+            mappingTracker.close();
+            scTracker.close();
+        } finally {
+            super.after();
+        }
+    }
+    
+    private static Map<String, String> getMappingParams() {
+        return ImmutableMap.of(PARAM_IDP_NAME, IDP_VALID_AM, PARAM_SYNC_HANDLER_NAME, "sh");
+    }
+    
+    private static Map<String, Object> getSyncHandlerParams() {
+        return ImmutableMap.of(
+                PARAM_USER_DYNAMIC_MEMBERSHIP, true,
+                PARAM_NAME, "sh",
+                PARAM_USER_AUTO_MEMBERSHIP, new String[] {AUTOMEMBERSHIP_GROUP_ID_1});
+    }
+    
+    @Test
+    public void testMissingDynamicMembership() {
+        assertSame(DynamicMembershipProvider.EMPTY, service.getDynamicMembershipProvider(root, userManager, getNamePathMapper()));
+    }
+
+    @Test
+    public void testDynamicMembership() {
+        context.registerService(SyncHandler.class, new DefaultSyncHandler(), getSyncHandlerParams());
+        assertTrue(scTracker.isEnabled());
+        
+        context.registerService(SyncHandlerMapping.class, new SyncHandlerMapping() {}, getMappingParams());
+        assertTrue(service.getDynamicMembershipProvider(root, userManager, getNamePathMapper()) instanceof AutoMembershipProvider);
+    }
+    
+    @Test
+    public void testRegistered() {
+        ExternalPrincipalConfiguration pc = new ExternalPrincipalConfiguration();
+        context.registerInjectActivateService(pc);
+
+        DynamicMembershipService s = context.getService(DynamicMembershipService.class);
+        assertNotNull(s);
+        assertTrue(s instanceof AutomembershipService);
+
+        assertSame(DynamicMembershipProvider.EMPTY, service.getDynamicMembershipProvider(root, userManager, getNamePathMapper()));
+        
+        context.registerService(SyncHandlerMapping.class, new SyncHandlerMapping() {}, getMappingParams());
+        assertSame(DynamicMembershipProvider.EMPTY, service.getDynamicMembershipProvider(root, userManager, getNamePathMapper()));
+
+        context.registerService(SyncHandler.class, new DefaultSyncHandler(), getSyncHandlerParams());
+        assertTrue(service.getDynamicMembershipProvider(root, userManager, getNamePathMapper()) instanceof AutoMembershipProvider);
+    }
+}
\ No newline at end of file
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableImpl.java
index 668f91a..d7e9834 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableImpl.java
@@ -16,14 +16,7 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
-import java.util.Collections;
-import java.util.Iterator;
-import javax.jcr.RepositoryException;
-import javax.jcr.Value;
-
 import com.google.common.base.Stopwatch;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterators;
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.User;
@@ -31,14 +24,19 @@ import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
-import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
 import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipProvider;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import java.util.Collections;
+import java.util.Iterator;
+
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 import static org.apache.jackrabbit.oak.api.Type.STRING;
 
@@ -278,22 +276,21 @@ abstract class AuthorizableImpl implements Authorizable, UserConstants {
     @NotNull
     private Iterator<Group> getMembership(boolean includeInherited) throws RepositoryException {
         if (isEveryone()) {
-            return Collections.<Group>emptySet().iterator();
+            return Collections.emptyIterator();
         }
 
+        DynamicMembershipProvider dmp = userManager.getDynamicMembershipProvider();
+        Iterator<Group> dynamicGroups = dmp.getMembership(this, includeInherited);
+        
         MembershipProvider mMgr = getMembershipProvider();
         Iterator<String> oakPaths = mMgr.getMembership(getTree(), includeInherited);
-
-        Authorizable everyoneGroup = userManager.getAuthorizable(EveryonePrincipal.getInstance());
-        if (everyoneGroup instanceof GroupImpl) {
-            String everyonePath = ((GroupImpl) everyoneGroup).getTree().getPath();
-            oakPaths = Iterators.concat(oakPaths, ImmutableSet.of(everyonePath).iterator());
-        }
-        if (oakPaths.hasNext()) {
-            AuthorizableIterator groups = AuthorizableIterator.create(oakPaths, userManager, AuthorizableType.GROUP);
-            return new RangeIteratorAdapter(groups, groups.getSize());
-        } else {
-            return RangeIteratorAdapter.EMPTY;
+        
+        if (!oakPaths.hasNext()) {
+            return dynamicGroups;
         }
+        
+        AuthorizableIterator groups = AuthorizableIterator.create(oakPaths, userManager, AuthorizableType.GROUP);
+        AuthorizableIterator allGroups = AuthorizableIterator.create(true, dynamicGroups, groups);
+        return new RangeIteratorAdapter(allGroups);
     }
 }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableIterator.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableIterator.java
index 9ac7e21..a9676a2 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableIterator.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/AuthorizableIterator.java
@@ -17,9 +17,9 @@
 package org.apache.jackrabbit.oak.security.user;
 
 import com.google.common.base.Function;
-import com.google.common.base.Predicates;
 import com.google.common.collect.Iterators;
 import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.oak.commons.LongUtils;
 import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
 import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
@@ -27,7 +27,10 @@ import org.slf4j.LoggerFactory;
 
 import javax.jcr.RangeIterator;
 import javax.jcr.RepositoryException;
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Objects;
+import java.util.Set;
 import java.util.function.Predicate;
 
 /**
@@ -39,17 +42,44 @@ final class AuthorizableIterator implements Iterator<Authorizable> {
 
     private final Iterator<Authorizable> authorizables;
     private final long size;
+    private final Set<String> servedIds;
 
     static AuthorizableIterator create(Iterator<String> authorizableOakPaths,
                                        UserManagerImpl userManager,
                                        AuthorizableType authorizableType) {
         Iterator<Authorizable> it = Iterators.transform(authorizableOakPaths, new PathToAuthorizable(userManager, authorizableType));
         long size = getSize(authorizableOakPaths);
-        return new AuthorizableIterator(Iterators.filter(it, Predicates.notNull()), size);
+        return new AuthorizableIterator(it, size, false);
+    }
+    
+    static AuthorizableIterator create(boolean filterDuplicates, @NotNull Iterator<? extends Authorizable> it1, @NotNull Iterator<? extends Authorizable> it2) {
+        long size = 0;
+        for (Iterator<?> it : new Iterator[] {it1, it2}) {
+            long l = getSize(it);
+            if (l == -1) {
+                size = -1;
+                break;
+            } else {
+                size = LongUtils.safeAdd(size, l);
+            }
+        }
+        return new AuthorizableIterator(Iterators.concat(it1, it2), size, filterDuplicates);
     }
 
-    private AuthorizableIterator(Iterator<Authorizable> authorizables, long size) {
-        this.authorizables = authorizables;
+    private AuthorizableIterator(Iterator<Authorizable> authorizables, long size, boolean filterDuplicates) {
+        if (filterDuplicates)  {
+            this.servedIds = new HashSet<>();
+            this.authorizables = Iterators.filter(authorizables, authorizable -> {
+                if (authorizable == null) {
+                    return false;
+                }
+                String id = Utils.getIdOrNull(authorizable);
+                return id != null && servedIds.add(id);
+            });
+        } else {
+            this.servedIds = null;
+            this.authorizables = Iterators.filter(authorizables, Objects::nonNull);
+        }
         this.size = size;
     }
 
@@ -76,9 +106,13 @@ final class AuthorizableIterator implements Iterator<Authorizable> {
 
     //--------------------------------------------------------------------------
 
-    private static long getSize(Iterator<String> it) {
+    private static long getSize(Iterator<?> it) {
         if (it instanceof RangeIterator) {
             return ((RangeIterator) it).getSize();
+        } else if (it instanceof AuthorizableIterator) {
+            return ((AuthorizableIterator) it).getSize();
+        } else if (!it.hasNext()) {
+            return 0;
         } else {
             return -1;
         }
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/DynamicMembershipTracker.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/DynamicMembershipTracker.java
new file mode 100644
index 0000000..6d37eb7
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/DynamicMembershipTracker.java
@@ -0,0 +1,127 @@
+/*
+ * 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.user;
+
+import com.google.common.collect.Iterators;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+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.spi.security.user.DynamicMembershipProvider;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipService;
+import org.apache.jackrabbit.oak.spi.whiteboard.AbstractServiceTracker;
+import org.jetbrains.annotations.NotNull;
+
+import javax.jcr.RepositoryException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class DynamicMembershipTracker extends AbstractServiceTracker<DynamicMembershipService>  implements DynamicMembershipService {
+
+    public DynamicMembershipTracker() {
+        super(DynamicMembershipService.class);
+    }
+
+    @Override
+    @NotNull
+    public DynamicMembershipProvider getDynamicMembershipProvider(@NotNull Root root, @NotNull UserManager userManager, @NotNull NamePathMapper namePathMapper) {
+        DynamicMembershipProvider defaultProvider = new EveryoneMembershipProvider(userManager, namePathMapper);
+        List<DynamicMembershipService> services = getServices();
+        if (services.isEmpty()) {
+            return defaultProvider;
+        } else {
+            return createProvider(root, userManager, namePathMapper, defaultProvider, services);
+        }
+    }
+    
+    /**
+     * Initialize DynamicMembershipProvider instances.
+     * NOTE: Since providers are are created on demand for a given {@code UserManager} instance and Session instances are 
+     * expected to be short-lived compared to the frequency of service registrations, no effort is made to keep the 
+     * the list updated.
+     */
+    private static DynamicMembershipProvider createProvider(@NotNull Root root, @NotNull UserManager userManager, 
+                                                            @NotNull NamePathMapper namePathMapper, 
+                                                            @NotNull DynamicMembershipProvider defaultProvider, 
+                                                            @NotNull List<DynamicMembershipService> services) {
+        List<DynamicMembershipProvider> providers = new ArrayList<>(1+services.size());
+        providers.add(defaultProvider);
+        for (DynamicMembershipService service : services) {
+            DynamicMembershipProvider dmp = service.getDynamicMembershipProvider(root, userManager, namePathMapper);
+            if (DynamicMembershipProvider.EMPTY != dmp) {
+                providers.add(dmp);
+            }
+        }
+
+        if (providers.size() == 1) {
+            return defaultProvider;
+        } else {
+            return new CompositeProvider(providers);
+        }
+    }
+
+    private static class CompositeProvider implements DynamicMembershipProvider {
+        
+        private final List<DynamicMembershipProvider> providers;
+        
+        private CompositeProvider(@NotNull List<DynamicMembershipProvider> providers) {
+            this.providers = providers;
+        }
+        
+        @Override
+        public boolean coversAllMembers(@NotNull Group group) {
+            for (DynamicMembershipProvider provider : providers) {
+                if (provider.coversAllMembers(group)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public @NotNull Iterator<Authorizable> getMembers(@NotNull Group group, boolean includeInherited) throws RepositoryException {
+            int size = providers.size();
+            Iterator<Authorizable>[] members = new Iterator[size];
+            for (int i = 0; i < size; i++) {
+                members[i] = providers.get(i).getMembers(group, includeInherited);
+            }
+            return Iterators.concat(members);
+        }
+
+        @Override
+        public boolean isMember(@NotNull Group group, @NotNull Authorizable authorizable, boolean includeInherited) throws RepositoryException {
+            for (DynamicMembershipProvider provider : providers) {
+                if (provider.isMember(group, authorizable, includeInherited)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public @NotNull Iterator<Group> getMembership(@NotNull Authorizable authorizable, boolean includeInherited) throws RepositoryException {
+            int size = providers.size();
+            Iterator<Group>[] groups = new Iterator[size];
+            for (int i = 0; i < size; i++) {
+                groups[i] = providers.get(i).getMembership(authorizable, includeInherited);
+            }
+            return Iterators.concat(groups);
+        }
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/EveryoneMembershipProvider.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/EveryoneMembershipProvider.java
new file mode 100644
index 0000000..2c3b4aa
--- /dev/null
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/EveryoneMembershipProvider.java
@@ -0,0 +1,75 @@
+/*
+ * 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.user;
+
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterators;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
+import org.apache.jackrabbit.oak.namepath.NamePathMapper;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipProvider;
+import org.jetbrains.annotations.NotNull;
+
+import javax.jcr.RepositoryException;
+import java.util.Collections;
+import java.util.Iterator;
+
+import static org.apache.jackrabbit.oak.spi.security.user.UserConstants.REP_PRINCIPAL_NAME;
+
+class EveryoneMembershipProvider implements DynamicMembershipProvider {
+
+    private final UserManager userManager;
+    private final String repPrincipalName;
+    
+    EveryoneMembershipProvider(@NotNull UserManager userManager, @NotNull NamePathMapper namePathMapper)  {
+        this.userManager = userManager;
+        this.repPrincipalName = namePathMapper.getJcrName(REP_PRINCIPAL_NAME);
+    }
+    
+    @Override
+    public boolean coversAllMembers(@NotNull Group group) {
+        return Utils.isEveryone(group);
+    }
+
+    @Override
+    public @NotNull Iterator<Authorizable> getMembers(@NotNull Group group, boolean includeInherited) throws RepositoryException {
+        if (Utils.isEveryone(group)) {
+            Iterator<Authorizable> result = Iterators.filter(userManager.findAuthorizables(repPrincipalName, null, UserManager.SEARCH_TYPE_AUTHORIZABLE), Predicates.notNull());
+            return Iterators.filter(result, authorizable -> !Utils.isEveryone(authorizable));
+        } else {
+            return RangeIteratorAdapter.EMPTY;
+        }
+    }
+
+    @Override
+    public boolean isMember(@NotNull Group group, @NotNull Authorizable authorizable, boolean includeInherited) throws RepositoryException {
+        return Utils.isEveryone(group);
+    }
+
+    @Override
+    public @NotNull Iterator<Group> getMembership(@NotNull Authorizable authorizable, boolean includeInherited) throws RepositoryException {
+        Authorizable everyoneGroup = userManager.getAuthorizable(EveryonePrincipal.getInstance());
+        if (everyoneGroup instanceof Group) {
+            return new RangeIteratorAdapter(Collections.singleton((Group) everyoneGroup));
+        } else {
+            return RangeIteratorAdapter.EMPTY;
+        }
+    }
+}
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java
index 26ee080..5e71533 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java
@@ -16,17 +16,8 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
-import java.security.Principal;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
-import javax.jcr.RepositoryException;
-import javax.jcr.nodetype.ConstraintViolationException;
-
-import com.google.common.base.Predicates;
 import com.google.common.base.Stopwatch;
 import com.google.common.base.Strings;
-import com.google.common.collect.Iterators;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import org.apache.jackrabbit.api.security.user.Authorizable;
@@ -35,12 +26,20 @@ import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
 import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipProvider;
 import org.apache.jackrabbit.oak.spi.security.user.util.UserUtil;
 import org.apache.jackrabbit.oak.spi.xml.ImportBehavior;
 import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import java.security.Principal;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
 /**
@@ -111,9 +110,14 @@ class GroupImpl extends AuthorizableImpl implements Group {
             return false;
         }
 
+        DynamicMembershipProvider dmp = getUserManager().getDynamicMembershipProvider();
+        if (dmp.coversAllMembers(this)) {
+            log.debug("Attempt to add member to dynamic group {}", getID());
+            return false;
+        }
         AuthorizableImpl authorizableImpl = ((AuthorizableImpl) authorizable);
-        if (isEveryone() || authorizableImpl.isEveryone()) {
-            log.debug("Attempt to add member to everyone group or create membership for it.");
+        if (authorizableImpl.isEveryone()) {
+            log.debug("Attempt to create membership for everyone group.");
             return false;
         }
 
@@ -159,9 +163,15 @@ class GroupImpl extends AuthorizableImpl implements Group {
             return false;
         }
 
+        DynamicMembershipProvider dmp = getUserManager().getDynamicMembershipProvider();
+        if (dmp.coversAllMembers(this)) {
+            log.debug("Attempt to remove member from dynamic group {}", getID());
+            return false;
+        }
+        
         AuthorizableImpl authorizableImpl = ((AuthorizableImpl) authorizable);
-        if (isEveryone() || authorizableImpl.isEveryone()) {
-            log.debug("Attempt to remove member from everyone group or remove membership for it.");
+        if (authorizableImpl.isEveryone()) {
+            log.debug("Attempt to remove membership for everyone group.");
             return false;
         } else {
             Tree memberTree = authorizableImpl.getTree();
@@ -199,20 +209,22 @@ class GroupImpl extends AuthorizableImpl implements Group {
     @NotNull
     private Iterator<Authorizable> getMembers(boolean includeInherited) throws RepositoryException {
         UserManagerImpl userMgr = getUserManager();
-        if (isEveryone()) {
-            String propName = userMgr.getNamePathMapper().getJcrName((REP_PRINCIPAL_NAME));
-            Iterator<Authorizable> result = Iterators.filter(userMgr.findAuthorizables(propName, null, UserManager.SEARCH_TYPE_AUTHORIZABLE), Predicates.notNull());
-            return Iterators.filter(result, authorizable -> !Utils.isEveryone(authorizable)
-            );
-        } else {
-            Iterator<String> oakPaths = getMembershipProvider().getMembers(getTree(), includeInherited);
-            if (oakPaths.hasNext()) {
-                AuthorizableIterator iterator = AuthorizableIterator.create(oakPaths, userMgr, AuthorizableType.AUTHORIZABLE);
-                return new RangeIteratorAdapter(iterator, iterator.getSize());
-            } else {
-                return RangeIteratorAdapter.EMPTY;
-            }
+
+        DynamicMembershipProvider dmp = getUserManager().getDynamicMembershipProvider();
+        Iterator<Authorizable> dynamicMembers = dmp.getMembers(this, includeInherited);
+        if (dmp.coversAllMembers(this)) {
+            return dynamicMembers;
+        }
+
+        // dynamic membership didn't cover all members -> extract from group-tree
+        Iterator<String> oakPaths = getMembershipProvider().getMembers(getTree(), includeInherited);
+        if (!oakPaths.hasNext()) {
+            return dynamicMembers;
         }
+        
+        AuthorizableIterator members = AuthorizableIterator.create(oakPaths, userMgr, AuthorizableType.AUTHORIZABLE);
+        AuthorizableIterator allMembers = AuthorizableIterator.create(true, dynamicMembers, members);
+        return new RangeIteratorAdapter(allMembers, allMembers.getSize()); 
     }
 
     /**
@@ -229,21 +241,22 @@ class GroupImpl extends AuthorizableImpl implements Group {
         if (!isValidAuthorizableImpl(authorizable)) {
             return false;
         }
-
-        if (getID().equals(authorizable.getID())) {
+        if (getID().equals(authorizable.getID()) || ((AuthorizableImpl) authorizable).isEveryone()) {
             return false;
-        } else if (isEveryone()) {
+        }
+
+        DynamicMembershipProvider dmp = getUserManager().getDynamicMembershipProvider();
+        if (dmp.isMember(this, authorizable, includeInherited)) {
             return true;
-        } else if (((AuthorizableImpl) authorizable).isEveryone()) {
-            return false;
+        }
+
+        // no dynamic membership -> regular membership provider needs to evaluate
+        Tree authorizableTree = ((AuthorizableImpl) authorizable).getTree();
+        MembershipProvider mgr = getUserManager().getMembershipProvider();
+        if (includeInherited) {
+            return mgr.isMember(this.getTree(), authorizableTree);
         } else {
-            Tree authorizableTree = ((AuthorizableImpl) authorizable).getTree();
-            MembershipProvider mgr = getUserManager().getMembershipProvider();
-            if (includeInherited) {
-                return mgr.isMember(this.getTree(), authorizableTree);
-            } else {
-                return mgr.isDeclaredMember(this.getTree(), authorizableTree);
-            }
+            return mgr.isDeclaredMember(this.getTree(), authorizableTree);
         }
     }
 
@@ -272,9 +285,10 @@ class GroupImpl extends AuthorizableImpl implements Group {
         Set<String> failedIds = Sets.newHashSet(memberIds);
         int importBehavior = UserUtil.getImportBehavior(getUserManager().getConfig());
 
-        if (isEveryone()) {
-            String msg = "Attempt to add or remove from everyone group.";
-            log.debug(msg);
+        DynamicMembershipProvider dmp = getUserManager().getDynamicMembershipProvider();
+        if (dmp.coversAllMembers(this)) {
+            String msg = "Attempt to add to or remove from dynamic group {}.";
+            log.debug(msg, getID());
             return failedIds;
         }
 
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java
index 4f47156..34f9faf 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java
@@ -27,6 +27,7 @@ import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.api.blob.BlobAccessProvider;
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
+import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
 import org.apache.jackrabbit.oak.plugins.value.jcr.PartialValueFactory;
 import org.apache.jackrabbit.oak.security.user.autosave.AutoSaveEnabledManager;
 import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
@@ -54,8 +55,10 @@ import org.apache.jackrabbit.oak.stats.Monitor;
 import org.apache.jackrabbit.oak.stats.StatisticsProvider;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
+import org.osgi.framework.BundleContext;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
 import org.osgi.service.component.annotations.Reference;
 import org.osgi.service.component.annotations.ReferenceCardinality;
 import org.osgi.service.component.annotations.ReferencePolicy;
@@ -177,6 +180,12 @@ public class UserConfigurationImpl extends ConfigurationBase implements UserConf
 
     private static final UserAuthenticationFactory DEFAULT_AUTH_FACTORY = new UserAuthenticationFactoryImpl();
 
+    private UserMonitor monitor = UserMonitor.NOOP;
+
+    private BlobAccessProvider blobAccessProvider;
+
+    private final DynamicMembershipTracker dynamicMembership = new DynamicMembershipTracker();
+    
     public UserConfigurationImpl() {
         super();
     }
@@ -192,16 +201,17 @@ public class UserConfigurationImpl extends ConfigurationBase implements UserConf
     @SuppressWarnings("UnusedDeclaration")
     @Activate
     // reference to @Configuration class needed for correct DS xml generation
-    private void activate(Configuration configuration, Map<String, Object> properties) {
+    private void activate(Configuration configuration, BundleContext bundleContext, Map<String, Object> properties) {
         setParameters(ConfigurationParameters.of(properties));
+        dynamicMembership.start(new OsgiWhiteboard(bundleContext));
+    }
+    
+    @Deactivate
+    private void deactivate() {
+        dynamicMembership.stop();
     }
-
-    private UserMonitor monitor = UserMonitor.NOOP;
-
-    private BlobAccessProvider blobAccessProvider;
     
-    @Reference(cardinality = ReferenceCardinality.OPTIONAL,
-            policy = ReferencePolicy.DYNAMIC)
+    @Reference(cardinality = ReferenceCardinality.OPTIONAL, policy = ReferencePolicy.DYNAMIC)
     void bindBlobAccessProvider(BlobAccessProvider bap) {
         blobAccessProvider = bap;
     }
@@ -251,7 +261,7 @@ public class UserConfigurationImpl extends ConfigurationBase implements UserConf
     @NotNull
     @Override
     public List<ProtectedItemImporter> getProtectedItemImporters() {
-        return Collections.<ProtectedItemImporter>singletonList(new UserImporter(getParameters()));
+        return Collections.singletonList(new UserImporter(getParameters()));
     }
 
     @NotNull
@@ -271,7 +281,7 @@ public class UserConfigurationImpl extends ConfigurationBase implements UserConf
     @Override
     public UserManager getUserManager(Root root, NamePathMapper namePathMapper) {
         PartialValueFactory vf = new PartialValueFactory(namePathMapper, getBlobAccessProvider());
-        UserManager umgr = new UserManagerImpl(root, vf, getSecurityProvider(), monitor);
+        UserManagerImpl umgr = new UserManagerImpl(root, vf, getSecurityProvider(), monitor, dynamicMembership);
         if (getParameters().getConfigValue(UserConstants.PARAM_SUPPORT_AUTOSAVE, false)) {
             return new AutoSaveEnabledManager(umgr, root);
         } else {
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java
index c0d88a1..19d77a4 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserManagerImpl.java
@@ -40,6 +40,8 @@ import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalConfiguration;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
 import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipProvider;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipService;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction;
@@ -84,11 +86,15 @@ public class UserManagerImpl implements UserManager {
 
     private UserQueryManager queryManager;
     private ReadOnlyNodeTypeManager ntMgr;
-
+    
+    private final DynamicMembershipService dynamicMembership;
+    private DynamicMembershipProvider dynamicMembershipProvider;
+    
     public UserManagerImpl(@NotNull Root root,
                            @NotNull PartialValueFactory valueFactory,
                            @NotNull SecurityProvider securityProvider,
-                           @NotNull UserMonitor monitor) {
+                           @NotNull UserMonitor monitor, 
+                           @NotNull DynamicMembershipService dynamicMembershipService) {
         this.root = root;
         this.valueFactory = valueFactory;
         this.namePathMapper = valueFactory.getNamePathMapper();
@@ -99,6 +105,7 @@ public class UserManagerImpl implements UserManager {
         this.config = uc.getParameters();
         this.userProvider = new UserProvider(root, config);
         this.membershipProvider = new MembershipProvider(root, config);
+        this.dynamicMembership = dynamicMembershipService;
         this.actionProvider = getActionProvider(config);
     }
 
@@ -446,6 +453,14 @@ public class UserManagerImpl implements UserManager {
     MembershipProvider getMembershipProvider() {
         return membershipProvider;
     }
+    
+    @NotNull 
+    DynamicMembershipProvider getDynamicMembershipProvider() {
+        if (dynamicMembershipProvider == null) {
+            dynamicMembershipProvider = dynamicMembership.getDynamicMembershipProvider(root, this, namePathMapper);
+        }
+        return dynamicMembershipProvider;
+    }
 
     @NotNull
     PrincipalManager getPrincipalManager() {
diff --git a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/Utils.java b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/Utils.java
index 70e697f..f0f1903 100644
--- a/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/Utils.java
+++ b/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/Utils.java
@@ -94,4 +94,13 @@ final class Utils {
             }
         }
     }
+    
+    @Nullable
+    static String getIdOrNull(@NotNull Authorizable authorizable) {
+        try {
+            return authorizable.getID();
+        } catch (RepositoryException e) {
+            return null;
+        }
+    }
 }
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java
index 6d5c1fb..3824174 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java
@@ -16,32 +16,30 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
-import java.util.Iterator;
-import java.util.Set;
-import java.util.UUID;
-import javax.jcr.RepositoryException;
-import javax.jcr.SimpleCredentials;
-import javax.jcr.nodetype.ConstraintViolationException;
-import javax.jcr.security.AccessControlManager;
-
 import com.google.common.collect.Iterables;
 import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
-import org.apache.jackrabbit.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.api.ContentSession;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.api.Type;
-import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
 import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
 import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Test;
 
+import javax.jcr.RepositoryException;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.security.AccessControlManager;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.UUID;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -51,26 +49,23 @@ import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
-public abstract class AbstractAddMembersByIdTest extends AbstractSecurityTest {
+public abstract class AbstractAddMembersByIdTest extends AbstractUserTest {
 
     static final String[] NON_EXISTING_IDS = new String[] {"nonExisting1", "nonExisting2"};
 
     Group testGroup;
     Group memberGroup;
-
-    private final UserMonitor monitor = mock(UserMonitor.class);
-
+    
     @Override
     public void before() throws Exception {
         super.before();
 
-        UserManager uMgr = new UserManagerImpl(root, getPartialValueFactory(), getSecurityProvider(), monitor);
+        UserManager uMgr = createUserManagerImpl(root);
         for (String id : NON_EXISTING_IDS) {
             assertNull(uMgr.getAuthorizable(id));
         }
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractRemoveMembersByIdTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractRemoveMembersByIdTest.java
index 03b0126..0832f49 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractRemoveMembersByIdTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractRemoveMembersByIdTest.java
@@ -16,24 +16,21 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
-import java.util.Set;
-import java.util.UUID;
-
-import javax.jcr.SimpleCredentials;
-import javax.jcr.nodetype.ConstraintViolationException;
-import javax.jcr.security.AccessControlManager;
-
 import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
-import org.apache.jackrabbit.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.api.ContentSession;
 import org.apache.jackrabbit.oak.api.Root;
-import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
 import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
 import org.junit.Test;
 
+import javax.jcr.SimpleCredentials;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.security.AccessControlManager;
+import java.util.Set;
+import java.util.UUID;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
@@ -41,24 +38,21 @@ import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 
-public abstract class AbstractRemoveMembersByIdTest extends AbstractSecurityTest {
+public abstract class AbstractRemoveMembersByIdTest extends AbstractUserTest {
 
     static final String[] NON_EXISTING_IDS = new String[] {"nonExisting1", "nonExisting2"};
 
     Group testGroup;
     Group memberGroup;
-
-    private final UserMonitor monitor = mock(UserMonitor.class);
-
+    
     @Override
     public void before() throws Exception {
         super.before();
 
-        UserManager uMgr = new UserManagerImpl(root, getPartialValueFactory(), getSecurityProvider(), monitor);
+        UserManager uMgr = createUserManagerImpl(root);
         for (String id : NON_EXISTING_IDS) {
             assertNull(uMgr.getAuthorizable(id));
         }
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractUserTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractUserTest.java
new file mode 100644
index 0000000..ede265f
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractUserTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.user;
+
+import org.apache.jackrabbit.oak.AbstractSecurityTest;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
+import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
+import org.apache.jackrabbit.oak.spi.whiteboard.DefaultWhiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.jackrabbit.oak.spi.whiteboard.WhiteboardAware;
+import org.jetbrains.annotations.NotNull;
+import org.junit.After;
+import org.junit.Before;
+
+import static org.mockito.Mockito.mock;
+
+public abstract class AbstractUserTest extends AbstractSecurityTest {
+
+    final UserMonitor monitor = mock(UserMonitor.class);
+    final DynamicMembershipTracker dynamicMembershipService = new DynamicMembershipTracker();
+
+    @Before
+    public void before() throws Exception {
+        super.before();
+        SecurityProvider sp = getSecurityProvider();
+        Whiteboard wb = null;
+        if (sp instanceof WhiteboardAware) {
+            wb = ((WhiteboardAware)sp).getWhiteboard();
+        }
+        if (wb == null) {
+            wb = new DefaultWhiteboard();
+        }
+        dynamicMembershipService.start(wb);
+    }
+    
+    @After
+    public void after() throws Exception {
+        try {
+            dynamicMembershipService.stop();
+        } finally {
+            super.after();
+        }
+    }
+    
+    protected UserMonitor getUserMonitor() {
+        return monitor;
+    }
+    
+    protected UserManagerImpl createUserManagerImpl(@NotNull Root root) {
+        return new UserManagerImpl(root, getPartialValueFactory(), getSecurityProvider(), getUserMonitor(), dynamicMembershipService);
+    }
+    
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AuthorizableIteratorTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AuthorizableIteratorTest.java
index eef697e..bf1358e 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AuthorizableIteratorTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AuthorizableIteratorTest.java
@@ -16,7 +16,10 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterators;
+import com.google.common.collect.Lists;
+import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.User;
 import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
 import org.apache.jackrabbit.oak.AbstractSecurityTest;
@@ -25,11 +28,16 @@ import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
 import org.junit.Before;
 import org.junit.Test;
 
+import javax.jcr.RepositoryException;
+import java.util.Collections;
 import java.util.Iterator;
+import java.util.List;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class AuthorizableIteratorTest extends AbstractSecurityTest {
 
@@ -87,4 +95,49 @@ public class AuthorizableIteratorTest extends AbstractSecurityTest {
         AuthorizableIterator it = AuthorizableIterator.create(Iterators.singletonIterator(PathUtils.ROOT_PATH), (UserManagerImpl) getUserManager(root), AuthorizableType.AUTHORIZABLE);
         assertFalse(it.hasNext());
     }
+    
+    @Test
+    public void testFilterDuplicates() throws Exception {
+        List<Authorizable> l = ImmutableList.of(getTestUser());
+        assertEquals(1, Iterators.size(AuthorizableIterator.create(true, l.iterator(), l.iterator())));
+        assertEquals(2, Iterators.size(AuthorizableIterator.create(false, l.iterator(), l.iterator())));
+        
+        // duplications are determined base on authorizableID
+        Authorizable a = when(mock(Authorizable.class).getID()).thenReturn(getTestUser().getID()).getMock();
+        assertEquals(1, Iterators.size(AuthorizableIterator.create(true, l.iterator(), Iterators.singletonIterator(a))));
+    }
+
+    @Test
+    public void testFilterDuplicatesHandlesNull() throws Exception {
+        List<User> l = Lists.newArrayList(getTestUser(), null, getTestUser());
+        assertEquals(1, Iterators.size(AuthorizableIterator.create(true, l.iterator(), l.iterator())));
+    }
+
+    @Test
+    public void testFilterDuplicatesGetIdFails() throws Exception {
+        Authorizable a = when(mock(Authorizable.class).getID()).thenThrow(new RepositoryException()).getMock();
+
+        List<Authorizable> l = ImmutableList.of(getTestUser(), a);
+        assertEquals(1, Iterators.size(AuthorizableIterator.create(true, l.iterator(), Collections.emptyIterator())));
+    }
+    
+    @Test
+    public void testGetSize3() throws Exception {
+        List<User> l = Lists.newArrayList(getTestUser());
+
+        // size cannot be computed from regular iterators
+        assertEquals(-1, AuthorizableIterator.create(false, l.iterator(), l.iterator()).getSize());
+        assertEquals(-1, AuthorizableIterator.create(true, l.iterator(), l.iterator()).getSize());
+
+        // size can be computed from regular iterators but filters are only apply upon iteration
+        RangeIteratorAdapter adapter = new RangeIteratorAdapter(l);
+        assertEquals(2, AuthorizableIterator.create(false, adapter, adapter).getSize());
+        assertEquals(2, AuthorizableIterator.create(true, adapter, adapter).getSize());
+    }
+    
+    @Test
+    public void testGetSize4() {
+        assertEquals(0, AuthorizableIterator.create(Iterators.emptyIterator(), (UserManagerImpl) getUserManager(root), AuthorizableType.AUTHORIZABLE).getSize());
+        assertEquals(0, AuthorizableIterator.create(true, Iterators.emptyIterator(), Iterators.emptyIterator()).getSize());
+    }
 }
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/DynamicMembershipTrackerTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/DynamicMembershipTrackerTest.java
new file mode 100644
index 0000000..94605c0
--- /dev/null
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/DynamicMembershipTrackerTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.user;
+
+import com.google.common.collect.Iterators;
+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.oak.AbstractSecurityTest;
+import org.apache.jackrabbit.oak.osgi.OsgiWhiteboard;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipProvider;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipService;
+import org.apache.jackrabbit.oak.spi.whiteboard.Whiteboard;
+import org.apache.sling.testing.mock.osgi.MapUtil;
+import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.osgi.framework.ServiceRegistration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DynamicMembershipTrackerTest extends AbstractSecurityTest {
+
+    @Rule
+    public final OsgiContext context = new OsgiContext();
+    
+    private final Whiteboard whiteboard = new OsgiWhiteboard(context.bundleContext());
+    private final DynamicMembershipTracker dynamicMembership = new DynamicMembershipTracker();
+    
+    private final List<ServiceRegistration> registrations = new ArrayList<>();
+    private Group gr;
+    
+    @Before
+    public void before() throws Exception {
+        super.before();
+        dynamicMembership.start(whiteboard);
+        gr = getUserManager(root).createGroup("groupId");
+        root.commit();
+    }
+    
+    @After
+    public void after() throws Exception {
+        try {
+            for (ServiceRegistration registration : registrations) {
+                registration.unregister();
+            }
+            dynamicMembership.stop();
+            gr.remove();
+            root.commit();
+        } finally {
+            super.after();
+        }
+    }
+    
+    @Test
+    public void testNoServiceRegistered() {
+        DynamicMembershipProvider dmp = dynamicMembership.getDynamicMembershipProvider(root, getUserManager(root), getNamePathMapper());
+        assertTrue(dmp instanceof EveryoneMembershipProvider);
+    }
+
+    @Test
+    public void testServiceWithDefaultProvider() {
+        DynamicMembershipService dms = (root, userManager, namePathMapper) -> DynamicMembershipProvider.EMPTY;
+        registrations.add(context.bundleContext().registerService(DynamicMembershipService.class.getName(), dms, MapUtil.toDictionary(Collections.emptyMap())));
+        
+        DynamicMembershipProvider dmp = dynamicMembership.getDynamicMembershipProvider(root, getUserManager(root), getNamePathMapper());
+        assertTrue(dmp instanceof EveryoneMembershipProvider);
+    }
+    
+    @Test
+    public void testServiceWithCustomProvider() throws Exception {
+        Authorizable a = mock(Authorizable.class);
+        User testUser = getTestUser();
+
+        DynamicMembershipProvider dmp = mock(DynamicMembershipProvider.class);
+        when(dmp.getMembership(eq(a), anyBoolean())).thenReturn(Iterators.singletonIterator(gr));
+        when(dmp.getMembership(eq(testUser), anyBoolean())).thenReturn(Iterators.emptyIterator());
+        
+        when(dmp.isMember(eq(gr), eq(a), anyBoolean())).thenReturn(true);
+        when(dmp.coversAllMembers(gr)).thenReturn(true);
+        when(dmp.getMembers(eq(gr), anyBoolean())).thenReturn(Iterators.singletonIterator(a));
+        
+        DynamicMembershipService dms = (root, userManager, namePathMapper) -> dmp;
+        registrations.add(context.bundleContext().registerService(DynamicMembershipService.class.getName(), dms, MapUtil.toDictionary(Collections.emptyMap())));
+
+        DynamicMembershipProvider provider = dynamicMembership.getDynamicMembershipProvider(root, getUserManager(root), getNamePathMapper());
+        assertFalse(dmp instanceof EveryoneMembershipProvider);
+        
+        // verify dmp is properly wired
+        assertTrue(Iterators.contains(provider.getMembership(a, false), gr));
+        assertFalse(Iterators.contains(provider.getMembership(testUser, false), gr));
+        
+        assertTrue(provider.coversAllMembers(gr));
+        assertFalse(provider.coversAllMembers(mock(Group.class)));
+        
+        assertTrue(provider.isMember(gr, a, false));
+        assertFalse(provider.isMember(gr, testUser, true));
+        
+        assertTrue(Iterators.contains(provider.getMembers(gr, true), a));
+        assertFalse(Iterators.contains(provider.getMembers(gr, true), testUser));
+        
+        // verify that EveryoneMembershipProvider is covered as well
+        Group everyone = mock(Group.class);
+        when(everyone.isGroup()).thenReturn(true);
+        when(everyone.getPrincipal()).thenReturn(EveryonePrincipal.getInstance());
+        assertTrue(provider.coversAllMembers(everyone));
+        assertTrue(provider.isMember(everyone, testUser, false));
+        assertTrue(provider.isMember(everyone, a, false));
+    }
+}
\ No newline at end of file
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/GroupImplTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/GroupImplTest.java
index c078491..6f1de72 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/GroupImplTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/GroupImplTest.java
@@ -16,25 +16,22 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
-import java.security.Principal;
-import java.util.Iterator;
-import java.util.UUID;
-
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterators;
 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.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.api.Type;
-import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
 import org.junit.Test;
 
 import javax.jcr.nodetype.ConstraintViolationException;
+import java.security.Principal;
+import java.util.Iterator;
+import java.util.UUID;
 
 import static org.apache.jackrabbit.oak.spi.security.user.UserConstants.REP_MEMBERS;
 import static org.junit.Assert.assertEquals;
@@ -48,19 +45,18 @@ import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
-public class GroupImplTest extends AbstractSecurityTest {
+public class GroupImplTest extends AbstractUserTest {
 
     private final String groupId = "gr" + UUID.randomUUID();
 
     private UserManagerImpl uMgr;
     private GroupImpl group;
-    private final UserMonitor monitor = mock(UserMonitor.class);
 
     @Override
     public void before() throws Exception {
         super.before();
 
-        uMgr = new UserManagerImpl(root, getPartialValueFactory(), getSecurityProvider(), monitor);
+        uMgr = createUserManagerImpl(root);
         Group g = uMgr.createGroup(groupId);
 
         group = new GroupImpl(groupId, root.getTree(g.getPath()), uMgr);
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/MembershipBaseTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/MembershipBaseTest.java
index 3e693a3..6aa3181 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/MembershipBaseTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/MembershipBaseTest.java
@@ -16,39 +16,36 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import javax.jcr.RepositoryException;
-
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import org.apache.jackrabbit.JcrConstants;
 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.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Tree;
-import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
-import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
+import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 
+import javax.jcr.RepositoryException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.mock;
 
-public abstract class MembershipBaseTest extends AbstractSecurityTest implements UserConstants {
+public abstract class MembershipBaseTest extends AbstractUserTest implements UserConstants {
 
     static final int SIZE_TH = 10;
 
@@ -57,7 +54,6 @@ public abstract class MembershipBaseTest extends AbstractSecurityTest implements
 
     UserManagerImpl userMgr;
     MembershipProvider mp;
-    final UserMonitor monitor = mock(UserMonitor.class);
 
     private final Set<String> testUsers = new HashSet<>();
     private final Set<String> testGroups = new HashSet<>();
@@ -65,7 +61,7 @@ public abstract class MembershipBaseTest extends AbstractSecurityTest implements
     @Before
     public void before() throws Exception {
         super.before();
-        userMgr = new UserManagerImpl(root, getPartialValueFactory(), getSecurityProvider(), monitor);
+        userMgr = createUserManagerImpl(root);
         mp = userMgr.getMembershipProvider();
         // set the threshold low for testing
         mp.setMembershipSizeThreshold(SIZE_TH);
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplOSGiTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplOSGiTest.java
index 2ed97a7..34576d6 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplOSGiTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImplOSGiTest.java
@@ -22,6 +22,7 @@ import org.apache.jackrabbit.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.api.blob.BlobAccessProvider;
 import org.apache.jackrabbit.oak.plugins.value.jcr.PartialValueFactory;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
+import org.apache.jackrabbit.oak.spi.security.SecurityConfiguration;
 import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
@@ -36,6 +37,8 @@ import java.util.Hashtable;
 
 import static org.apache.jackrabbit.oak.spi.security.user.UserConstants.PARAM_DEFAULT_DEPTH;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.mock;
@@ -55,6 +58,16 @@ public class UserConfigurationImplOSGiTest extends AbstractSecurityTest {
         ConfigurationParameters params = userConfiguration.getParameters();
         assertEquals(8, params.getConfigValue(PARAM_DEFAULT_DEPTH, UserConstants.DEFAULT_DEPTH).intValue());
     }
+    
+    @Test
+    public void testDeactivate() {
+        UserConfiguration userConfiguration = new UserConfigurationImpl(getSecurityProvider());
+        ServiceRegistration sr = context.bundleContext().registerService(new String[] {UserConfiguration.class.getName(), SecurityConfiguration.class.getName()}, 
+                userConfiguration, null);
+        assertNotNull(context.getService(UserConfiguration.class));
+        sr.unregister();
+        assertNull(context.getService(UserConfiguration.class));
+    }
 
     @Test
     public void testBlobAccessProviderFromNullWhiteboard() throws Exception {
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplActionsTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplActionsTest.java
index 32df7f5..98deca9 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplActionsTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplActionsTest.java
@@ -19,11 +19,9 @@ package org.apache.jackrabbit.oak.security.user;
 import com.google.common.collect.ImmutableSet;
 import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.User;
-import org.apache.jackrabbit.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.commons.UUIDUtils;
 import org.apache.jackrabbit.oak.namepath.NamePathMapper;
-import org.apache.jackrabbit.oak.plugins.value.jcr.PartialValueFactory;
 import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
@@ -57,7 +55,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.withSettings;
 
-public class UserManagerImplActionsTest extends AbstractSecurityTest {
+public class UserManagerImplActionsTest extends AbstractUserTest {
 
     private final AuthorizableActionProvider actionProvider = mock(AuthorizableActionProvider.class);
     private final AuthorizableAction action = mock(AuthorizableAction.class, withSettings().extraInterfaces(GroupAction.class, UserAction.class));
@@ -67,7 +65,7 @@ public class UserManagerImplActionsTest extends AbstractSecurityTest {
     @Before
     public void before() throws Exception {
         super.before();
-        userMgr = new UserManagerImpl(root, new PartialValueFactory(getNamePathMapper()), securityProvider, UserMonitor.NOOP);
+        userMgr = createUserManagerImpl(root);
         reset(action);
     }
 
@@ -82,6 +80,11 @@ public class UserManagerImplActionsTest extends AbstractSecurityTest {
     }
 
     @Override
+    protected UserMonitor getUserMonitor() {
+        return UserMonitor.NOOP;
+    }
+
+    @Override
     protected ConfigurationParameters getSecurityConfigParameters() {
         List actions = Collections.singletonList(action);
         when(actionProvider.getAuthorizableActions(any(SecurityProvider.class))).thenReturn(actions);
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.java
index a829e86..e57838a 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserManagerImplTest.java
@@ -37,6 +37,7 @@ import org.apache.jackrabbit.oak.plugins.value.jcr.PartialValueFactory;
 import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
 import org.apache.jackrabbit.oak.security.user.monitor.UserMonitorImpl;
 import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
+import org.apache.jackrabbit.oak.spi.security.user.DynamicMembershipService;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.apache.jackrabbit.oak.spi.security.user.util.PasswordUtil;
@@ -90,7 +91,7 @@ public class UserManagerImplTest extends AbstractSecurityTest {
     }
 
     private UserManagerImpl createUserManager(@NotNull Root root, @NotNull PartialValueFactory pvf) {
-        return new UserManagerImpl(root, pvf, getSecurityProvider(), UserMonitor.NOOP);
+        return new UserManagerImpl(root, pvf, getSecurityProvider(), UserMonitor.NOOP, mock(DynamicMembershipService.class));
     }
 
     /**
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/query/ResultRowToAuthorizableTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/query/ResultRowToAuthorizableTest.java
index 9be3576..7a25c62 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/query/ResultRowToAuthorizableTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/query/ResultRowToAuthorizableTest.java
@@ -18,7 +18,6 @@ package org.apache.jackrabbit.oak.security.user.query;
 
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.User;
-import org.apache.jackrabbit.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.api.ContentSession;
 import org.apache.jackrabbit.oak.api.PropertyValue;
 import org.apache.jackrabbit.oak.api.ResultRow;
@@ -27,8 +26,8 @@ import org.apache.jackrabbit.oak.api.Tree;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.plugins.memory.PropertyValues;
 import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
+import org.apache.jackrabbit.oak.security.user.AbstractUserTest;
 import org.apache.jackrabbit.oak.security.user.UserManagerImpl;
-import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
 import org.apache.jackrabbit.oak.spi.query.QueryConstants;
 import org.apache.jackrabbit.oak.spi.security.user.AuthorizableType;
 import org.jetbrains.annotations.NotNull;
@@ -45,7 +44,7 @@ import static org.junit.Assert.assertNull;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
-public class ResultRowToAuthorizableTest extends AbstractSecurityTest {
+public class ResultRowToAuthorizableTest extends AbstractUserTest {
 
     private ResultRowToAuthorizable groupRrta;
     private ResultRowToAuthorizable userRrta;
@@ -61,7 +60,7 @@ public class ResultRowToAuthorizableTest extends AbstractSecurityTest {
 
     @NotNull
     private ResultRowToAuthorizable createResultRowToAuthorizable(@NotNull Root r, @Nullable AuthorizableType targetType) {
-        UserManagerImpl umgr = new UserManagerImpl(r, getPartialValueFactory(), getSecurityProvider(), UserMonitor.NOOP);
+        UserManagerImpl umgr = createUserManagerImpl(r);
         return new ResultRowToAuthorizable(umgr, r, targetType);
     }
 
diff --git a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/query/UserQueryManagerTest.java b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/query/UserQueryManagerTest.java
index 563a120..599bc10 100644
--- a/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/query/UserQueryManagerTest.java
+++ b/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/query/UserQueryManagerTest.java
@@ -25,14 +25,13 @@ import org.apache.jackrabbit.api.security.user.Query;
 import org.apache.jackrabbit.api.security.user.QueryBuilder;
 import org.apache.jackrabbit.api.security.user.User;
 import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
-import org.apache.jackrabbit.oak.AbstractSecurityTest;
 import org.apache.jackrabbit.oak.api.ContentSession;
 import org.apache.jackrabbit.oak.api.Root;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.query.QueryEngineSettings;
 import org.apache.jackrabbit.oak.security.internal.SecurityProviderBuilder;
+import org.apache.jackrabbit.oak.security.user.AbstractUserTest;
 import org.apache.jackrabbit.oak.security.user.UserManagerImpl;
-import org.apache.jackrabbit.oak.security.user.monitor.UserMonitor;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
 import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
@@ -71,7 +70,7 @@ import static org.junit.Assert.assertTrue;
  * This class include the original jr2.x test-cases provided by
  * {@code NodeResolverTest} and {@code IndexNodeResolverTest}.
  */
-public class UserQueryManagerTest extends AbstractSecurityTest {
+public class UserQueryManagerTest extends AbstractUserTest {
 
     private ValueFactory valueFactory;
     private UserQueryManager queryMgr;
@@ -556,7 +555,7 @@ public class UserQueryManagerTest extends AbstractSecurityTest {
     public void testFindWhenRootTreeIsSearchRoot() throws Exception {
         ConfigurationParameters config = ConfigurationParameters.of(PARAM_GROUP_PATH, PathUtils.ROOT_PATH);
         SecurityProvider sp = SecurityProviderBuilder.newBuilder().with(ConfigurationParameters.of(UserConfiguration.NAME, config)).withRootProvider(getRootProvider()).withTreeProvider(getTreeProvider()).build();
-        UserManagerImpl umgr = new UserManagerImpl(root, getPartialValueFactory(), sp, UserMonitor.NOOP);
+        UserManagerImpl umgr = createUserManagerImpl(root);
         UserQueryManager uqm = new UserQueryManager(umgr, getNamePathMapper(), config, root);
 
         Iterator<Authorizable> result = uqm.findAuthorizables(REP_AUTHORIZABLE_ID, DEFAULT_ADMIN_ID, AuthorizableType.AUTHORIZABLE);
@@ -589,7 +588,7 @@ public class UserQueryManagerTest extends AbstractSecurityTest {
 
         try (ContentSession cs = login(new SimpleCredentials(user.getID(), user.getID().toCharArray()))) {
             Root r = cs.getLatestRoot();
-            UserManagerImpl uMgr = new UserManagerImpl(r, getPartialValueFactory(), getSecurityProvider(), UserMonitor.NOOP);
+            UserManagerImpl uMgr = createUserManagerImpl(r);
             UserQueryManager uqm = new UserQueryManager(uMgr, getNamePathMapper(), ConfigurationParameters.EMPTY, r);
 
             Iterator<Authorizable> result = uqm.findAuthorizables("name", "userName", AuthorizableType.USER);
diff --git a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/DynamicMembershipProvider.java b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/DynamicMembershipProvider.java
new file mode 100644
index 0000000..c773cfc
--- /dev/null
+++ b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/DynamicMembershipProvider.java
@@ -0,0 +1,89 @@
+/*
+ * 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.user;
+
+import com.google.common.collect.Iterators;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.jetbrains.annotations.NotNull;
+
+import javax.jcr.RepositoryException;
+import java.util.Iterator;
+
+public interface DynamicMembershipProvider {
+
+    /**
+     * Returns {@code true} if this implementation of {@link DynamicMembershipProvider} covers all members for the given 
+     * {@link Group} making it a fully dynamic group.
+     * 
+     * @param group The target group
+     * @return {@code true} if the provider covers all members of the given target group i.e. making it a fully dynamic group (like for example the 'everyone' group); {@code false} otherwise.
+     */
+    boolean coversAllMembers(@NotNull Group group);
+
+    /**
+     * Returns the dynamic members for the given group.
+     *
+     * @param group The target group.
+     * @param includeInherited If {@code true} inherited members should be included in the resulting iterator.
+     * @return An iterator of user/groups that are dynamic members of the given target group.
+     * @throws RepositoryException If an error occurs.
+     */
+    @NotNull Iterator<Authorizable> getMembers(@NotNull Group group, boolean includeInherited) throws RepositoryException;
+
+    /**
+     * Returns {@code true} if the given {@code authorizable} is a dynamic member of the given target group.
+     * @param group The target group.
+     * @param authorizable The user/group that may or may not be dynamic member of the given target group.
+     * @param includeInherited If set to {@code true} inherited group membership will be evaluated.
+     * @return {@code true} if the given {@code authorizable} is a dynamic member of the given target group.
+     * @throws RepositoryException If an error occurs.
+     */
+    boolean isMember(@NotNull Group group, @NotNull Authorizable authorizable, boolean includeInherited) throws RepositoryException;
+
+    /**
+     * Returns an iterator over all groups the given {@code authorizable} is a dynamic member of.
+     * @param authorizable The target user/group for which to evaluate membership.
+     * @param includeInherited If set to {@code true} inherited group membership will be included in the result.
+     * @return An iterator over all groups the given {@code authorizable} is a dynamic member of.
+     * @throws RepositoryException If an error occurs.
+     */
+    @NotNull Iterator<Group> getMembership(@NotNull Authorizable authorizable, boolean includeInherited) throws RepositoryException;
+    
+    DynamicMembershipProvider EMPTY = new DynamicMembershipProvider() {
+
+        @Override
+        public boolean coversAllMembers(@NotNull Group group) {
+            return false;
+        }
+
+        @Override
+        public @NotNull Iterator<Authorizable> getMembers(@NotNull Group group, boolean includeInherited) {
+            return Iterators.emptyIterator();
+        }
+
+        @Override
+        public boolean isMember(@NotNull Group group, @NotNull Authorizable authorizable, boolean includeInherited) {
+            return false;
+        }
+
+        @Override
+        public @NotNull Iterator<Group> getMembership(@NotNull Authorizable authorizable, boolean includeInherited) {
+            return Iterators.emptyIterator();
+        }
+    };
+}
\ No newline at end of file
diff --git a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/DynamicMembershipService.java b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/DynamicMembershipService.java
new file mode 100644
index 0000000..5228fc9
--- /dev/null
+++ b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/DynamicMembershipService.java
@@ -0,0 +1,36 @@
+/*
+ * 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.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.jetbrains.annotations.NotNull;
+
+public interface DynamicMembershipService {
+
+    /**
+     * Returns in instance of {@link DynamicMembershipProvider} for the given root, user manager and name-path mapper.
+     * 
+     * @param root The root associated with the {@link DynamicMembershipProvider}
+     * @param userManager The user manager associated with the {@link DynamicMembershipProvider}
+     * @param namePathMapper The name-path mapper associated with the {@link DynamicMembershipProvider}
+     * @return an new instance of {@link DynamicMembershipProvider}
+     */
+    @NotNull
+    DynamicMembershipProvider getDynamicMembershipProvider(@NotNull Root root, @NotNull UserManager userManager, @NotNull NamePathMapper namePathMapper);
+}
diff --git a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java
index b7d70e4..a371b95 100644
--- a/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java
+++ b/oak-security-spi/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-@Version("2.4.0")
+@Version("2.5.0")
 package org.apache.jackrabbit.oak.spi.security.user;
 
 import org.osgi.annotation.versioning.Version;