Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserConfigurationImpl.java (revision ) @@ -37,6 +37,7 @@ import org.apache.jackrabbit.oak.namepath.NamePathMapper; import org.apache.jackrabbit.oak.security.user.autosave.AutoSaveEnabledManager; import org.apache.jackrabbit.oak.spi.commit.MoveTracker; +import org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler; import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; import org.apache.jackrabbit.oak.spi.lifecycle.WorkspaceInitializer; import org.apache.jackrabbit.oak.spi.security.ConfigurationBase; @@ -178,6 +179,12 @@ @Override public List getValidators(@Nonnull String workspaceName, @Nonnull Set principals, @Nonnull MoveTracker moveTracker) { return ImmutableList.of(new UserValidatorProvider(getParameters()), new CacheValidatorProvider(principals)); + } + + @Nonnull + @Override + public List getConflictHandlers() { + return ImmutableList.of(new RepMembersConflictHandler()); } @Nonnull Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/package-info.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/token/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("1.4.1") +@Version("1.5.0") @Export(optional = "provide:=true") package org.apache.jackrabbit.oak.spi.security.authentication.token; \ No newline at end of file Index: oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/CompositeConfigurationTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/CompositeConfigurationTest.java (revision 1799858) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/spi/security/CompositeConfigurationTest.java (revision ) @@ -21,6 +21,7 @@ import javax.annotation.Nullable; import com.google.common.collect.ImmutableList; +import org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler; import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter; import org.junit.Before; import org.junit.Test; @@ -193,7 +194,7 @@ } @Test - public void testGetProtectedItemImporter() { + public void testGetProtectedItemImporters() { assertTrue(compositeConfiguration.getProtectedItemImporters().isEmpty()); addConfiguration(new SecurityConfiguration.Default()); @@ -209,5 +210,25 @@ addConfiguration(withImporter); assertEquals(1, compositeConfiguration.getProtectedItemImporters().size()); + } + + + @Test + public void testGetConflictHandlers() { + assertTrue(compositeConfiguration.getConflictHandlers().isEmpty()); + + addConfiguration(new SecurityConfiguration.Default()); + assertTrue(compositeConfiguration.getConflictHandlers().isEmpty()); + + SecurityConfiguration withConflictHandler = new SecurityConfiguration.Default() { + @Nonnull + @Override + public List getConflictHandlers() { + return ImmutableList.of(Mockito.mock(ThreeWayConflictHandler.class)); + } + }; + addConfiguration(withConflictHandler); + + assertEquals(1, compositeConfiguration.getConflictHandlers().size()); } } \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/user/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("2.2.0") +@Version("2.3.0") @Export(optional = "provide:=true") package org.apache.jackrabbit.oak.spi.security.user; \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/RepMembersConflictHandler.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/RepMembersConflictHandler.java (revision ) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/RepMembersConflictHandler.java (revision ) @@ -0,0 +1,162 @@ +/* + * 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 java.util.Set; + +import javax.annotation.Nonnull; + +import org.apache.jackrabbit.oak.api.PropertyState; +import org.apache.jackrabbit.oak.api.Type; +import org.apache.jackrabbit.oak.plugins.memory.PropertyBuilder; +import org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.apache.jackrabbit.oak.spi.state.NodeBuilder; +import org.apache.jackrabbit.oak.spi.state.NodeState; + +import com.google.common.collect.Sets; + +/** + * The {@code RepMembersConflictHandler} takes care of merging the {@code rep:members} property + * during parallel updates. + *

+ * The conflict handler deals with the following conflicts: + *

    + *
  • {@code addExistingProperty} : {@code Resolution.MERGED},
  • + *
  • {@code changeDeletedProperty}: {@code Resolution.THEIRS}, removing the members property takes precedence. + *
  • {@code changeChangedProperty}: {@code Resolution.MERGED}, merge of the 2 members sets into a single one + *
  • {@code deleteChangedProperty}: {@code Resolution.OURS} removing the members property takes precedence. + *
+ */ +public class RepMembersConflictHandler implements ThreeWayConflictHandler { + + @Nonnull + @Override + public Resolution addExistingProperty(@Nonnull NodeBuilder parent, @Nonnull PropertyState ours, + @Nonnull PropertyState theirs) { + if (isRepMembersProperty(theirs)) { + mergeChange(parent, ours, theirs,Sets.newHashSet()); + return Resolution.MERGED; + } else { + return Resolution.IGNORED; + } + } + + @Override + @Nonnull + public Resolution changeDeletedProperty(@Nonnull NodeBuilder parent, @Nonnull PropertyState ours, + @Nonnull PropertyState base) { + if (isRepMembersProperty(ours)) { + // removing the members property takes precedence + return Resolution.THEIRS; + } else { + return Resolution.IGNORED; + } + } + + @Nonnull + @Override + public Resolution changeChangedProperty(@Nonnull NodeBuilder parent, @Nonnull PropertyState ours, + @Nonnull PropertyState theirs, @Nonnull PropertyState base) { + if (isRepMembersProperty(theirs)) { + Set baseMembers = Sets.newHashSet(base.getValue(Type.STRINGS)); + mergeChange(parent, ours, theirs, baseMembers); + return Resolution.MERGED; + } else { + return Resolution.IGNORED; + } + } + + @Nonnull + @Override + public Resolution deleteDeletedProperty(@Nonnull NodeBuilder parent, @Nonnull PropertyState base) { + // both are removing the members property, ignoring + return Resolution.IGNORED; + } + + @Nonnull + @Override + public Resolution deleteChangedProperty(@Nonnull NodeBuilder parent, @Nonnull PropertyState theirs, + @Nonnull PropertyState base) { + if (isRepMembersProperty(theirs)) { + // removing the members property takes precedence + return Resolution.OURS; + } else { + return Resolution.IGNORED; + } + } + + + @Nonnull + @Override + public Resolution addExistingNode(@Nonnull NodeBuilder parent, @Nonnull String name, @Nonnull NodeState ours, + @Nonnull NodeState theirs) { + return Resolution.IGNORED; + } + + @Nonnull + @Override + public Resolution changeDeletedNode(@Nonnull NodeBuilder parent, @Nonnull String name, @Nonnull NodeState ours, + @Nonnull NodeState base) { + return Resolution.IGNORED; + } + + @Nonnull + @Override + public Resolution deleteChangedNode(@Nonnull NodeBuilder parent, @Nonnull String name, @Nonnull NodeState theirs, + @Nonnull NodeState base) { + return Resolution.IGNORED; + } + + @Nonnull + @Override + public Resolution deleteDeletedNode(@Nonnull NodeBuilder parent, @Nonnull String name, @Nonnull NodeState base) { + return Resolution.IGNORED; + } + + //----------------------------< internal >---------------------------------- + + private static void mergeChange(NodeBuilder parent, PropertyState ours, PropertyState theirs, Set base) { + PropertyBuilder merged = PropertyBuilder.array(Type.STRING); + merged.setName(UserConstants.REP_MEMBERS); + + Set theirMembers = Sets.newHashSet(theirs.getValue(Type.STRINGS)); + Set ourMembers = Sets.newHashSet(ours.getValue(Type.STRINGS)); + + // merge ours and theirs to a de-duplicated set + Set combined = Sets.newHashSet(Sets.intersection(ourMembers, theirMembers)); + for (String m : Sets.difference(ourMembers, theirMembers)) { + if (!base.contains(m)) { + combined.add(m); + } + } + for (String m : Sets.difference(theirMembers, ourMembers)) { + if (!base.contains(m)) { + combined.add(m); + } + } + merged.addValues(combined); + parent.setProperty(merged.getPropertyState()); + } + + private static boolean isRepMembersProperty(PropertyState p) { + return UserConstants.REP_MEMBERS.equals(p.getName()); + } + +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityConfiguration.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityConfiguration.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/SecurityConfiguration.java (revision ) @@ -23,8 +23,10 @@ import javax.annotation.Nonnull; +import aQute.bnd.annotation.ProviderType; import org.apache.jackrabbit.oak.spi.commit.CommitHook; import org.apache.jackrabbit.oak.spi.commit.MoveTracker; +import org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler; import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer; import org.apache.jackrabbit.oak.spi.lifecycle.WorkspaceInitializer; @@ -33,6 +35,7 @@ /** * Base interface for all security related configurations. */ +@ProviderType public interface SecurityConfiguration { /** @@ -99,6 +102,14 @@ @Nonnull MoveTracker moveTracker); /** + * Returns the list of conflict handlers available for this security configuration. + * + * @return A list of {@link org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler}. + */ + @Nonnull + List getConflictHandlers(); + + /** * @return The list of protected item importers defined by this configuration. */ @Nonnull @@ -150,6 +161,12 @@ @Override public List getValidators( @Nonnull String workspaceName, @Nonnull Set principals, @Nonnull MoveTracker moveTracker) { + return Collections.emptyList(); + } + + @Nonnull + @Override + public List getConflictHandlers() { return Collections.emptyList(); } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/package-info.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/privilege/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("1.1.0") +@Version("1.2.0") @Export(optional = "provide:=true") package org.apache.jackrabbit.oak.spi.security.privilege; \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/Oak.java (revision ) @@ -79,6 +79,7 @@ import org.apache.jackrabbit.oak.plugins.index.property.jmx.PropertyIndexAsyncReindex; import org.apache.jackrabbit.oak.plugins.index.property.jmx.PropertyIndexAsyncReindexMBean; import org.apache.jackrabbit.oak.plugins.memory.MemoryNodeStore; +import org.apache.jackrabbit.oak.spi.commit.CompositeConflictHandler; import org.apache.jackrabbit.oak.spi.query.QueryEngineSettings; import org.apache.jackrabbit.oak.spi.commit.CommitHook; import org.apache.jackrabbit.oak.spi.commit.CommitInfo; @@ -146,6 +147,8 @@ private List editorProviders = newArrayList(); + private CompositeConflictHandler conflictHandler; + private SecurityProvider securityProvider; private ScheduledExecutorService scheduledExecutor; @@ -484,7 +487,11 @@ if (ri != RepositoryInitializer.DEFAULT) { initializers.add(ri); } + + for (ThreeWayConflictHandler tch : sc.getConflictHandlers()) { + with(tch); - } + } + } return this; } @@ -505,7 +512,18 @@ public Oak with(@Nonnull ThreeWayConflictHandler conflictHandler) { checkNotNull(conflictHandler); withEditorHook(); + + if (this.conflictHandler == null) { + if (conflictHandler instanceof CompositeConflictHandler) { + this.conflictHandler = (CompositeConflictHandler) conflictHandler; + } else { + this.conflictHandler = new CompositeConflictHandler(); + this.conflictHandler.addHandler(conflictHandler); + } - commitHooks.add(new ConflictHook(conflictHandler)); + commitHooks.add(new ConflictHook(conflictHandler)); + } else { + this.conflictHandler.addHandler(conflictHandler); + } return this; } Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/package-info.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authorization/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("1.0") +@Version("1.1.0") @Export(optional = "provide:=true") package org.apache.jackrabbit.oak.spi.security.authorization; \ No newline at end of file Index: oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/RepMembersConflictHandlerTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/RepMembersConflictHandlerTest.java (revision ) +++ oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/RepMembersConflictHandlerTest.java (revision ) @@ -0,0 +1,287 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +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.AbstractSecurityTest; +import org.apache.jackrabbit.oak.api.Root; +import org.apache.jackrabbit.oak.spi.security.user.UserConstants; +import org.junit.Test; + +public class RepMembersConflictHandlerTest extends AbstractSecurityTest { + + /** + * The id of the test group + */ + private static final String GROUP_ID = "test-groupId"; + + private Group group; + private User[] users; + + @Override + public void before() throws Exception { + super.before(); + UserManager um = getUserManager(root); + // create a group to receive users + group = um.createGroup(GROUP_ID); + // create future members of the above group + User u1 = um.createUser("u1", "pass"); + User u2 = um.createUser("u2", "pass"); + User u3 = um.createUser("u3", "pass"); + User u4 = um.createUser("u4", "pass"); + User u5 = um.createUser("u5", "pass"); + root.commit(); + users = new User[] { u1, u2, u3, u4, u5 }; + } + + @Test + public void addExistingProperty() throws Exception { + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + add(ours, users[1].getID()); + add(theirs, users[2].getID()); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[1])); + assertTrue(group.isDeclaredMember(users[2])); + } + + /** + * Add-Add test + */ + @Test + public void changeChangedPropertyAA() throws Exception { + add(root, users[0].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + add(ours, users[1].getID()); + add(theirs, users[2].getID()); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[0])); + assertTrue(group.isDeclaredMember(users[1])); + assertTrue(group.isDeclaredMember(users[2])); + } + + /** + * Remove-Remove test + */ + @Test + public void changeChangedPropertyRR() throws Exception { + add(root, users[0].getID(), users[1].getID(), users[2].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + rm(ours, users[1].getID()); + rm(theirs, users[2].getID()); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[0])); + assertFalse(group.isDeclaredMember(users[1])); + assertFalse(group.isDeclaredMember(users[2])); + } + + /** + * Remove-Add with different ids test + */ + @Test + public void changeChangedPropertyRA() throws Exception { + add(root, users[0].getID(), users[1].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + rm(ours, users[1].getID()); + add(theirs, users[2].getID()); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[0])); + assertFalse(group.isDeclaredMember(users[1])); + assertTrue(group.isDeclaredMember(users[2])); + } + + /** + * Add-Remove with different ids test + */ + @Test + public void changeChangedPropertyAR() throws Exception { + add(root, users[0].getID(), users[1].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + add(ours, users[2].getID()); + rm(theirs, users[1].getID()); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[0])); + assertFalse(group.isDeclaredMember(users[1])); + assertTrue(group.isDeclaredMember(users[2])); + } + + /** + * Remove-Add same value test. value was already part of the group + */ + @Test + public void changeChangedPropertyRA2() throws Exception { + add(root, users[0].getID(), users[1].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + String id = users[1].getID(); + rm(ours, id); + add(theirs, id); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[0])); + assertFalse(group.isDeclaredMember(users[1])); + } + + /** + * Add-Remove same value test. value was already part of the group + */ + @Test + public void changeChangedPropertyAR2() throws Exception { + add(root, users[0].getID(), users[1].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + String id = users[1].getID(); + add(ours, id); + rm(theirs, id); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[0])); + assertFalse(group.isDeclaredMember(users[1])); + } + + /** + * Remove-Add same value test. value was not part of the group + */ + @Test + public void changeChangedPropertyRA3() throws Exception { + add(root, users[0].getID(), users[1].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + String id = users[2].getID(); + // we are removing an item that does not yet exist, this should + // not overlap/conflict with the other session adding the same item + rm(ours, id); + add(theirs, id); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[0])); + assertTrue(group.isDeclaredMember(users[1])); + assertTrue(group.isDeclaredMember(users[2])); + } + + /** + * Add-Remove same value test. value was not part of the group + */ + @Test + public void changeChangedPropertyAR3() throws Exception { + add(root, users[0].getID(), users[1].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + String id = users[2].getID(); + add(ours, id); + // we are removing an item that does not yet exist, this should + // not overlap/conflict with the other session adding the same item + rm(theirs, id); + + root.refresh(); + assertTrue(group.isDeclaredMember(users[0])); + assertTrue(group.isDeclaredMember(users[1])); + assertTrue(group.isDeclaredMember(users[2])); + } + + /** + * Delete-Changed. Delete takes precedence + */ + @Test + public void deleteChangedProperty() throws Exception { + add(root, users[0].getID(), users[1].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + add(theirs, users[2].getID()); + wipeGroup(ours); + + root.refresh(); + assertFalse(group.isDeclaredMember(users[0])); + assertFalse(group.isDeclaredMember(users[1])); + } + + /** + * Changed-Deleted. Delete takes precedence + */ + @Test + public void changeDeletedProperty() throws Exception { + add(root, users[0].getID(), users[1].getID()); + + Root ours = login(getAdminCredentials()).getLatestRoot(); + Root theirs = login(getAdminCredentials()).getLatestRoot(); + + wipeGroup(theirs); + add(ours, users[2].getID()); + + root.refresh(); + assertFalse(group.isDeclaredMember(users[0])); + assertFalse(group.isDeclaredMember(users[1])); + } + + private void add(Root r, String... ids) throws Exception { + UserManager um = getUserManager(r); + Group g = (Group) um.getAuthorizable(GROUP_ID); + for (String id : ids) { + g.addMember(um.getAuthorizable(id)); + } + r.commit(); + } + + private void rm(Root r, String... ids) throws Exception { + UserManager um = getUserManager(r); + Group g = (Group) um.getAuthorizable(GROUP_ID); + for (String id : ids) { + g.removeMember(um.getAuthorizable(id)); + } + r.commit(); + } + + private void wipeGroup(Root r) throws Exception { + UserManager um = getUserManager(r); + Group g = (Group) um.getAuthorizable(GROUP_ID); + r.getTree(g.getPath()).removeProperty(UserConstants.REP_MEMBERS); + r.commit(); + } +} Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/CompositeConfiguration.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/CompositeConfiguration.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/CompositeConfiguration.java (revision ) @@ -28,6 +28,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import aQute.bnd.annotation.ProviderType; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; @@ -39,6 +40,7 @@ import org.apache.jackrabbit.oak.plugins.tree.TreeLocation; import org.apache.jackrabbit.oak.spi.commit.CommitHook; import org.apache.jackrabbit.oak.spi.commit.MoveTracker; +import org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler; import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; import org.apache.jackrabbit.oak.spi.lifecycle.CompositeInitializer; import org.apache.jackrabbit.oak.spi.lifecycle.CompositeWorkspaceInitializer; @@ -51,6 +53,7 @@ * Abstract base implementation for {@link SecurityConfiguration}s that can * combine different implementations. */ +@ProviderType public abstract class CompositeConfiguration implements SecurityConfiguration { /** @@ -210,6 +213,12 @@ return securityConfiguration.getValidators(workspaceName, principals, moveTracker); } }))); + } + + @Nonnull + @Override + public List getConflictHandlers() { + return ImmutableList.copyOf(Iterables.concat(Lists.transform(getConfigurations(), securityConfiguration -> securityConfiguration.getConflictHandlers()))); } @Nonnull Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/package-info.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/authentication/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("1.2.0") +@Version("1.3.0") package org.apache.jackrabbit.oak.spi.security.authentication; import aQute.bnd.annotation.Version; Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/package-info.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("2.1.0") +@Version("2.2.0") @Export(optional = "provide:=true") package org.apache.jackrabbit.oak.spi.security; \ No newline at end of file Index: oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/package-info.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/package-info.java (revision 1799858) +++ oak-core/src/main/java/org/apache/jackrabbit/oak/spi/security/principal/package-info.java (revision ) @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -@Version("1.4.0") +@Version("1.5.0") @Export(optional = "provide:=true") package org.apache.jackrabbit.oak.spi.security.principal; \ No newline at end of file Index: oak-benchmarks/src/main/java/org/apache/jackrabbit/oak/benchmark/CompositeAuthorizationTest.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- oak-benchmarks/src/main/java/org/apache/jackrabbit/oak/benchmark/CompositeAuthorizationTest.java (revision 1799858) +++ oak-benchmarks/src/main/java/org/apache/jackrabbit/oak/benchmark/CompositeAuthorizationTest.java (revision ) @@ -41,6 +41,7 @@ import org.apache.jackrabbit.oak.security.authorization.composite.CompositeAuthorizationConfiguration; import org.apache.jackrabbit.oak.spi.commit.CommitHook; import org.apache.jackrabbit.oak.spi.commit.MoveTracker; +import org.apache.jackrabbit.oak.spi.commit.ThreeWayConflictHandler; import org.apache.jackrabbit.oak.spi.commit.ValidatorProvider; import org.apache.jackrabbit.oak.spi.lifecycle.RepositoryInitializer; import org.apache.jackrabbit.oak.spi.lifecycle.WorkspaceInitializer; @@ -163,6 +164,12 @@ @Nonnull @Override public List getValidators(@Nonnull String workspaceName, @Nonnull Set principals, @Nonnull MoveTracker moveTracker) { + return Collections.EMPTY_LIST; + } + + @Nonnull + @Override + public List getConflictHandlers() { return Collections.EMPTY_LIST; } \ No newline at end of file