SLING-10192 Add option to set/edit properties on users and groups (#13)

diff --git a/src/main/java/org/apache/sling/jcr/repoinit/impl/NodePropertiesVisitor.java b/src/main/java/org/apache/sling/jcr/repoinit/impl/NodePropertiesVisitor.java
index d04c4c4..3483d69 100644
--- a/src/main/java/org/apache/sling/jcr/repoinit/impl/NodePropertiesVisitor.java
+++ b/src/main/java/org/apache/sling/jcr/repoinit/impl/NodePropertiesVisitor.java
@@ -16,24 +16,27 @@
  */
 package org.apache.sling.jcr.repoinit.impl;
 
-import java.util.List;
+import java.util.ArrayList;
 import java.util.Calendar;
+import java.util.List;
 
-import javax.jcr.Session;
 import javax.jcr.Node;
 import javax.jcr.PathNotFoundException;
-import javax.jcr.Value;
 import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
 
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.util.Text;
 import org.apache.jackrabbit.value.BooleanValue;
 import org.apache.jackrabbit.value.DateValue;
 import org.apache.jackrabbit.value.DoubleValue;
 import org.apache.jackrabbit.value.LongValue;
 import org.apache.jackrabbit.value.StringValue;
-
 import org.apache.sling.repoinit.parser.operations.PropertyLine;
 import org.apache.sling.repoinit.parser.operations.SetProperties;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * OperationVisitor which processes only operations related to setting node
@@ -41,6 +44,16 @@
  * the execution order.
  */
 class NodePropertiesVisitor extends DoNothingVisitor {
+    /**
+     * The repoinit.parser transforms the authorizable(ids)[/relative_path] path
+     * syntax from the original source into ":authorizable:ids#/relative_path" in the 
+     * values provided from {@link SetProperties#getPaths()}
+     * 
+     * These constants are used to unwind those values into the parts for processing
+     */
+    private static final String PATH_AUTHORIZABLE = ":authorizable:";
+    private static final char ID_DELIMINATOR = ',';
+    private static final char SUBTREE_DELIMINATOR = '#';
 
     /**
      * Create a visitor using the supplied JCR Session.
@@ -70,30 +83,135 @@
         return(!n.hasProperty(name) || n.getProperty(name) == null);
     }
 
+    /**
+     * True if the property needs to be set - if false, it is not touched. This
+     * handles the "default" repoinit instruction, which means "do not change the
+     * property if already set"
+     *
+     * @throws RepositoryException
+     * @throws PathNotFoundException
+     */
+    private static boolean needToSetProperty(Authorizable a, String pRelPath, boolean isDefault) throws RepositoryException {
+        if (!isDefault) {
+            // It's a "set" line -> overwrite existing value if any
+            return true;
+        }
+
+        // Otherwise set the property only if not set yet
+        return(!a.hasProperty(pRelPath) || a.getProperty(pRelPath) == null);
+    }
+
+    /**
+     * Build relative property path from a subtree path and a property name
+     * @param subTreePath the subtree path (may be null or empty)
+     * @param name the property name
+     * @return the relative path of the property
+     */
+    private static String toRelPath(String subTreePath, final String name) {
+        final String pRelPath;
+        if (subTreePath == null || subTreePath.isEmpty()) {
+            pRelPath = name;
+        } else {
+            if (subTreePath.startsWith("/")) {
+                subTreePath = subTreePath.substring(1);
+            }
+            pRelPath = String.format("%s/%s", subTreePath, name);
+        }
+        return pRelPath;
+    }
+
+    /**
+     * Lookup the authorizables for the given ids
+     * @param session the jcr session
+     * @param ids delimited list of authorizable ids
+     * @return iterator over the found authorizables
+     */
+    @NotNull
+    private static Iterable<Authorizable> getAuthorizables(@NotNull Session session, @NotNull String ids) throws RepositoryException {
+        List<Authorizable> authorizables = 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 authorizable with id '" + id + "'.");
+            }
+            authorizables.add(a);
+        }
+        return authorizables;
+    }
+
+    /**
+     * Set properties on a user or group
+     * 
+     * @param nodePath the target path
+     * @param propertyLines the property lines to process to set the properties
+     */
+    private void setAuthorizableProperties(String nodePath, List<PropertyLine> propertyLines) throws RepositoryException {
+        int lastHashIndex = nodePath.lastIndexOf(SUBTREE_DELIMINATOR);
+        if (lastHashIndex == -1) {
+            throw new IllegalStateException("Invalid format of authorizable path: # deliminator expected.");
+        }
+        String ids = nodePath.substring(PATH_AUTHORIZABLE.length(), lastHashIndex);
+        String subTreePath = nodePath.substring(lastHashIndex + 1);
+        for (Authorizable a : getAuthorizables(session, ids)) {
+            log.info("Setting properties on authorizable '{}'", a.getID());
+            for (PropertyLine pl : propertyLines) {
+                final String pName = pl.getPropertyName();
+                final String pRelPath = toRelPath(subTreePath, pName);
+                if (needToSetProperty(a, pRelPath, pl.isDefault())) {
+                    final List<Object> values = pl.getPropertyValues();
+                    if (values.size() > 1) {
+                        Value[] pValues = convertToValues(values);
+                        a.setProperty(pRelPath, pValues);
+                    } else {
+                        Value pValue = convertToValue(values.get(0));
+                        a.setProperty(pRelPath, pValue);
+                    }
+                } else {
+                    log.info("Property '{}' already set on authorizable '{}', existing value will not be overwritten in 'default' mode",
+                        pRelPath, a.getID());
+                }
+            }
+        }
+    }
+
+    /**
+     * Set properties on a JCR node
+     * 
+     * @param nodePath the target path
+     * @param propertyLines the property lines to process to set the properties
+     */
+    private void setNodeProperties(String nodePath, List<PropertyLine> propertyLines) throws RepositoryException {
+        log.info("Setting properties on nodePath '{}'", nodePath);
+        Node n = session.getNode(nodePath);
+        for (PropertyLine pl : propertyLines) {
+            final String pName = pl.getPropertyName();
+            if (needToSetProperty(n, pl)) {
+                final PropertyLine.PropertyType pType = pl.getPropertyType();
+                final int type = PropertyType.valueFromName(pType.name());
+                final List<Object> values = pl.getPropertyValues();
+                if (values.size() > 1) {
+                    Value[] pValues = convertToValues(values);
+                    n.setProperty(pName, pValues, type);
+                } else {
+                    Value pValue = convertToValue(values.get(0));
+                    n.setProperty(pName, pValue, type);
+                }
+            } else {
+                log.info("Property '{}' already set on path '{}', existing value will not be overwritten in 'default' mode",
+                    pName, nodePath);
+            }
+        }
+    }
+
     @Override
     public void visitSetProperties(SetProperties sp) {
         for (String nodePath : sp.getPaths()) {
             try {
-                log.info("Setting properties on nodePath '{}'", nodePath);
-                Node n = session.getNode(nodePath);
-                for (PropertyLine pl : sp.getPropertyLines()) {
-                    final String pName = pl.getPropertyName();
-                    final PropertyLine.PropertyType pType = pl.getPropertyType();
-                    final List<Object> values = pl.getPropertyValues();
-                    final int type = PropertyType.valueFromName(pType.name());
-                    if (needToSetProperty(n, pl)) {
-                        if (values.size() > 1) {
-                            Value[] pValues = convertToValues(values);
-                            n.setProperty(pName, pValues, type);
-                        } else {
-                            Value pValue = convertToValue(values.get(0));
-                            n.setProperty(pName, pValue, type);
-                        }
-                    } else {
-                        log.info(
-                            "Property '{}' already set on path '{}', existing value will not be overwritten in 'default' mode",
-                            pName, nodePath);
-                    }
+                if (nodePath.startsWith(PATH_AUTHORIZABLE)) {
+                    // special case for setting properties on authorizable
+                    setAuthorizableProperties(nodePath, sp.getPropertyLines());
+                } else {
+                    setNodeProperties(nodePath, sp.getPropertyLines());
                 }
             } catch (RepositoryException e) {
                 report(e, "Unable to set properties on path [" + nodePath + "]:" + e);
diff --git a/src/test/java/org/apache/sling/jcr/repoinit/SetPropertiesTest.java b/src/test/java/org/apache/sling/jcr/repoinit/SetPropertiesTest.java
index 67a886a..2914b5b 100644
--- a/src/test/java/org/apache/sling/jcr/repoinit/SetPropertiesTest.java
+++ b/src/test/java/org/apache/sling/jcr/repoinit/SetPropertiesTest.java
@@ -128,4 +128,173 @@
         U.assertSVPropertyExists(path3, "one", vf.createValue("oneB"));
         U.assertSVPropertyExists(path3, "two", vf.createValue("twoA"));
     }
+
+    @Test
+    public void setUserProperties() throws Exception {
+        String userid = "user" + UUID.randomUUID();
+
+        U.assertUser("before creating user", userid, false);
+        U.parseAndExecute("create user " + userid);
+        U.assertUser("after creating user", userid, true);
+
+        assertAuthorizableProperties(userid);
+        assertAuthorizablePropertiesAgain(userid);
+    }
+
+    @Test
+    public void setSubTreeUserProperties() throws Exception {
+        String userid = "user" + UUID.randomUUID();
+
+        U.assertUser("before creating user", userid, false);
+        U.parseAndExecute("create user " + userid);
+        U.assertUser("after creating user", userid, true);
+
+        assertAuthorizableSubTreeProperties(userid);
+        assertAuthorizableSubTreePropertiesAgain(userid);
+    }
+
+    @Test
+    public void setGroupProperties() throws Exception {
+        String groupid = "group" + UUID.randomUUID();
+
+        U.assertGroup("before creating group", groupid, false);
+        U.parseAndExecute("create group " + groupid);
+        U.assertGroup("after creating group", groupid, true);
+
+        assertAuthorizableProperties(groupid);
+        assertAuthorizablePropertiesAgain(groupid);
+    }
+
+    @Test
+    public void setSubTreeGroupProperties() throws Exception {
+        String groupid = "group" + UUID.randomUUID();
+
+        U.assertGroup("before creating group", groupid, false);
+        U.parseAndExecute("create group " + groupid);
+        U.assertGroup("after creating group", groupid, true);
+
+        assertAuthorizableSubTreeProperties(groupid);
+        assertAuthorizableSubTreePropertiesAgain(groupid);
+    }
+
+    /**
+     * Set properties on an authorizable and then verify that the values were set
+     */
+    protected void assertAuthorizableProperties(String id) throws RepositoryException, RepoInitParsingException {
+        final String setPropsA =
+                "set properties on authorizable(" +id + ")\n"
+                        + "set one to oneA\n"
+                        + "default two to twoA\n"
+                        + "set nested/one to oneA\n"
+                        + "default nested/two to twoA\n"
+                        + "set three to threeA, \"threeB\", threeC\n"
+                        + "default four to fourA, \"fourB\"\n"
+                        + "set nested/three to threeA, \"threeB\", threeC\n"
+                        + "default nested/four to fourA, \"fourB\"\n"
+                + "end";
+
+        U.parseAndExecute(setPropsA);
+
+        U.assertAuthorizableSVPropertyExists(id, "one", vf.createValue("oneA"));
+        U.assertAuthorizableSVPropertyExists(id, "nested/one", vf.createValue("oneA"));
+        U.assertAuthorizableSVPropertyExists(id, "two", vf.createValue("twoA"));
+        U.assertAuthorizableSVPropertyExists(id, "nested/two", vf.createValue("twoA"));
+        U.assertAuthorizableMVPropertyExists(id, "three", new Value[] {
+                vf.createValue("threeA"),
+                vf.createValue("threeB"),
+                vf.createValue("threeC")
+                });
+        U.assertAuthorizableMVPropertyExists(id, "nested/three", new Value[] {
+                vf.createValue("threeA"),
+                vf.createValue("threeB"),
+                vf.createValue("threeC")
+                });
+        U.assertAuthorizableMVPropertyExists(id, "four", new Value[] {
+                vf.createValue("fourA"),
+                vf.createValue("fourB")
+                });
+        U.assertAuthorizableMVPropertyExists(id, "nested/four", new Value[] {
+                vf.createValue("fourA"),
+                vf.createValue("fourB")
+                });
+    }
+
+    /**
+     * Change values for existing properties on an authorizable and then verify that the values were set
+     * or not as appropriate
+     */
+    protected void assertAuthorizablePropertiesAgain(String id) throws RepositoryException, RepoInitParsingException {
+        final String setPropsA =
+                "set properties on authorizable(" + id + ")\n"
+                        + "set one to changed_oneA\n"
+                        + "default two to changed_twoA\n"
+                        + "set nested/one to changed_oneA\n"
+                        + "default nested/two to changed_twoA\n"
+                        + "set three to changed_threeA, \"changed_threeB\", changed_threeC\n"
+                        + "default four to changed_fourA, \"changed_fourB\"\n"
+                        + "set nested/three to changed_threeA, \"changed_threeB\", changed_threeC\n"
+                        + "default nested/four to changed_fourA, \"changed_fourB\"\n"
+                + "end";
+
+        U.parseAndExecute(setPropsA);
+
+        U.assertAuthorizableSVPropertyExists(id, "one", vf.createValue("changed_oneA"));
+        U.assertAuthorizableSVPropertyExists(id, "nested/one", vf.createValue("changed_oneA"));
+        U.assertAuthorizableSVPropertyExists(id, "two", vf.createValue("twoA"));
+        U.assertAuthorizableSVPropertyExists(id, "nested/two", vf.createValue("twoA"));
+        U.assertAuthorizableMVPropertyExists(id, "three", new Value[] {
+                vf.createValue("changed_threeA"),
+                vf.createValue("changed_threeB"),
+                vf.createValue("changed_threeC")
+                });
+        U.assertAuthorizableMVPropertyExists(id, "nested/three", new Value[] {
+                vf.createValue("changed_threeA"),
+                vf.createValue("changed_threeB"),
+                vf.createValue("changed_threeC")
+                });
+        U.assertAuthorizableMVPropertyExists(id, "four", new Value[] {
+                vf.createValue("fourA"),
+                vf.createValue("fourB")
+                });
+        U.assertAuthorizableMVPropertyExists(id, "nested/four", new Value[] {
+                vf.createValue("fourA"),
+                vf.createValue("fourB")
+                });
+    }
+
+    /**
+     * Set properties on a subtree of an authorizable and then verify that the values were set
+     */
+    protected void assertAuthorizableSubTreeProperties(String id)
+            throws RepositoryException, RepoInitParsingException {
+        final String setPropsA =
+                "set properties on authorizable(" + id + ")/nested\n"
+                        + "set one to oneA\n"
+                        + "default two to twoA\n"
+                + "end";
+
+        U.parseAndExecute(setPropsA);
+
+        U.assertAuthorizableSVPropertyExists(id, "nested/one", vf.createValue("oneA"));
+        U.assertAuthorizableSVPropertyExists(id, "nested/two", vf.createValue("twoA"));
+    }
+
+    /**
+     * Change values for existing properties on a subtree of an authorizable and then verify 
+     * that the values were set or not as appropriate
+     */
+    protected void assertAuthorizableSubTreePropertiesAgain(String id)
+            throws RepositoryException, RepoInitParsingException {
+        final String setPropsA =
+                "set properties on authorizable(" + id + ")/nested\n"
+                        + "set one to changed_oneA\n"
+                        + "default two to changed_twoA\n"
+                + "end";
+
+        U.parseAndExecute(setPropsA);
+
+        U.assertAuthorizableSVPropertyExists(id, "nested/one", vf.createValue("changed_oneA"));
+        U.assertAuthorizableSVPropertyExists(id, "nested/two", vf.createValue("twoA"));
+    }
+
 }
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 fb7e201..306c877 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
@@ -197,6 +197,32 @@
         assertEquals(message +  " to be member of " + groupId, expectToBeMember, isMember);
     }
 
+    public void assertAuthorizableSVPropertyExists(String id, String propertyName, Value expectedValue) throws RepositoryException {
+        final Authorizable a = UserUtil.getAuthorizable(adminSession, id);
+        assertNotNull("failed to get authorizable for " + id, a);
+        if (!a.hasProperty(propertyName)) {
+            fail("No " + propertyName + " property for " + a.getID());
+        } else {
+            Value[] property = a.getProperty(propertyName);
+            assertNotNull("Expected non-null value for property: " + propertyName, property);
+            assertEquals("Expected one value for property: " + propertyName, 1, property.length);
+            Value actualValue = property[0];
+            assertEquals("Value mismatch for property: " + propertyName, expectedValue, actualValue);
+        }
+    }
+
+    public void assertAuthorizableMVPropertyExists(String id, String propertyName, Value[] expectedValues) throws RepositoryException {
+        final Authorizable a = UserUtil.getAuthorizable(adminSession, id);
+        assertNotNull("failed to get authorizable for " + id, a);
+        if (!a.hasProperty(propertyName)) {
+            fail("No " + propertyName + " property for " + a.getID());
+        } else {
+            Value[] actualValues = a.getProperty(propertyName);
+            assertNotNull("Expected non-null value for property: " + propertyName, actualValues);
+            assertArrayEquals("Values mismatch for property: " + propertyName, expectedValues, actualValues);
+        }
+    }
+
     public void assertSVPropertyExists(String path, String propertyName, Value expectedValue) throws RepositoryException {
         final Node n = adminSession.getNode(path);
         if(!n.hasProperty(propertyName)) {
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 11e2e2b..3bebc3c 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
@@ -24,11 +24,14 @@
 import java.io.InputStreamReader;
 import java.util.UUID;
 
-import javax.jcr.PropertyType;
-import javax.jcr.ValueFactory;
-import javax.jcr.Value;
 import javax.inject.Inject;
+import javax.jcr.PropertyType;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
 
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.sling.jcr.repoinit.JcrRepoInitOpsProcessor;
 import org.apache.sling.repoinit.parser.RepoInitParser;
 import org.junit.Before;
@@ -211,4 +214,126 @@
             }
         };
     }
+
+    @Test
+    public void setAuthorizableProperties() throws Exception {
+        new Retry() {
+            @Override
+            public Void call() throws Exception {
+                if(!(session instanceof JackrabbitSession)) {
+                    throw new IllegalArgumentException("Session is not a JackrabbitSession");
+                }
+                UserManager um = ((JackrabbitSession)session).getUserManager();
+
+                Authorizable [] authorizables = new Authorizable[] {
+                        um.getAuthorizable(ALICE),
+                        um.getAuthorizable(GROUP_A)
+                };
+
+                for (Authorizable authorizable : authorizables) {
+                    assertNotNull("Expected authorizable to not be null", authorizable);
+                    ValueFactory vf = session.getValueFactory();
+                    Value[] expectedValues1 = new Value[2];
+                    expectedValues1[0] = vf.createValue("/d/e/f/*");
+                    expectedValues1[1] = vf.createValue("m/n/*");
+                    assertTrue("Expecting array type property " + PROP_A + " to be present ", U.hasProperty(authorizable, PROP_A, expectedValues1));
+
+                    Value expectedValue2 = vf.createValue("42", PropertyType.valueFromName("Long"));
+                    assertTrue("Expecting Long type default property " + PROP_B + " to be present ", U.hasProperty(authorizable, PROP_B, expectedValue2));
+
+                    Value expectedValue3  = vf.createValue("true", PropertyType.valueFromName("Boolean"));
+                    assertTrue("Expecting bool type property " + PROP_C + " to be present ", U.hasProperty(authorizable, PROP_C, expectedValue3));
+
+                    Value expectedValue4 = vf.createValue("2020-03-19T11:39:33.437+05:30", PropertyType.valueFromName("Date"));
+                    assertTrue("Expecting date type property " + PROP_D + " to be present " , U.hasProperty(authorizable, PROP_D, expectedValue4));
+
+                    Value expectedValue5 = vf.createValue("test");
+                    assertTrue("Expecting string type property " + PROP_E + " to be present " , U.hasProperty(authorizable, PROP_E, expectedValue5));
+
+                    Value expectedValue6 = vf.createValue("hello, you!");
+                    assertTrue("Expecting quoted string type property " + PROP_F + " to be present " , U.hasProperty(authorizable, PROP_F, expectedValue6));
+
+                    Value[] expectedValues7 = new Value[2];
+                    expectedValues7[0] = vf.createValue("test1");
+                    expectedValues7[1] = vf.createValue("test2");
+                    assertTrue("Expecting string array type property " + PROP_G + " to be present " , U.hasProperty(authorizable, PROP_G, expectedValues7));
+
+                    Value expectedValue8 = vf.createValue("Here's a \"double quoted string\" with suffix");
+                    assertTrue("Expecting quoted string type property " + PROP_H + " to be present " , U.hasProperty(authorizable, PROP_H, expectedValue8));
+
+                    Value[] expectedValues9 = new Value[3];
+                    expectedValues9[0] = vf.createValue("quoted");
+                    expectedValues9[1] = vf.createValue("non-quoted");
+                    expectedValues9[2] = vf.createValue("the last \" one");
+                    assertTrue("Expecting string array type property " + PROP_I + " to be present " , U.hasProperty(authorizable, PROP_I, expectedValues9));
+
+                    Value nestedExpectedValue = vf.createValue("42", PropertyType.valueFromName("Long"));
+                    assertTrue("Expecting Long type default property nested/" + PROP_B + " to be present ", U.hasProperty(authorizable, "nested/" +PROP_B, nestedExpectedValue));
+                }
+
+                return null;
+            }
+        };
+    }
+
+    @Test
+    public void setAuthorizableSubTreeProperties() throws Exception {
+        new Retry() {
+            @Override
+            public Void call() throws Exception {
+                if(!(session instanceof JackrabbitSession)) {
+                    throw new IllegalArgumentException("Session is not a JackrabbitSession");
+                }
+                UserManager um = ((JackrabbitSession)session).getUserManager();
+
+                Authorizable [] authorizables = new Authorizable[] {
+                        um.getAuthorizable(BOB),
+                        um.getAuthorizable(GROUP_B)
+                };
+
+                for (Authorizable authorizable : authorizables) {
+                    assertNotNull("Expected authorizable to not be null", authorizable);
+                    ValueFactory vf = session.getValueFactory();
+                    Value[] expectedValues1 = new Value[2];
+                    expectedValues1[0] = vf.createValue("/d/e/f/*");
+                    expectedValues1[1] = vf.createValue("m/n/*");
+                    assertTrue("Expecting array type property nested/" + PROP_A + " to be present ", U.hasProperty(authorizable, "nested/" + PROP_A, expectedValues1));
+
+                    Value expectedValue2 = vf.createValue("42", PropertyType.valueFromName("Long"));
+                    assertTrue("Expecting Long type default property nested/" + PROP_B + " to be present ", U.hasProperty(authorizable, "nested/" + PROP_B, expectedValue2));
+
+                    Value expectedValue3  = vf.createValue("true", PropertyType.valueFromName("Boolean"));
+                    assertTrue("Expecting bool type property nested/" + PROP_C + " to be present ", U.hasProperty(authorizable, "nested/" + PROP_C, expectedValue3));
+
+                    Value expectedValue4 = vf.createValue("2020-03-19T11:39:33.437+05:30", PropertyType.valueFromName("Date"));
+                    assertTrue("Expecting date type property nested/" + PROP_D + " to be present " , U.hasProperty(authorizable, "nested/" + PROP_D, expectedValue4));
+
+                    Value expectedValue5 = vf.createValue("test");
+                    assertTrue("Expecting string type property nested/" + PROP_E + " to be present " , U.hasProperty(authorizable, "nested/" + PROP_E, expectedValue5));
+
+                    Value expectedValue6 = vf.createValue("hello, you!");
+                    assertTrue("Expecting quoted string type property nested/" + PROP_F + " to be present " , U.hasProperty(authorizable, "nested/" + PROP_F, expectedValue6));
+
+                    Value[] expectedValues7 = new Value[2];
+                    expectedValues7[0] = vf.createValue("test1");
+                    expectedValues7[1] = vf.createValue("test2");
+                    assertTrue("Expecting string array type property nested/" + PROP_G + " to be present " , U.hasProperty(authorizable, "nested/" + PROP_G, expectedValues7));
+
+                    Value expectedValue8 = vf.createValue("Here's a \"double quoted string\" with suffix");
+                    assertTrue("Expecting quoted string type property nested/" + PROP_H + " to be present " , U.hasProperty(authorizable, "nested/" + PROP_H, expectedValue8));
+
+                    Value[] expectedValues9 = new Value[3];
+                    expectedValues9[0] = vf.createValue("quoted");
+                    expectedValues9[1] = vf.createValue("non-quoted");
+                    expectedValues9[2] = vf.createValue("the last \" one");
+                    assertTrue("Expecting string array type property nested/" + PROP_I + " to be present " , U.hasProperty(authorizable, "nested/" + PROP_I, expectedValues9));
+
+                    Value nestedExpectedValue = vf.createValue("42", PropertyType.valueFromName("Long"));
+                    assertTrue("Expecting Long type default property nested/nested/" + PROP_B + " to be present ", U.hasProperty(authorizable, "nested/nested/" +PROP_B, nestedExpectedValue));
+                }
+
+                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 1a404ff..381d2a9 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
@@ -135,4 +135,29 @@
         }
         return false;
     }
+
+    public static boolean hasProperty(Authorizable a, String propertyName, Value propertyValue) throws  RepositoryException {
+        if (a != null) {
+            boolean isPropertyPresent = a.hasProperty(propertyName);
+            if (isPropertyPresent) {
+                Value[] values = a.getProperty(propertyName);
+                if (values != null && values.length == 1) {
+                    Value v = values[0];
+                    return v.equals(propertyValue);
+                }
+            }
+        }
+        return false;
+    }
+
+    public static boolean hasProperty(Authorizable a, String propertyName, Value[] propertyValues) throws  RepositoryException {
+        if (a != null) {
+            boolean isPropertyPresent = a.hasProperty(propertyName);
+            if (isPropertyPresent) {
+                Value[] v = a.getProperty(propertyName);
+                return Arrays.equals(v, propertyValues);
+            }
+        }
+        return false;
+    }
 }
\ No newline at end of file
diff --git a/src/test/resources/repoinit.txt b/src/test/resources/repoinit.txt
index d6fd943..2dbbe24 100644
--- a/src/test/resources/repoinit.txt
+++ b/src/test/resources/repoinit.txt
@@ -81,4 +81,34 @@
   default someInteger{Long} to 65
   set quotedA to "Here's a \"double quoted string\" with suffix"
   set quotedMix to "quoted", non-quoted, "the last \" one"
-end
\ No newline at end of file
+end
+
+# SLING-10192 set properties on user or group profile
+set properties on authorizable(alice),authorizable(grpA)
+  set pathArray to /d/e/f/*, m/n/*
+  default someInteger{Long} to 42
+  set someFlag{Boolean} to true
+  default someDate{Date} to "2020-03-19T11:39:33.437+05:30"
+  set customSingleValueStringProp to test
+  set customSingleValueQuotedStringProp to "hello, you!"
+  set stringArray to test1, test2
+  default someInteger{Long} to 65
+  set quotedA to "Here's a \"double quoted string\" with suffix"
+  set quotedMix to "quoted", non-quoted, "the last \" one"
+  set nested/someInteger{Long} to 42
+end
+
+# SLING-10192 set properties on a subtree of the user or group profile
+set properties on authorizable(bob)/nested,authorizable(grpB)/nested
+  set pathArray to /d/e/f/*, m/n/*
+  default someInteger{Long} to 42
+  set someFlag{Boolean} to true
+  default someDate{Date} to "2020-03-19T11:39:33.437+05:30"
+  set customSingleValueStringProp to test
+  set customSingleValueQuotedStringProp to "hello, you!"
+  set stringArray to test1, test2
+  default someInteger{Long} to 65
+  set quotedA to "Here's a \"double quoted string\" with suffix"
+  set quotedMix to "quoted", non-quoted, "the last \" one"
+  set nested/someInteger{Long} to 42
+end