Merge pull request #8 from nit23uec/master

SLING-9084 Support for group membership in repoinit
diff --git a/src/main/java/org/apache/sling/jcr/repoinit/impl/DoNothingVisitor.java b/src/main/java/org/apache/sling/jcr/repoinit/impl/DoNothingVisitor.java
index 083e257..29c5d75 100644
--- a/src/main/java/org/apache/sling/jcr/repoinit/impl/DoNothingVisitor.java
+++ b/src/main/java/org/apache/sling/jcr/repoinit/impl/DoNothingVisitor.java
@@ -35,6 +35,8 @@
 import org.apache.sling.repoinit.parser.operations.SetAclPaths;
 import org.apache.sling.repoinit.parser.operations.SetAclPrincipalBased;
 import org.apache.sling.repoinit.parser.operations.SetAclPrincipals;
+import org.apache.sling.repoinit.parser.operations.AddGroupMembers;
+import org.apache.sling.repoinit.parser.operations.RemoveGroupMembers;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/src/main/java/org/apache/sling/jcr/repoinit/impl/GroupMembershipVisitor.java b/src/main/java/org/apache/sling/jcr/repoinit/impl/GroupMembershipVisitor.java
new file mode 100644
index 0000000..3918a40
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/repoinit/impl/GroupMembershipVisitor.java
@@ -0,0 +1,77 @@
+/*
+ * 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.sling.jcr.repoinit.impl;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.sling.repoinit.parser.operations.AddGroupMembers;
+import org.apache.sling.repoinit.parser.operations.RemoveGroupMembers;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import java.util.List;
+
+/**
+ * OperationVisitor which processes only operations related to group memberships. Having several such specialized visitors makes it easy to control the
+ * execution order.
+ */
+class GroupMembershipVisitor extends DoNothingVisitor {
+
+    /**
+     * Create a visitor using the supplied JCR Session.
+     *
+     * @param s must have sufficient rights to add/remove members to/from a group.
+     */
+    public GroupMembershipVisitor(Session s) {
+        super(s);
+    }
+
+    @Override
+    public void visitAddGroupMembers(AddGroupMembers am) {
+        List<String> members = am.getMembers();
+        String groupname = am.getGroupname();
+        Authorizable group = null;
+        log.info("Adding members '{}' to group '{}'", members, groupname);
+        try {
+            group = UserUtil.getAuthorizable(session, groupname);
+            if (group == null || !group.isGroup()) {
+                throw new RuntimeException(groupname + " is not a group");
+            }
+            ((Group) group).addMembers(members.toArray(new String[0]));
+        } catch (RepositoryException e) {
+            report(e, "Unable to add members to group [" + groupname + "]:" + e);
+        }
+    }
+
+    @Override
+    public void visitRemoveGroupMembers(RemoveGroupMembers rm) {
+        List<String> members = rm.getMembers();
+        String groupname = rm.getGroupname();
+        Authorizable group = null;
+        log.info("Removing members '{}' from group '{}'", members, groupname);
+        try {
+            group = UserUtil.getAuthorizable(session, groupname);
+            if (group == null || !group.isGroup()) {
+                throw new RuntimeException(groupname + " is not a group");
+            }
+            ((Group) group).removeMembers(members.toArray(new String[0]));
+        } catch (RepositoryException e) {
+            report(e, "Unable to remove members from group [" + groupname + "]:" + e);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/jcr/repoinit/impl/JcrRepoInitOpsProcessorImpl.java b/src/main/java/org/apache/sling/jcr/repoinit/impl/JcrRepoInitOpsProcessorImpl.java
index 3fa9bb9..51347c0 100644
--- a/src/main/java/org/apache/sling/jcr/repoinit/impl/JcrRepoInitOpsProcessorImpl.java
+++ b/src/main/java/org/apache/sling/jcr/repoinit/impl/JcrRepoInitOpsProcessorImpl.java
@@ -46,7 +46,8 @@
                 new NodetypesVisitor(session),
                 new PrivilegeVisitor(session),
                 new UserVisitor(session),
-                new AclVisitor(session)
+                new AclVisitor(session),
+                new GroupMembershipVisitor(session)
         };
 
         for(OperationVisitor v : visitors) {
diff --git a/src/test/java/org/apache/sling/jcr/repoinit/GroupMembershipTest.java b/src/test/java/org/apache/sling/jcr/repoinit/GroupMembershipTest.java
new file mode 100644
index 0000000..a4e22e1
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/repoinit/GroupMembershipTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.sling.jcr.repoinit;
+
+import org.apache.sling.jcr.repoinit.impl.TestUtil;
+import org.apache.sling.repoinit.parser.RepoInitParsingException;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import javax.jcr.RepositoryException;
+import java.util.Random;
+
+/** Test the group membership */
+public class GroupMembershipTest {
+
+    @Rule
+    public final SlingContext context = new SlingContext(ResourceResolverType.JCR_OAK);
+
+    private static final Random random = new Random(42);
+    private String grpNamePrefix;
+    private String userNamePrefix;
+    private String groupId;
+    private String userId;
+    private String secondUserId;
+    private TestUtil U;
+
+    @Before
+    public void setup() throws RepositoryException, RepoInitParsingException {
+        U = new TestUtil(context);
+        grpNamePrefix = "group_" + random.nextInt();
+        userNamePrefix = "user_" + random.nextInt();
+        groupId = grpNamePrefix + "_cg";
+        userId = userNamePrefix + "_cdst";
+        secondUserId = userNamePrefix + "_cdst_2";
+        U.parseAndExecute("create user " + userId);
+        U.parseAndExecute("create user " + secondUserId);
+        U.parseAndExecute("create group " + groupId);
+    }
+
+    @Test
+    public void addMemberToGroup() throws Exception {
+        U.parseAndExecute("add " + userId + " to group " + groupId);
+        U.assertGroupMembership(userId, groupId, true);
+    }
+    @Test
+    public void removeMemberFromGroup() throws Exception {
+        U.parseAndExecute("add " + secondUserId + " to group " + groupId);
+        U.assertGroupMembership(secondUserId, groupId, true);
+        U.parseAndExecute("remove " + secondUserId + " from group " + groupId);
+        U.assertGroupMembership(secondUserId, groupId, false);
+    }
+    
+    @Test
+    public void addNonExistingMemberToGroup() throws Exception {
+        String nonExistingUserId = userNamePrefix + "_non";
+        U.assertUser("User should not exist", nonExistingUserId, false);
+        U.parseAndExecute("add " + nonExistingUserId + " to group " + groupId);
+        U.assertGroupMembership(userId, groupId, false);
+    }
+
+    @Test
+    public void cyclicMembership() throws Exception {
+        U.parseAndExecute("add " + groupId + " to group " + groupId);
+        U.assertGroupMembership(userId, groupId, false);
+    }
+
+    @Test
+    public void addMemberToNonGroupAuthorizable() throws Exception {
+        String otherUserId = userNamePrefix + "_abc";
+        U.parseAndExecute("create user " + otherUserId);
+        try {
+            U.parseAndExecute("add " + userId + " to group " + otherUserId);
+            Assert.fail();
+        } catch (RuntimeException e) {
+            Assert.assertEquals("expected runtime exception",  otherUserId + " is not a group", e.getMessage());
+        }
+    }
+
+    @Test
+    public void addMemberToNonExistingGroup() throws Exception {
+        String nonExistingGroupId =  grpNamePrefix + "_non";
+        U.assertGroup("Group should not exist", nonExistingGroupId, false);
+        try {
+            U.parseAndExecute("add " + userId + " to group " + nonExistingGroupId);
+            Assert.fail();
+        } catch (RuntimeException e) {
+            Assert.assertEquals("expected runtime exception",  nonExistingGroupId + " is not a group", e.getMessage());
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/repoinit/impl/TestUtil.java b/src/test/java/org/apache/sling/jcr/repoinit/impl/TestUtil.java
index 9067a6d..1e6c6fb 100644
--- a/src/test/java/org/apache/sling/jcr/repoinit/impl/TestUtil.java
+++ b/src/test/java/org/apache/sling/jcr/repoinit/impl/TestUtil.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assert.assertEquals;
 
 import java.io.Reader;
 import java.io.StringReader;
@@ -180,6 +181,17 @@
         }
     }
 
+    public void assertGroupMembership(String userId, String groupId, boolean expectToBeMember) throws RepositoryException {
+        final Authorizable a = UserUtil.getUserManager(adminSession).getAuthorizable(groupId);
+        final Authorizable member = UserUtil.getUserManager(adminSession).getAuthorizable(userId);
+        boolean isMember = ((Group) a).isMember(member);
+        String message = "Expecting user " + userId;
+        if (!expectToBeMember) {
+            message += " not";
+        }
+        assertEquals(message +  " to be member of " + groupId, expectToBeMember, isMember);
+    }
+
     public void parseAndExecute(String input) throws RepositoryException, RepoInitParsingException {
         final JcrRepoInitOpsProcessorImpl p = new JcrRepoInitOpsProcessorImpl();
         p.apply(adminSession, parse(input));
diff --git a/src/test/java/org/apache/sling/jcr/repoinit/it/RepoInitTextIT.java b/src/test/java/org/apache/sling/jcr/repoinit/it/RepoInitTextIT.java
index 10b363f..0e666d7 100644
--- a/src/test/java/org/apache/sling/jcr/repoinit/it/RepoInitTextIT.java
+++ b/src/test/java/org/apache/sling/jcr/repoinit/it/RepoInitTextIT.java
@@ -46,6 +46,8 @@
     private static final String ANOTHER = "anotherService";
     private static final String ALICE = "alice";
     private static final String BOB = "bob";
+    private static final String GROUP_A = "grpA";
+    private static final String GROUP_B = "grpB";
 
     public static final String REPO_INIT_FILE = "/repoinit.txt";
 
@@ -131,4 +133,19 @@
             }
         };
     }
+
+    @Test
+    public void groupMembership() throws Exception {
+        new Retry() {
+            @Override
+            public Void call() throws Exception {
+                assertTrue("Expecting user " + FRED_WILMA + "to be member of " + GROUP_A, U.isMember(session, FRED_WILMA, GROUP_A));
+                assertTrue("Expecting user " + ALICE + "to be member of " + GROUP_A,U.isMember(session, ALICE, GROUP_A));
+                assertTrue("Expecting user " + ANOTHER + "to be member of " + GROUP_B,U.isMember(session, ANOTHER, GROUP_B));
+                assertFalse("Expecting user " + BOB + "not to be member of " + GROUP_B,U.isMember(session, BOB, GROUP_B));
+                assertFalse("Expecting group " + GROUP_A + "not to be member of " + GROUP_B,U.isMember(session, GROUP_A, GROUP_B));
+                return null;
+            }
+        };
+    }
 }
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/jcr/repoinit/it/U.java b/src/test/java/org/apache/sling/jcr/repoinit/it/U.java
index 499e94f..15ffb04 100644
--- a/src/test/java/org/apache/sling/jcr/repoinit/it/U.java
+++ b/src/test/java/org/apache/sling/jcr/repoinit/it/U.java
@@ -29,6 +29,7 @@
 
 import org.apache.jackrabbit.api.JackrabbitSession;
 import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
 import org.apache.jackrabbit.api.security.user.User;
 
 /** Test utilities */
@@ -102,4 +103,10 @@
         final Authorizable a = ((JackrabbitSession)session).getUserManager().getAuthorizable(userId);
         return a.getPath();
     }
+
+    public static boolean isMember(Session session, String userId, String groupId) throws  RepositoryException {
+        final Authorizable a = ((JackrabbitSession)session).getUserManager().getAuthorizable(groupId);
+        final Authorizable member = ((JackrabbitSession)session).getUserManager().getAuthorizable(userId);
+        return ((Group) a).isMember(member);
+    }
 }
\ No newline at end of file
diff --git a/src/test/resources/repoinit.txt b/src/test/resources/repoinit.txt
index 897f262..9a743aa 100644
--- a/src/test/resources/repoinit.txt
+++ b/src/test/resources/repoinit.txt
@@ -55,4 +55,12 @@
 set ACL on home(fredWilmaService)
   allow jcr:all for alice
   deny jcr:all for bob
-end
\ No newline at end of file
+end
+
+create group grpA
+add fredWilmaService,alice to group grpA
+
+create group grpB
+add anotherService,bob,grpA to group grpB
+remove bob,grpA from group grpB
+