SLING-1411 Add replaceAccessControlEntry method to AccessControlUtil
Thanks to Ray Davis for the contribution.

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@916893 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/src/main/java/org/apache/sling/jcr/base/util/AccessControlUtil.java b/src/main/java/org/apache/sling/jcr/base/util/AccessControlUtil.java
index d84a0b7..924880e 100644
--- a/src/main/java/org/apache/sling/jcr/base/util/AccessControlUtil.java
+++ b/src/main/java/org/apache/sling/jcr/base/util/AccessControlUtil.java
@@ -18,10 +18,22 @@
  */
 package org.apache.sling.jcr.base.util;
 
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.principal.PrincipalManager;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.jcr.AccessDeniedException;
 import javax.jcr.RepositoryException;
@@ -31,12 +43,10 @@
 import javax.jcr.security.AccessControlException;
 import javax.jcr.security.AccessControlList;
 import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.AccessControlPolicy;
+import javax.jcr.security.AccessControlPolicyIterator;
 import javax.jcr.security.Privilege;
 
-import org.apache.jackrabbit.api.JackrabbitSession;
-import org.apache.jackrabbit.api.security.principal.PrincipalManager;
-import org.apache.jackrabbit.api.security.user.UserManager;
-
 /**
  * A simple utility class providing utilities with respect to
  * access control over repositories.
@@ -60,7 +70,7 @@
     // the name of the JackrabbitAccessControlEntry method
     private static final String METHOD_JACKRABBIT_ACE_IS_ALLOW = "isAllow";
 
-
+    private static final Logger log = LoggerFactory.getLogger(AccessControlUtil.class);
 
     // ---------- SessionImpl methods -----------------------------------------------------
 
@@ -201,6 +211,141 @@
     	Class[] types = new Class[] {Principal.class, Privilege[].class, boolean.class, Map.class};
 		return safeInvokeRepoMethod(acl, METHOD_JACKRABBIT_ACL_ADD_ENTRY, Boolean.class, args, types);
     }
+    
+    /**
+     * Replaces existing access control entries in the ACL for the specified
+     * <code>principal</code> and <code>resourcePath</code>. Any existing granted
+     * or denied privileges which do not conflict with the specified privileges
+     * are maintained. Where conflicts exist, existing privileges are dropped.
+     * The end result will be at most two ACEs for the principal: one for grants
+     * and one for denies. Aggregate privileges are disaggregated before checking
+     * for conflicts.
+     * @param session
+     * @param resourcePath
+     * @param principal
+     * @param grantedPrivilegeNames
+     * @param deniedPrivilegeNames
+     * @param removedPrivilegeNames privileges which, if they exist, should be
+     * removed for this principal and resource
+     * @throws RepositoryException
+     */
+    public static void replaceAccessControlEntry(Session session, String resourcePath, Principal principal, 
+    			String[] grantedPrivilegeNames, String[] deniedPrivilegeNames, String[] removedPrivilegeNames)
+        		throws RepositoryException {
+    	AccessControlManager accessControlManager = getAccessControlManager(session);
+    	Set<String> specifiedPrivilegeNames = new HashSet<String>();
+    	Set<String> newGrantedPrivilegeNames = disaggregateToPrivilegeNames(accessControlManager, grantedPrivilegeNames, specifiedPrivilegeNames);
+    	Set<String> newDeniedPrivilegeNames = disaggregateToPrivilegeNames(accessControlManager, deniedPrivilegeNames, specifiedPrivilegeNames);
+    	disaggregateToPrivilegeNames(accessControlManager, removedPrivilegeNames, specifiedPrivilegeNames);
+
+    	// Get or create the ACL for the node.
+    	AccessControlList acl = null;
+    	AccessControlPolicy[] policies = accessControlManager.getPolicies(resourcePath);
+    	for (AccessControlPolicy policy : policies) {
+    		if (policy instanceof AccessControlList) {
+    			acl = (AccessControlList) policy;
+    			break;
+    		}
+    	}
+    	if (acl == null) {
+    		AccessControlPolicyIterator applicablePolicies = accessControlManager.getApplicablePolicies(resourcePath);
+    		while (applicablePolicies.hasNext()) {
+    			AccessControlPolicy policy = applicablePolicies.nextAccessControlPolicy();
+    			if (policy instanceof AccessControlList) {
+    				acl = (AccessControlList) policy;
+    				break;
+    			}
+    		}
+    	}
+    	if (acl == null) {
+    		throw new RepositoryException("Could not obtain ACL for resource " + resourcePath);
+    	}
+    	// Used only for logging.
+    	Set<Privilege> oldGrants = null;
+    	Set<Privilege> oldDenies = null;
+    	if (log.isDebugEnabled()) {
+    		oldGrants = new HashSet<Privilege>();
+    		oldDenies = new HashSet<Privilege>();
+    	}
+      
+    	// Combine all existing ACEs for the target principal.
+    	AccessControlEntry[] accessControlEntries = acl.getAccessControlEntries();
+    	for (AccessControlEntry ace : accessControlEntries) {
+    		if (principal.equals(ace.getPrincipal())) {
+    			if (log.isDebugEnabled()) {
+    				log.debug("Found Existing ACE for principal {} on resource {}", new Object[] {principal.getName(), resourcePath});
+    			}
+    			boolean isAllow = isAllow(ace);
+    			Privilege[] privileges = ace.getPrivileges();
+    			if (log.isDebugEnabled()) {
+    				if (isAllow) {
+    					oldGrants.addAll(Arrays.asList(privileges));
+    				} else {
+    					oldDenies.addAll(Arrays.asList(privileges));
+    				}
+    			}
+    			for (Privilege privilege : privileges) {
+    				Set<String> maintainedPrivileges = disaggregateToPrivilegeNames(privilege);
+    				// If there is any overlap with the newly specified privileges, then
+    				// break the existing privilege down; otherwise, maintain as is.
+    				if (!maintainedPrivileges.removeAll(specifiedPrivilegeNames)) {
+    					// No conflicts, so preserve the original.
+    					maintainedPrivileges.clear();
+    					maintainedPrivileges.add(privilege.getName());
+    				}
+    				if (!maintainedPrivileges.isEmpty()) {
+    					if (isAllow) {
+    						newGrantedPrivilegeNames.addAll(maintainedPrivileges);
+    					} else {
+    						newDeniedPrivilegeNames.addAll(maintainedPrivileges);
+    					}
+    				}
+    			}
+    			// Remove the old ACE.
+    			acl.removeAccessControlEntry(ace);
+    		}
+    	}
+
+    	//add a fresh ACE with the granted privileges
+    	List<Privilege> grantedPrivilegeList = new ArrayList<Privilege>();
+    	for (String name : newGrantedPrivilegeNames) {
+    		Privilege privilege = accessControlManager.privilegeFromName(name);
+    		grantedPrivilegeList.add(privilege);
+    	}
+    	if (grantedPrivilegeList.size() > 0) {
+    		acl.addAccessControlEntry(principal, grantedPrivilegeList.toArray(new Privilege[grantedPrivilegeList.size()]));
+    	}
+
+    	//if the authorizable is a user (not a group) process any denied privileges
+    	UserManager userManager = getUserManager(session);
+    	Authorizable authorizable = userManager.getAuthorizable(principal);
+    	if (!authorizable.isGroup()) {
+    		//add a fresh ACE with the denied privileges
+    		List<Privilege> deniedPrivilegeList = new ArrayList<Privilege>();
+    		for (String name : newDeniedPrivilegeNames) {
+    			Privilege privilege = accessControlManager.privilegeFromName(name);
+    			deniedPrivilegeList.add(privilege);
+    		}        
+    		if (deniedPrivilegeList.size() > 0) {
+    			addEntry(acl, principal, deniedPrivilegeList.toArray(new Privilege[deniedPrivilegeList.size()]), false);
+    		}
+    	}
+
+    	accessControlManager.setPolicy(resourcePath, acl);
+    	if (log.isDebugEnabled()) {
+    		List<String> oldGrantedNames = new ArrayList<String>(oldGrants.size());
+    		for (Privilege privilege : oldGrants) {
+    			oldGrantedNames.add(privilege.getName());
+    		}
+    		List<String> oldDeniedNames = new ArrayList<String>(oldDenies.size());
+    		for (Privilege privilege : oldDenies) {
+    			oldDeniedNames.add(privilege.getName());
+    		}
+    		log.debug("Updated ACE for principalId {} for resource {} from grants {}, denies {} to grants {}, denies {}", new Object [] {
+    				authorizable.getID(), resourcePath, oldGrantedNames, oldDeniedNames, newGrantedPrivilegeNames, newDeniedPrivilegeNames
+    			});
+    	}
+	}
 
     // ---------- AccessControlEntry methods -----------------------------------------------
 
@@ -264,4 +409,40 @@
 		else
 			return null;
 	}
+  
+	/**
+	 * Helper routine to transform an input array of privilege names into a set in
+	 * a null-safe way while also adding its disaggregated privileges to an input set.
+	 */
+	private static Set<String> disaggregateToPrivilegeNames(AccessControlManager accessControlManager, 
+			String[] privilegeNames, Set<String> disaggregatedPrivilegeNames)
+      throws RepositoryException {
+		Set<String> originalPrivilegeNames = new HashSet<String>();
+		if (privilegeNames != null) {
+			for (String privilegeName : privilegeNames) {
+				originalPrivilegeNames.add(privilegeName);
+				Privilege privilege = accessControlManager.privilegeFromName(privilegeName);
+				disaggregatedPrivilegeNames.addAll(disaggregateToPrivilegeNames(privilege));
+			}
+		}
+		return originalPrivilegeNames;
+	}
+
+	/**
+	 * Transform an aggregated privilege into a set of disaggregated privilege
+	 * names. If the privilege is not an aggregate, the set will contain the
+	 * original name.
+	 */
+	private static Set<String> disaggregateToPrivilegeNames(Privilege privilege) {
+		Set<String> disaggregatedPrivilegeNames = new HashSet<String>();
+		if (privilege.isAggregate()) {
+			Privilege[] privileges = privilege.getAggregatePrivileges();
+			for (Privilege disaggregate : privileges) {
+				disaggregatedPrivilegeNames.add(disaggregate.getName());
+			}
+		} else {
+			disaggregatedPrivilegeNames.add(privilege.getName());
+		}
+		return disaggregatedPrivilegeNames;
+	}
 }