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/22 13:26:10 UTC

svn commit: r1890973 - in /jackrabbit/oak/trunk/oak-auth-external/src: main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/

Author: angela
Date: Tue Jun 22 13:26:10 2021
New Revision: 1890973

URL: http://svn.apache.org/viewvc?rev=1890973&view=rev
Log:
OAK-9468 : Define mechanism to prevent cross-IDP membership

Added:
    jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProvider.java   (with props)
    jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProviderTest.java   (with props)

Added: jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProvider.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProvider.java?rev=1890973&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProvider.java (added)
+++ jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProvider.java Tue Jun 22 13:26:10 2021
@@ -0,0 +1,215 @@
+/*
+ * 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 org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.oak.api.Root;
+import org.apache.jackrabbit.oak.commons.PropertiesUtil;
+import org.apache.jackrabbit.oak.namepath.NamePathMapper;
+import org.apache.jackrabbit.oak.spi.security.SecurityProvider;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncContext;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncConfigImpl;
+import org.apache.jackrabbit.oak.spi.security.user.action.AbstractGroupAction;
+import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableAction;
+import org.apache.jackrabbit.oak.spi.security.user.action.AuthorizableActionProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.ConfigurationPolicy;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.apache.jackrabbit.oak.spi.security.RegistrationConstants.OAK_SECURITY_NAME;
+
+@Component(
+        service = AuthorizableActionProvider.class,
+        configurationPolicy = ConfigurationPolicy.REQUIRE,
+        property = OAK_SECURITY_NAME + "=org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal.ExternalAuthorizableActionProvider")
+@Designate(ocd = ExternalAuthorizableActionProvider.Configuration.class)
+public final class ExternalAuthorizableActionProvider implements AuthorizableActionProvider {
+    
+    @ObjectClassDefinition(name = "Apache Jackrabbit Oak ExternalAuthorizableActionProvider", description = "Implementation of the AuthorizableActionProvider that specifically covers operations for users/groups defined by an external IDP that get synced into the repository.")
+    @interface Configuration {
+        @AttributeDefinition(
+                name = "Behavior when Adding Members from Different IDP",
+                description = "Defines if Group.addMember should fail when adding a member defined by a different IDP. If set to false only a warning is logged.")
+        boolean failAddMembersForDifferentIdp() default false;
+    }
+    
+    private static final Logger log = LoggerFactory.getLogger(ExternalAuthorizableActionProvider.class);
+    
+    private boolean failAddMembersForDifferentIdp = false;
+    
+    private SyncHandlerMappingTracker syncHandlerMappingTracker;
+    private AutomembershipTracker automembershipTracker;
+    
+    @Activate
+    public void activate(@NotNull BundleContext bundleContext, @NotNull Configuration configuration) {
+        failAddMembersForDifferentIdp = configuration.failAddMembersForDifferentIdp();
+
+        syncHandlerMappingTracker = new SyncHandlerMappingTracker(bundleContext);
+        syncHandlerMappingTracker.open();
+
+        automembershipTracker = new AutomembershipTracker(bundleContext, syncHandlerMappingTracker);
+        automembershipTracker.open();
+    }
+
+    @Deactivate
+    public void deactivate() {
+        if (automembershipTracker != null) {
+            automembershipTracker.close();
+        }
+        if (syncHandlerMappingTracker != null) {
+            syncHandlerMappingTracker.close();
+        }
+    }
+    
+    @Override
+    public @NotNull List<? extends AuthorizableAction> getAuthorizableActions(@NotNull SecurityProvider securityProvider) {
+        return Collections.singletonList(new IdpBoundaryAction());
+    }
+
+    @Nullable
+    private static String getIdpName(@Nullable ExternalIdentityRef ref) {
+        return (ref == null) ? null : ref.getProviderName();
+    }
+    
+    private final class IdpBoundaryAction extends AbstractGroupAction {
+
+        @Override
+        public void onMemberAdded(@NotNull Group group, @NotNull Authorizable member, @NotNull Root root, @NotNull NamePathMapper namePathMapper) throws RepositoryException {
+            ExternalIdentityRef memberRef = DefaultSyncContext.getIdentityRef(member);
+            String idpName = getIdpName(memberRef);
+            if (idpName == null) {
+                // member is not an external members identity -> ignore
+                return;
+            }
+
+            String groupIdpName = getIdpName(DefaultSyncContext.getIdentityRef(group));
+            if (idpName.equals(groupIdpName)) {
+                // same IDP -> nothing to verify
+                return;
+            }
+
+            // not the same IDP -> validate
+            String groupId = group.getID();
+            if (automembershipTracker != null && automembershipTracker.isAutoMembership(idpName, groupId, member.isGroup())) {
+                // ignore groups that are configured in 'automembership' option for the given IDP
+                return;
+            }
+
+            String extId = memberRef.getString();
+            String msg = String.format("Attempt to add external identity '%s' as member of group '%s' defined by IDP '%s'.", extId, groupId, groupIdpName);
+            log.warn(msg);
+            if (failAddMembersForDifferentIdp) {
+                throw new ConstraintViolationException(msg);
+            }
+        }
+    }
+
+    private static final class AutomembershipTracker extends ServiceTracker {
+
+        private final SyncHandlerMappingTracker mappingTracker;
+        private final Set<ServiceReference> refs = new HashSet<>();
+
+        private final Map<String, Set<String>> userAutoMembership = new HashMap<>();
+        private final Map<String, Set<String>> groupAutoMembership = new HashMap<>();
+
+        AutomembershipTracker(@NotNull BundleContext context, @NotNull SyncHandlerMappingTracker mappingTracker) {
+            super(context, SyncHandler.class.getName(), null);
+            this.mappingTracker = mappingTracker;
+        }
+
+        @Override
+        public Object addingService(ServiceReference reference) {
+            refs.add(reference);
+            userAutoMembership.clear(); // recalculate
+            groupAutoMembership.clear(); // recalculate
+            return super.addingService(reference);
+        }
+
+        @Override
+        public void modifiedService(ServiceReference reference, Object service) {
+            refs.add(reference);
+            userAutoMembership.clear(); // recalculate
+            groupAutoMembership.clear(); // recalculate            
+            super.modifiedService(reference, service);
+        }
+
+        @Override
+        public void removedService(ServiceReference reference, Object service) {
+            refs.remove(reference);
+            userAutoMembership.clear(); // recalculate
+            groupAutoMembership.clear(); // recalculate
+            super.removedService(reference, service);
+        }
+        
+        @NotNull
+        private Map<String, Set<String>> getAutomembership(boolean memberIsGroup) {
+            Map<String, Set<String>> autoMembership = (memberIsGroup) ? groupAutoMembership : userAutoMembership;
+            if (autoMembership.isEmpty() && !refs.isEmpty()) {
+                for (ServiceReference ref : refs) {
+                    String syncHandlerName = PropertiesUtil.toString(ref.getProperty(DefaultSyncConfigImpl.PARAM_NAME), DefaultSyncConfigImpl.PARAM_NAME_DEFAULT);
+                    String[] userMembership = PropertiesUtil.toStringArray(ref.getProperty(DefaultSyncConfigImpl.PARAM_USER_AUTO_MEMBERSHIP), new String[0]);
+                    String[] groupMembership = PropertiesUtil.toStringArray(ref.getProperty(DefaultSyncConfigImpl.PARAM_GROUP_AUTO_MEMBERSHIP), new String[0]);
+
+                    for (String idpName : mappingTracker.getIdpNames(syncHandlerName)) {
+                        updateAutoMembershipMap(userAutoMembership, syncHandlerName, idpName, userMembership);
+                        updateAutoMembershipMap(groupAutoMembership, syncHandlerName, idpName, groupMembership);
+                    }
+                }
+            }
+            return autoMembership;
+        }
+        
+        private static void updateAutoMembershipMap(@NotNull Map<String, Set<String>> map, @NotNull String syncHandlerName, 
+                                                    @NotNull String idpName, @NotNull String[] membership) {
+            Set<String> userMembership = ImmutableSet.copyOf(membership);
+            Set<String> previous = map.put(idpName, userMembership);
+            if (previous != null) {
+                String msg = previous.equals(userMembership) ? "Duplicate" : "Colliding";
+                log.debug("{} auto-membership configuration for IDP '{}'; replacing previous values {} by {} defined by SyncHandler '{}'", msg, idpName, previous, userMembership, syncHandlerName);
+            }
+        }
+        
+        private boolean isAutoMembership(@NotNull String idpName, @NotNull String groupId, boolean memberIsGroup) {
+            Set<String> ids = getAutomembership(memberIsGroup).get(idpName);
+            return ids != null && ids.contains(groupId);
+        }
+    }
+}
\ No newline at end of file

Propchange: jackrabbit/oak/trunk/oak-auth-external/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProvider.java
------------------------------------------------------------------------------
    svn:eol-style = native

Added: jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProviderTest.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProviderTest.java?rev=1890973&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProviderTest.java (added)
+++ jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProviderTest.java Tue Jun 22 13:26:10 2021
@@ -0,0 +1,342 @@
+/*
+ * 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 com.google.common.collect.Lists;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.AbstractExternalAuthTest;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.ExternalIdentityRef;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.SyncHandler;
+import org.apache.jackrabbit.oak.spi.security.authentication.external.impl.DefaultSyncConfigImpl;
+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.action.AuthorizableAction;
+import org.apache.jackrabbit.oak.spi.security.user.action.GroupAction;
+import org.apache.sling.testing.mock.osgi.MapUtil;
+import org.jetbrains.annotations.NotNull;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.nodetype.ConstraintViolationException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+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.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+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;
+
+@RunWith(Parameterized.class)
+public class ExternalAuthorizableActionProviderTest extends AbstractExternalAuthTest {
+    
+    private static final String IDP_NAME = "idp1";
+    private static final String LOCAL_GROUP_ID = "localGroup";
+
+    private final ExternalAuthorizableActionProvider eap = new ExternalAuthorizableActionProvider();
+    
+    private final Group localGroup = mock(Group.class);
+    private final Group externalGroup = mock(Group.class);
+    private final Authorizable externalUser = mock(Authorizable.class);
+    
+    private SyncHandler sh;
+    private SyncHandlerMapping shMapping;
+
+    @Parameterized.Parameters(name = "name={1}")
+    public static Collection<Object[]> parameters() {
+        return Lists.newArrayList(
+                new Object[] { true, "Fail" },
+                new Object[] { false, "Only Warn" });
+    }
+    
+    private final boolean failOnViolation;
+    
+    public ExternalAuthorizableActionProviderTest(boolean failOnViolation, String name) {
+        this.failOnViolation = failOnViolation;
+    }
+    @Before
+    public void before() throws Exception {
+        super.before();
+        
+        eap.activate(context.bundleContext(), new ExternalAuthorizableActionProvider.Configuration() {
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return Annotation.class;
+            }
+
+            @Override
+            public boolean failAddMembersForDifferentIdp() {
+                return failOnViolation;
+            }
+        });
+        
+        doReturn(LOCAL_GROUP_ID).when(localGroup).getID();
+        doReturn("externalGroup").when(externalGroup).getID();
+        
+        Value v = getValueFactory(root).createValue(new ExternalIdentityRef("externalUserId", IDP_NAME).getString());
+        when(externalUser.getProperty(REP_EXTERNAL_ID)).thenReturn(new Value[] {v});
+        when(externalUser.isGroup()).thenReturn(false);
+
+        v = getValueFactory(root).createValue(new ExternalIdentityRef("externalGroupId", IDP_NAME).getString());
+        when(externalGroup.getProperty(REP_EXTERNAL_ID)).thenReturn(new Value[] {v});
+        when(externalGroup.isGroup()).thenReturn(true);
+        
+        sh = new DefaultSyncHandler();
+        shMapping = new SyncHandlerMapping() {};
+    }
+    
+    @After
+    public void after() throws Exception {
+        try {
+            reset(localGroup, externalGroup, externalUser);
+        } finally {
+            super.after();
+        }
+    }
+    
+    private GroupAction getGroupAction() {
+        return (GroupAction) eap.getAuthorizableActions(getSecurityProvider()).get(0);
+    }
+
+    @Test
+    public void testGetActions() {
+        List<? extends AuthorizableAction> actions = eap.getAuthorizableActions(getSecurityProvider());
+        assertEquals(1, actions.size());
+        assertTrue(actions.get(0) instanceof GroupAction);
+    }
+    
+    @Test
+    public void testMemberNotExternal() throws Exception {
+        GroupAction ga = getGroupAction();
+        
+        ga.onMemberAdded(localGroup, getTestUser(), root, getNamePathMapper());
+        ga.onMemberAdded(externalGroup, getTestUser(), root, getNamePathMapper());
+        
+        verifyNoInteractions(localGroup);
+        verifyNoInteractions(externalGroup);
+    }
+
+    @Test
+    public void testSameIDP() throws Exception {
+        GroupAction ga = getGroupAction();
+        ga.onMemberAdded(externalGroup, externalUser, root, getNamePathMapper());
+
+        verify(externalGroup).getProperty(REP_EXTERNAL_ID);
+        verifyNoMoreInteractions(externalGroup);
+    }
+    
+    @Test
+    public void testUserMamaberGroupInUserAutomembership() throws Exception {
+        registerSyncHandlerSyncMapping(IDP_NAME, LOCAL_GROUP_ID, true);
+
+        GroupAction ga = getGroupAction();
+        ga.onMemberAdded(localGroup, externalUser, root, getNamePathMapper());
+        
+        verify(localGroup).getProperty(REP_EXTERNAL_ID);
+        verify(localGroup).getID();
+        verifyNoMoreInteractions(localGroup);
+
+        ga.onMemberAdded(localGroup, externalUser, root, getNamePathMapper());
+        verify(localGroup, times(2)).getProperty(REP_EXTERNAL_ID);
+        verify(localGroup, times(2)).getID();
+        verifyNoMoreInteractions(localGroup);
+    }
+
+    @Test
+    public void testUserMemberGroupInGroupAutomembership() throws Exception {
+        registerSyncHandlerSyncMapping(IDP_NAME, LOCAL_GROUP_ID, false);
+        assertViolationDetected(externalUser);
+        verifyInvocations();
+    }
+
+    @Test
+    public void testGroupMemberGroupInAutomembership() throws Exception {
+        registerSyncHandlerSyncMapping(IDP_NAME, LOCAL_GROUP_ID, false);
+
+        GroupAction ga = getGroupAction();
+        ga.onMemberAdded(localGroup, externalGroup, root, getNamePathMapper());
+
+        verify(localGroup).getProperty(REP_EXTERNAL_ID);
+        verify(localGroup).getID();
+        verifyNoMoreInteractions(localGroup);
+
+        ga.onMemberAdded(localGroup, externalGroup, root, getNamePathMapper());
+        verify(localGroup, times(2)).getProperty(REP_EXTERNAL_ID);
+        verify(localGroup, times(2)).getID();
+        verifyNoMoreInteractions(localGroup);
+    }
+
+    @Test
+    public void testGroupMemberGroupInUserAutomembership() throws Exception {
+        registerSyncHandlerSyncMapping(IDP_NAME, LOCAL_GROUP_ID, true);
+        assertViolationDetected(externalGroup);
+        verifyInvocations();
+    }
+
+    @Test
+    public void testGroupInAutomembershipDifferentIDP() throws Exception {
+        registerSyncHandlerSyncMapping("anotherIDP", LOCAL_GROUP_ID, true);
+        assertViolationDetected(externalUser);
+        verifyInvocations();
+    }
+
+    @Test
+    public void testGroupNotInAutomembership() throws Exception {
+        registerSyncHandlerSyncMapping(IDP_NAME, "anotherGroup", false);
+        assertViolationDetected(externalGroup);
+        verifyInvocations();
+    }
+
+    @Test
+    public void testModifySyncHandler() throws Exception {
+        Map<String,Object> config = createSyncConfig(LOCAL_GROUP_ID, true);
+        ServiceRegistration sr = context.bundleContext().registerService(SyncHandler.class.getName(), sh, MapUtil.toDictionary(config));
+        context.registerService(SyncHandlerMapping.class, shMapping, MapUtil.toDictionary(createMappingConfig(IDP_NAME)));
+
+        getGroupAction().onMemberAdded(localGroup, externalUser, root, getNamePathMapper());
+
+        // modify sync-handler
+        sr.setProperties(MapUtil.toDictionary(createSyncConfig("anotherGroup", true)));
+        // FIXME: use osgi-mock to invoke the method
+        Field f = ExternalAuthorizableActionProvider.class.getDeclaredField("automembershipTracker");
+        f.setAccessible(true);
+        Object automembershipTracker = f.get(eap);
+        Method m = automembershipTracker.getClass().getDeclaredMethod("modifiedService", ServiceReference.class, Object.class);
+        m.setAccessible(true);
+        m.invoke(automembershipTracker, sr.getReference(), sh);
+        // now onMemberAdd will spot the violation
+        assertViolationDetected(externalUser);
+    }
+    
+    @Test
+    public void testRemoveSyncHandler() throws RepositoryException {
+        Map<String,Object> config = createSyncConfig(LOCAL_GROUP_ID, false);
+        ServiceRegistration sr = context.bundleContext().registerService(SyncHandler.class.getName(), sh, MapUtil.toDictionary(config));
+        context.registerService(SyncHandlerMapping.class, shMapping, MapUtil.toDictionary(createMappingConfig(IDP_NAME)));
+        
+        getGroupAction().onMemberAdded(localGroup, externalGroup, root, getNamePathMapper());
+        
+        // remove sync-handler
+        sr.unregister();
+        // now onMemberAdd will spot the violation
+        assertViolationDetected(externalGroup);
+    }
+    
+    @Test
+    public void testDuplicateSyncHandlerMapping() throws Exception {
+        registerSyncHandlerSyncMapping("anotherIdpName", LOCAL_GROUP_ID, true);
+        // register another sync-handler-mapping with the same properties
+        context.registerService(SyncHandlerMapping.class, new SyncHandlerMapping() {}, createMappingConfig("anotherIdpName"));
+        assertViolationDetected(externalUser);
+    }
+
+    @Test
+    public void testCollidingSyncHandler() throws Exception {
+        registerSyncHandlerSyncMapping("anotherIdpName", LOCAL_GROUP_ID, true);
+        // register another sync-handler with the same properties
+        SyncHandler anotherSh = new DefaultSyncHandler();
+        context.registerInjectActivateService(anotherSh, createSyncConfig("anotherGroupId", true));
+
+        assertViolationDetected(externalUser);
+    }
+    
+    @Test
+    public void testDeactivate() throws RepositoryException {
+        // onMemberAdd will spot the violation
+        assertViolationDetected(externalUser);
+        
+        // after deactivation, changes to sync-handler registration will no longer be tracked
+        eap.deactivate();
+        registerSyncHandlerSyncMapping(IDP_NAME, "localGroup", true);
+
+        // -> violation still detected
+        assertViolationDetected(externalUser);
+    }
+
+    @Test
+    public void testDeactivatePriorToActivate() throws RepositoryException {
+        ExternalAuthorizableActionProvider provider = new ExternalAuthorizableActionProvider();
+        provider.deactivate();
+        
+        registerSyncHandlerSyncMapping(IDP_NAME, "localGroup", true);
+        // -> violation still detected
+        GroupAction groupAction = (GroupAction) provider.getAuthorizableActions(getSecurityProvider()).get(0);
+        // note: without prior activation 'failOnViolation' is not initialized and thus 'false'
+        assertViolationDetected(externalUser, groupAction, false);
+    }
+
+    private void assertViolationDetected(@NotNull Authorizable externalMember) throws RepositoryException {
+        assertViolationDetected(externalMember, getGroupAction(), failOnViolation);
+    }
+
+    private void assertViolationDetected(@NotNull Authorizable externalMember, @NotNull GroupAction ga, boolean failOnViolation) throws RepositoryException {
+        try {
+            ga.onMemberAdded(localGroup, externalMember, root, getNamePathMapper());
+            if (failOnViolation) {
+                fail("ConstraintViolationException expected");
+            }
+        } catch (ConstraintViolationException e) {
+            if (!failOnViolation) {
+                fail("No ConstraintViolationException expected");
+            }
+        }
+    }
+    
+    private void verifyInvocations() throws RepositoryException {
+        verify(localGroup).getProperty(REP_EXTERNAL_ID);
+        verify(localGroup).getID();
+        verifyNoMoreInteractions(localGroup);
+    }
+    
+    private void registerSyncHandlerSyncMapping(@NotNull String idpName, @NotNull String automembershipId, boolean isUserAutoMembership) {
+        context.registerInjectActivateService(sh, createSyncConfig(automembershipId, isUserAutoMembership));
+        context.registerService(SyncHandlerMapping.class, shMapping, createMappingConfig(idpName));
+    }
+    
+    private static Map<String, Object> createSyncConfig(@NotNull String automembershipId, boolean isUserAutoMembership) {
+        String autoMembershipName = (isUserAutoMembership) ? DefaultSyncConfigImpl.PARAM_USER_AUTO_MEMBERSHIP : DefaultSyncConfigImpl.PARAM_GROUP_AUTO_MEMBERSHIP;
+        return ImmutableMap.of(
+                DefaultSyncConfigImpl.PARAM_NAME, "sh",
+                autoMembershipName, new String[] {automembershipId});
+    }
+
+    private static Map<String, Object> createMappingConfig(@NotNull String idpName) {
+        return ImmutableMap.of(
+                SyncHandlerMapping.PARAM_IDP_NAME, idpName,
+                SyncHandlerMapping.PARAM_SYNC_HANDLER_NAME, "sh");
+    }
+}
\ No newline at end of file

Propchange: jackrabbit/oak/trunk/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/ExternalAuthorizableActionProviderTest.java
------------------------------------------------------------------------------
    svn:eol-style = native