You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by en...@apache.org on 2021/06/01 17:17:36 UTC

[sling-org-apache-sling-jcr-jackrabbit-accessmanager] branch master updated: SLING-10436 Move integration tests (#3)

This is an automated email from the ASF dual-hosted git repository.

enorman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-jcr-jackrabbit-accessmanager.git


The following commit(s) were added to refs/heads/master by this push:
     new fc200f8  SLING-10436 Move integration tests (#3)
fc200f8 is described below

commit fc200f8fa18d1751cfdb4b1013ce0fee055e06fe
Author: Eric Norman <er...@gmail.com>
AuthorDate: Tue Jun 1 10:17:26 2021 -0700

    SLING-10436 Move integration tests (#3)
---
 pom.xml                                            |  116 +-
 .../it/AccessManagerClientTestSupport.java         |  448 +++++++
 .../accessmanager/it/AccessManagerTestSupport.java |  276 ++++
 .../accessmanager/it/AccessPrivilegesInfoIT.java   |  475 +++++++
 .../it/CustomPostResponseCreatorImpl.java          |   55 +
 .../jcr/jackrabbit/accessmanager/it/GetAclIT.java  |  487 +++++++
 .../jackrabbit/accessmanager/it/ModifyAceIT.java   | 1402 ++++++++++++++++++++
 .../jackrabbit/accessmanager/it/RemoveAcesIT.java  |  316 +++++
 .../apps/nt/unstructured/privileges-info.json.esp  |  111 ++
 9 files changed, 3684 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 169ed1d..5e25700 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,6 +44,9 @@
     <properties>
         <sling.java.version>8</sling.java.version>
         <project.build.outputTimestamp>2020-10-15T22:30:27Z</project.build.outputTimestamp>
+        <org.ops4j.pax.exam.version>4.13.3</org.ops4j.pax.exam.version>
+        <!-- To debug the pax process, override this with -D -->
+        <pax.vm.options>-Xmx512M</pax.vm.options>
     </properties>
 
     <build>
@@ -55,6 +58,43 @@
                     <additionalparam>-Xdoclint:none</additionalparam>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.apache.servicemix.tooling</groupId>
+                <artifactId>depends-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>integration-test</id>
+                        <phase>integration-test</phase>
+                        <goals>
+                            <goal>integration-test</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>verify</id>
+                        <phase>integration-test</phase>
+                        <goals>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                    <systemProperties>
+                        <property>
+                            <name>bundle.filename</name>
+                            <value>${basedir}/target/${project.build.finalName}.jar</value>
+                        </property>
+                        <property>
+                            <name>pax.vm.options</name>
+                            <value>${pax.vm.options}</value>
+                        </property>
+                    </systemProperties>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 
@@ -68,12 +108,12 @@
             <groupId>org.osgi</groupId>
             <artifactId>osgi.cmpn</artifactId>
             <scope>provided</scope>
-        </dependency>        
+        </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.annotation.versioning</artifactId>
         </dependency>
-            
+
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
@@ -140,6 +180,78 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-cm</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-container-forked</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit4</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-link-mvn</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.paxexam</artifactId>
+            <version>3.1.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpcore</artifactId>
+            <version>4.4.13</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.13</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.healthcheck.api</artifactId>
+            <version>2.0.4</version>
+            <scope>test</scope>
+        </dependency>
+       <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <version>4.0.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>7.0.0</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-simple</artifactId>
         </dependency>
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessManagerClientTestSupport.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessManagerClientTestSupport.java
new file mode 100644
index 0000000..ebda239
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessManagerClientTestSupport.java
@@ -0,0 +1,448 @@
+/*
+ * 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.jackrabbit.accessmanager.it;
+
+import static org.apache.sling.testing.paxexam.SlingOptions.slingCommonsCompiler;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingJcrJackrabbitSecurity;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingScriptingJavascript;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.ops4j.pax.exam.CoreOptions.when;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.factoryConfiguration;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.AuthCache;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.config.CookieSpecs;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.BasicAuthCache;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.ops4j.pax.exam.Option;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+/**
+ * base class for tests doing http requests to verify calls to the accessmanager
+ * servlets
+ */
+public abstract class AccessManagerClientTestSupport extends AccessManagerTestSupport {
+    protected static final String TEST_FOLDER_JSON = "{'jcr:primaryType': 'nt:unstructured'}";
+
+    protected static final String CONTENT_TYPE_JSON = "application/json";
+    protected static final String CONTENT_TYPE_HTML = "text/html";
+
+    protected static long randomId = System.currentTimeMillis();
+
+    protected static synchronized long getNextInt() {
+        final long val = randomId;
+        randomId++;
+        return val;
+    }
+
+    @Inject
+    protected ConfigurationAdmin cm;
+
+    protected static final String COOKIE_SLING_FORMAUTH = "sling.formauth";
+    protected static final String COOKIE_SLING_FORMAUTH_DOMAIN = "sling.formauth.cookie.domain";
+    protected static final String HEADER_SET_COOKIE = "Set-Cookie";
+
+    protected URI baseServerUri;
+    protected HttpClientContext httpContext;
+    protected CloseableHttpClient httpClient;
+
+    protected String testUserId = null;
+    protected String testUserId2 = null;
+    protected String testGroupId = null;
+    protected String testFolderUrl = null;
+
+    @Override
+    protected Option[] additionalOptions() throws IOException {
+        // optionally create a tinybundle that contains a test script
+        final Option bundle = buildBundleResourcesBundle();
+
+        return new Option[]{
+            // for usermanager support
+            slingJcrJackrabbitSecurity(),
+            // add javascript support for the test script
+            slingCommonsCompiler(),
+            when(bundle != null).useOptions(slingScriptingJavascript()),
+
+            // add the test script tinybundle
+            when(bundle != null).useOptions(bundle),
+
+            // enable the healthcheck configuration for checking when the server is ready to
+            //  receive http requests.  (adapted from the starter healthcheck.json configuration)
+            factoryConfiguration("org.apache.felix.hc.generalchecks.FrameworkStartCheck")
+                .put("hc.tags", new String[] {"systemalive"})
+                .put("targetStartLevel", 5)
+                .asOption(),
+            factoryConfiguration("org.apache.felix.hc.generalchecks.ServicesCheck")
+                .put("hc.tags", new String[] {"systemalive"})
+                .put("services.list", new String[] {
+                        "org.apache.sling.jcr.api.SlingRepository",
+                        "org.apache.sling.engine.auth.Authenticator",
+                        "org.apache.sling.api.resource.ResourceResolverFactory",
+                        "org.apache.sling.api.servlets.ServletResolver",
+                        "javax.script.ScriptEngineManager"
+                })
+                .asOption(),
+            factoryConfiguration("org.apache.felix.hc.generalchecks.BundlesStartedCheck")
+                .put("hc.tags", new String[] {"bundles"})
+                .asOption(),
+            factoryConfiguration("org.apache.sling.jcr.contentloader.hc.BundleContentLoadedCheck")
+                .put("hc.tags", new String[] {"bundles"})
+                .asOption(),
+        };
+    }
+
+    @Before
+    public void before() throws IOException, URISyntaxException {
+        // wait for the health checks to be OK
+        waitForServerReady(Duration.ofMinutes(1).toMillis(), 500);
+
+        // calculate the address of the http server
+        baseServerUri = getBaseServerUri();
+        assertNotNull(baseServerUri);
+
+        HttpHost targetHost = new HttpHost(baseServerUri.getHost(), baseServerUri.getPort(), baseServerUri.getScheme());
+        AuthCache authCache = new BasicAuthCache();
+        authCache.put(targetHost, new BasicScheme());
+
+        // prepare the http client for the test user
+        httpContext = HttpClientContext.create();
+        httpContext.setCookieStore(new BasicCookieStore());
+        httpContext.setCredentialsProvider(new BasicCredentialsProvider());
+        httpContext.setAuthCache(authCache);
+        RequestConfig requestConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD_STRICT).build();
+        httpContext.setRequestConfig(requestConfig);
+        httpClient = HttpClients.custom()
+                .disableRedirectHandling()
+                .build();
+    }
+
+    @After
+    public void after() throws IOException {
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+
+        if (testFolderUrl != null) {
+            //remove the test user if it exists.
+            List<NameValuePair> postParams = new ArrayList<>();
+            postParams.add(new BasicNameValuePair(":operation", "delete"));
+            assertAuthenticatedPostStatus(creds, testFolderUrl, HttpServletResponse.SC_OK, postParams, null);
+        }
+        if (testGroupId != null) {
+            //remove the test user if it exists.
+            String postUrl = String.format("%s/system/userManager/group/%s.delete.html", baseServerUri, testGroupId);
+            assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, Collections.emptyList(), null);
+        }
+        if (testUserId != null) {
+            //remove the test user if it exists.
+            String postUrl = String.format("%s/system/userManager/user/%s.delete.html", baseServerUri, testUserId);
+            assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, Collections.emptyList(), null);
+        }
+        if (testUserId2 != null) {
+            //remove the test user if it exists.
+            String postUrl = String.format("%s/system/userManager/user/%s.delete.html", baseServerUri, testUserId2);
+            assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, Collections.emptyList(), null);
+        }
+
+        // close/cleanup the test user http client
+        if (httpClient != null) {
+            httpClient.close();
+            httpClient = null;
+        }
+
+        // clear out other state
+        httpContext = null;
+        baseServerUri = null;
+    }
+
+    /**
+     * Calculate the base server URI from the current configuration of the
+     * httpservice
+     */
+    protected URI getBaseServerUri() throws IOException, URISyntaxException {
+        assertNotNull(cm);
+        Configuration httpServiceConfiguration = cm.getConfiguration("org.apache.felix.http");
+        Dictionary<String, Object> properties = httpServiceConfiguration.getProperties();
+
+        String host;
+        Object hostObj = properties.get("org.apache.felix.http.host");
+        if (hostObj == null) {
+            host = "localhost";
+        } else {
+            assertTrue(hostObj instanceof String);
+            host = (String)hostObj;
+        }
+        assertNotNull(host);
+
+        String scheme = null;
+        Object portObj = null;
+        Object httpsEnableObj = properties.get("org.apache.felix.https.enable");
+        if ("true".equals(httpsEnableObj)) {
+            scheme = "https";
+            portObj = properties.get("org.osgi.service.http.port.secure");
+        } else {
+            Object httpEnableObj = properties.get("org.apache.felix.http.enable");
+            if (httpEnableObj == null || "true".equals(httpEnableObj)) {
+                scheme = "http";
+                portObj = properties.get("org.osgi.service.http.port");
+            } else {
+                fail("Expected either http or https to be enabled");
+            }
+        }
+        int port = -1;
+        if (portObj instanceof Number) {
+            port = ((Number)portObj).intValue();
+        }
+        assertTrue(port > 0);
+
+        return new URI(String.format("%s://%s:%d", scheme, host, port));
+    }
+
+    protected void assertPrivilege(Collection<String> privileges, boolean expected, String privilegeName) {
+        if(expected != privileges.contains(privilegeName)) {
+            fail("Expected privilege " + privilegeName + " to be "
+                    + (expected ? "included" : "NOT INCLUDED")
+                    + " in supplied list: " + privileges + ")");
+        }
+    }
+
+    protected Object doAuthenticatedWork(Credentials creds, AuthenticatedWorker worker) throws IOException {
+        Object result = null;
+        AuthScope authScope = new AuthScope(baseServerUri.getHost(), baseServerUri.getPort(), baseServerUri.getScheme());
+        CredentialsProvider oldCredentialsProvider = httpContext.getCredentialsProvider();
+        try {
+            BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+            httpContext.setCredentialsProvider(credentialsProvider);
+            credentialsProvider.setCredentials(authScope, creds);
+
+            result = worker.doWork();
+        } finally {
+            httpContext.setCredentialsProvider(oldCredentialsProvider);
+        }
+        return result;
+    }
+
+    protected void assertAuthenticatedPostStatus(Credentials creds, String url, int expectedStatusCode, List<NameValuePair> postParams, String assertMessage) throws IOException {
+        doAuthenticatedWork(creds, () -> {
+            HttpPost postRequest = new HttpPost(url);
+            postRequest.setEntity(new UrlEncodedFormEntity(postParams));
+            try (CloseableHttpResponse response = httpClient.execute(postRequest, httpContext)) {
+                assertEquals(assertMessage, expectedStatusCode, response.getStatusLine().getStatusCode());
+            }
+            return null;
+        });
+    }
+
+    protected void assertAuthenticatedHttpStatus(Credentials creds, String urlString, int expectedStatusCode, String assertMessage) throws IOException {
+        doAuthenticatedWork(creds, () -> {
+            HttpGet getRequest = new HttpGet(urlString);
+            try (CloseableHttpResponse response = httpClient.execute(getRequest, httpContext)) {
+                assertEquals(assertMessage, expectedStatusCode, response.getStatusLine().getStatusCode());
+                return null;
+            }
+        });
+    }
+
+    protected String getAuthenticatedContent(Credentials creds, String url, String expectedContentType, int expectedStatusCode) throws IOException {
+        return (String)doAuthenticatedWork(creds, () -> {
+            HttpGet getRequest = new HttpGet(url);
+            try (CloseableHttpResponse response = httpClient.execute(getRequest, httpContext)) {
+                assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode());
+                final Header h = response.getFirstHeader("Content-Type");
+                if (expectedContentType == null) {
+                    if (h != null) {
+                        fail("Expected null Content-Type, got " + h.getValue());
+                    }
+                } else if (h == null) {
+                    fail(
+                            "Expected Content-Type that starts with '" + expectedContentType
+                            +" but got no Content-Type header at " + url
+                    );
+                } else {
+                    assertTrue(
+                        "Expected Content-Type that starts with '" + expectedContentType
+                        + "' for " + url + ", got '" + h.getValue() + "'",
+                        h.getValue().startsWith(expectedContentType)
+                    );
+                }
+                return EntityUtils.toString(response.getEntity());
+            }
+        });
+    }
+
+    protected String getAuthenticatedPostContent(Credentials creds, String url, String expectedContentType, List<NameValuePair> postParams, int expectedStatusCode) throws IOException {
+        return (String)doAuthenticatedWork(creds, () -> {
+            HttpPost postRequest = new HttpPost(url);
+            postRequest.setEntity(new UrlEncodedFormEntity(postParams));
+            try (CloseableHttpResponse response = httpClient.execute(postRequest, httpContext)) {
+                assertEquals(expectedStatusCode, response.getStatusLine().getStatusCode());
+                final Header h = response.getFirstHeader("Content-Type");
+                if (expectedContentType == null) {
+                    if (h != null) {
+                        fail("Expected null Content-Type, got " + h.getValue());
+                    }
+                } else if (h == null) {
+                    fail(
+                            "Expected Content-Type that starts with '" + expectedContentType
+                            +" but got no Content-Type header at " + url
+                    );
+                } else {
+                    assertTrue(
+                        "Expected Content-Type that starts with '" + expectedContentType
+                        + "' for " + url + ", got '" + h.getValue() + "'",
+                        h.getValue().startsWith(expectedContentType)
+                    );
+                }
+                return EntityUtils.toString(response.getEntity());
+            }
+        });
+    }
+
+    protected String createTestUser() throws IOException {
+        String postUrl = String.format("%s/system/userManager/user.create.html", baseServerUri);
+
+        String userId = "testUser" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", userId));
+        postParams.add(new BasicNameValuePair("pwd", "testPwd"));
+        postParams.add(new BasicNameValuePair("pwdConfirm", "testPwd"));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        final String msg = "Unexpected status while attempting to create test user at " + postUrl;
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, msg);
+
+        final String sessionInfoUrl = String.format("%s/system/sling/info.sessionInfo.json", baseServerUri);
+        assertAuthenticatedHttpStatus(creds, sessionInfoUrl, HttpServletResponse.SC_OK,
+                "session info failed for user " + userId);
+
+        return userId;
+    }
+
+    protected String createTestGroup() throws IOException {
+        String postUrl = String.format("%s/system/userManager/group.create.html", baseServerUri);
+
+        String groupId = "testGroup" + getNextInt();
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":name", groupId));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        final String msg = "Unexpected status while attempting to create test group at " + postUrl;
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, msg);
+
+        return groupId;
+    }
+
+    protected String createTestFolder() throws IOException {
+        return createTestFolder(null, "sling-tests");
+    }
+
+    protected String createTestFolder(String parentPath, String nameHint) throws IOException {
+        return createTestFolder(parentPath, nameHint, TEST_FOLDER_JSON);
+    }
+    protected String createTestFolder(String parentPath, String nameHint, String jsonImport) throws IOException {
+        JsonObject json = importJSON(parentPath, nameHint, jsonImport);
+        return String.format("%s%s", baseServerUri, json.getString("path"));
+    }
+
+    protected JsonObject importJSON(String nameHint, String jsonImport) throws IOException {
+        return importJSON(null, nameHint, jsonImport);
+    }
+
+    protected JsonObject importJSON(String parentPath, String nameHint, String jsonImport) throws IOException {
+        JsonObject result = null;
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        result = (JsonObject)doAuthenticatedWork(creds, () -> {
+            List<NameValuePair> parameters = new ArrayList<>();
+            parameters.add(new BasicNameValuePair(":operation", "import"));
+            if (nameHint != null) {
+                parameters.add(new BasicNameValuePair(":nameHint", nameHint));
+            }
+            parameters.add(new BasicNameValuePair(":content", jsonImport));
+            parameters.add(new BasicNameValuePair(":contentType", "json"));
+            parameters.add(new BasicNameValuePair(":replaceProperties", "true"));
+
+            String postUrl = String.format("%s%s", baseServerUri, parentPath != null ? parentPath : "/content");
+            HttpPost postRequest = new HttpPost(postUrl);
+            postRequest.setEntity(new UrlEncodedFormEntity(parameters));
+            postRequest.addHeader(new BasicHeader("Accept", "application/json,*/*;q=0.9"));
+            JsonObject jsonObj = null;
+            try (CloseableHttpResponse response = httpClient.execute(postRequest, httpContext)) {
+                assertEquals(HttpServletResponse.SC_CREATED, response.getStatusLine().getStatusCode());
+                jsonObj = parseJson(EntityUtils.toString(response.getEntity()));
+            }
+            return jsonObj;
+        });
+        return result;
+    }
+
+    /**
+     * @param json the json string to parse
+     * @return the parsed JsonObject
+     */
+    protected JsonObject parseJson(String json) {
+        JsonObject jsonObj = null;
+        try (JsonReader reader = Json.createReader(new StringReader(json))) {
+            jsonObj = reader.readObject();
+        }
+        return jsonObj;
+    }
+
+    protected static interface AuthenticatedWorker {
+        public Object doWork() throws IOException;
+    }
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessManagerTestSupport.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessManagerTestSupport.java
new file mode 100644
index 0000000..b29c1c2
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessManagerTestSupport.java
@@ -0,0 +1,276 @@
+/*
+ * 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.jackrabbit.accessmanager.it;
+
+import static org.apache.felix.hc.api.FormattingResultLog.msHumanReadable;
+import static org.apache.sling.testing.paxexam.SlingOptions.awaitility;
+import static org.apache.sling.testing.paxexam.SlingOptions.slingQuickstartOakTar;
+import static org.apache.sling.testing.paxexam.SlingOptions.versionResolver;
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertNotNull;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.mavenBundle;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.streamBundle;
+import static org.ops4j.pax.exam.CoreOptions.vmOption;
+import static org.ops4j.pax.exam.CoreOptions.when;
+import static org.ops4j.pax.tinybundles.core.TinyBundles.withBnd;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+
+import org.apache.felix.hc.api.Result;
+import org.apache.felix.hc.api.ResultLog;
+import org.apache.felix.hc.api.execution.HealthCheckExecutionResult;
+import org.apache.felix.hc.api.execution.HealthCheckExecutor;
+import org.apache.felix.hc.api.execution.HealthCheckSelector;
+import org.apache.sling.testing.paxexam.TestSupport;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.options.ModifiableCompositeOption;
+import org.ops4j.pax.exam.options.extra.VMOption;
+import org.ops4j.pax.tinybundles.core.TinyBundle;
+import org.ops4j.pax.tinybundles.core.TinyBundles;
+import org.osgi.framework.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class AccessManagerTestSupport extends TestSupport {
+    private static final String BUNDLE_SYMBOLICNAME = "TEST-CONTENT-BUNDLE";
+    private static final String SLING_BUNDLE_RESOURCES_HEADER = "Sling-Bundle-Resources";
+
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Inject
+    private HealthCheckExecutor hcExecutor;
+
+
+    @Configuration
+    public Option[] configuration() throws IOException {
+        final String vmOpt = System.getProperty("pax.vm.options");
+        VMOption vmOption = null;
+        if (vmOpt != null && !vmOpt.isEmpty()) {
+            vmOption = new VMOption(vmOpt);
+        }
+
+        final String jacocoOpt = System.getProperty("jacoco.command");
+        VMOption jacocoCommand = null;
+        if (jacocoOpt != null && !jacocoOpt.isEmpty()) {
+            jacocoCommand = new VMOption(jacocoOpt);
+        }
+
+        // newer version of sling.api and dependencies for SLING-10034
+        //   may remove at a later date if the superclass includes these versions or later
+        versionResolver.setVersionFromProject("org.apache.sling", "org.apache.sling.api");
+        versionResolver.setVersion("org.apache.sling", "org.apache.sling.resourceresolver", "1.7.0"); // to be compatible with current o.a.sling.api
+        versionResolver.setVersion("org.apache.sling", "org.apache.sling.scripting.core", "2.3.4"); // to be compatible with current o.a.sling.api
+        versionResolver.setVersion("org.apache.sling", "org.apache.sling.scripting.api", "2.2.0"); // to be compatible with current o.a.sling.api
+        versionResolver.setVersion("org.apache.sling", "org.apache.sling.servlets.resolver", "2.7.12"); // to be compatible with current o.a.sling.api
+        versionResolver.setVersion("org.apache.sling", "org.apache.sling.commons.compiler", "2.4.0"); // to be compatible with current o.a.sling.scripting.core
+
+        return options(
+            composite(
+                super.baseConfiguration(),
+                when(vmOption != null).useOptions(vmOption),
+                when(jacocoCommand != null).useOptions(jacocoCommand),
+                optionalRemoteDebug(),
+                slingQuickstart(),
+                testBundle("bundle.filename"),
+                junitBundles(),
+                awaitility()
+            ).add(
+                // needed by latest version of org.apache.sling.api
+                mavenBundle().groupId("org.apache.felix").artifactId("org.apache.felix.converter").version("1.0.14")
+            ).add(
+                additionalOptions()
+            ).remove(
+                // remove our bundle under test to avoid duplication
+                mavenBundle().groupId("org.apache.sling").artifactId("org.apache.sling.jcr.jackrabbit.accessmanager").version(versionResolver)
+            )
+        );
+    }
+
+    protected Option[] additionalOptions() throws IOException { // NOSONAR
+        return new Option[]{};
+    }
+
+    protected Option slingQuickstart() {
+        final String workingDirectory = workingDirectory();
+        final int httpPort = findFreePort();
+        return composite(
+            slingQuickstartOakTar(workingDirectory, httpPort)
+        );
+    }
+
+    public String getTestFileUrl(String path) {
+        return getClass().getResource(path).toExternalForm();
+    }
+
+    /**
+     * Optionally configure remote debugging on the port supplied by the "debugPort"
+     * system property.
+     */
+    protected ModifiableCompositeOption optionalRemoteDebug() {
+        VMOption option = null;
+        String property = System.getProperty("debugPort");
+        if (property != null) {
+            option = vmOption(String.format("-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=%s", property));
+        }
+        return composite(option);
+    }
+
+    /**
+     * Wait for the health check to be ok
+     *
+     * @param timeoutMsec the max time to wait for the health check to be ok
+     * @param nextIterationDelay the sleep time between the check attempts
+     */
+    protected void waitForServerReady(long timeoutMsec, long nextIterationDelay) {
+        // retry until the exec call returns true and doesn't throw any exception
+        await().atMost(timeoutMsec, TimeUnit.MILLISECONDS)
+                .pollInterval(nextIterationDelay, TimeUnit.MILLISECONDS)
+                .until(this::doHealthCheck);
+    }
+
+    /**
+     * @return true if health checks are ok
+     */
+    protected boolean doHealthCheck() throws IOException {
+        boolean isOk = true;
+        logger.info("Performing health check");
+        HealthCheckSelector hcs = HealthCheckSelector.tags("systemalive");
+        List<HealthCheckExecutionResult> results = hcExecutor.execute(hcs);
+        logger.info("systemalive health check got {} results", results.size());
+        isOk &= !results.isEmpty();
+        for (final HealthCheckExecutionResult exR : results) {
+            final Result r = exR.getHealthCheckResult();
+            if (logger.isInfoEnabled()) {
+                logger.info("systemalive health check: {}", toHealthCheckResultInfo(exR, false));
+            }
+            isOk &= r.isOk();
+            if (!isOk) {
+                break; // found a failure so stop checking further
+            }
+        }
+
+        if (isOk) {
+            hcs = HealthCheckSelector.tags("bundles");
+            results = hcExecutor.execute(hcs);
+            logger.info("bundles health check got {} results", results.size());
+            isOk &= !results.isEmpty();
+            for (final HealthCheckExecutionResult exR : results) {
+                final Result r = exR.getHealthCheckResult();
+                if (logger.isInfoEnabled()) {
+                    logger.info("bundles health check: {}", toHealthCheckResultInfo(exR, false));
+                }
+                isOk &= r.isOk();
+                if (!isOk) {
+                    break; // found a failure so stop checking further
+                }
+            }
+        }
+        return isOk;
+    }
+
+    /**
+     * Produce a human readable report of the health check results that is suitable for
+     * debugging or writing to a log
+     */
+    protected String toHealthCheckResultInfo(final HealthCheckExecutionResult exResult, final boolean debug)  throws IOException {
+        String value = null;
+        try (StringWriter resultWriter = new StringWriter(); BufferedWriter writer = new BufferedWriter(resultWriter)) {
+            final Result result = exResult.getHealthCheckResult();
+
+            writer.append('"').append(exResult.getHealthCheckMetadata().getTitle()).append('"');
+            writer.append(" result is: ").append(result.getStatus().toString());
+            writer.newLine();
+            writer.append("   Finished: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(exResult.getFinishedAt()) + " after "
+                    + msHumanReadable(exResult.getElapsedTimeInMs()));
+
+            for (final ResultLog.Entry e : result) {
+                if (!debug && e.isDebug()) {
+                    continue;
+                }
+                writer.newLine();
+                writer.append("   ");
+                writer.append(e.getStatus().toString());
+                writer.append(' ');
+                writer.append(e.getMessage());
+                if (e.getException() != null) {
+                    writer.append(" ");
+                    writer.append(e.getException().toString());
+                }
+            }
+            writer.flush();
+            value = resultWriter.toString();
+        }
+        return value;
+    }
+
+    /**
+     * Add content to our test bundle
+     */
+    protected void addContent(final TinyBundle bundle, String resourcePath) throws IOException {
+        String pathInBundle = resourcePath;
+        resourcePath = "/content" + resourcePath;
+        try (final InputStream is = getClass().getResourceAsStream(resourcePath)) {
+            assertNotNull("Expecting resource to be found:" + resourcePath, is);
+            logger.info("Adding resource to bundle, path={}, resource={}", pathInBundle, resourcePath);
+            bundle.add(pathInBundle, is);
+        }
+    }
+
+    /**
+     * Override to provide the option for your test
+     *
+     * @return the tinybundle Option or null if none
+     */
+    protected Option buildBundleResourcesBundle() throws IOException {
+        return null;
+    }
+
+    /**
+     * Build a test bundle containing the specified bundle resources
+     *
+     * @param header the value for the {@link #SLING_BUNDLE_RESOURCES_HEADER} header
+     * @param content the collection of files to embed in the tinybundle
+     * @return the tinybundle Option
+     */
+    protected Option buildBundleResourcesBundle(final String header, final Collection<String> content) throws IOException {
+        final TinyBundle bundle = TinyBundles.bundle();
+        bundle.set(Constants.BUNDLE_SYMBOLICNAME, BUNDLE_SYMBOLICNAME);
+        bundle.set(SLING_BUNDLE_RESOURCES_HEADER, header);
+        bundle.set("Require-Capability", "osgi.extender;filter:=\"(&(osgi.extender=org.apache.sling.bundleresource)(version<=1.1.0)(!(version>=2.0.0)))\"");
+
+        for (final String entry : content) {
+            addContent(bundle, entry);
+        }
+        return streamBundle(
+            bundle.build(withBnd())
+        ).start();
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessPrivilegesInfoIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessPrivilegesInfoIT.java
new file mode 100644
index 0000000..337c1eb
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/AccessPrivilegesInfoIT.java
@@ -0,0 +1,475 @@
+/*
+ * 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.jackrabbit.accessmanager.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.json.JsonArray;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class AccessPrivilegesInfoIT extends AccessManagerClientTestSupport {
+
+    @Override
+    protected Option buildBundleResourcesBundle() throws IOException {
+        final List<String> resourcePaths = Arrays.asList("/apps/nt/unstructured/privileges-info.json.esp");
+        final String bundleResourcesHeader = String.join(",", resourcePaths);
+        return buildBundleResourcesBundle(bundleResourcesHeader, resourcePaths);
+    }
+
+    /*
+     * testuser granted read / denied write
+     */
+    @Test
+    public void testDeniedWriteForUser() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        //assign some privileges
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:readAccessControl", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+
+        Credentials adminCreds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(adminCreds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        String getUrl = testFolderUrl + ".privileges-info.json";
+
+        //fetch the JSON for the test page to verify the settings.
+        Credentials testUserCreds = new UsernamePasswordCredentials(testUserId, "testPwd");
+
+        String json = getAuthenticatedContent(testUserCreds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObj = parseJson(json);
+        assertNotNull(jsonObj);
+
+        assertEquals(false, jsonObj.getBoolean("canAddChildren"));
+        assertEquals(false, jsonObj.getBoolean("canDeleteChildren"));
+        assertEquals(false, jsonObj.getBoolean("canDelete"));
+        assertEquals(false, jsonObj.getBoolean("canModifyProperties"));
+        assertEquals(true, jsonObj.getBoolean("canReadAccessControl"));
+        assertEquals(false, jsonObj.getBoolean("canModifyAccessControl"));
+    }
+
+    /*
+     * testuser granted read / granted write
+     */
+    @Test
+    public void testGrantedWriteForUser() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        //assign some privileges
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:readAccessControl", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "granted"));
+
+        Credentials adminCreds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(adminCreds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        String getUrl = testFolderUrl + ".privileges-info.json";
+
+        //fetch the JSON for the test page to verify the settings.
+        Credentials testUserCreds = new UsernamePasswordCredentials(testUserId, "testPwd");
+
+        String json = getAuthenticatedContent(testUserCreds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObj = parseJson(json);
+
+        assertEquals(true, jsonObj.getBoolean("canAddChildren"));
+        assertEquals(true, jsonObj.getBoolean("canDeleteChildren"));
+        //the parent node must also have jcr:removeChildren granted for 'canDelete' to be true
+        assertEquals(false, jsonObj.getBoolean("canDelete"));
+        assertEquals(true, jsonObj.getBoolean("canModifyProperties"));
+        assertEquals(true, jsonObj.getBoolean("canReadAccessControl"));
+        assertEquals(true, jsonObj.getBoolean("canModifyAccessControl"));
+
+        //add a child node to verify the 'canDelete' use case
+        String parentPath = testFolderUrl.substring(baseServerUri.toString().length());
+        String childFolderUrl = createTestFolder(parentPath, "testFolder");
+        String childPostUrl = childFolderUrl + ".modifyAce.html";
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:removeNode", "granted"));
+        assertAuthenticatedPostStatus(adminCreds, childPostUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        String childGetUrl = childFolderUrl + ".privileges-info.json";
+        String childJson = getAuthenticatedContent(testUserCreds, childGetUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(childJson);
+        JsonObject childJsonObj = parseJson(childJson);
+        assertEquals(true, childJsonObj.getBoolean("canDelete"));
+    }
+
+    /*
+     * group testuser granted read / denied write
+     */
+    @Test
+    public void testDeniedWriteForGroup() throws IOException, JsonException {
+        testGroupId = createTestGroup();
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        Credentials adminCreds = new UsernamePasswordCredentials("admin", "admin");
+
+        //add testUserId to testGroup
+        String groupPostUrl = String.format("%s/system/userManager/group/%s.update.html", baseServerUri, testGroupId);
+        List<NameValuePair> groupPostParams = new ArrayList<>();
+        groupPostParams.add(new BasicNameValuePair(":member", testUserId));
+        assertAuthenticatedPostStatus(adminCreds, groupPostUrl, HttpServletResponse.SC_OK, groupPostParams, null);
+
+        //assign some privileges
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testGroupId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:readAccessControl", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+
+        assertAuthenticatedPostStatus(adminCreds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        String getUrl = testFolderUrl + ".privileges-info.json";
+
+        //fetch the JSON for the test page to verify the settings.
+        Credentials testUserCreds = new UsernamePasswordCredentials(testUserId, "testPwd");
+
+        String json = getAuthenticatedContent(testUserCreds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObj = parseJson(json);
+
+        assertEquals(false, jsonObj.getBoolean("canAddChildren"));
+        assertEquals(false, jsonObj.getBoolean("canDeleteChildren"));
+        assertEquals(false, jsonObj.getBoolean("canDelete"));
+        assertEquals(false, jsonObj.getBoolean("canModifyProperties"));
+        assertEquals(true, jsonObj.getBoolean("canReadAccessControl"));
+        assertEquals(false, jsonObj.getBoolean("canModifyAccessControl"));
+    }
+
+    /*
+     * group testuser granted read / granted write
+     */
+    @Test
+    public void testGrantedWriteForGroup() throws IOException, JsonException {
+        testGroupId = createTestGroup();
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        Credentials adminCreds = new UsernamePasswordCredentials("admin", "admin");
+
+        //add testUserId to testGroup
+        String groupPostUrl = String.format("%s/system/userManager/group/%s.update.html", baseServerUri, testGroupId);
+        List<NameValuePair> groupPostParams = new ArrayList<>();
+        groupPostParams.add(new BasicNameValuePair(":member", testUserId));
+        assertAuthenticatedPostStatus(adminCreds, groupPostUrl, HttpServletResponse.SC_OK, groupPostParams, null);
+
+        //assign some privileges
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testGroupId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:readAccessControl", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "granted"));
+
+        assertAuthenticatedPostStatus(adminCreds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        String getUrl = testFolderUrl + ".privileges-info.json";
+
+        //fetch the JSON for the test page to verify the settings.
+        Credentials testUserCreds = new UsernamePasswordCredentials(testUserId, "testPwd");
+
+        String json = getAuthenticatedContent(testUserCreds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObj = parseJson(json);
+
+        assertEquals(true, jsonObj.getBoolean("canAddChildren"));
+        assertEquals(true, jsonObj.getBoolean("canDeleteChildren"));
+        //the parent node must also have jcr:removeChildren granted for 'canDelete' to be true
+        assertEquals(false, jsonObj.getBoolean("canDelete"));
+        assertEquals(true, jsonObj.getBoolean("canModifyProperties"));
+        assertEquals(true, jsonObj.getBoolean("canReadAccessControl"));
+        assertEquals(true, jsonObj.getBoolean("canModifyAccessControl"));
+
+        //add a child node to verify the 'canDelete' use case
+        String parentPath = testFolderUrl.substring(baseServerUri.toString().length());
+        String childFolderUrl = createTestFolder(parentPath, "testFolder");
+        String childPostUrl = childFolderUrl + ".modifyAce.html";
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testGroupId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:removeNode", "granted"));
+        assertAuthenticatedPostStatus(adminCreds, childPostUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        String childGetUrl = childFolderUrl + ".privileges-info.json";
+        String childJson = getAuthenticatedContent(testUserCreds, childGetUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(childJson);
+        JsonObject childJsonObj = parseJson(childJson);
+        assertEquals(true, childJsonObj.getBoolean("canDelete"));
+    }
+
+
+    /**
+     * Test the fix for SLING-1090
+     */
+    @Test
+    public void testSLING1090() throws IOException {
+        testUserId = createTestUser();
+
+        //grant jcr: removeChildNodes to the root node
+        ArrayList<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:removeChildNodes", "granted"));
+        Credentials adminCreds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(adminCreds, String.format("%s/.modifyAce.html", baseServerUri), HttpServletResponse.SC_OK, postParams, null);
+
+        //create a node as a child of the root folder
+        testFolderUrl = createTestFolder("/", "testSLING1090");
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //grant jcr:removeNode to the test node
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:removeNode", "granted"));
+        assertAuthenticatedPostStatus(adminCreds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        //fetch the JSON for the test page to verify the settings.
+        String getUrl = testFolderUrl + ".privileges-info.json";
+        Credentials testUserCreds = new UsernamePasswordCredentials(testUserId, "testPwd");
+        String json = getAuthenticatedContent(testUserCreds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObj = parseJson(json);
+        assertEquals(true, jsonObj.getBoolean("canDelete"));
+    }
+
+    /**
+     * Test for SLING-7835, PrivilegesInfo#getDeclaredAccessRights returns incorrect information
+     */
+    @Test
+    public void testDeclaredAclForUser() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testUserId2 = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId2));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId2));
+        postParams.add(new BasicNameValuePair("privilege@jcr:lockManagement", "granted"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.privileges-info.json";
+        Credentials testUserCreds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(testUserCreds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+        jsonObject = jsonObject.getJsonObject("declaredAccessRights");
+
+        assertNull(jsonObject.get(testUserId));
+
+        JsonObject aceObject2 = jsonObject.getJsonObject(testUserId2);
+        assertNotNull(aceObject2);
+
+        JsonArray grantedArray2 = aceObject2.getJsonArray("granted");
+        assertNotNull(grantedArray2);
+        assertEquals(1, grantedArray2.size());
+        Set<String> grantedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < grantedArray2.size(); i++) {
+            grantedPrivilegeNames2.add(grantedArray2.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames2, true, "jcr:lockManagement");
+
+        JsonArray deniedArray2 = aceObject2.getJsonArray("denied");
+        assertNotNull(deniedArray2);
+        assertEquals(0, deniedArray2.size());
+
+
+        getUrl = testFolderUrl + ".privileges-info.json";
+        json = getAuthenticatedContent(testUserCreds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        jsonObject = parseJson(json);
+        jsonObject = jsonObject.getJsonObject("declaredAccessRights");
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames,true, PrivilegeConstants.JCR_WRITE);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(0, deniedArray.size());
+
+        aceObject2 = jsonObject.getJsonObject(testUserId2);
+        assertNotNull(aceObject2);
+
+        grantedArray2 = aceObject2.getJsonArray("granted");
+        assertNotNull(grantedArray2);
+        assertEquals(1, grantedArray2.size());
+        grantedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < grantedArray2.size(); i++) {
+            grantedPrivilegeNames2.add(grantedArray2.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_WRITE);
+
+        deniedArray2 = aceObject2.getJsonArray("denied");
+        assertNotNull(deniedArray2);
+        assertEquals(0, deniedArray2.size());
+    }
+
+    /**
+     * Test for SLING-7835, PrivilegesInfo#getEffectiveAccessRights returns incorrect information
+     */
+    @Test
+    public void testEffectiveAclForUser() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testUserId2 = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId2));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId2));
+        postParams.add(new BasicNameValuePair("privilege@jcr:lockManagement", "granted"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.privileges-info.json";
+        Credentials testUserCreds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(testUserCreds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+        jsonObject = jsonObject.getJsonObject("effectiveAccessRights");
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames,true, PrivilegeConstants.JCR_WRITE);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(0, deniedArray.size());
+
+        JsonObject aceObject2 = jsonObject.getJsonObject(testUserId2);
+        assertNotNull(aceObject2);
+
+        JsonArray grantedArray2 = aceObject2.getJsonArray("granted");
+        assertNotNull(grantedArray2);
+        assertEquals(2, grantedArray2.size());
+        Set<String> grantedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < grantedArray2.size(); i++) {
+            grantedPrivilegeNames2.add(grantedArray2.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_WRITE);
+        assertPrivilege(grantedPrivilegeNames2, true, "jcr:lockManagement");
+
+        JsonArray deniedArray2 = aceObject2.getJsonArray("denied");
+        assertNotNull(deniedArray2);
+        assertEquals(0, deniedArray2.size());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/CustomPostResponseCreatorImpl.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/CustomPostResponseCreatorImpl.java
new file mode 100644
index 0000000..4af6407
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/CustomPostResponseCreatorImpl.java
@@ -0,0 +1,55 @@
+/*
+ * 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.jackrabbit.accessmanager.it;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.servlets.post.AbstractPostResponse;
+import org.apache.sling.servlets.post.PostResponse;
+import org.apache.sling.servlets.post.PostResponseCreator;
+
+/**
+ * Sample implementation of the PostResponseCreator interface.
+ */
+public class CustomPostResponseCreatorImpl implements PostResponseCreator {
+
+    public PostResponse createPostResponse(SlingHttpServletRequest req) {
+        if ("custom".equals(req.getParameter(":responseType"))) {
+            return new AbstractPostResponse() {
+
+                public void onChange(String type, String... arguments) {
+                    // NO-OP
+                }
+
+                @Override
+                protected void doSend(HttpServletResponse response) throws IOException {
+                    response.setContentType("text/html");
+                    response.setCharacterEncoding("UTF-8");
+
+                    response.getWriter().write("Thanks!");
+                    response.getWriter().flush();
+                }
+
+            };
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetAclIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetAclIT.java
new file mode 100644
index 0000000..29c7d96
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetAclIT.java
@@ -0,0 +1,487 @@
+/*
+ * 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.jackrabbit.accessmanager.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.json.JsonArray;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+/**
+ * Tests for the 'acl' and 'eacl' Sling Get Operation
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class GetAclIT extends AccessManagerClientTestSupport {
+
+    /**
+     * Test for SLING-2600, Effective ACL servlet returns incorrect information
+     */
+    @Test
+    public void testEffectiveAclForUser() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testUserId2 = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId2));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId2));
+        postParams.add(new BasicNameValuePair("privilege@jcr:lockManagement", "granted"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.eacl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_WRITE);
+
+        Object deniedArray = aceObject.get("denied");
+        assertNull(deniedArray);
+
+        JsonObject aceObject2 = jsonObject.getJsonObject(testUserId2);
+        assertNotNull(aceObject2);
+
+        String principalString2 = aceObject2.getString("principal");
+        assertEquals(testUserId2, principalString2);
+
+        JsonArray grantedArray2 = aceObject2.getJsonArray("granted");
+        assertNotNull(grantedArray2);
+        assertEquals(2, grantedArray2.size());
+        Set<String> grantedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < grantedArray2.size(); i++) {
+            grantedPrivilegeNames2.add(grantedArray2.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_WRITE);
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_LOCK_MANAGEMENT);
+
+        Object deniedArray2 = aceObject2.get("denied");
+        assertNull(deniedArray2);
+
+    }
+
+    /**
+     * Test for SLING-2600, Effective ACL servlet returns incorrect information
+     */
+    @Test
+    public void testEffectiveAclMergeForUserReplacePrivilegeOnChild() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.eacl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_WRITE);
+
+        Object deniedArray = aceObject.get("denied");
+        assertNull(deniedArray);
+    }
+
+    /**
+     * Test for SLING-2600, Effective ACL servlet returns incorrect information
+     */
+    @Test
+    public void testEffectiveAclMergeForUserFewerPrivilegesGrantedOnChild() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:all", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.eacl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_ALL);
+
+        Object deniedArray = aceObject.get("denied");
+        assertNull(deniedArray);
+    }
+
+    /**
+     * Test for SLING-2600, Effective ACL servlet returns incorrect information
+     */
+    @Test
+    public void testEffectiveAclMergeForUserMorePrivilegesGrantedOnChild() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:all", "granted"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.eacl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_ALL);
+
+        Object deniedArray = aceObject.get("denied");
+        assertNull(deniedArray);
+    }
+
+    /**
+     * Test for SLING-2600, Effective ACL servlet returns incorrect information
+     */
+    @Test
+    public void testEffectiveAclMergeForUserSubsetOfPrivilegesDeniedOnChild2() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:all", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:removeNode", "denied"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.eacl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertTrue(grantedArray.size() >= 11);
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames,false,PrivilegeConstants.JCR_ALL);
+        assertPrivilege(grantedPrivilegeNames,false,PrivilegeConstants.JCR_WRITE);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_READ);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_READ_ACCESS_CONTROL);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_MODIFY_ACCESS_CONTROL);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_LOCK_MANAGEMENT);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_VERSION_MANAGEMENT);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_NODE_TYPE_MANAGEMENT);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_RETENTION_MANAGEMENT);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_LIFECYCLE_MANAGEMENT);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_MODIFY_PROPERTIES);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_ADD_CHILD_NODES);
+        assertPrivilege(grantedPrivilegeNames,true,PrivilegeConstants.JCR_REMOVE_CHILD_NODES);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(1, deniedArray.size());
+        Set<String> deniedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < deniedArray.size(); i++) {
+            deniedPrivilegeNames.add(deniedArray.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames, true, PrivilegeConstants.JCR_REMOVE_NODE);
+    }
+
+    /**
+     * Test for SLING-2600, Effective ACL servlet returns incorrect information
+     */
+    @Test
+    public void testEffectiveAclMergeForUserSupersetOfPrivilegesDeniedOnChild() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:all", "denied"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.eacl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        Object grantedArray = aceObject.get("granted");
+        assertNull(grantedArray);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(1, deniedArray.size());
+        Set<String> deniedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < deniedArray.size(); i++) {
+            deniedPrivilegeNames.add(deniedArray.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames, true, PrivilegeConstants.JCR_ALL);
+    }
+
+    /**
+     * Test for SLING-2600, Effective ACL servlet returns incorrect information
+     */
+    @Test
+    public void testEffectiveAclMergeForUserSupersetOfPrivilegesDeniedOnChild2() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder(null, "sling-tests",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"propOne\" : \"propOneValue\", \"child\" : { \"childPropOne\" : true } }");
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyProperties", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:all", "denied"));
+
+        postUrl = testFolderUrl + "/child.modifyAce.html";
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the eacl to verify the settings.
+        String getUrl = testFolderUrl + "/child.eacl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        Object grantedArray = aceObject.get("granted");
+        assertNull(grantedArray);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(1, deniedArray.size());
+        Set<String> deniedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < deniedArray.size(); i++) {
+            deniedPrivilegeNames.add(deniedArray.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames, true, PrivilegeConstants.JCR_ALL);
+    }
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/ModifyAceIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/ModifyAceIT.java
new file mode 100644
index 0000000..1398027
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/ModifyAceIT.java
@@ -0,0 +1,1402 @@
+/*
+ * 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.jackrabbit.accessmanager.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.json.JsonArray;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonString;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.jackrabbit.oak.spi.security.privilege.PrivilegeConstants;
+import org.apache.sling.servlets.post.JSONResponse;
+import org.apache.sling.servlets.post.PostResponseCreator;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * Tests for the 'modifyAce' Sling Post Operation
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class ModifyAceIT extends AccessManagerClientTestSupport {
+
+    private ServiceRegistration<PostResponseCreator> serviceReg;
+
+    @Before
+    @Override
+    public void before() throws IOException, URISyntaxException {
+        Bundle bundle = FrameworkUtil.getBundle(getClass());
+        Dictionary<String, Object> props = new Hashtable<>(); // NOSONAR
+        serviceReg = bundle.getBundleContext().registerService(PostResponseCreator.class,
+                new CustomPostResponseCreatorImpl(), props);
+
+        super.before();
+    }
+
+    @After
+    @Override
+    public void after() throws IOException {
+        if (serviceReg != null) {
+            serviceReg.unregister();
+        }
+
+        super.after();
+    }
+
+    @Test
+    public void testModifyAceForUser() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "bogus")); //invalid value should be ignored.
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(1, jsonObject.size());
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+            int order = aceObject.getInt("order");
+            assertEquals(0, order);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        assertEquals(PrivilegeConstants.JCR_READ, grantedArray.getString(0));
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(1, deniedArray.size());
+        assertEquals(PrivilegeConstants.JCR_WRITE, deniedArray.getString(0));
+    }
+
+    /**
+     * Test for SLING-7831
+     */
+    @Test
+    public void testModifyAceCustomPostResponse() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":responseType", "custom"));
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String content = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_HTML, postParams, HttpServletResponse.SC_OK);
+        assertEquals("Thanks!", content); //verify that the content matches the custom response
+    }
+
+    @Test
+    public void testModifyAceForGroup() throws IOException, JsonException {
+        testGroupId = createTestGroup();
+
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testGroupId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "bogus")); //invalid value should be ignored.
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(1, jsonObject.size());
+
+        JsonObject aceObject = jsonObject.getJsonObject(testGroupId);
+        assertNotNull(aceObject);
+
+            int order = aceObject.getInt("order");
+            assertEquals(0, order);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testGroupId, principalString);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        assertEquals(PrivilegeConstants.JCR_READ, grantedArray.getString(0));
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(PrivilegeConstants.JCR_WRITE, deniedArray.getString(0));
+    }
+
+    /**
+     * Test for SLING-997, preserve privileges that were not posted with the modifyAce
+     * request.
+     */
+    @Test
+    public void testMergeAceForUser() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:readAccessControl", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:addChildNodes", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:removeChildNodes", "denied"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(1, jsonObject.size());
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+            int order = aceObject.getInt("order");
+            assertEquals(0, order);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(3, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_READ);
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_READ_ACCESS_CONTROL);
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_ADD_CHILD_NODES);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(2, deniedArray.size());
+        Set<String> deniedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < deniedArray.size(); i++) {
+            deniedPrivilegeNames.add(deniedArray.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames, true, PrivilegeConstants.JCR_MODIFY_ACCESS_CONTROL);
+        assertPrivilege(deniedPrivilegeNames, true, PrivilegeConstants.JCR_REMOVE_CHILD_NODES);
+
+
+
+        //2. post a new set of privileges to merge with the existing privileges
+        List<NameValuePair> postParams2 = new ArrayList<>();
+        postParams2.add(new BasicNameValuePair("principalId", testUserId));
+        //jcr:read and jcr:addChildNodes are not posted, so they should remain in the granted ACE
+        postParams2.add(new BasicNameValuePair("privilege@jcr:readAccessControl", "none")); //clear the existing privilege
+        postParams2.add(new BasicNameValuePair("privilege@jcr:modifyProperties", "granted")); //add a new privilege
+        //jcr:modifyAccessControl is not posted, so it should remain in the denied ACE
+        postParams2.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "denied")); //deny the modifyAccessControl privilege
+        postParams2.add(new BasicNameValuePair("privilege@jcr:removeChildNodes", "none")); //clear the existing privilege
+        postParams2.add(new BasicNameValuePair("privilege@jcr:removeNode", "denied")); //deny a new privilege
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams2, null);
+
+
+        //fetch the JSON for the acl to verify the settings.
+        String json2 = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json2);
+        JsonObject jsonObject2 = parseJson(json2);
+        assertEquals(1, jsonObject2.size());
+
+        JsonObject aceObject2 = jsonObject2.getJsonObject(testUserId);
+        assertNotNull(aceObject2);
+
+        String principalString2 = aceObject2.getString("principal");
+        assertEquals(testUserId, principalString2);
+
+        JsonArray grantedArray2 = aceObject2.getJsonArray("granted");
+        assertNotNull(grantedArray2);
+        assertEquals(3, grantedArray2.size());
+        Set<String> grantedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < grantedArray2.size(); i++) {
+            grantedPrivilegeNames2.add(grantedArray2.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_READ);
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_ADD_CHILD_NODES);
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_MODIFY_PROPERTIES);
+
+        JsonArray deniedArray2 = aceObject2.getJsonArray("denied");
+        assertNotNull(deniedArray2);
+        assertEquals(2, deniedArray2.size());
+        Set<String> deniedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < deniedArray2.size(); i++) {
+            deniedPrivilegeNames2.add(deniedArray2.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames2, true, PrivilegeConstants.JCR_MODIFY_ACCESS_CONTROL);
+        assertPrivilege(deniedPrivilegeNames2, true, PrivilegeConstants.JCR_REMOVE_NODE);
+    }
+
+
+    /**
+     * Test for SLING-997, preserve privileges that were not posted with the modifyAce
+     * request.
+     */
+    @Test
+    public void testMergeAceForUserSplitAggregatePrincipal() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(1, jsonObject.size());
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        assertEquals(testUserId, aceObject.getString("principal"));
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_READ);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(1, deniedArray.size());
+        Set<String> deniedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < deniedArray.size(); i++) {
+            deniedPrivilegeNames.add(deniedArray.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames, true, PrivilegeConstants.JCR_WRITE);
+
+
+
+        //2. post a new set of privileges to merge with the existing privileges
+        List<NameValuePair> postParams2 = new ArrayList<>();
+        postParams2.add(new BasicNameValuePair("principalId", testUserId));
+        //jcr:read is not posted, so it should remain in the granted ACE
+        postParams2.add(new BasicNameValuePair("privilege@jcr:modifyProperties", "granted")); //add a new privilege
+        //jcr:write is not posted, but one of the aggregate privileges is now granted, so the aggregate priviledge should be disagreaged into
+        //  the remaining denied privileges in the denied ACE
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams2, null);
+
+
+        //fetch the JSON for the acl to verify the settings.
+        String json2 = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json2);
+
+        JsonObject jsonObject2 = parseJson(json2);
+        assertEquals(1, jsonObject2.size());
+
+        JsonObject aceObject2 = jsonObject2.getJsonObject(testUserId);
+        assertNotNull(aceObject2);
+
+        assertEquals(testUserId, aceObject2.getString("principal"));
+
+        JsonArray grantedArray2 = aceObject2.getJsonArray("granted");
+        assertNotNull(grantedArray2);
+        assertEquals(2, grantedArray2.size());
+        Set<String> grantedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < grantedArray2.size(); i++) {
+            grantedPrivilegeNames2.add(grantedArray2.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_READ);
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_MODIFY_PROPERTIES);
+
+        JsonArray deniedArray2 = aceObject2.getJsonArray("denied");
+        assertNotNull(deniedArray2);
+        assertEquals(3, deniedArray2.size());
+        Set<String> deniedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < deniedArray2.size(); i++) {
+            deniedPrivilegeNames2.add(deniedArray2.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames2, false, PrivilegeConstants.JCR_WRITE);
+        //only the remaining privileges from the disaggregated jcr:write collection should remain.
+        assertPrivilege(deniedPrivilegeNames2, true, PrivilegeConstants.JCR_ADD_CHILD_NODES);
+        assertPrivilege(deniedPrivilegeNames2, true, PrivilegeConstants.JCR_REMOVE_NODE);
+        assertPrivilege(deniedPrivilegeNames2, true, PrivilegeConstants.JCR_REMOVE_CHILD_NODES);
+    }
+
+    /**
+     * Test for SLING-997, preserve privileges that were not posted with the modifyAce
+     * request.
+     */
+    @Test
+    public void testMergeAceForUserCombineAggregatePrivilege() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:removeNode", "denied"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(1, jsonObject.size());
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        assertEquals(testUserId, aceObject.getString("principal"));
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_READ);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(1, deniedArray.size());
+        Set<String> deniedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < deniedArray.size(); i++) {
+            deniedPrivilegeNames.add(deniedArray.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames, true, PrivilegeConstants.JCR_REMOVE_NODE);
+
+
+
+        //2. post a new set of privileges to merge with the existing privileges
+        List<NameValuePair> postParams2 = new ArrayList<>();
+        postParams2.add(new BasicNameValuePair("principalId", testUserId));
+        //jcr:read is not posted, so it should remain in the granted ACE
+
+        //deny the full jcr:write aggregate privilege, which should merge with the
+        //existing part.
+        postParams2.add(new BasicNameValuePair("privilege@jcr:write", "denied")); //add a new privilege
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams2, null);
+
+
+        //fetch the JSON for the acl to verify the settings.
+        String json2 = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json2);
+
+        JsonObject jsonObject2 = parseJson(json2);
+        assertEquals(1, jsonObject2.size());
+
+        JsonObject aceObject2 = jsonObject2.getJsonObject(testUserId);
+        assertNotNull(aceObject2);
+
+        assertEquals(testUserId, aceObject.getString("principal"));
+
+        JsonArray grantedArray2 = aceObject2.getJsonArray("granted");
+        assertNotNull(grantedArray2);
+        assertEquals(1, grantedArray2.size());
+        Set<String> grantedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < grantedArray2.size(); i++) {
+            grantedPrivilegeNames2.add(grantedArray2.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_READ);
+
+        JsonArray deniedArray2 = aceObject2.getJsonArray("denied");
+        assertNotNull(deniedArray2);
+        assertEquals(1, deniedArray2.size());
+        Set<String> deniedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < deniedArray2.size(); i++) {
+            deniedPrivilegeNames2.add(deniedArray2.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames2, true, PrivilegeConstants.JCR_WRITE);
+    }
+
+
+    /**
+     * Test ACE update with a deny privilege for an ACE that already contains
+     * a grant privilege
+     */
+    @Test
+    public void testMergeAceForUserDenyPrivilegeAfterGrantPrivilege() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(1, jsonObject.size());
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        assertEquals(testUserId, aceObject.getString("principal"));
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(1, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_WRITE);
+
+        assertFalse(aceObject.containsKey("denied"));
+
+
+        //2. post a new set of privileges to merge with the existing privileges
+        List<NameValuePair> postParams2 = new ArrayList<>();
+        postParams2.add(new BasicNameValuePair("principalId", testUserId));
+        //jcr:write is not posted, so it should remain in the granted ACE
+
+        //deny the jcr:nodeTypeManagement privilege, which should merge with the
+        //existing ACE.
+        postParams2.add(new BasicNameValuePair("privilege@jcr:nodeTypeManagement", "denied")); //add a new privilege
+
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams2, null);
+
+
+        //fetch the JSON for the acl to verify the settings.
+        String json2 = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json2);
+
+        JsonObject jsonObject2 = parseJson(json2);
+        assertEquals(1, jsonObject2.size());
+
+        JsonObject aceObject2 = jsonObject2.getJsonObject(testUserId);
+        assertNotNull(aceObject2);
+
+        assertEquals(testUserId, aceObject2.getString("principal"));
+
+        JsonArray grantedArray2 = aceObject2.getJsonArray("granted");
+        assertNotNull(grantedArray2);
+        assertEquals(1, grantedArray2.size());
+        Set<String> grantedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < grantedArray2.size(); i++) {
+            grantedPrivilegeNames2.add(grantedArray2.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames2, true, PrivilegeConstants.JCR_WRITE);
+
+        JsonArray deniedArray2 = aceObject2.getJsonArray("denied");
+        assertNotNull(deniedArray2);
+        assertEquals(1, deniedArray2.size());
+        Set<String> deniedPrivilegeNames2 = new HashSet<>();
+        for (int i=0; i < deniedArray2.size(); i++) {
+            deniedPrivilegeNames2.add(deniedArray2.getString(i));
+        }
+        assertPrivilege(deniedPrivilegeNames2, true, PrivilegeConstants.JCR_NODE_TYPE_MANAGEMENT);
+    }
+
+
+
+    /**
+     * Test to verify adding an ACE in the first position of
+     * the ACL
+     */
+    @Test
+    public void testAddAceOrderByFirst() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first");
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(2, jsonObject.size());
+
+        JsonObject group = jsonObject.getJsonObject(testGroupId);
+        assertNotNull(group);
+        assertEquals(testGroupId, group.getString("principal"));
+                assertEquals(0, group.getInt("order"));
+        JsonObject user =  jsonObject.getJsonObject(testUserId);
+                assertNotNull(user);
+                assertEquals(testUserId, user.getString("principal"));
+                assertEquals(1, user.getInt("order"));
+    }
+
+    /**
+     * Test to verify adding an ACE at the end
+     * the ACL
+     */
+    @Test
+    public void testAddAceOrderByLast() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "last");
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(2, jsonObject.size());
+
+                JsonObject user =  jsonObject.getJsonObject(testUserId);
+                assertNotNull(user);
+                assertEquals(testUserId, user.getString("principal"));
+                assertEquals(0, user.getInt("order"));
+                JsonObject group = jsonObject.getJsonObject(testGroupId);
+                assertNotNull(group);
+                assertEquals(testGroupId, group.getString("principal"));
+                assertEquals(1, group.getInt("order"));
+
+    }
+
+    /**
+     * Test to verify adding an ACE before an existing ACE
+     * the ACL
+     */
+    @Test
+    public void testAddAceOrderByBefore() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "before " + testUserId);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+
+                JsonObject jsonObject = parseJson(json);
+                assertEquals(2, jsonObject.size());
+
+
+                JsonObject group = jsonObject.getJsonObject(testGroupId);
+                assertNotNull(group);
+                assertEquals(testGroupId, group.getString("principal"));
+                assertEquals(0, group.getInt("order"));
+                JsonObject user =  jsonObject.getJsonObject(testUserId);
+                assertNotNull(user);
+                assertEquals(testUserId, user.getString("principal"));
+                assertEquals(1, user.getInt("order"));
+
+    }
+
+    /**
+     * Test to verify adding an ACE after an existing ACE
+     * the ACL
+     */
+    @Test
+    public void testAddAceOrderByAfter() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "after " + testUserId);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+                JsonObject jsonObject = parseJson(json);
+                assertEquals(2, jsonObject.size());
+
+                JsonObject user =  jsonObject.getJsonObject(testUserId);
+                assertNotNull(user);
+                assertEquals(testUserId, user.getString("principal"));
+                assertEquals(0, user.getInt("order"));
+                JsonObject group = jsonObject.getJsonObject(testGroupId);
+                assertNotNull(group);
+                assertEquals(testGroupId, group.getString("principal"));
+                assertEquals(1, group.getInt("order"));
+
+    }
+
+    /**
+     * Test to verify adding an ACE at a specific index inside
+     * the ACL
+     */
+    @Test
+    public void testAddAceOrderByNumeric() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "0");
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+
+                JsonObject jsonObject = parseJson(json);
+                assertEquals(2, jsonObject.size());
+
+                JsonObject group = jsonObject.getJsonObject(testGroupId);
+                assertNotNull(group);
+                assertEquals(testGroupId, group.getString("principal"));
+                assertEquals(0, group.getInt("order"));
+
+                JsonObject user =  jsonObject.getJsonObject(testUserId);
+                assertNotNull(user);
+                assertEquals(testUserId, user.getString("principal"));
+                assertEquals(1, user.getInt("order"));
+
+
+
+        //add another principal between the testGroupId and testUserId
+        testUserId2 = createTestUser();
+        addOrUpdateAce(testFolderUrl, testUserId2, true, "1");
+
+        String json2 = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json2);
+
+                JsonObject jsonObject2 = parseJson(json2);
+                assertEquals(3, jsonObject2.size());
+
+                JsonObject group2 = jsonObject2.getJsonObject(testGroupId);
+                assertNotNull(group2);
+                assertEquals(testGroupId, group2.getString("principal"));
+                assertEquals(0, group2.getInt("order"));
+
+                JsonObject user3 =  jsonObject2.getJsonObject(testUserId2);
+                assertNotNull(user3);
+                assertEquals(testUserId2, user3.getString("principal"));
+                assertEquals(1, user3.getInt("order"));
+
+                JsonObject user2 =  jsonObject2.getJsonObject(testUserId);
+                assertNotNull(user2);
+                assertEquals(testUserId, user2.getString("principal"));
+                assertEquals(2, user2.getInt("order"));
+
+    }
+
+    /**
+     * Test to make sure modifying an existing ace without changing the order
+     * leaves the ACE in the same position in the ACL
+     */
+    @Test
+    public void testUpdateAcePreservePosition() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first");
+
+        //update the ace to make sure the update does not change the ACE order
+        addOrUpdateAce(testFolderUrl, testGroupId, false, null);
+
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+                JsonObject jsonObject = parseJson(json);
+                assertEquals(2, jsonObject.size());
+
+                JsonObject group = jsonObject.getJsonObject(testGroupId);
+                assertNotNull(group);
+                assertEquals(testGroupId, group.getString("principal"));
+                assertEquals(0, group.getInt("order"));
+                JsonObject user =  jsonObject.getJsonObject(testUserId);
+                assertNotNull(user);
+                assertEquals(testUserId, user.getString("principal"));
+                assertEquals(1, user.getInt("order"));
+
+    }
+
+
+    /**
+     * Helper to create a test folder with a single ACE pre-created
+     */
+    private void createAceOrderTestFolderWithOneAce() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder();
+
+        addOrUpdateAce(testFolderUrl, testUserId, true, null);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+                JsonObject jsonObject = parseJson(json);
+                assertEquals(1, jsonObject.size());
+
+                JsonObject user = jsonObject.getJsonObject(testUserId);
+                assertNotNull(user);
+                assertEquals(testUserId, user.getString("principal"));
+                assertEquals(0, user.getInt("order"));
+
+    }
+
+    /**
+     * Helper to add or update an ace for testing
+     */
+    private void addOrUpdateAce(String folderUrl, String principalId, boolean readGranted, String order) throws IOException, JsonException {
+        addOrUpdateAce(folderUrl, principalId, readGranted, order, null);
+    }
+    private void addOrUpdateAce(String folderUrl, String principalId, boolean readGranted, String order, Map<String, Object> restrictions) throws IOException, JsonException {
+        String postUrl = folderUrl + ".modifyAce.html";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", principalId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", readGranted ? "granted" : "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+        if (order != null) {
+            postParams.add(new BasicNameValuePair("order", order));
+        }
+        if (restrictions != null) {
+            Set<Entry<String, Object>> entrySet = restrictions.entrySet();
+            for (Entry<String, Object> entry : entrySet) {
+                Object value = entry.getValue();
+                if (value != null) {
+                    String rname = entry.getKey();
+                    String paramName = String.format("restriction@%s", rname);
+
+                    if (value.getClass().isArray()) {
+                        int length = Array.getLength(value);
+                        for (int i=0; i < length; i++) {
+                            Object rvalue = Array.get(value, i);
+                            if (rvalue instanceof String) {
+                                postParams.add(new BasicNameValuePair(paramName, (String)rvalue));
+                            }
+                        }
+                    } else if (value instanceof String) {
+                        postParams.add(new BasicNameValuePair(paramName, (String)value));
+                    }
+                }
+            }
+        }
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+    }
+
+    /**
+     * Test for SLING-1677
+     */
+    @Test
+    public void testModifyAceResponseAsJSON() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.json";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "bogus")); //invalid value should be ignored.
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObject = parseJson(json);
+        assertNotNull(jsonObject);
+    }
+
+
+    /**
+     * Test for SLING-3010
+     */
+    @Test
+    public void testMergeAceForUserGrantNestedAggregatePrivilegeAfterDenySuperAggregatePrivilege() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.json";
+
+        //1. setup an initial set of denied privileges for the test user
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:versionManagement", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@rep:write", "denied"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        /*String json = */getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+
+        //2. now grant the jcr:write subset from the rep:write aggregate privilege
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:versionManagement", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted")); //sub-aggregate of rep:write
+
+        /*String json = */getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //3. verify that the acl has the correct values
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(1, jsonObject.size());
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        assertEquals(testUserId, aceObject.getString("principal"));
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals(4, grantedArray.size());
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_VERSION_MANAGEMENT);
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_READ);
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_MODIFY_ACCESS_CONTROL);
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_WRITE);
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals(1, deniedArray.size());
+        Set<String> deniedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < deniedArray.size(); i++) {
+            deniedPrivilegeNames.add(deniedArray.getString(i));
+        }
+        //the leftovers from the denied rep:write that were not granted with jcr:write
+        assertPrivilege(deniedPrivilegeNames, true, PrivilegeConstants.JCR_NODE_TYPE_MANAGEMENT);
+    }
+
+    /**
+     * Test for SLING-3010
+     */
+    @Test
+    public void testMergeAceForUserGrantAggregatePrivilegePartsAfterDenyAggregatePrivilege() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.json";
+
+        //1. setup an initial set of denied privileges for the test user
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:versionManagement", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@rep:write", "denied"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        /*String json = */getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //2. now grant the all the privileges contained in the rep:write privilege
+        postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:versionManagement", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:nodeTypeManagement", "granted")); //sub-privilege of rep:write
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "granted")); //sub-aggregate of rep:write
+
+        /*String json = */getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //3. verify that the acl has the correct values
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(1, jsonObject.size());
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        assertEquals(testUserId, aceObject.getString("principal"));
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        Set<String> grantedPrivilegeNames = new HashSet<>();
+        for (int i=0; i < grantedArray.size(); i++) {
+            grantedPrivilegeNames.add(grantedArray.getString(i));
+        }
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_VERSION_MANAGEMENT);
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_READ);
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.JCR_MODIFY_ACCESS_CONTROL);
+        assertPrivilege(grantedPrivilegeNames, true, PrivilegeConstants.REP_WRITE); //jcr:nodeTypeManagement + jcr:write
+        assertEquals("Expecting the correct number of privileges in " + grantedPrivilegeNames, 4, grantedPrivilegeNames.size());
+
+        //should be nothing left in the denied set.
+        Object deniedArray = aceObject.get("denied");
+        assertNull(deniedArray);
+    }
+
+    /**
+     * SLING-8117 - Test to verify adding an ACE with restriction to
+     * the ACL
+     */
+    @Test
+    public void testAddAceWithRestriction() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        Map<String, Object> restrictions = new HashMap<>();
+        restrictions.put("rep:glob", "/hello");
+        restrictions.put("rep:itemNames", new String[] {"child1", "child2"});
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first", restrictions);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(2, jsonObject.size());
+
+
+        JsonObject group = jsonObject.getJsonObject(testGroupId);
+        assertNotNull(group);
+        assertEquals(testGroupId, group.getString("principal"));
+        assertEquals(0, group.getInt("order"));
+
+        //verify restrictions are returned
+        assertTrue(group.containsKey("restrictions"));
+        JsonObject restrictionsObj = group.getJsonObject("restrictions");
+        assertNotNull(restrictionsObj);
+
+        Object repGlob = restrictionsObj.get("rep:glob");
+        assertNotNull(repGlob);
+        assertTrue(repGlob instanceof JsonString);
+        assertEquals("/hello", ((JsonString)repGlob).getString());
+
+        Object itemNames = restrictionsObj.get("rep:itemNames");
+        assertNotNull(itemNames);
+        assertTrue(itemNames instanceof JsonArray);
+        assertEquals(2, ((JsonArray)itemNames).size());
+
+
+        JsonObject user =  jsonObject.getJsonObject(testUserId);
+        assertNotNull(user);
+        assertEquals(testUserId, user.getString("principal"));
+        assertEquals(1, user.getInt("order"));
+        assertFalse(user.containsKey("restrictions"));
+
+    }
+
+    /**
+     * SLING-8117 - Test to verify merging an ACE with an existing restriction to
+     * the ACL
+     */
+    @Test
+    public void testUpdateAceToMergeNewRestriction() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        //first create an ACE with the first restriction
+        Map<String, Object> restrictions = new HashMap<>();
+        restrictions.put("rep:glob", "/hello");
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first", restrictions);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(2, jsonObject.size());
+
+        JsonObject group = jsonObject.getJsonObject(testGroupId);
+        assertNotNull(group);
+        assertEquals(testGroupId, group.getString("principal"));
+        assertEquals(0, group.getInt("order"));
+
+        //verify restrictions are returned
+        assertTrue(group.containsKey("restrictions"));
+        JsonObject restrictionsObj = group.getJsonObject("restrictions");
+        assertNotNull(restrictionsObj);
+        assertEquals(1, restrictionsObj.size());
+
+        Object repGlob = restrictionsObj.get("rep:glob");
+        assertNotNull(repGlob);
+        assertTrue(repGlob instanceof JsonString);
+        assertEquals("/hello", ((JsonString)repGlob).getString());
+
+
+
+        //second update the ACE with a second restriction
+        Map<String, Object> restrictions2 = new HashMap<>();
+        restrictions2.put("rep:itemNames", new String[] {"child1", "child2"});
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first", restrictions2);
+
+        String json2 = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json2);
+
+        JsonObject jsonObject2 = parseJson(json2);
+        assertEquals(2, jsonObject2.size());
+
+        JsonObject group2 = jsonObject2.getJsonObject(testGroupId);
+        assertNotNull(group2);
+        assertEquals(testGroupId, group2.getString("principal"));
+        assertEquals(0, group2.getInt("order"));
+
+        //verify restrictions are returned
+        assertTrue(group2.containsKey("restrictions"));
+        JsonObject restrictionsObj2 = group2.getJsonObject("restrictions");
+        assertNotNull(restrictionsObj2);
+        assertEquals(2, restrictionsObj2.size());
+
+        Object repGlob2 = restrictionsObj2.get("rep:glob");
+        assertNotNull(repGlob2);
+        assertTrue(repGlob2 instanceof JsonString);
+        assertEquals("/hello", ((JsonString)repGlob2).getString());
+
+        Object itemNames2 = restrictionsObj2.get("rep:itemNames");
+        assertNotNull(itemNames2);
+        assertTrue(itemNames2 instanceof JsonArray);
+        assertEquals(2, ((JsonArray)itemNames2).size());
+    }
+
+    /**
+     * SLING-8117 - Test to verify removing a restriction from an ACE
+     */
+    @Test
+    public void testUpdateAceToRemoveRestriction() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        //first create an ACE with the restrictions
+        Map<String, Object> restrictions = new HashMap<>();
+        restrictions.put("rep:glob", "/hello");
+        restrictions.put("rep:itemNames", new String[] {"child1", "child2"});
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first", restrictions);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(2, jsonObject.size());
+
+        JsonObject group = jsonObject.getJsonObject(testGroupId);
+        assertNotNull(group);
+        assertEquals(testGroupId, group.getString("principal"));
+        assertEquals(0, group.getInt("order"));
+
+        //verify restrictions are returned
+        assertTrue(group.containsKey("restrictions"));
+        JsonObject restrictionsObj = group.getJsonObject("restrictions");
+        assertNotNull(restrictionsObj);
+
+        Object repGlob = restrictionsObj.get("rep:glob");
+        assertNotNull(repGlob);
+        assertTrue(repGlob instanceof JsonString);
+        assertEquals("/hello", ((JsonString)repGlob).getString());
+
+        Object itemNames = restrictionsObj.get("rep:itemNames");
+        assertNotNull(itemNames);
+        assertTrue(itemNames instanceof JsonArray);
+        assertEquals(2, ((JsonArray)itemNames).size());
+
+
+        //second remove the restrictions
+        Map<String, Object> restrictions2 = new HashMap<>();
+        restrictions2.put("rep:glob@Delete", "true");
+        restrictions2.put("rep:itemNames@Delete", new String[] {"value does not", "matter"});
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first", restrictions2);
+
+        String json2 = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json2);
+
+        JsonObject jsonObject2 = parseJson(json2);
+        assertEquals(2, jsonObject2.size());
+
+        JsonObject group2 = jsonObject2.getJsonObject(testGroupId);
+        assertNotNull(group2);
+        assertEquals(testGroupId, group2.getString("principal"));
+        assertEquals(0, group2.getInt("order"));
+
+        //verify no restrictions are returned
+        assertFalse(group2.containsKey("restrictions"));
+    }
+
+    /**
+     * SLING-8117 - Test to verify removing a restriction from an ACE does not happen
+     * if a new value with the same name has also been supplied
+     */
+    @Test
+    public void testUpdateAceToRemoveRestrictionWithConflict() throws IOException, JsonException {
+        createAceOrderTestFolderWithOneAce();
+
+        testGroupId = createTestGroup();
+
+        //first create an ACE with the restrictions
+        Map<String, Object> restrictions = new HashMap<>();
+        restrictions.put("rep:glob", "/hello");
+
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first", restrictions);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals(2, jsonObject.size());
+
+        JsonObject group = jsonObject.getJsonObject(testGroupId);
+        assertNotNull(group);
+        assertEquals(testGroupId, group.getString("principal"));
+        assertEquals(0, group.getInt("order"));
+
+        //verify restrictions are returned
+        assertTrue(group.containsKey("restrictions"));
+        JsonObject restrictionsObj = group.getJsonObject("restrictions");
+        assertNotNull(restrictionsObj);
+
+        Object repGlob = restrictionsObj.get("rep:glob");
+        assertNotNull(repGlob);
+        assertTrue(repGlob instanceof JsonString);
+        assertEquals("/hello", ((JsonString)repGlob).getString());
+
+
+        //second remove the restriction and also supply a new value of the same
+        Map<String, Object> restrictions2 = new HashMap<>();
+        restrictions2.put("rep:glob@Delete", "true");
+        restrictions2.put("rep:glob", "/hello_again");
+        addOrUpdateAce(testFolderUrl, testGroupId, true, "first", restrictions2);
+
+        String json2 = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json2);
+
+        JsonObject jsonObject2 = parseJson(json2);
+        assertEquals(2, jsonObject2.size());
+
+        JsonObject group2 = jsonObject2.getJsonObject(testGroupId);
+        assertNotNull(group2);
+        assertEquals(testGroupId, group2.getString("principal"));
+        assertEquals(0, group2.getInt("order"));
+
+        //verify restrictions are returned
+        assertTrue(group2.containsKey("restrictions"));
+        JsonObject restrictionsObj2 = group2.getJsonObject("restrictions");
+        assertNotNull(restrictionsObj2);
+
+        Object repGlob2 = restrictionsObj2.get("rep:glob");
+        assertNotNull(repGlob2);
+        assertTrue(repGlob2 instanceof JsonString);
+        assertEquals("/hello_again", ((JsonString)repGlob2).getString());
+    }
+
+    /**
+     * SLING-8809 - Test to verify submitting an invalid principalId returns a
+     * good error message instead of a NullPointerException
+     */
+    @Test
+    public void testModifyAceForInvalidUser() throws IOException, JsonException {
+        String invalidUserId = "notRealUser123";
+
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.json";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":http-equiv-accept", JSONResponse.RESPONSE_CONTENT_TYPE));
+        postParams.add(new BasicNameValuePair("principalId", invalidUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "bogus")); //invalid value should be ignored.
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals("javax.jcr.RepositoryException: Invalid principalId was submitted.", jsonObject.getString("status.message"));
+    }
+
+    /**
+     * SLING-8811 - Test to verify that the "changes" list of a modifyAce response
+     * returns the list of principals that were changed
+     */
+    @Test
+    public void testModifyAceChangesInResponse() throws IOException, JsonException {
+        testUserId = createTestUser();
+
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.json";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":http-equiv-accept", JSONResponse.RESPONSE_CONTENT_TYPE));
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:modifyAccessControl", "bogus")); //invalid value should be ignored.
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        JsonArray changesArray = jsonObject.getJsonArray("changes");
+        assertNotNull(changesArray);
+        assertEquals(1, changesArray.size());
+        JsonObject change = changesArray.getJsonObject(0);
+        assertEquals("modified", change.getString("type"));
+        assertEquals(testUserId, change.getString("argument"));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/RemoveAcesIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/RemoveAcesIT.java
new file mode 100644
index 0000000..77db5e8
--- /dev/null
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/RemoveAcesIT.java
@@ -0,0 +1,316 @@
+/*
+ * 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.jackrabbit.accessmanager.it;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.json.JsonArray;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.NameValuePair;
+import org.apache.http.auth.Credentials;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.sling.servlets.post.JSONResponse;
+import org.apache.sling.servlets.post.PostResponseCreator;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * Tests for the 'removeAce' Sling POST operation
+ */
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class RemoveAcesIT extends AccessManagerClientTestSupport {
+
+    private ServiceRegistration<PostResponseCreator> serviceReg;
+
+    @Before
+    @Override
+    public void before() throws IOException, URISyntaxException {
+        Bundle bundle = FrameworkUtil.getBundle(getClass());
+        Dictionary<String, Object> props = new Hashtable<>(); // NOSONAR
+        serviceReg = bundle.getBundleContext().registerService(PostResponseCreator.class,
+                new CustomPostResponseCreatorImpl(), props);
+
+        super.before();
+    }
+
+    @After
+    @Override
+    public void after() throws IOException {
+        if (serviceReg != null) {
+            serviceReg.unregister();
+        }
+
+        super.after();
+    }
+
+
+    private String createFolderWithAces(boolean addGroupAce) throws IOException, JsonException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".modifyAce.html";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair("principalId", testUserId));
+        postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+        postParams.add(new BasicNameValuePair("privilege@jcr:write", "denied"));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        if (addGroupAce) {
+            testGroupId = createTestGroup();
+
+            postParams = new ArrayList<>();
+            postParams.add(new BasicNameValuePair("principalId", testGroupId));
+            postParams.add(new BasicNameValuePair("privilege@jcr:read", "granted"));
+
+            assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+        }
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = testFolderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+
+        if (addGroupAce) {
+            assertEquals(2, jsonObject.size());
+        } else {
+            assertEquals(1, jsonObject.size());
+        }
+
+        JsonObject aceObject = jsonObject.getJsonObject(testUserId);
+        assertNotNull(aceObject);
+
+        assertEquals(0, aceObject.getInt("order"));
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        JsonArray grantedArray = aceObject.getJsonArray("granted");
+        assertNotNull(grantedArray);
+        assertEquals("jcr:read", grantedArray.getString(0));
+
+        JsonArray deniedArray = aceObject.getJsonArray("denied");
+        assertNotNull(deniedArray);
+        assertEquals("jcr:write", deniedArray.getString(0));
+
+        if (addGroupAce) {
+            aceObject = jsonObject.getJsonObject(testGroupId);
+            assertNotNull(aceObject);
+
+            principalString = aceObject.getString("principal");
+            assertEquals(testGroupId, principalString);
+
+                assertEquals(1, aceObject.getInt("order"));
+
+            grantedArray = aceObject.getJsonArray("granted");
+            assertNotNull(grantedArray);
+            assertEquals("jcr:read", grantedArray.getString(0));
+        }
+
+        return testFolderUrl;
+    }
+
+    //test removing a single ace
+    @Test
+    public void testRemoveAce() throws IOException, JsonException {
+        String folderUrl = createFolderWithAces(false);
+
+        //remove the ace for the testUser principal
+        String postUrl = folderUrl + ".deleteAce.html";
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":applyTo", testUserId));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = folderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertNotNull(jsonObject);
+        assertEquals(0, jsonObject.size());
+    }
+
+    /**
+     * Test for SLING-7831
+     */
+    @Test
+    public void testRemoveAceCustomPostResponse() throws IOException, JsonException {
+        String folderUrl = createFolderWithAces(false);
+
+        //remove the ace for the testUser principal
+        String postUrl = folderUrl + ".deleteAce.html";
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":responseType", "custom"));
+        postParams.add(new BasicNameValuePair(":applyTo", testUserId));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String content = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_HTML, postParams, HttpServletResponse.SC_OK);
+        assertEquals("Thanks!", content); //verify that the content matches the custom response
+    }
+
+    //test removing multiple aces
+    @Test
+    public void testRemoveAces() throws IOException, JsonException {
+        String folderUrl = createFolderWithAces(true);
+
+        //remove the ace for the testUser principal
+        String postUrl = folderUrl + ".deleteAce.html";
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":applyTo", testUserId));
+        postParams.add(new BasicNameValuePair(":applyTo", testGroupId));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        assertAuthenticatedPostStatus(creds, postUrl, HttpServletResponse.SC_OK, postParams, null);
+
+        //fetch the JSON for the acl to verify the settings.
+        String getUrl = folderUrl + ".acl.json";
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertNotNull(jsonObject);
+        assertEquals(0, jsonObject.size());
+    }
+
+    /**
+     * Test for SLING-1677
+     */
+    @Test
+    public void testRemoveAcesResponseAsJSON() throws IOException, JsonException {
+        String folderUrl = createFolderWithAces(true);
+
+        //remove the ace for the testUser principal
+        String postUrl = folderUrl + ".deleteAce.json";
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":applyTo", testUserId));
+        postParams.add(new BasicNameValuePair(":applyTo", testGroupId));
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+
+        //make sure the json response can be parsed as a JSON object
+        JsonObject jsonObject = parseJson(json);
+        assertNotNull(jsonObject);
+    }
+
+    /**
+     * SLING-8810 - Test that a attempt to remove an ACE from a
+     * node that does not yet have an AccessControlList responds
+     * in a consistent way to other scenarios
+     */
+    @Test
+    public void testRemoveAceWhenAccessControlListDoesNotExist() throws IOException, JsonException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder();
+
+        String postUrl = testFolderUrl + ".deleteAce.json";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":http-equiv-accept", JSONResponse.RESPONSE_CONTENT_TYPE));
+        postParams.add(new BasicNameValuePair(":applyTo", testUserId));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        JsonArray changesArray = jsonObject.getJsonArray("changes");
+        assertNotNull(changesArray);
+        assertEquals(0, changesArray.size());
+    }
+
+    /**
+     * SLING-8812 - Test to verify submitting an invalid principalId returns a
+     * good error message instead of a NullPointerException
+     */
+    @Test
+    public void testRemoveAceForInvalidUser() throws IOException, JsonException {
+        String invalidUserId = "notRealUser123";
+
+        String folderUrl = createFolderWithAces(true);
+
+        String postUrl = folderUrl + ".deleteAce.json";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":http-equiv-accept", JSONResponse.RESPONSE_CONTENT_TYPE));
+        postParams.add(new BasicNameValuePair(":applyTo", invalidUserId));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        assertEquals("javax.jcr.RepositoryException: Invalid principalId was submitted.", jsonObject.getString("status.message"));
+    }
+
+    /**
+     * SLING-8811 - Test to verify that the "changes" list of a modifyAce response
+     * returns the list of principals that were changed
+     */
+    @Test
+    public void testRemoveAceChangesInResponse() throws IOException, JsonException {
+        String folderUrl = createFolderWithAces(true);
+
+        String postUrl = folderUrl + ".deleteAce.json";
+
+        List<NameValuePair> postParams = new ArrayList<>();
+        postParams.add(new BasicNameValuePair(":http-equiv-accept", JSONResponse.RESPONSE_CONTENT_TYPE));
+        postParams.add(new BasicNameValuePair(":applyTo", testUserId));
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+        String json = getAuthenticatedPostContent(creds, postUrl, CONTENT_TYPE_JSON, postParams, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+
+        JsonObject jsonObject = parseJson(json);
+        JsonArray changesArray = jsonObject.getJsonArray("changes");
+        assertNotNull(changesArray);
+        assertEquals(1, changesArray.size());
+        JsonObject change = changesArray.getJsonObject(0);
+        assertEquals("deleted", change.getString("type"));
+        assertEquals(testUserId, change.getString("argument"));
+    }
+
+}
+
diff --git a/src/test/resources/content/apps/nt/unstructured/privileges-info.json.esp b/src/test/resources/content/apps/nt/unstructured/privileges-info.json.esp
new file mode 100644
index 0000000..01b32b6
--- /dev/null
+++ b/src/test/resources/content/apps/nt/unstructured/privileges-info.json.esp
@@ -0,0 +1,111 @@
+<% 
+/*
+ * 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.
+ */
+
+var factory = Packages.javax.json.Json.createBuilderFactory(Packages.java.util.Collections.emptyMap());
+var jsonObjBuilder = factory.createObjectBuilder();
+
+var privilegesInfo = new Packages.org.apache.sling.jcr.jackrabbit.accessmanager.PrivilegesInfo();
+ 
+jsonObjBuilder.add("canAddChildren", privilegesInfo.canAddChildren(currentNode));
+jsonObjBuilder.add("canDeleteChildren", privilegesInfo.canDeleteChildren(currentNode));
+jsonObjBuilder.add("canDelete", privilegesInfo.canDelete(currentNode));
+jsonObjBuilder.add("canModifyProperties", privilegesInfo.canModifyProperties(currentNode));
+jsonObjBuilder.add("canReadAccessControl", privilegesInfo.canReadAccessControl(currentNode));
+jsonObjBuilder.add("canModifyAccessControl", privilegesInfo.canModifyAccessControl(currentNode));
+
+if (privilegesInfo.canReadAccessControl(currentNode)) {
+  var declaredBuilder = factory.createObjectBuilder();
+  var declared = privilegesInfo.getDeclaredAccessRights(currentNode);
+  if (declared != null) {
+    var iterator = declared.entrySet().iterator();
+    while (iterator.hasNext()) {
+      var next = iterator.next();
+      var principal = next.getKey();
+      var accessRights = next.getValue();
+
+      var entryBuilder = factory.createObjectBuilder();
+			
+      var grantedBuilder = factory.createArrayBuilder();
+      var granted = accessRights.getGranted();
+      var grantedIt = granted.iterator();
+      while (grantedIt.hasNext()) {
+        var next = grantedIt.next();
+        var name = next.getName();
+        grantedBuilder.add(name);
+      }
+      entryBuilder.add("granted", grantedBuilder);
+    
+      var deniedBuilder = factory.createArrayBuilder();
+      var denied = accessRights.getDenied();
+      var deniedIt = denied.iterator();
+      while (deniedIt.hasNext()) {
+        var next = deniedIt.next();
+        var name = next.getName();
+        deniedBuilder.add(name);
+      }
+      entryBuilder.add("denied", deniedBuilder);
+    
+      declaredBuilder.add(principal.getName(), entryBuilder);
+    }
+  }
+  jsonObjBuilder.add("declaredAccessRights", declaredBuilder);
+
+  var effectiveBuilder = factory.createObjectBuilder();
+  var effective = privilegesInfo.getEffectiveAccessRights(currentNode);
+  if (effective != null) {
+    var iterator = effective.entrySet().iterator();
+    while (iterator.hasNext()) {
+      var next = iterator.next();
+      var principal = next.getKey();
+      var accessRights = next.getValue();
+
+      var entryBuilder = factory.createObjectBuilder();
+			
+      var grantedBuilder = factory.createArrayBuilder();
+      var granted = accessRights.getGranted();
+      var grantedIt = granted.iterator();
+      while (grantedIt.hasNext()) {
+        var next = grantedIt.next();
+        var name = next.getName();
+        grantedBuilder.add(name);
+      }
+      entryBuilder.add("granted", grantedBuilder);
+    
+      var deniedBuilder = factory.createArrayBuilder();
+      var denied = accessRights.getDenied();
+      var deniedIt = denied.iterator();
+      while (deniedIt.hasNext()) {
+        var next = deniedIt.next();
+        var name = next.getName();
+        deniedBuilder.add(name);
+      }
+      entryBuilder.add("denied", deniedBuilder);
+    
+      effectiveBuilder.add(principal.getName(), entryBuilder);
+    }
+  }
+  jsonObjBuilder.add("effectiveAccessRights", effectiveBuilder);
+}
+ 
+var jsonObj = jsonObjBuilder.build();
+var jsonWriter = Packages.javax.json.Json.createWriter(response.getWriter());
+jsonWriter.writeObject(jsonObj);
+jsonWriter.close(); 
+%>
\ No newline at end of file