Author: angela
Date: Wed Apr 26 08:02:40 2017
New Revision: 1792709

URL: http://svn.apache.org/viewvc?rev=1792709&view=rev
Log:
OAK-6072 : Move check for cyclic membership to GroupImpl

Modified:
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java
    
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdAbortTest.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdBestEffortTest.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdIgnoreTest.java
    
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/default.md
    jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/membership.md
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportAbortTest.java
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportBestEffortTest.java
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportIgnoreTest.java
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupTest.java
    
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/NestedGroupTest.java

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/GroupImpl.java
 Wed Apr 26 08:02:40 2017
@@ -111,7 +111,10 @@ class GroupImpl extends AuthorizableImpl
                 log.debug(msg);
                 return false;
             }
-            // NOTE: detection of circular membership is postponed to the 
commit (=> UserValidator)
+            if (isCyclicMembership((Group) authorizable)) {
+                String msg = "Cyclic group membership detected for group " + 
getID() + " and member " + authorizable.getID();
+                throw new ConstraintViolationException(msg);
+            }
         }
 
         boolean success = getMembershipProvider().addMember(getTree(), 
authorizableImpl.getTree());
@@ -266,18 +269,25 @@ class GroupImpl extends AuthorizableImpl
 
             if (ImportBehavior.BESTEFFORT != importBehavior) {
                 Authorizable member = 
getUserManager().getAuthorizable(memberId);
+                String msg = null;
                 if (member == null) {
+                    msg = "Attempt to add or remove a non-existing member '" + 
memberId + "' with ImportBehavior = " + 
ImportBehavior.nameFromValue(importBehavior);
+                } else if (member.isGroup()) {
+                    if (((AuthorizableImpl) member).isEveryone()) {
+                        log.debug("Attempt to add everyone group as member.");
+                        continue;
+                    } else if (isCyclicMembership((Group) member)) {
+                        msg = "Cyclic group membership detected for group " + 
getID() + " and member " + member.getID();
+                    }
+                }
+                if (msg != null) {
                     if (ImportBehavior.ABORT == importBehavior) {
-                        throw new ConstraintViolationException("Attempt to add 
or remove a non-existing member " + memberId);
+                        throw new ConstraintViolationException(msg);
                     } else {
                         // ImportBehavior.IGNORE is default in 
UserUtil.getImportBehavior
-                        String msg = "Attempt to add or remove non-existing 
member '" + getID() + "' with ImportBehavior = IGNORE.";
                         log.debug(msg);
                         continue;
                     }
-                } else if (member.isGroup() && ((AuthorizableImpl) 
member).isEveryone()) {
-                    log.debug("Attempt to add everyone group as member.");
-                    continue;
                 }
             }
 
@@ -302,6 +312,10 @@ class GroupImpl extends AuthorizableImpl
         return failedIds;
     }
 
+    private boolean isCyclicMembership(@Nonnull Group member) throws 
RepositoryException {
+        return member.isMember(this);
+    }
+
     /**
      * Principal representation of this group instance.
      */

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/MembershipProvider.java
 Wed Apr 26 08:02:40 2017
@@ -220,7 +220,7 @@ class MembershipProvider extends Authori
      */
     @Nonnull
     Iterator<String> getMembers(@Nonnull Tree groupTree, boolean 
includeInherited) {
-        return getMembers(groupTree, includeInherited, new HashSet<String>());
+        return getMembers(groupTree, getContentID(groupTree), 
includeInherited, new HashSet<String>());
     }
 
     /**
@@ -233,11 +233,16 @@ class MembershipProvider extends Authori
      */
     @Nonnull
     private Iterator<String> getMembers(@Nonnull final Tree groupTree,
+                                        @Nonnull final String groupContentId,
                                         final boolean includeInherited,
                                         @Nonnull final Set<String> 
processedRefs) {
         MemberReferenceIterator mrit = new MemberReferenceIterator(groupTree) {
             @Override
             protected boolean hasProcessedReference(@Nonnull String value) {
+                if (groupContentId.equals(value)) {
+                    log.warn("Cyclic group membership detected for contentId " 
+ groupContentId);
+                    return false;
+                }
                 return processedRefs.add(value);
             }
         };
@@ -261,7 +266,7 @@ class MembershipProvider extends Authori
             @Nonnull
             @Override
             protected Iterator<String> getNextIterator(@Nonnull Tree 
groupTree) {
-                return getMembers(groupTree, true, processedRefs);
+                return getMembers(groupTree, groupContentId, true, 
processedRefs);
             }
         };
     }

Modified: 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/main/java/org/apache/jackrabbit/oak/security/user/UserValidator.java
 Wed Apr 26 08:02:40 2017
@@ -16,12 +16,9 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
-import java.util.Set;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
 import org.apache.jackrabbit.JcrConstants;
 import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.api.PropertyState;
@@ -80,10 +77,6 @@ class UserValidator extends DefaultValid
             String msg = "Invalid jcr:uuid for authorizable " + 
parentAfter.getName();
             throw constraintViolation(21, msg);
         }
-
-        if (REP_MEMBERS.equals(name)) {
-            checkForCyclicMembership(after.getValue(Type.STRINGS));
-        }
     }
 
     @Override
@@ -112,12 +105,6 @@ class UserValidator extends DefaultValid
             String msg = "Password may not be plain text.";
             throw constraintViolation(24, msg);
         }
-
-        if (REP_MEMBERS.equals(name)) {
-            Set<String> addedValues = 
Sets.newHashSet(after.getValue(Type.STRINGS));
-            
addedValues.removeAll(ImmutableSet.copyOf(before.getValue(Type.STRINGS)));
-            checkForCyclicMembership(addedValues);
-        }
     }
 
 
@@ -183,20 +170,6 @@ class UserValidator extends DefaultValid
         }
     }
 
-    private void checkForCyclicMembership(@Nonnull Iterable<String> 
memberRefs) throws CommitFailedException {
-        String groupContentId = TreeUtil.getString(parentAfter, 
JcrConstants.JCR_UUID);
-        if (groupContentId == null) {
-            throw constraintViolation(30, "Missing content id for group " + 
UserUtil.getAuthorizableId(parentAfter) + "; cannot check for cyclic group 
membership.");
-        }
-        MembershipProvider mp = provider.getMembershipProvider();
-        for (String memberContentId : memberRefs) {
-            Tree memberTree = mp.getByContentID(memberContentId, 
AuthorizableType.GROUP);
-            if (memberTree != null && mp.isMember(memberTree, parentAfter)) {
-                throw constraintViolation(31, "Cyclic group membership 
detected in group" + UserUtil.getAuthorizableId(parentAfter));
-            }
-        }
-    }
-
     private void validateAuthorizable(@Nonnull Tree tree, @Nullable 
AuthorizableType type) throws CommitFailedException {
         boolean isSystemUser = (type == AuthorizableType.USER) && 
UserUtil.isSystemUser(tree);
         String authRoot = 
UserUtil.getAuthorizableRootPath(provider.getConfig(), type);

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AbstractAddMembersByIdTest.java
 Wed Apr 26 08:02:40 2017
@@ -31,7 +31,6 @@ import org.apache.jackrabbit.api.securit
 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.CommitFailedException;
 import org.apache.jackrabbit.oak.api.ContentSession;
 import org.apache.jackrabbit.oak.api.PropertyState;
 import org.apache.jackrabbit.oak.api.Root;
@@ -48,7 +47,6 @@ import static org.junit.Assert.assertNot
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 public abstract class AbstractAddMembersByIdTest extends AbstractSecurityTest {
 
@@ -237,16 +235,8 @@ public abstract class AbstractAddMembers
         root.commit();
 
         Set<String> failed = testGroup.addMembers(memberGroup.getID());
-        assertTrue(failed.isEmpty());
-
-        try {
-            root.commit();
-            fail("cyclic group membership must be detected upon commit");
-        } catch (CommitFailedException e) {
-            // success
-            assertTrue(e.isConstraintViolation());
-            assertEquals(31, e.getCode());
-        }
+        assertFalse(failed.isEmpty());
+        assertTrue(failed.contains(memberGroup.getID()));
     }
 
     @Test

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdAbortTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdAbortTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdAbortTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdAbortTest.java
 Wed Apr 26 08:02:40 2017
@@ -16,14 +16,20 @@
  */
 package org.apache.jackrabbit.oak.security.user;
 
+import java.util.Set;
 import javax.jcr.nodetype.ConstraintViolationException;
 
+import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.xml.ImportBehavior;
 import org.apache.jackrabbit.oak.spi.xml.ProtectedItemImporter;
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
 public class AddMembersByIdAbortTest extends AbstractAddMembersByIdTest {
 
     @Override
@@ -43,4 +49,23 @@ public class AddMembersByIdAbortTest ext
     public void testExistingMemberWithoutAccess() throws Exception {
         addExistingMemberWithoutAccess();
     }
+
+    @Test
+    public void testCyclicMembership() throws Exception {
+        memberGroup.addMember(testGroup);
+        root.commit();
+
+        try {
+            Set<String> failed = testGroup.addMembers(memberGroup.getID());
+            assertTrue(failed.isEmpty());
+            root.commit();
+            fail("cyclic group membership must be detected latest upon 
commit");
+        } catch (CommitFailedException e) {
+            // success
+            assertTrue(e.isConstraintViolation());
+            assertEquals(31, e.getCode());
+        }  catch (ConstraintViolationException e) {
+            // success
+        }
+    }
 }
\ No newline at end of file

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdBestEffortTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdBestEffortTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdBestEffortTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdBestEffortTest.java
 Wed Apr 26 08:02:40 2017
@@ -23,9 +23,10 @@ import java.util.Set;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+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.oak.api.CommitFailedException;
+import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
@@ -38,7 +39,6 @@ import static org.junit.Assert.assertEqu
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
 
 public class AddMembersByIdBestEffortTest extends AbstractAddMembersByIdTest {
 
@@ -143,21 +143,43 @@ public class AddMembersByIdBestEffortTes
         assertTrue(testGroup.isMember(memberGroup));
     }
 
+    /**
+     * @since Oak 1.8 : for performance reasons cyclic membership is only check
+     * within GroupImpl and those import-behaviours that actually resolve the 
id to a user/group
+     */
+    @Override
+    @Test
+    public void testCyclicMembership() throws Exception {
+        memberGroup.addMember(testGroup);
+        root.commit();
+
+        Set<String> failed = testGroup.addMembers(memberGroup.getID());
+        assertTrue(failed.isEmpty());
+
+        root.commit();
+
+        // cyclic membership must be spotted upon membership resolution
+        assertEquals(1, Iterators.size(memberGroup.getMembers()));
+        assertEquals(1, Iterators.size(testGroup.getMembers()));
+    }
+
+    /**
+     * @since Oak 1.8 : for performance reasons cyclic membership is only check
+     * within GroupImpl and those import-behaviours that actually resolve the 
id to a user/group
+     */
     @Test
     public void testCyclicWithoutAccess() throws Exception {
         memberGroup.addMember(testGroup);
         root.commit();
 
-        try {
-            Set<String> failed = addExistingMemberWithoutAccess();
-            fail("CommitFailedException expected");
-        } catch (CommitFailedException e) {
-            assertTrue(e.isConstraintViolation());
-            assertEquals(31, e.getCode());
-        } finally {
-            root.refresh();
-            assertFalse(testGroup.isMember(memberGroup));
-        }
+        Set<String> failed = addExistingMemberWithoutAccess();
+        assertTrue(failed.isEmpty());
+
+        // cyclic membership must be spotted upon membership resolution
+        root.refresh();
+        UserManager uMgr = getUserManager(root);
+        assertEquals(1, 
Iterators.size(uMgr.getAuthorizable(memberGroup.getID(), 
Group.class).getMembers()));
+        assertEquals(1, Iterators.size(uMgr.getAuthorizable(testGroup.getID(), 
Group.class).getMembers()));
     }
 
     @Test

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdIgnoreTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdIgnoreTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdIgnoreTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/AddMembersByIdIgnoreTest.java
 Wed Apr 26 08:02:40 2017
@@ -18,7 +18,10 @@ package org.apache.jackrabbit.oak.securi
 
 import java.util.Set;
 
+import javax.jcr.nodetype.ConstraintViolationException;
+
 import com.google.common.collect.ImmutableSet;
+import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.spi.security.ConfigurationParameters;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.xml.ImportBehavior;
@@ -27,6 +30,8 @@ import org.junit.Test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class AddMembersByIdIgnoreTest extends AbstractAddMembersByIdTest {
 

Modified: 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-core/src/test/java/org/apache/jackrabbit/oak/security/user/UserValidatorTest.java
 Wed Apr 26 08:02:40 2017
@@ -301,76 +301,6 @@ public class UserValidatorTest extends A
         }
     }
 
-    
-    /**
-     * @since oak 1.0 cyclic group membership added in a single set of 
transient
-     *        modifications must be detected upon save.
-     */
-    @Test
-    public void testDetectCyclicMembership() throws Exception {
-        Group group1 = null;
-        Group group2 = null;
-        Group group3 = null;
-        
-        UserManager userMgr = getUserManager(root);
-        try {
-            group1 = userMgr.createGroup("group1");
-            group2 = userMgr.createGroup("group2");
-            group3 = userMgr.createGroup("group3");
-
-            group1.addMember(group2);
-            group2.addMember(group3);
-
-            // manually create the cyclic membership
-            Tree group3Tree = root.getTree(group3.getPath());
-            Set<String> values = 
Collections.singleton(root.getTree(group1.getPath()).getProperty(JcrConstants.JCR_UUID).getValue(Type.STRING));
-            PropertyState prop = PropertyStates.createProperty(REP_MEMBERS, 
values, Type.WEAKREFERENCES);
-            group3Tree.setProperty(prop);
-            root.commit();
-            fail("Cyclic group membership must be detected");
-        } catch (CommitFailedException e) {
-            // success
-        } finally {
-            if (group1 != null) group1.remove();
-            if (group2 != null) group2.remove();
-            if (group3 != null) group3.remove();
-            root.commit();
-        }
-    }
-
-    @Test
-    public void testDetectCyclicMembershipWithIntermediateCommit() throws 
Exception {
-        Group group1 = null;
-        Group group2 = null;
-        Group group3 = null;
-
-        UserManager userMgr = getUserManager(root);
-        try {
-            group1 = userMgr.createGroup("group1");
-            group2 = userMgr.createGroup("group2");
-            group3 = userMgr.createGroup("group3");
-
-            group1.addMember(group2);
-            group2.addMember(group3);
-            root.commit();
-
-            // manually create the cyclic membership
-            Tree group3Tree = root.getTree(group3.getPath());
-            Set<String> values = 
Collections.singleton(root.getTree(group1.getPath()).getProperty(JcrConstants.JCR_UUID).getValue(Type.STRING));
-            PropertyState prop = PropertyStates.createProperty(REP_MEMBERS, 
values, Type.WEAKREFERENCES);
-            group3Tree.setProperty(prop);
-            root.commit();
-            fail("Cyclic group membership must be detected");
-        } catch (CommitFailedException e) {
-            // success
-        } finally {
-            if (group1 != null) group1.remove();
-            if (group2 != null) group2.remove();
-            if (group3 != null) group3.remove();
-            root.commit();
-        }
-    }
-
     @Test
     public void testCreateNestedUser() throws Exception {
         Tree userTree = root.getTree(getTestUser().getPath());

Modified: 
jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/default.md
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/default.md?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/default.md 
(original)
+++ jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/default.md Wed 
Apr 26 08:02:40 2017
@@ -220,7 +220,7 @@ The corresponding errors are all of type
 | 0028              | Attempt to create outside of configured scope            
|
 | 0029              | Intermediate folders not rep:AuthorizableFolder          
|
 | 0030              | Missing uuid for group (check for cyclic membership)     
|
-| 0031              | Cyclic group membership                                  
|
+| <s>0031</s>        | <s>Cyclic group membership</s> (see [OAK-6072])         
|
 | 0032              | Attempt to set password with system user                 
|
 | 0033              | Attempt to add rep:pwd node to a system user             
|
 
@@ -327,3 +327,4 @@ implementation.
 [UserConfiguration]: 
/oak/docs/apidocs/org/apache/jackrabbit/oak/spi/security/user/UserConfiguration.html
 [UserAuthenticationFactory]: 
/oak/docs/apidocs/org/apache/jackrabbit/oak/spi/security/user/UserAuthenticationFactory.html
 [Authentication]: 
/oak/docs/apidocs/org/apache/jackrabbit/oak/spi/security/authentication/Authentication.html
+[OAK-6072]: https://issues.apache.org/jira/browse/OAK-6072

Modified: 
jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/membership.md
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/membership.md?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/membership.md 
(original)
+++ jackrabbit/oak/trunk/oak-doc/src/site/markdown/security/user/membership.md 
Wed Apr 26 08:02:40 2017
@@ -148,11 +148,11 @@ group membership by specifying the membe
 The following details are worth mentioning:
 
 - a `null` or empty String id will immediately fail the operation with 
`ConstraintViolationException`; changes already made will not be reverted,
-- an attemt to make the same group member of itself will list that id in the 
return value but will not fail the operation,
+- an attempt to make the same group member of itself will list that id in the 
return value but will not fail the operation,
 - duplicate ids in the parameter list will be silently ignored,
-- cyclic membership validation is postponed to the validator called upon 
`Root.commit`
+- <s>cyclic membership validation is postponed to the validator called upon 
`Root.commit`
   and will only fail at that point; the cyclic membership then needs to be 
manually
-  resolved by the application,
+  resolved by the application</s> (see [OAK-3170] and below)
 - whether or not a non-existing (or not accessible) authorizable can be added 
or
   removed depends on the configured `ImportBehavior`:
     - ABORT: each id is resolved to the corresponding authorizable; if it 
doesn't
@@ -164,6 +164,41 @@ The following details are worth mentioni
     - IGNORE: each id is resolved to the corresponding authorizable; if it 
doesn't
       exist it will be returned as _failed_ in the return value.
 
+#### Invalid Membership
+
+##### Invalid Authorizable
+
+Adding a different implementation of `Authorizable` is not allowed. This is 
always 
+verified when calling `Group.addMember(Authorizable)`.
+
+##### Same Group as Member
+
+Adding the target group as member of itself will not succeed. When adding 
+members by ID (`Group.addMembers(String...)`) the violation is spotted by 
+simple ID comparison.
+
+##### Everyone Group and Everyone as Member
+
+The group representing the `EveryonePrincipal` is specially handled. Due to 
+it's dynamic nature adding members to this group is not allowed and adding it 
+as a member to any other group would cause a cyclic membership.
+
+Note however, that this validation is omitted in case of 
`Group.addMembers(String...)`
+with `ImportBehavior.BESTEFFORT` (see above).
+
+##### Cyclic Membership
+
+Since Oak 1.7.0 the explicit check for cyclic group membership has been 
+moved from the `Validator` to the `Group` implementation. As before cyclic 
+membership might not be spotted and the membership resolution will log the 
+cycle upon collection of all members/groups.
+
+The following scenarios may leave the cycle unnoticed upon adding members:
+- `Group.addMember(Authorizable)` when the editing `Session` cannot read all 
groups included in the cycle.
+- `Group.addMembers(String...)` with `ImportBehavior.BESTEFFORT` where the 
member ID is not resolved.
+
+See [OAK-3170] for additional information. 
+
 ### Configuration
 
 Note that as of Oak 1.0 the implementation is responsible for defining the
@@ -171,7 +206,6 @@ content structure and will expand the mu
 Consequently, the following configuration option `groupMembershipSplitSize` 
present
 with Jackrabbit 2.x is not supported anymore.
 
-
 <!-- hidden references -->
 [org.apache.jackrabbit.api.security.user.Group]: 
http://svn.apache.org/repos/asf/jackrabbit/trunk/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Group.java
 [org.apache.jackrabbit.api.security.user.Authorizable]: 
http://svn.apache.org/repos/asf/jackrabbit/trunk/jackrabbit-api/src/main/java/org/apache/jackrabbit/api/security/user/Authorizable.java

Modified: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportAbortTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportAbortTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportAbortTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportAbortTest.java
 Wed Apr 26 08:02:40 2017
@@ -20,10 +20,15 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.ConstraintViolationException;
 
+import org.apache.jackrabbit.oak.api.CommitFailedException;
 import org.apache.jackrabbit.oak.spi.xml.ImportBehavior;
+import org.apache.jackrabbit.test.NotExecutableException;
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 /**
@@ -88,4 +93,43 @@ public class GroupImportAbortTest extend
             // success.
         }
     }
+
+    @Test
+    public void testImportCircularMembership() throws Exception {
+        String g1Id = "0120a4f9-196a-3f9e-b9f5-23f31f914da7";
+        String gId = "b2f5ff47-4366-31b6-a533-d8dc3614845d"; // groupId of 'g' 
group.
+        if (getUserManager().getAuthorizable("g") != null) {
+            throw new NotExecutableException();
+        }
+
+        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
+                "<sv:node sv:name=\"gFolder\" 
xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\"; 
xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\"; 
xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\"; 
xmlns:fn=\"http://www.w3.org/2005/xpath-functions\"; 
xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"; 
xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\"; xmlns:rep=\"internal\" 
xmlns:jcr=\"http://www.jcp.org/jcr/1.0\";>" +
+                "   <sv:property sv:name=\"jcr:primaryType\" 
sv:type=\"Name\"><sv:value>rep:AuthorizableFolder</sv:value></sv:property>" +
+                    "<sv:node sv:name=\"g1\"><sv:property 
sv:name=\"jcr:primaryType\" 
sv:type=\"Name\"><sv:value>rep:Group</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"jcr:uuid\" 
sv:type=\"String\"><sv:value>" + g1Id + "</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"rep:principalName\" 
sv:type=\"String\"><sv:value>g1</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"rep:members\" 
sv:type=\"WeakReference\"><sv:value>" +gId+ "</sv:value></sv:property>" +
+                    "</sv:node>" +
+                    "<sv:node sv:name=\"g\"><sv:property 
sv:name=\"jcr:primaryType\" 
sv:type=\"Name\"><sv:value>rep:Group</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"jcr:uuid\" 
sv:type=\"String\"><sv:value>" + gId + "</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"rep:principalName\" 
sv:type=\"String\"><sv:value>g1</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"rep:members\" 
sv:type=\"WeakReference\"><sv:value>" +g1Id+ "</sv:value></sv:property>" +
+                    "</sv:node>" +
+                "</sv:node>";
+
+        /*
+        try to import 'g1' with 'g' as member and the 'g' group that has a 
circular group membership references with ABORT.
+        expected:
+        - group is imported
+        - circular membership is spotted latest upon save
+        */
+        try {
+            doImport(getTargetPath(), xml);
+            getImportSession().save();
+
+            fail("Circular membership must be detected latest upon save.");
+        } catch (ConstraintViolationException e) {
+            // success
+        }
+    }
 }

Modified: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportBestEffortTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportBestEffortTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportBestEffortTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportBestEffortTest.java
 Wed Apr 26 08:02:40 2017
@@ -23,12 +23,11 @@ import javax.jcr.Node;
 import javax.jcr.PropertyType;
 import javax.jcr.Session;
 import javax.jcr.Value;
-import javax.jcr.nodetype.ConstraintViolationException;
 
+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.api.CommitFailedException;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
 import org.apache.jackrabbit.oak.spi.xml.ImportBehavior;
 import org.apache.jackrabbit.test.NotExecutableException;
@@ -168,13 +167,11 @@ public class GroupImportBestEffortTest e
         doImport(getTargetPath(), xml);
 
         /*
-        now try to import the 'g' group that has a circular group
-        membership references.
+        now try to import the 'g' group that has a circular group membership 
references.
         expected:
         - group is imported
-        - circular membership is ignored (OR fails upon save)
-        - g is member of g1
-        - g1 isn't member of g  (circular membership detected) OR saving 
changes fails
+        - circular membership is not spotted due to best-effort optimization
+        - circular membership is spotted upon resolution of members
         */
         doImport(getTargetPath() + "/gFolder", xml2);
 
@@ -185,19 +182,11 @@ public class GroupImportBestEffortTest e
         assertNotNull("'g1' was not imported as Group.", g1);
 
         assertTrue(g1.isDeclaredMember(g));
-        if (g.isDeclaredMember(g1)) {
-            // circular membership created during import
-            try {
-                getImportSession().save();
-                fail("Circular membership must be detected upon save.");
-            } catch (ConstraintViolationException e) {
-                Throwable th = e.getCause();
-                assertTrue(th instanceof CommitFailedException);
-                assertEquals(31, ((CommitFailedException) th).getCode());
-            }
-        } else {
-            assertNotDeclaredMember(g, g1Id, getImportSession());
-        }
+        assertTrue(g.isDeclaredMember(g1));
+
+        // circular membership created during import -> must be spotted upon 
member-access
+        assertEquals(1, Iterators.size(g1.getMembers()));
+        assertEquals(1, Iterators.size(g.getMembers()));
     }
 
     @Test

Modified: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportIgnoreTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportIgnoreTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportIgnoreTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupImportIgnoreTest.java
 Wed Apr 26 08:02:40 2017
@@ -20,11 +20,16 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.UUID;
 
+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.oak.spi.xml.ImportBehavior;
+import org.apache.jackrabbit.test.NotExecutableException;
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 /**
@@ -95,4 +100,47 @@ public class GroupImportIgnoreTest exten
             }
         }
     }
+
+    @Test
+    public void testImportCircularMembership() throws Exception {
+        String g1Id = "0120a4f9-196a-3f9e-b9f5-23f31f914da7";
+        String gId = "b2f5ff47-4366-31b6-a533-d8dc3614845d"; // groupId of 'g' 
group.
+        if (getUserManager().getAuthorizable("g") != null) {
+            throw new NotExecutableException();
+        }
+
+        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
+                "<sv:node sv:name=\"gFolder\" 
xmlns:mix=\"http://www.jcp.org/jcr/mix/1.0\"; 
xmlns:nt=\"http://www.jcp.org/jcr/nt/1.0\"; 
xmlns:fn_old=\"http://www.w3.org/2004/10/xpath-functions\"; 
xmlns:fn=\"http://www.w3.org/2005/xpath-functions\"; 
xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"; 
xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\"; xmlns:rep=\"internal\" 
xmlns:jcr=\"http://www.jcp.org/jcr/1.0\";>" +
+                "   <sv:property sv:name=\"jcr:primaryType\" 
sv:type=\"Name\"><sv:value>rep:AuthorizableFolder</sv:value></sv:property>" +
+                    "<sv:node sv:name=\"g1\"><sv:property 
sv:name=\"jcr:primaryType\" 
sv:type=\"Name\"><sv:value>rep:Group</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"jcr:uuid\" 
sv:type=\"String\"><sv:value>" + g1Id + "</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"rep:principalName\" 
sv:type=\"String\"><sv:value>g1</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"rep:members\" 
sv:type=\"WeakReference\"><sv:value>" +gId+ "</sv:value></sv:property>" +
+                    "</sv:node>" +
+                    "<sv:node sv:name=\"g\"><sv:property 
sv:name=\"jcr:primaryType\" 
sv:type=\"Name\"><sv:value>rep:Group</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"jcr:uuid\" 
sv:type=\"String\"><sv:value>" + gId + "</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"rep:principalName\" 
sv:type=\"String\"><sv:value>g1</sv:value></sv:property>" +
+                    "   <sv:property sv:name=\"rep:members\" 
sv:type=\"WeakReference\"><sv:value>" +g1Id+ "</sv:value></sv:property>" +
+                    "</sv:node>" +
+                "</sv:node>";
+
+        /*
+        import groups with cyclic membership with with IGNORE.
+        expected:
+        - group is imported
+        - circular membership is ignored
+        - g is member of g1
+        - g1 isn't member of g  (circular membership detected)
+        */
+        doImport(getTargetPath(), xml);
+
+        Group g1 = userMgr.getAuthorizable("g1", Group.class);
+        Group g = userMgr.getAuthorizable("g", Group.class);
+
+        boolean b = g1.isDeclaredMember(g);
+        assertEquals("Circular membership must be detected", !b, 
g.isDeclaredMember(g1));
+
+        assertEquals("Circular membership must be detected", b, 
Iterators.contains(g1.getMembers(), g));
+        assertEquals("Circular membership must be detected", !b, 
Iterators.contains(g.getMembers(), g1));
+    }
 }

Modified: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/GroupTest.java
 Wed Apr 26 08:02:40 2017
@@ -644,8 +644,10 @@ public class GroupTest extends AbstractU
 
     private static void assertCyclicCommitFailed(RepositoryException e) {
         Throwable th = e.getCause();
-        assertTrue(th instanceof CommitFailedException);
-        assertEquals(31, ((CommitFailedException) th).getCode());
+        if (th != null) {
+            assertTrue(th instanceof CommitFailedException);
+            assertEquals(31, ((CommitFailedException) th).getCode());
+        }
     }
 
     @Test

Modified: 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/NestedGroupTest.java
URL: 
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/NestedGroupTest.java?rev=1792709&r1=1792708&r2=1792709&view=diff
==============================================================================
--- 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/NestedGroupTest.java
 (original)
+++ 
jackrabbit/oak/trunk/oak-jcr/src/test/java/org/apache/jackrabbit/oak/jcr/security/user/NestedGroupTest.java
 Wed Apr 26 08:02:40 2017
@@ -159,10 +159,12 @@ public class NestedGroupTest extends Abs
 
     private static void assertCyclicMembershipError(Exception e) {
         Throwable th = e.getCause();
-        assertTrue(th instanceof CommitFailedException);
-        CommitFailedException ce = (CommitFailedException) th;
-        assertEquals(CommitFailedException.CONSTRAINT, ce.getType());
-        assertEquals(31, ce.getCode());
+        if (th != null) {
+            assertTrue(th instanceof CommitFailedException);
+            CommitFailedException ce = (CommitFailedException) th;
+            assertEquals(CommitFailedException.CONSTRAINT, ce.getType());
+            assertEquals(31, ce.getCode());
+        }
     }
 
     @Test


Reply via email to