SLING-8757 Add option to set/edit access control policies at user homes (add extraction of home-paths using constants added to sling-repoinit-parser)
diff --git a/pom.xml b/pom.xml
index 7120259..4f26c54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -229,7 +229,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.repoinit.parser</artifactId>
-            <version>1.3.1-SNAPSHOT</version>
+            <version>1.4.0-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/jcr/repoinit/impl/AclUtil.java b/src/main/java/org/apache/sling/jcr/repoinit/impl/AclUtil.java
index da409d1..4150302 100644
--- a/src/main/java/org/apache/sling/jcr/repoinit/impl/AclUtil.java
+++ b/src/main/java/org/apache/sling/jcr/repoinit/impl/AclUtil.java
@@ -17,6 +17,7 @@
 package org.apache.sling.jcr.repoinit.impl;
 
 import java.security.Principal;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
@@ -42,13 +43,20 @@
 import org.apache.jackrabbit.api.security.authorization.PrincipalAccessControlList;
 import org.apache.jackrabbit.api.security.user.Authorizable;
 import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
+import org.apache.jackrabbit.util.Text;
 import org.apache.sling.repoinit.parser.operations.AclLine;
 import org.apache.sling.repoinit.parser.operations.RestrictionClause;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static org.apache.sling.repoinit.parser.operations.AclLine.ID_DELIMINATOR;
+import static org.apache.sling.repoinit.parser.operations.AclLine.PATH_HOME;
+import static org.apache.sling.repoinit.parser.operations.AclLine.PATH_REPOSITORY;
 import static org.apache.sling.repoinit.parser.operations.AclLine.PROP_PATHS;
 import static org.apache.sling.repoinit.parser.operations.AclLine.PROP_PRIVILEGES;
+import static org.apache.sling.repoinit.parser.operations.AclLine.SUBTREE_DELIMINATOR;
 
 /** Utilities for ACL management */
 public class AclUtil {
@@ -108,26 +116,22 @@
 
     public static void setAcl(Session session, List<String> principals, List<String> paths, List<String> privileges, boolean isAllow, List<RestrictionClause> restrictionClauses)
             throws RepositoryException {
-        for (String path : paths) {
-            if (AclLine.PATH_REPOSITORY.equals(path)) {
-                setRepositoryAcl(session, principals, privileges, isAllow, restrictionClauses);
-            } else {
-                if (!session.nodeExists(path)) {
-                    throw new PathNotFoundException("Cannot set ACL on non-existent path " + path);
-                }
-                setAcl(session, principals, path, privileges, isAllow, restrictionClauses);
+        for (String jcrPath : getJcrPaths(session, paths)) {
+            if (jcrPath != null && !session.nodeExists(jcrPath)) {
+                throw new PathNotFoundException("Cannot set ACL on non-existent path " + jcrPath);
             }
+            setAcl(session, principals, jcrPath, privileges, isAllow, restrictionClauses);
         }
     }
 
-    private static void setAcl(Session session, List<String> principals, String path, List<String> privileges, boolean isAllow, List<RestrictionClause> restrictionClauses)
+    private static void setAcl(Session session, List<String> principals, String jcrPath, List<String> privileges, boolean isAllow, List<RestrictionClause> restrictionClauses)
             throws RepositoryException {
 
         final String [] privArray = privileges.toArray(new String[privileges.size()]);
         final Privilege[] jcrPriv = AccessControlUtils.privilegesFromNames(session, privArray);
 
-        JackrabbitAccessControlList acl = AccessControlUtils.getAccessControlList(session, path);
-        checkState(acl != null, "No JackrabbitAccessControlList available for path " + path);
+        JackrabbitAccessControlList acl = AccessControlUtils.getAccessControlList(session, jcrPath);
+        checkState(acl != null, "No JackrabbitAccessControlList available for path " + jcrPath);
 
         LocalRestrictions localRestrictions = createLocalRestrictions(restrictionClauses, acl, session);
 
@@ -145,7 +149,7 @@
             checkState(principal != null, "Principal not found: " + name);
             LocalAccessControlEntry newAce = new LocalAccessControlEntry(principal, jcrPriv, isAllow, localRestrictions);
             if (contains(existingAces, newAce)) {
-                LOG.info("Not adding {} to path {} since an equivalent access control entry already exists", newAce, path);
+                LOG.info("Not adding {} to path {} since an equivalent access control entry already exists", newAce, jcrPath);
                 continue;
             }
             acl.addEntry(newAce.principal, newAce.privileges, newAce.isAllow,
@@ -153,7 +157,7 @@
             changed = true;
         }
         if ( changed ) {
-            session.getAccessControlManager().setPolicy(path, acl);
+            session.getAccessControlManager().setPolicy(jcrPath, acl);
         }
     }
 
@@ -176,11 +180,10 @@
             LocalRestrictions restrictions = createLocalRestrictions(line.getRestrictions(), acl, session);
             Privilege[] privileges = AccessControlUtils.privilegesFromNames(session, line.getProperty(PROP_PRIVILEGES).toArray(new String[0]));
 
-            for (String path : line.getProperty(PROP_PATHS)) {
-                String effectivePath = (path == null || path.isEmpty() || AclLine.PATH_REPOSITORY.equals(path)) ? null : path;
+            for (String effectivePath : getJcrPaths(session, line.getProperty(PROP_PATHS))) {
                 boolean added = acl.addEntry(effectivePath, privileges, restrictions.getRestrictions(), restrictions.getMVRestrictions());
                 if (!added) {
-                    LOG.info("Equivalent principal-based entry already exists for principal {} and effective path {} ", principalName, path);
+                    LOG.info("Equivalent principal-based entry already exists for principal {} and effective path {} ", principalName, effectivePath);
                 } else {
                     modified = true;
                 }
@@ -211,6 +214,39 @@
         return acl;
     }
 
+    @NotNull
+    private static List<String> getJcrPaths(@NotNull Session session, @NotNull List<String> paths) throws RepositoryException {
+        List<String> jcrPaths = new ArrayList<>(paths.size());
+        for (String path : paths) {
+            if (PATH_REPOSITORY.equals(path) || path == null || path.isEmpty()) {
+                jcrPaths.add(null);
+            } else if (path.startsWith(PATH_HOME)) {
+                int lastHashIndex = path.lastIndexOf(SUBTREE_DELIMINATOR);
+                checkState(lastHashIndex > -1, "Invalid format of home path: # deliminator expected.");
+                String subTreePath = path.substring(lastHashIndex+1);
+                for (String aPath : getAuthorizablePaths(session, path.substring(PATH_HOME.length(), lastHashIndex))) {
+                    jcrPaths.add(aPath + subTreePath);
+                }
+            } else {
+                jcrPaths.add(path);
+            }
+        }
+        return jcrPaths;
+    }
+
+    @NotNull
+    private static Iterable<String> getAuthorizablePaths(@NotNull Session session, @NotNull String ids) throws RepositoryException {
+        List<String> paths = new ArrayList<>();
+        for (String id : Text.explode(ids, ID_DELIMINATOR)) {
+            Authorizable a = UserUtil.getAuthorizable(session, id);
+            if (a == null) {
+                throw new PathNotFoundException("Cannot resolve path of user/group with id '" + id + "'.");
+            }
+            paths.add(a.getPath());
+        }
+        return paths;
+    }
+
     // visible for testing
     static boolean contains(AccessControlEntry[] existingAces, LocalAccessControlEntry newAce) throws RepositoryException {
         for (int i = 0 ; i < existingAces.length; i++) {
diff --git a/src/test/java/org/apache/sling/jcr/repoinit/PrincipalBasedAclTest.java b/src/test/java/org/apache/sling/jcr/repoinit/PrincipalBasedAclTest.java
index ad42344..9d14301 100644
--- a/src/test/java/org/apache/sling/jcr/repoinit/PrincipalBasedAclTest.java
+++ b/src/test/java/org/apache/sling/jcr/repoinit/PrincipalBasedAclTest.java
@@ -17,6 +17,11 @@
 package org.apache.sling.jcr.repoinit;
 
 import org.apache.jackrabbit.api.JackrabbitRepository;
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
+import org.apache.jackrabbit.api.security.authorization.PrincipalAccessControlList;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
 import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.jackrabbit.oak.jcr.Jcr;
@@ -32,9 +37,13 @@
 import org.apache.jackrabbit.oak.spi.security.principal.SystemUserPrincipal;
 import org.apache.jackrabbit.oak.spi.security.user.UserConfiguration;
 import org.apache.jackrabbit.oak.spi.security.user.UserConstants;
+import org.apache.sling.jcr.repoinit.impl.AclUtil;
 import org.apache.sling.jcr.repoinit.impl.TestUtil;
 import org.apache.sling.repoinit.parser.RepoInitParsingException;
+import org.apache.sling.repoinit.parser.operations.AclLine;
 import org.apache.sling.testing.mock.osgi.junit.OsgiContext;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
@@ -47,11 +56,18 @@
 import javax.jcr.Session;
 import javax.jcr.SimpleCredentials;
 import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.AccessControlPolicy;
+import javax.jcr.security.Privilege;
 import javax.security.auth.Subject;
+import java.security.Principal;
 import java.security.PrivilegedExceptionAction;
 import java.util.Collections;
+import java.util.List;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 public class PrincipalBasedAclTest {
@@ -390,4 +406,38 @@
 
         U.parseAndExecute(setup);
     }
+
+    @Test
+    public void testHomePath() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+        Authorizable a = uMgr.getAuthorizable(U.username);
+        Principal principal = a.getPrincipal();
+        String userHomePath = a.getPath();
+
+        JackrabbitAccessControlManager accessControlManager = AclUtil.getJACM(U.adminSession);
+        assertNull(getAcl(principal, accessControlManager));
+
+        AclLine line = new AclLine(AclLine.Action.ALLOW);
+        line.setProperty(AclLine.PROP_PRINCIPALS, Collections.singletonList(principal.getName()));
+        line.setProperty(AclLine.PROP_PRIVILEGES, Collections.singletonList(Privilege.JCR_READ));
+        line.setProperty(AclLine.PROP_PATHS, Collections.singletonList(":home:"+U.username+"#"));
+        AclUtil.setPrincipalAcl(U.adminSession, U.username, Collections.singletonList(line));
+
+        PrincipalAccessControlList acl = getAcl(principal, accessControlManager);
+        assertNotNull(acl);
+        assertEquals(1, acl.size());
+        PrincipalAccessControlList.Entry entry = (PrincipalAccessControlList.Entry) acl.getAccessControlEntries()[0];
+        assertArrayEquals(AccessControlUtils.privilegesFromNames(accessControlManager, Privilege.JCR_READ), entry.getPrivileges());
+        assertEquals(a.getPath(), entry.getEffectivePath());
+    }
+
+    @Nullable
+    private static PrincipalAccessControlList getAcl(@NotNull Principal principal, @NotNull JackrabbitAccessControlManager jacm) throws RepositoryException {
+        for (AccessControlPolicy policy : jacm.getPolicies(principal)) {
+            if (policy instanceof PrincipalAccessControlList) {
+                return (PrincipalAccessControlList) policy;
+            }
+        }
+        return null;
+    }
 }
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/jcr/repoinit/impl/AclUtilTest.java b/src/test/java/org/apache/sling/jcr/repoinit/impl/AclUtilTest.java
index 8ea5f38..7a434f7 100644
--- a/src/test/java/org/apache/sling/jcr/repoinit/impl/AclUtilTest.java
+++ b/src/test/java/org/apache/sling/jcr/repoinit/impl/AclUtilTest.java
@@ -21,14 +21,18 @@
 import static org.junit.Assert.assertTrue;
 
 import java.security.Principal;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.List;
 
+import javax.jcr.PathNotFoundException;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 import javax.jcr.security.Privilege;
 
 import org.apache.jackrabbit.api.JackrabbitSession;
 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.commons.PathUtils;
@@ -40,6 +44,7 @@
 import org.junit.After;
 import static org.junit.Assert.assertEquals;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 
@@ -70,6 +75,9 @@
 
     @After
     public void cleanup() throws RepositoryException, RepoInitParsingException {
+        if (U.adminSession != null) {
+            U.adminSession.refresh(false);
+        }
         U.cleanupUser();
     }
 
@@ -246,6 +254,135 @@
         }
     }
 
+    @Test
+    public void testSetAclWithHomePath() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+        String userHomePath = uMgr.getAuthorizable(U.username).getPath();
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, userHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        List<String> paths = Collections.singletonList(":home:"+U.username+"#");
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, userHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+    }
+
+    @Test
+    public void testSetAclWithHomePathMultipleIds() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+        String userHomePath = uMgr.getAuthorizable(U.username).getPath();
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, userHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        Group gr = uMgr.createGroup("groupId");
+        String groupHomePath = gr.getPath();
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        List<String> paths = Collections.singletonList(":home:"+U.username+","+gr.getID()+"#");
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, userHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+    }
+
+    @Test
+    public void testSetAclWithHomePathMultiplePath() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+        String userHomePath = uMgr.getAuthorizable(U.username).getPath();
+
+        List<String> paths = Arrays.asList(":home:" + U.username + "#", ":repository", PathUtils.ROOT_PATH);
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_ALL), true);
+
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, userHomePath), U.username, new String[] {Privilege.JCR_ALL}, true);
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, null), U.username, new String[] {Privilege.JCR_ALL}, true);
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, PathUtils.ROOT_PATH), U.username, new String[] {Privilege.JCR_ALL}, true);
+    }
+
+    @Test
+    public void testSetAclWithHomePathAndSubtree() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+        String userHomePath = U.adminSession.getNode(uMgr.getAuthorizable(U.username).getPath()).addNode("profiles").addNode("private").getPath();
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, userHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        Group gr = uMgr.createGroup("groupId");
+        String groupHomePath = U.adminSession.getNode(gr.getPath()).addNode("profiles").addNode("private").getPath();
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        List<String> paths = Collections.singletonList(":home:"+U.username+","+gr.getID()+"#/profiles/private");
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, userHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+    }
+
+    @Test(expected = PathNotFoundException.class)
+    public void testSetAclWithHomePathAndMissingSubtree() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+        String userHomePath = uMgr.getAuthorizable(U.username).getPath() + "/profiles/private";
+        assertFalse(U.adminSession.nodeExists(userHomePath));
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, userHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        Group gr = uMgr.createGroup("groupId");
+        String groupHomePath = U.adminSession.getNode(gr.getPath()).addNode("profiles").addNode("private").getPath();
+
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        List<String> paths = Collections.singletonList(":home:"+U.username+","+gr.getID()+"#/profiles/private");
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+    }
+
+    @Test
+    public void testSetAclWithHomePathIdWithHash() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+
+        Group gr = uMgr.createGroup("g#roupId#");
+        String groupHomePath = gr.getPath();
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        List<String> paths = Collections.singletonList(":home:"+gr.getID()+","+U.username+"#");
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+    }
+
+    @Test
+    public void testSetAclWithHomePathIdWithColon() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+
+        Group gr = uMgr.createGroup(":group:Id");
+        String groupHomePath = gr.getPath();
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        List<String> paths = Collections.singletonList(":home:"+gr.getID()+","+U.username+"#");
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+    }
+
+    @Ignore("TODO: user/groupId containing , will fail ac setup")
+    @Test
+    public void testSetAclWithHomePathIdWithComma() throws Exception {
+        UserManager uMgr = ((JackrabbitSession) U.adminSession).getUserManager();
+
+        Group gr = uMgr.createGroup(",group,Id,");
+        String groupHomePath = gr.getPath();
+        assertIsNotContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+
+        List<String> paths = Collections.singletonList(":home:"+gr.getID()+","+U.username+"#");
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+
+        assertIsContained(AccessControlUtils.getAccessControlList(U.adminSession, groupHomePath), U.username, new String[] {Privilege.JCR_READ}, true);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testSetAclWithHomePathMissingTrailingHash() throws Exception {
+        List<String> paths = Collections.singletonList(":home:"+U.username);
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+    }
+
+    @Test(expected = PathNotFoundException.class)
+    public void testSetAclWithHomePathUnknownUser() throws Exception {
+        List<String> paths = Collections.singletonList(":home:alice#");
+        AclUtil.setAcl(U.adminSession, Collections.singletonList(U.username), paths, Collections.singletonList(Privilege.JCR_READ), true);
+    }
+
     private void assertIsContained(JackrabbitAccessControlList acl, String username, String[] privilegeNames, boolean isAllow) throws RepositoryException {
         assertIsContained0(acl, username, privilegeNames, isAllow, true);
     }