SLING-11321 add declaredAt structure for effective acl/ace (#12)

effective acl/ace json output should contain the paths where the privileges were declared
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/impl/JsonConvert.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/impl/JsonConvert.java
index 6918aa9..cc4e029 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/impl/JsonConvert.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/impl/JsonConvert.java
@@ -21,6 +21,7 @@
 import java.security.Principal;
 import java.util.Collection;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 import javax.jcr.PropertyType;
@@ -33,6 +34,7 @@
 
 import org.apache.sling.jcr.jackrabbit.accessmanager.LocalPrivilege;
 import org.apache.sling.jcr.jackrabbit.accessmanager.LocalRestriction;
+import org.apache.sling.jcr.jackrabbit.accessmanager.post.DeclarationType;
 
 /**
  * Utilities to help convert ACL/ACE data to JSON
@@ -43,6 +45,7 @@
     public static final String KEY_PRIVILEGES = "privileges";
     public static final String KEY_ALLOW = "allow";
     public static final String KEY_DENY = "deny";
+    public static final String KEY_DECLARED_AT = "declaredAt";
 
     private JsonConvert() {
         // no-op
@@ -77,6 +80,30 @@
         return principalObj;
     }
 
+    /**
+     * Add details about where the privileges were declared, usually
+     * for viewing the effective access list or entry
+     */
+    public static void addDeclaredAt(JsonObjectBuilder principalObj, Map<DeclarationType, Set<String>> declaredAt) {
+        JsonObjectBuilder declaredAtBuilder = Json.createObjectBuilder();
+        for (Entry<DeclarationType, Set<String>> daentry : declaredAt.entrySet()) {
+            DeclarationType type = daentry.getKey();
+            if (type != null) {
+                Set<String> value = daentry.getValue();
+                if (value.size() == 1) {
+                    declaredAtBuilder.add(type.getJsonKey(), value.iterator().next());
+                } else {
+                    JsonArrayBuilder typeBuilder = Json.createArrayBuilder();
+                    for (String at : value) {
+                        typeBuilder.add(at);
+                    }
+                    declaredAtBuilder.add(type.getJsonKey(), typeBuilder);
+                }
+            }
+        }
+        principalObj.add(JsonConvert.KEY_DECLARED_AT, declaredAtBuilder);
+    }
+
     public static void addRestrictions(JsonObjectBuilder privilegeObj, String key, Set<LocalRestriction> restrictions) {
         if (restrictions.isEmpty()) {
             privilegeObj.add(key, true);
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractAccessGetServlet.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractAccessGetServlet.java
index 752baff..6adc061 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractAccessGetServlet.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractAccessGetServlet.java
@@ -224,7 +224,8 @@
      * @return map of sorted entries, key is the effectivePath and value is the list of entries for that path
      */
     protected @NotNull Map<String, List<AccessControlEntry>> entriesSortedByEffectivePath(@NotNull AccessControlPolicy[] policies,
-            @NotNull Predicate<? super AccessControlEntry> accessControlEntryFilter) throws RepositoryException {
+            @NotNull Predicate<? super AccessControlEntry> accessControlEntryFilter,
+            Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) throws RepositoryException {
         Comparator<? super String> effectivePathComparator = (k1, k2) -> Objects.compare(k1, k2, Comparator.nullsFirst(String::compareTo));
         Map<String, List<AccessControlEntry>> effectivePathToEntriesMap = new TreeMap<>(effectivePathComparator);
 
@@ -235,6 +236,7 @@
                 Stream.of(accessControlEntries)
                     .filter(accessControlEntryFilter)
                     .forEach(entry -> {
+                        DeclarationType dt = null;
                         String effectivePath = null;
                         if (entry instanceof PrincipalAccessControlList.Entry) {
                             // for principal-based ACE, the effectivePath comes from the entry
@@ -243,12 +245,18 @@
                                 // special case
                                 effectivePath = PrincipalAceHelper.RESOURCE_PATH_REPOSITORY;
                             }
+                            dt = DeclarationType.PRINCIPAL;
                         } else if (accessControlPolicy instanceof JackrabbitAccessControlList) {
                             // for basic ACE, the effectivePath comes from the ACL path
                             effectivePath = ((JackrabbitAccessControlList)accessControlPolicy).getPath();
+                            dt = DeclarationType.NODE;
                         }
                         List<AccessControlEntry> entriesForPath = effectivePathToEntriesMap.computeIfAbsent(effectivePath, key -> new ArrayList<>());
                         entriesForPath.add(entry);
+
+                        Map<DeclarationType, Set<String>> map = declaredAtPaths.computeIfAbsent(entry.getPrincipal(), k -> new HashMap<>());
+                        Set<String> set = map.computeIfAbsent(dt, k -> new HashSet<>());
+                        set.add(effectivePath);
                     });
             }
         }
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractGetAceServlet.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractGetAceServlet.java
index 4d56cad..a200611 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractGetAceServlet.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractGetAceServlet.java
@@ -50,7 +50,8 @@
     protected JsonObject internalGetAce(Session jcrSession, String resourcePath, String principalId) throws RepositoryException {
         Principal principal = validateArgs(jcrSession, resourcePath, principalId);
 
-        Map<String, List<AccessControlEntry>> effectivePathToEntriesMap = getAccessControlEntriesMap(jcrSession, resourcePath, principal);
+        Map<Principal, Map<DeclarationType, Set<String>>> principalToDeclaredAtPaths = new HashMap<>();
+        Map<String, List<AccessControlEntry>> effectivePathToEntriesMap = getAccessControlEntriesMap(jcrSession, resourcePath, principal, principalToDeclaredAtPaths);
         if (effectivePathToEntriesMap == null || effectivePathToEntriesMap.isEmpty()) {
             throw new ResourceNotFoundException(resourcePath, "No access control entries were found");
         }
@@ -81,18 +82,32 @@
         PrivilegesHelper.consolidateAggregates(jcrSession, resourcePath, privilegeToLocalPrivilegesMap, privilegeLongestDepthMap);
 
         // convert the data to JSON
-        JsonObjectBuilder jsonObj = JsonConvert.convertToJson(principal, privilegeToLocalPrivilegesMap, -1);
-        return jsonObj.build();
+        JsonObjectBuilder principalObj = JsonConvert.convertToJson(principal, privilegeToLocalPrivilegesMap, -1);
+        addExtraInfo(principalObj, principal, principalToDeclaredAtPaths);
+        return principalObj.build();
     }
 
-    protected abstract Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath, Principal principal) throws RepositoryException;
+    /**
+     * Override to add additional data to the principal object
+     * 
+     * @param principalObj the current principal object
+     * @param principal the current principal
+     * @param principalToDeclaredAtPaths a map of principal the paths where ACEs are declared
+     */
+    protected void addExtraInfo(JsonObjectBuilder principalJson,
+            Principal principal, Map<Principal, Map<DeclarationType, Set<String>>> principalToDeclaredAtPaths) {
+        // no-op 
+    }
+
+    protected abstract Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath, Principal principal,
+            Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) throws RepositoryException;
 
     /**
      * @deprecated use {@link #getAccessControlEntriesMap(Session, String, Principal, Map)} instead
      */
     @Deprecated
     protected AccessControlEntry[] getAccessControlEntries(Session session, String absPath, Principal principal) throws RepositoryException {
-        return getAccessControlEntriesMap(session, absPath, principal).values().stream()
+        return getAccessControlEntriesMap(session, absPath, principal, new HashMap<>()).values().stream()
             .toArray(size -> new AccessControlEntry[size]);
     }
 
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractGetAclServlet.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractGetAclServlet.java
index a51902f..910c364 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractGetAclServlet.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/AbstractGetAclServlet.java
@@ -79,7 +79,8 @@
             srMap.put(restrictionDefinition.getName(), restrictionDefinition);
         }
 
-        Map<String, List<AccessControlEntry>> effectivePathToEntriesMap = getAccessControlEntriesMap(jcrSession, resourcePath);
+        Map<Principal, Map<DeclarationType, Set<String>>> principalToDeclaredAtPaths = new HashMap<>();
+        Map<String, List<AccessControlEntry>> effectivePathToEntriesMap = getAccessControlEntriesMap(jcrSession, resourcePath, principalToDeclaredAtPaths);
         Map<Principal, Integer> principalToOrderMap = new HashMap<>();
         Map<Principal, Map<Privilege, LocalPrivilege>> principalToPrivilegesMap = new HashMap<>();
         for (Entry<String, List<AccessControlEntry>> entry : effectivePathToEntriesMap.entrySet()) {
@@ -115,22 +116,37 @@
         Collections.sort(entrySetList, (e1, e2) -> principalToOrderMap.get(e1.getKey()).compareTo(principalToOrderMap.get(e2.getKey())));
 
         // convert the data to JSON
-        JsonObjectBuilder jsonObj = convertToJson(entrySetList);
+        JsonObjectBuilder jsonObj = convertToJson(entrySetList, principalToDeclaredAtPaths);
         return jsonObj.build();
     }
 
-    protected JsonObjectBuilder convertToJson(List<Entry<Principal, Map<Privilege, LocalPrivilege>>> entrySetList) {
+    protected JsonObjectBuilder convertToJson(List<Entry<Principal, Map<Privilege, LocalPrivilege>>> entrySetList,
+            Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) {
         JsonObjectBuilder jsonObj = Json.createObjectBuilder();
         for (int i = 0; i < entrySetList.size(); i++) {
             Entry<Principal, Map<Privilege, LocalPrivilege>> entry = entrySetList.get(i);
             Principal principal = entry.getKey();
             JsonObjectBuilder principalObj = JsonConvert.convertToJson(entry.getKey(), entry.getValue(), i);
+            addExtraInfo(principalObj, principal, declaredAtPaths);
             jsonObj.add(principal.getName(), principalObj);
         }
         return jsonObj;
     }
 
     /**
+     * Override to add additional data to the principal object
+     * 
+     * @param principalObj the current principal object
+     * @param principal the current principal
+     * @param principalToDeclaredAtPaths a map of principal the paths where ACEs are declared
+     */
+    protected void addExtraInfo(JsonObjectBuilder principalJson,
+            Principal principal, Map<Principal, Map<DeclarationType, Set<String>>> principalToDeclaredAtPaths) {
+        // no-op 
+    }
+
+
+    /**
      * @deprecated use {@link JsonConvert#addRestrictions(JsonObjectBuilder, String, Set)} instead
      */
     @Deprecated
@@ -154,14 +170,15 @@
         return JsonConvert.addTo(builder, value);
     }
 
-    protected abstract Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath) throws RepositoryException;
+    protected abstract Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath,
+            Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) throws RepositoryException;
 
     /**
      * @deprecated use {@link #getAccessControlEntriesMap(Session, String, Map)} instead
      */
     @Deprecated
     protected AccessControlEntry[] getAccessControlEntries(Session session, String absPath) throws RepositoryException {
-        return getAccessControlEntriesMap(session, absPath).values().stream()
+        return getAccessControlEntriesMap(session, absPath, new HashMap<>()).values().stream()
                 .toArray(size -> new AccessControlEntry[size]);
     }
 
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/DeclarationType.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/DeclarationType.java
new file mode 100644
index 0000000..eb863a2
--- /dev/null
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/DeclarationType.java
@@ -0,0 +1,32 @@
+/*
+ * 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.post;
+
+/**
+ * Enumerates the types of ACE declarations, typically
+ * used for constructing the output of the declaredAt structure of
+ * the effective ace/acl json
+ */
+public enum DeclarationType {
+    PRINCIPAL,
+    NODE;
+
+    public String getJsonKey() {
+        return name().toLowerCase();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetAceServlet.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetAceServlet.java
index 19edf38..17cc42c 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetAceServlet.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetAceServlet.java
@@ -21,6 +21,7 @@
 import java.security.Principal;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
@@ -93,10 +94,10 @@
 
     @Override
     protected Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath,
-            Principal principal) throws RepositoryException {
+            Principal principal, Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) throws RepositoryException {
         AccessControlManager acMgr = session.getAccessControlManager();
         AccessControlPolicy[] policies = acMgr.getPolicies(absPath);
-        return entriesSortedByEffectivePath(policies, ace -> principal.equals(ace.getPrincipal()));
+        return entriesSortedByEffectivePath(policies, ace -> principal.equals(ace.getPrincipal()), declaredAtPaths);
     }
 
 }
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetAclServlet.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetAclServlet.java
index 0017e1c..d325c42 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetAclServlet.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetAclServlet.java
@@ -16,8 +16,10 @@
  */
 package org.apache.sling.jcr.jackrabbit.accessmanager.post;
 
+import java.security.Principal;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
@@ -121,10 +123,11 @@
     }
 
     @Override
-    protected Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath) throws RepositoryException {
+    protected Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath,
+            Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) throws RepositoryException {
         AccessControlManager accessControlManager = session.getAccessControlManager();
         AccessControlPolicy[] policies = accessControlManager.getPolicies(absPath);
-        return entriesSortedByEffectivePath(policies, ace -> true);
+        return entriesSortedByEffectivePath(policies, ace -> true, declaredAtPaths);
     }
 
 }
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetEffectiveAceServlet.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetEffectiveAceServlet.java
index d001350..738bd8a 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetEffectiveAceServlet.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetEffectiveAceServlet.java
@@ -21,6 +21,7 @@
 import java.security.Principal;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
@@ -28,11 +29,13 @@
 import javax.jcr.security.AccessControlManager;
 import javax.jcr.security.AccessControlPolicy;
 import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
 import javax.servlet.Servlet;
 
 import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionProvider;
 import org.apache.sling.jcr.base.util.AccessControlUtil;
 import org.apache.sling.jcr.jackrabbit.accessmanager.GetEffectiveAce;
+import org.apache.sling.jcr.jackrabbit.accessmanager.impl.JsonConvert;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 
@@ -92,12 +95,22 @@
         return internalGetAce(jcrSession, resourcePath, principalId);
     }
 
+    /**
+     * Overridden to add the declaredAt data to the json
+     */
+    @Override
+    protected void addExtraInfo(JsonObjectBuilder principalJson, Principal principal,
+            Map<Principal, Map<DeclarationType, Set<String>>> principalToDeclaredAtPaths) {
+        Map<DeclarationType, Set<String>> map = principalToDeclaredAtPaths.get(principal);
+        JsonConvert.addDeclaredAt(principalJson, map);
+    }
+
     @Override
     protected Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath,
-            Principal principal) throws RepositoryException {
+            Principal principal, Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) throws RepositoryException {
         AccessControlManager acMgr = AccessControlUtil.getAccessControlManager(session);
         AccessControlPolicy[] policies = acMgr.getEffectivePolicies(absPath);
-        return entriesSortedByEffectivePath(policies, ace -> principal.equals(ace.getPrincipal()));
+        return entriesSortedByEffectivePath(policies, ace -> principal.equals(ace.getPrincipal()), declaredAtPaths);
     }
 
 }
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetEffectiveAclServlet.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetEffectiveAclServlet.java
index f917e75..224de8b 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetEffectiveAclServlet.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetEffectiveAclServlet.java
@@ -16,8 +16,10 @@
  */
 package org.apache.sling.jcr.jackrabbit.accessmanager.post;
 
+import java.security.Principal;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
@@ -25,10 +27,12 @@
 import javax.jcr.security.AccessControlManager;
 import javax.jcr.security.AccessControlPolicy;
 import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
 import javax.servlet.Servlet;
 
 import org.apache.jackrabbit.oak.spi.security.authorization.restriction.RestrictionProvider;
 import org.apache.sling.jcr.jackrabbit.accessmanager.GetEffectiveAcl;
+import org.apache.sling.jcr.jackrabbit.accessmanager.impl.JsonConvert;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 
@@ -120,11 +124,22 @@
         return internalGetAcl(jcrSession, resourcePath);
     }
 
+    /**
+     * Overridden to add the declaredAt data to the json
+     */
     @Override
-    protected Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath) throws RepositoryException {
+    protected void addExtraInfo(JsonObjectBuilder principalJson, Principal principal,
+            Map<Principal, Map<DeclarationType, Set<String>>> principalToDeclaredAtPaths) {
+        Map<DeclarationType, Set<String>> map = principalToDeclaredAtPaths.get(principal);
+        JsonConvert.addDeclaredAt(principalJson, map);
+    }
+
+    @Override
+    protected Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath,
+            Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) throws RepositoryException {
         AccessControlManager accessControlManager = session.getAccessControlManager();
         AccessControlPolicy[] policies = accessControlManager.getEffectivePolicies(absPath);
-        return entriesSortedByEffectivePath(policies, ace -> true);
+        return entriesSortedByEffectivePath(policies, ace -> true, declaredAtPaths);
     }
 
 }
diff --git a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetPrincipalAceServlet.java b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetPrincipalAceServlet.java
index 6dba636..4d40d88 100644
--- a/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetPrincipalAceServlet.java
+++ b/src/main/java/org/apache/sling/jcr/jackrabbit/accessmanager/post/GetPrincipalAceServlet.java
@@ -22,6 +22,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
@@ -111,12 +112,12 @@
 
     @Override
     protected Map<String, List<AccessControlEntry>> getAccessControlEntriesMap(Session session, String absPath,
-            Principal principal) throws RepositoryException {
+            Principal principal, Map<Principal, Map<DeclarationType, Set<String>>> declaredAtPaths) throws RepositoryException {
         AccessControlManager acMgr = session.getAccessControlManager();
         if (acMgr instanceof JackrabbitAccessControlManager) {
             JackrabbitAccessControlManager jacMgr = (JackrabbitAccessControlManager)acMgr;
             JackrabbitAccessControlPolicy[] policies = jacMgr.getPolicies(principal);
-            return entriesSortedByEffectivePath(policies, ace -> matchesPrincipalAccessControlEntry(ace, absPath, principal));
+            return entriesSortedByEffectivePath(policies, ace -> matchesPrincipalAccessControlEntry(ace, absPath, principal), declaredAtPaths);
         } else {
             return Collections.emptyMap();
         }
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
index 34c86c2..7837055 100644
--- 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
@@ -83,21 +83,24 @@
         }
 
         // switch to the minimum oak version that supports principalbased access control
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-api", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-blob", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-blob-plugins", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-commons", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-core", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-core-spi", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-jcr", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-lucene", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-query-spi", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-security-spi", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-segment-tar", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-store-composite", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-store-document", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-store-spi", "1.16.0");
-        versionResolver.setVersion("org.apache.jackrabbit", "oak-jackrabbit-api", "1.16.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-api", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-blob", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-blob-plugins", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-commons", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-core", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-core-spi", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-jcr", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-lucene", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-query-spi", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-security-spi", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-segment-tar", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-store-composite", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-store-document", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-store-spi", "1.18.0");
+        versionResolver.setVersion("org.apache.jackrabbit", "oak-jackrabbit-api", "1.18.0");
+        versionResolver.setVersion("commons-codec", "commons-codec", "1.14");
+        versionResolver.setVersion("org.apache.tika", "tika-core", "1.24");
+        versionResolver.setVersion("org.apache.tika", "tika-parsers", "1.24");
 
         // newer version of sling.api and dependencies for SLING-10034
         //   may remove at a later date if the superclass includes these versions or later
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetEaceIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetEaceIT.java
index 9812d13..14bb7b7 100644
--- a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetEaceIT.java
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetEaceIT.java
@@ -21,8 +21,10 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.net.URI;
 import java.util.List;
 
+import javax.json.JsonArray;
 import javax.json.JsonException;
 import javax.json.JsonObject;
 import javax.json.JsonString;
@@ -75,6 +77,11 @@
         assertEquals(1, privilegesObject.size());
         //allow privilege
         assertPrivilege(privilegesObject, true, PrivilegeValues.ALLOW, PrivilegeConstants.JCR_WRITE);
+
+        JsonObject declaredAtObj = aceObject.getJsonObject("declaredAt");
+        assertNotNull(declaredAtObj);
+        String testFolderPath = URI.create(testFolderUrl).getPath();
+        assertEquals(testFolderPath, declaredAtObj.getString("node"));
     }
 
     /**
@@ -205,5 +212,58 @@
                 });
     }
 
+    /**
+     * Verify that when the effective ace is a merge of ACEs in multiple
+     * ancestor nodes, that an array of those node paths is returned in the
+     * declaredAt structure
+     */
+    @Test
+    public void testDeclaredAtArrayInEffectiveAceForUser() throws IOException {
+        testUserId = createTestUser();
+        testFolderUrl = createTestFolder(null, "sling-tests1",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"child\" : { \"childPropOne\" : true } }");
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new AcePostParamsBuilder(testUserId)
+                .withPrivilege(PrivilegeConstants.JCR_WRITE, PrivilegeValues.ALLOW)
+                .build();
+        addOrUpdateAce(testFolderUrl, postParams);
+
+        List<NameValuePair> postParams2 = new AcePostParamsBuilder(testUserId)
+                .withPrivilege(PrivilegeConstants.JCR_READ, PrivilegeValues.ALLOW)
+                .build();
+        addOrUpdateAce(testFolderUrl + "/child", postParams2);
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+
+        //fetch the JSON for the ace to verify the settings.
+        String getUrl = testFolderUrl + "/child.eace.json?pid=" + testUserId;
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject aceObject = parseJson(json);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testUserId, principalString);
+
+        JsonObject privilegesObject = aceObject.getJsonObject("privileges");
+        assertNotNull(privilegesObject);
+        assertEquals(2, privilegesObject.size());
+        //allow privilege
+        assertPrivilege(privilegesObject, true, PrivilegeValues.ALLOW, PrivilegeConstants.JCR_WRITE);
+        assertPrivilege(privilegesObject, true, PrivilegeValues.ALLOW, PrivilegeConstants.JCR_READ);
+
+        JsonObject declaredAtObj = aceObject.getJsonObject("declaredAt");
+        assertNotNull(declaredAtObj);
+        JsonValue nodeObj = declaredAtObj.get("node");
+        assertTrue (nodeObj instanceof JsonArray);
+        JsonArray nodeArray = (JsonArray)nodeObj;
+        assertEquals(2, nodeArray.size());
+        String testFolderPath = URI.create(testFolderUrl).getPath();
+        assertTrue (nodeArray.get(0) instanceof JsonString);
+        assertEquals(testFolderPath + "/child", ((JsonString)nodeArray.get(0)).getString());
+        assertTrue (nodeArray.get(1) instanceof JsonString);
+        assertEquals(testFolderPath, ((JsonString)nodeArray.get(1)).getString());
+    }
 
 }
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetPaceIT.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetPaceIT.java
index 5b80ed0..80e7cc9 100644
--- a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetPaceIT.java
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/GetPaceIT.java
@@ -21,8 +21,10 @@
 import static org.junit.Assert.assertTrue;
 
 import java.io.IOException;
+import java.net.URI;
 import java.util.List;
 
+import javax.json.JsonArray;
 import javax.json.JsonException;
 import javax.json.JsonObject;
 import javax.json.JsonString;
@@ -187,5 +189,58 @@
                 });
     }
 
+    /**
+     * Verify that when the effective ace is a merge of ACEs in multiple
+     * ancestor nodes, that an array of those node paths is returned in the
+     * declaredAt structure
+     */
+    @Test
+    public void testDeclaredAtArrayInEffectiveAceForServiceUser() throws IOException {
+        testFolderUrl = createTestFolder(null, "sling-tests5",
+                "{ \"jcr:primaryType\": \"nt:unstructured\", \"child\" : { \"childPropOne\" : true } }");
+        String testServiceUserId = "pacetestuser";
+
+        //1. create an initial set of privileges
+        List<NameValuePair> postParams = new AcePostParamsBuilder(testServiceUserId)
+                .withPrivilege(PrivilegeConstants.JCR_WRITE, PrivilegeValues.ALLOW)
+                .build();
+        addOrUpdatePrincipalAce(testFolderUrl, postParams);
+
+        List<NameValuePair> postParams2 = new AcePostParamsBuilder(testServiceUserId)
+                .withPrivilege(PrivilegeConstants.JCR_READ, PrivilegeValues.ALLOW)
+                .build();
+        addOrUpdatePrincipalAce(testFolderUrl + "/child", postParams2);
+
+        Credentials creds = new UsernamePasswordCredentials("admin", "admin");
+
+        //fetch the JSON for the ace to verify the settings.
+        String getUrl = testFolderUrl + "/child.eace.json?pid=" + testServiceUserId;
+
+        String json = getAuthenticatedContent(creds, getUrl, CONTENT_TYPE_JSON, HttpServletResponse.SC_OK);
+        assertNotNull(json);
+        JsonObject aceObject = parseJson(json);
+
+        String principalString = aceObject.getString("principal");
+        assertEquals(testServiceUserId, principalString);
+
+        JsonObject privilegesObject = aceObject.getJsonObject("privileges");
+        assertNotNull(privilegesObject);
+        assertEquals(2, privilegesObject.size());
+        //allow privilege
+        assertPrivilege(privilegesObject, true, PrivilegeValues.ALLOW, PrivilegeConstants.JCR_WRITE);
+        assertPrivilege(privilegesObject, true, PrivilegeValues.ALLOW, PrivilegeConstants.JCR_READ);
+
+        JsonObject declaredAtObj = aceObject.getJsonObject("declaredAt");
+        assertNotNull(declaredAtObj);
+        JsonValue nodeObj = declaredAtObj.get("principal");
+        assertTrue (nodeObj instanceof JsonArray);
+        JsonArray nodeArray = (JsonArray)nodeObj;
+        assertEquals(2, nodeArray.size());
+        String testFolderPath = URI.create(testFolderUrl).getPath();
+        assertTrue (nodeArray.get(0) instanceof JsonString);
+        assertEquals(testFolderPath + "/child", ((JsonString)nodeArray.get(0)).getString());
+        assertTrue (nodeArray.get(1) instanceof JsonString);
+        assertEquals(testFolderPath, ((JsonString)nodeArray.get(1)).getString());
+    }
 
 }
diff --git a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/PrincipalAceTestSupport.java b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/PrincipalAceTestSupport.java
index 1412893..4bede5c 100644
--- a/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/PrincipalAceTestSupport.java
+++ b/src/test/java/org/apache/sling/jcr/jackrabbit/accessmanager/it/PrincipalAceTestSupport.java
@@ -44,7 +44,7 @@
     @Override
     protected Option[] additionalOptions() throws IOException {
         return composite(super.additionalOptions())
-           .add(mavenBundle().groupId("org.apache.jackrabbit").artifactId("oak-authorization-principalbased").version("1.16.0"),
+           .add(mavenBundle().groupId("org.apache.jackrabbit").artifactId("oak-authorization-principalbased").version("1.18.0"),
                 newConfiguration("org.apache.jackrabbit.oak.spi.security.authorization.principalbased.impl.PrincipalBasedAuthorizationConfiguration")
                     .put("enableAggregationFilter", true)
                     .asOption(),