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