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

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

commit 7764be2df57fd787d1dad07a1c418333185b7daf
Author: angela <anch...@adobe.com>
AuthorDate: Fri Jul 21 12:03:26 2023 +0200

    OAK-10318 : Improve AutoMembershipPrincipals#isInheritedMember
---
 .../impl/principal/AutoMembershipPrincipals.java   |  67 ++++++---
 .../impl/principal/AutoMembershipCycleTest.java    | 154 +++++++++++++++------
 2 files changed, 162 insertions(+), 59 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 fc9c664940..f5ceb84d16 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
@@ -16,12 +16,12 @@
  */
 package 
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.principal;
 
-import org.apache.jackrabbit.guava.common.collect.ImmutableSet;
-import org.apache.jackrabbit.guava.common.collect.Iterators;
-import org.apache.jackrabbit.guava.common.collect.Maps;
 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.guava.common.collect.ImmutableSet;
+import org.apache.jackrabbit.guava.common.collect.Iterators;
+import org.apache.jackrabbit.guava.common.collect.Maps;
 import 
org.apache.jackrabbit.oak.spi.security.authentication.external.basic.AutoMembershipConfig;
 import org.apache.jackrabbit.oak.spi.security.principal.GroupPrincipals;
 import org.jetbrains.annotations.NotNull;
@@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory;
 import javax.jcr.RepositoryException;
 import java.security.Principal;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -103,7 +104,7 @@ final class AutoMembershipPrincipals {
      * 
      * @param idpName The name of an IDP
      * @param groupId The target group id
-     * @param authorizable The authorizable for which to evaluation if it is a 
automatic member of the group identified by {@code groupId}.
+     * @param authorizable The authorizable for which to evaluation if it is 
an automatic member of the group identified by {@code groupId}.
      * @return {@code true} if the given authorizable is an automatic member 
of the group identified by {@code groupId}; {@code false} otherwise.
      * @see AutoMembershipProvider#isMember(Group, Authorizable, boolean) 
      */
@@ -127,23 +128,55 @@ final class AutoMembershipPrincipals {
     }
 
     boolean isInheritedMember(@NotNull String idpName, @NotNull Group group, 
@NotNull Authorizable authorizable) throws RepositoryException {
-        return isInheritedMember(idpName, group, authorizable, new 
HashSet<>());
-    }
-
-    boolean isInheritedMember(@NotNull String idpName, @NotNull Group group, 
@NotNull Authorizable authorizable, @NotNull Set<String> processedIds) throws 
RepositoryException {
         String groupId = group.getID();
-        if (!processedIds.add(groupId)) {
-            log.error("Cyclic group membership detected for group id {}", 
groupId);
-            return false;
-        }
         if (isMember(idpName, groupId, authorizable)) {
+            // groupId is listed in auto-membership configuration
             return true;
         }
 
-        Iterator<Authorizable> declaredGroupMembers = 
Iterators.filter(group.getDeclaredMembers(), Authorizable::isGroup);
-        while (declaredGroupMembers.hasNext()) {
-            Group grMember = (Group) declaredGroupMembers.next();
-            if (isInheritedMember(idpName, grMember, authorizable, 
processedIds)) {
+        // to test for inherited membership collect automembership-ids and 
loop auto-membership groups
+        Set<String> automembershipIds = new 
HashSet<>(Arrays.asList(autoMembershipMapping.get(idpName)));
+        AutoMembershipConfig config = autoMembershipConfigMap.get(idpName);
+        if (config != null) {
+            automembershipIds.addAll(config.getAutoMembership(authorizable));
+        }
+
+        // keep track of processed ids over all 'automembership' ids to avoid 
repeated evaluation 
+        Set<String> processed = new HashSet<>();
+        for (String automembershipId : automembershipIds) {
+            Authorizable gr = userManager.getAuthorizable(automembershipId);
+            if (gr == null || !gr.isGroup()) {
+                log.warn("Configured automembership id '{}' is not a valid 
group", automembershipId);
+            } else if (hasInheritedMembership(gr.declaredMemberOf(), groupId, 
automembershipId, processed)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasInheritedMembership(@NotNull Iterator<Group> 
declared, @NotNull String groupId, 
+                                                  @NotNull String memberId, 
@NotNull Set<String> processed) throws RepositoryException {
+        List<Group> groups = new ArrayList<>();
+        while (declared.hasNext()) {
+            Group gr = declared.next();
+            String grId = gr.getID();
+            if (memberId.equals(grId)) {
+                log.error("Cyclic group membership detected for group id {}", 
memberId);
+            }
+            if (!processed.add(grId)) {
+                // group has already been processed before (shared membership 
e.g. for the 'everyone' group)
+                return false;
+            }
+            if (groupId.equals(grId)) {
+                // the specified groupId defines inherited membership of a 
configured auto-membership group
+                return true;
+            }
+            // remember group for traversing up the membership hierarchy
+            groups.add(gr);
+        }
+        // recursively call to search for inherited membership
+        for (Group group : groups) {
+            if (hasInheritedMembership(group.declaredMemberOf(), groupId, 
memberId, processed)) {
                 return true;
             }
         }
@@ -152,7 +185,7 @@ final class AutoMembershipPrincipals {
 
     /**
      * Returns the group principal that given authorizable is an automatic 
member of. This method evaluates both the 
-     * global auto-membership settings as well as {@link AutoMembershipConfig} 
if they exist for the given IDP name.
+     * global auto-membership settings and {@link AutoMembershipConfig} if 
they exist for the given IDP name.
      * 
      * @param idpName The name of an IDP
      * @param authorizable The target user/group
diff --git 
a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipCycleTest.java
 
b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipCycleTest.java
index 60a088f821..c8664a3add 100644
--- 
a/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipCycleTest.java
+++ 
b/oak-auth-external/src/test/java/org/apache/jackrabbit/oak/spi/security/authentication/external/impl/principal/AutoMembershipCycleTest.java
@@ -18,92 +18,162 @@ package 
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.prin
 
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.api.security.user.Group;
-import org.apache.jackrabbit.guava.common.collect.Iterators;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
+import 
org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncConfig;
+import 
org.apache.jackrabbit.oak.spi.security.authentication.external.basic.DefaultSyncContext;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
+import org.apache.jackrabbit.oak.spi.xml.ImportBehavior;
+import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Before;
 import org.junit.Test;
 
-import javax.jcr.RepositoryException;
-import java.util.Arrays;
+import javax.jcr.ValueFactory;
 import java.util.Collections;
-import java.util.List;
 import java.util.Map;
 
+import static 
org.apache.jackrabbit.oak.spi.security.authentication.external.impl.ExternalIdentityConstants.PARAM_PROTECT_EXTERNAL_IDS;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
-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;
 
 public class AutoMembershipCycleTest extends AbstractAutoMembershipTest {
 
     private AutoMembershipPrincipals amp;
+    private UserManager umMock;
+    private Group gr;
+    private Group gr2;
+    private Group amGr1;
     private final Authorizable authorizable = mock(Authorizable.class);
     
     @Before
     public void before() throws Exception {
         super.before();
         Map<String, String[]> mapping = Collections.singletonMap(IDP_VALID_AM, 
new String[] {AUTOMEMBERSHIP_GROUP_ID_1});
-        amp = new AutoMembershipPrincipals(userManager, mapping, 
getAutoMembershipConfigMapping());
+        
+        umMock = spy(userManager);
+        amGr1 = spy(automembershipGroup1);
+        gr = spy(umMock.createGroup("testGroup"));
+        gr2 = spy(umMock.createGroup("testGroup2"));
+        
when(umMock.getAuthorizable(AUTOMEMBERSHIP_GROUP_ID_1)).thenReturn(amGr1);
+        
+        umMock.createGroup(EveryonePrincipal.getInstance());
+        root.commit();
+        
+        
+        amp = new AutoMembershipPrincipals(umMock, mapping, 
getAutoMembershipConfigMapping());
+        clearInvocations(umMock, amGr1, gr, gr2);
     }
-    
+
+    @Override
+    public void after() throws Exception {
+        try {
+            amGr1.removeMembers("testGroup");
+            gr.remove();
+            gr2.remove();
+            root.commit();
+        } finally {
+            super.after();
+        }
+    }
+
+    @Override
+    protected ConfigurationParameters getSecurityConfigParameters() {
+        return ConfigurationParameters.of(UserConfiguration.NAME,
+                ConfigurationParameters.of(
+                        ProtectedItemImporter.PARAM_IMPORT_BEHAVIOR, 
ImportBehavior.NAME_BESTEFFORT)
+        );
+    }
+
+    @Override
+    protected @NotNull DefaultSyncConfig createSyncConfig() {
+        DefaultSyncConfig dsc = super.createSyncConfig();
+        
dsc.user().setDynamicMembership(true).setAutoMembership(AUTOMEMBERSHIP_GROUP_ID_1);
+        
dsc.group().setDynamicGroups(true).setAutoMembership(AUTOMEMBERSHIP_GROUP_ID_1);
+        return dsc;
+    }
+
     @Test
     public void testIsInheritedMemberCircularIncludingAutoMembership() throws 
Exception {
-        Group amGr1 = mockGroup(AUTOMEMBERSHIP_GROUP_ID_1);
-        Group gr = mockGroup("testGroup");
-
-        // mock a membership cycle
-        List<Authorizable> members = Arrays.asList(authorizable, amGr1);
-        when(gr.getDeclaredMembers()).thenReturn(members.iterator(), 
members.iterator(), members.iterator()).thenThrow(new 
RuntimeException("cicle"));
-        
when(amGr1.getDeclaredMembers()).thenReturn(Iterators.singletonIterator(gr), 
Iterators.singletonIterator(gr), Iterators.singletonIterator(gr)).thenThrow(new 
RuntimeException("cicle"));
+        // create cycle
+        assertTrue(amGr1.addMembers(gr.getID()).isEmpty());
+        assertTrue(gr.addMembers(amGr1.getID()).isEmpty());
+        root.commit();
+        clearInvocations(gr, amGr1);
 
         // declared automembership
         assertTrue(amp.isInheritedMember(IDP_VALID_AM, amGr1, authorizable));
-        verify(amGr1, never()).getDeclaredMembers();
+        verify(amGr1).getID();
+        verifyNoMoreInteractions(amGr1);
+        verifyNoInteractions(authorizable, gr);
+        clearInvocations(authorizable, gr, amGr1);
 
-        // declared automembership in amGr1
+        // declared automembership in amGr1 (cycle)
         assertTrue(amp.isInheritedMember(IDP_VALID_AM, gr, authorizable));
-        verify(gr).getDeclaredMembers();
-        verify(amGr1, never()).getDeclaredMembers();
-        clearInvocations(amGr1, gr);
+        verifyNoInteractions(authorizable);
+        verify(gr).getID();
+        verify(amGr1).declaredMemberOf();
+        verify(umMock).getAuthorizable(AUTOMEMBERSHIP_GROUP_ID_1);
+        verifyNoMoreInteractions(gr);
+        clearInvocations(authorizable, amGr1, gr, umMock);
 
-        // declared automembership
+        // declared automembership for group
         assertTrue(amp.isInheritedMember(IDP_VALID_AM, amGr1, gr));
-        verify(amGr1, never()).getDeclaredMembers();
-        verify(gr, never()).getDeclaredMembers();
-        
+        verify(amGr1).getID();
+        verifyNoMoreInteractions(amGr1);
+        verifyNoInteractions(gr, umMock);
+        clearInvocations(amGr1, gr, umMock);
+
+        // declared automembership in amGr1 (cycle)
         assertTrue(amp.isInheritedMember(IDP_VALID_AM, gr, amGr1));
-        verify(gr).getDeclaredMembers();
-        verify(amGr1, never()).getDeclaredMembers();
+        verify(gr).getID();
+        verify(amGr1).declaredMemberOf();
+        verify(umMock).getAuthorizable(AUTOMEMBERSHIP_GROUP_ID_1);
+
+        verifyNoMoreInteractions(gr, umMock);
     }
 
     @Test
     public void testIsInheritedMemberCircularWithoutAutoMembership() throws 
Exception {
-        // mock a membership cycle
-        Group gr = mockGroup("testGroup");
-        Group gr2 = mockGroup("testGroup2");
-
-        
when(gr.getDeclaredMembers()).thenReturn(Iterators.singletonIterator(gr2)).thenThrow(new
 RuntimeException("circle"));
-        
when(gr2.getDeclaredMembers()).thenReturn(Iterators.singletonIterator(gr)).thenThrow(new
 RuntimeException("circle"));
-
+        // create cycle
+        assertTrue(gr.addMembers(gr2.getID()).isEmpty());
+        assertTrue(gr2.addMembers(gr.getID()).isEmpty());
+        root.commit();
+        clearInvocations(gr, gr2);
+        
         assertFalse(amp.isInheritedMember(IDP_VALID_AM, gr, gr2));
         
-        verify(gr, times(1)).getDeclaredMembers();
-        verify(gr, times(2)).getID();
-        verify(gr, times(1)).isGroup();
-        verify(gr2, times(1)).getDeclaredMembers();
-        verify(gr2, times(1)).getID();
-        verify(gr2, times(1)).isGroup();
+        verify(gr, times(1)).getID();
         verifyNoMoreInteractions(gr, gr2);
     }
     
-    private static @NotNull Group mockGroup(@NotNull String id) throws 
RepositoryException {
-        Group gr = 
when(mock(Group.class).isGroup()).thenReturn(true).getMock();
-        when(gr.getID()).thenReturn(id);
-        return gr;
+    @Test
+    public void testIsInheritedMemberCircularIncludingExternalGroup() throws 
Exception {
+        
externalPrincipalConfiguration.setParameters(ConfigurationParameters.of(
+                PARAM_PROTECT_EXTERNAL_IDS, false));
+
+        // create cycle
+        assertTrue(amGr1.addMembers(gr.getID()).isEmpty());
+        assertTrue(gr.addMembers(amGr1.getID()).isEmpty());
+        root.commit();
+
+        // mark groups as external identities
+        ValueFactory vf = getValueFactory(root);
+        gr.setProperty(DefaultSyncContext.REP_EXTERNAL_ID, 
vf.createValue(gr.getID() + ';' + idp.getName()));
+        amGr1.setProperty(DefaultSyncContext.REP_EXTERNAL_ID, 
vf.createValue(amGr1.getID() + ';' + idp.getName()));
+        root.commit();
+        clearInvocations(gr, amGr1);
+
+        assertTrue(amp.isInheritedMember(IDP_VALID_AM, gr, authorizable));
+        assertTrue(amp.isInheritedMember(IDP_VALID_AM, gr, amGr1));
     }
 }
\ No newline at end of file

Reply via email to