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;