/* * 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.base.util; 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; import javax.jcr.Session; import javax.jcr.UnsupportedRepositoryOperationException; import javax.jcr.security.AccessControlEntry; 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.JackrabbitAccessControlList; import org.apache.jackrabbit.api.security.principal.PrincipalManager; import org.apache.jackrabbit.api.security.user.UserManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A simple utility class providing utilities with respect to * access control over repositories. */ public class AccessControlUtil { // the name of the accessor method for the AccessControlManager private static final String METHOD_GET_ACCESS_CONTROL_MANAGER = "getAccessControlManager"; // the name of the accessor method for the UserManager private static final String METHOD_GET_USER_MANAGER = "getUserManager"; // the name of the accessor method for the PrincipalManager private static final String METHOD_GET_PRINCIPAL_MANAGER = "getPrincipalManager"; // the name of the JackrabbitAccessControlList method getPath private static final String METHOD_JACKRABBIT_ACL_GET_PATH = "getPath"; // the name of the JackrabbitAccessControlList method private static final String METHOD_JACKRABBIT_ACL_IS_EMPTY = "isEmpty"; // the name of the JackrabbitAccessControlList method private static final String METHOD_JACKRABBIT_ACL_SIZE = "size"; // the name of the JackrabbitAccessControlList method private static final String METHOD_JACKRABBIT_ACL_ADD_ENTRY = "addEntry"; // 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 ----------------------------------------------------- /** * Returns the <code>AccessControlManager</code> for the given * <code>session</code>. If the session does not have a * <code>getAccessControlManager</code> method, a * <code>UnsupportedRepositoryOperationException</code> is thrown. Otherwise * the <code>AccessControlManager</code> is returned or if the call fails, * the respective exception is thrown. * * @param session The JCR Session whose <code>AccessControlManager</code> is * to be returned. If the session is a pooled session, the * session underlying the pooled session is actually used. * @return The <code>AccessControlManager</code> of the session * @throws UnsupportedRepositoryOperationException If the session has no * <code>getAccessControlManager</code> method or the exception * thrown by the method. * @throws RepositoryException Forwarded from the * <code>getAccessControlManager</code> method call. */ public static AccessControlManager getAccessControlManager(Session session) throws UnsupportedRepositoryOperationException, RepositoryException { return safeInvokeRepoMethod(session, METHOD_GET_ACCESS_CONTROL_MANAGER, AccessControlManager.class); } // ---------- JackrabbitSession methods ----------------------------------------------- /** * Returns the <code>UserManager</code> for the given * <code>session</code>. If the session does not have a * <code>getUserManager</code> method, a * <code>UnsupportedRepositoryOperationException</code> is thrown. Otherwise * the <code>UserManager</code> is returned or if the call fails, * the respective exception is thrown. * * @param session The JCR Session whose <code>UserManager</code> is * to be returned. If the session is not a <code>JackrabbitSession</code> * uses reflection to retrive the manager from the repository. * @return The <code>UserManager</code> of the session. * @throws AccessDeniedException If this session is not allowed * to access user data. * @throws UnsupportedRepositoryOperationException If the session has no * <code>getUserManager</code> method or the exception * thrown by the method. * @throws RepositoryException Forwarded from the * <code>getUserManager</code> method call. */ public static UserManager getUserManager(Session session) throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { JackrabbitSession jackrabbitSession = getJackrabbitSession(session); if(jackrabbitSession != null) { return jackrabbitSession.getUserManager(); } else { return safeInvokeRepoMethod(session, METHOD_GET_USER_MANAGER, UserManager.class); } } /** * Returns the <code>PrincipalManager</code> for the given * <code>session</code>. If the session does not have a * <code>PrincipalManager</code> method, a * <code>UnsupportedRepositoryOperationException</code> is thrown. Otherwise * the <code>PrincipalManager</code> is returned or if the call fails, * the respective exception is thrown. * * @param session The JCR Session whose <code>PrincipalManager</code> is * to be returned. If the session is not a <code>JackrabbitSession</code> * uses reflection to retrive the manager from the repository. * @return The <code>PrincipalManager</code> of the session. * @throws AccessDeniedException * @throws UnsupportedRepositoryOperationException If the session has no * <code>PrincipalManager</code> method or the exception * thrown by the method. * @throws RepositoryException Forwarded from the * <code>PrincipalManager</code> method call. */ public static PrincipalManager getPrincipalManager(Session session) throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException { JackrabbitSession jackrabbitSession = getJackrabbitSession(session); if(jackrabbitSession != null) { return jackrabbitSession.getPrincipalManager(); } else { return safeInvokeRepoMethod(session, METHOD_GET_PRINCIPAL_MANAGER, PrincipalManager.class); } } // ---------- AccessControlList methods ----------------------------------------------- /** * Returns the path of the node <code>AccessControlList</code> acl * has been created for. */ public static String getPath(AccessControlList acl) throws RepositoryException { return safeInvokeRepoMethod(acl, METHOD_JACKRABBIT_ACL_GET_PATH, String.class); } /** * Returns <code>true</code> if <code>AccessControlList</code> acl * does not yet define any entries. */ public static boolean isEmpty(AccessControlList acl) throws RepositoryException { return safeInvokeRepoMethod(acl, METHOD_JACKRABBIT_ACL_IS_EMPTY, Boolean.class); } /** * Returns the number of acl entries or 0 if the acl is empty. */ public static int size(AccessControlList acl) throws RepositoryException { return safeInvokeRepoMethod(acl, METHOD_JACKRABBIT_ACL_SIZE, Integer.class); } /** * Same as {@link #addEntry(AccessControlList, Principal, Privilege[], boolean, Map)} using * some implementation specific restrictions. */ public static boolean addEntry(AccessControlList acl, Principal principal, Privilege privileges[], boolean isAllow) throws AccessControlException, RepositoryException { Object[] args = new Object[] {principal, privileges, isAllow}; @SuppressWarnings("rawtypes") Class[] types = new Class[] {Principal.class, Privilege[].class, boolean.class}; return safeInvokeRepoMethod(acl, METHOD_JACKRABBIT_ACL_ADD_ENTRY, Boolean.class, args, types); } /** * Adds an access control entry to the acl consisting of the specified * <code>principal</code>, the specified <code>privileges</code>, the * <code>isAllow</code> flag and an optional map containing additional * restrictions. * <p/> * This method returns <code>true</code> if this policy was modified, * <code>false</code> otherwise. */ public static boolean addEntry(AccessControlList acl, Principal principal, Privilege privileges[], boolean isAllow, @SuppressWarnings("rawtypes") Map restrictions) throws UnsupportedRepositoryOperationException, RepositoryException { Object[] args = new Object[] {principal, privileges, isAllow, restrictions}; @SuppressWarnings("rawtypes") 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 * @deprecated use @link {@link #replaceAccessControlEntry(Session, String, Principal, String[], String[], String[], String)} instead. */ @Deprecated public static void replaceAccessControlEntry(Session session, String resourcePath, Principal principal, String[] grantedPrivilegeNames, String[] deniedPrivilegeNames, String[] removedPrivilegeNames) throws RepositoryException { replaceAccessControlEntry(session, resourcePath, principal, grantedPrivilegeNames, deniedPrivilegeNames, removedPrivilegeNames, null); } /** * 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 * @param order where the access control entry should go in the list. * Value should be one of these: * <table> * <tr><td>null</td><td>If the ACE for the principal doesn't exist add at the end, otherwise leave the ACE at it's current position.</td></tr> * <tr><td>first</td><td>Place the target ACE as the first amongst its siblings</td></tr> * <tr><td>last</td><td>Place the target ACE as the last amongst its siblings</td></tr> * <tr><td>before xyz</td><td>Place the target ACE immediately before the sibling whose name is xyz</td></tr> * <tr><td>after xyz</td><td>Place the target ACE immediately after the sibling whose name is xyz</td></tr> * <tr><td>numeric</td><td>Place the target ACE at the specified numeric index</td></tr> * </table> * @throws RepositoryException */ public static void replaceAccessControlEntry(Session session, String resourcePath, Principal principal, String[] grantedPrivilegeNames, String[] deniedPrivilegeNames, String[] removedPrivilegeNames, String order) 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 (int i=0; i < accessControlEntries.length; i++) { AccessControlEntry ace = accessControlEntries[i]; if (principal.equals(ace.getPrincipal())) { if (log.isDebugEnabled()) { log.debug("Found Existing ACE for principal {} on resource {}", new Object[] {principal.getName(), resourcePath}); } if (order == null || order.length() == 0) { //order not specified, so keep track of the original ACE position. order = String.valueOf(i); } 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()])); } //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); } //order the ACL reorderAccessControlEntries(acl, principal, order); 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 principalName {} for resource {} from grants {}, denies {} to grants {}, denies {}", new Object [] { principal.getName(), resourcePath, oldGrantedNames, oldDeniedNames, newGrantedPrivilegeNames, newDeniedPrivilegeNames }); } } // ---------- AccessControlEntry methods ----------------------------------------------- /** * Returns true if the AccessControlEntry represents 'allowed' rights or false * it it represents 'denied' rights. */ public static boolean isAllow(AccessControlEntry ace) throws RepositoryException { return safeInvokeRepoMethod(ace, METHOD_JACKRABBIT_ACE_IS_ALLOW, Boolean.class); } // ---------- internal ----------------------------------------------------- /** * Use reflection to invoke a repository method. */ @SuppressWarnings("unchecked") private static <T> T safeInvokeRepoMethod(Object target, String methodName, Class<T> returnType, Object[] args, @SuppressWarnings("rawtypes") Class[] argsTypes) throws UnsupportedRepositoryOperationException, RepositoryException { try { Method m = target.getClass().getMethod(methodName, argsTypes); if (!m.isAccessible()) { m.setAccessible(true); } return (T) m.invoke(target, args); } catch (InvocationTargetException ite) { // wraps the exception thrown by the method Throwable t = ite.getCause(); if (t instanceof UnsupportedRepositoryOperationException) { throw (UnsupportedRepositoryOperationException) t; } else if (t instanceof AccessDeniedException) { throw (AccessDeniedException) t; } else if (t instanceof AccessControlException) { throw (AccessControlException) t; } else if (t instanceof RepositoryException) { throw (RepositoryException) t; } else if (t instanceof RuntimeException) { throw (RuntimeException) t; } else if (t instanceof Error) { throw (Error) t; } else { throw new RepositoryException(methodName, t); } } catch (Throwable t) { // any other problem is just encapsulated throw new RepositoryException(methodName, t); } } private static <T> T safeInvokeRepoMethod(Object target, String methodName, Class<T> returnType, Object... args) throws UnsupportedRepositoryOperationException, RepositoryException { return safeInvokeRepoMethod(target, methodName, returnType, args, new Class[0]); } /** * Unwrap the jackrabbit session. */ private static JackrabbitSession getJackrabbitSession(Session session) { if (session instanceof JackrabbitSession) return (JackrabbitSession) session; 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) { if (disaggregate.isAggregate()) { continue; //nested aggregate, so skip it since the privileges are already included. } disaggregatedPrivilegeNames.add(disaggregate.getName()); } } else { disaggregatedPrivilegeNames.add(privilege.getName()); } return disaggregatedPrivilegeNames; } /** * Move the ACE(s) for the specified principal to the position specified by the 'order' * parameter. * * @param acl the acl of the node containing the ACE to position * @param principal the user or group of the ACE to position * @param order where the access control entry should go in the list. * Value should be one of these: * <table> * <tr><td>first</td><td>Place the target ACE as the first amongst its siblings</td></tr> * <tr><td>last</td><td>Place the target ACE as the last amongst its siblings</td></tr> * <tr><td>before xyz</td><td>Place the target ACE immediately before the sibling whose name is xyz</td></tr> * <tr><td>after xyz</td><td>Place the target ACE immediately after the sibling whose name is xyz</td></tr> * <tr><td>numeric</td><td>Place the target ACE at the specified index</td></tr> * </table> * @throws RepositoryException * @throws UnsupportedRepositoryOperationException * @throws AccessControlException */ private static void reorderAccessControlEntries(AccessControlList acl, Principal principal, String order) throws RepositoryException { if (order == null || order.length() == 0) { return; //nothing to do } if (acl instanceof JackrabbitAccessControlList) { JackrabbitAccessControlList jacl = (JackrabbitAccessControlList)acl; AccessControlEntry[] accessControlEntries = jacl.getAccessControlEntries(); if (accessControlEntries.length <= 1) { return; //only one ACE, so nothing to reorder. } AccessControlEntry beforeEntry = null; if ("first".equals(order)) { beforeEntry = accessControlEntries[0]; } else if ("last".equals(order)) { beforeEntry = null; } else if (order.startsWith("before ")) { String beforePrincipalName = order.substring(7); //find the index of the ACE of the 'before' principal for (int i=0; i < accessControlEntries.length; i++) { if (beforePrincipalName.equals(accessControlEntries[i].getPrincipal().getName())) { //found it! beforeEntry = accessControlEntries[i]; break; } } if (beforeEntry == null) { //didn't find an ACE that matched the 'before' principal throw new IllegalArgumentException("No ACE was found for the specified principal: " + beforePrincipalName); } } else if (order.startsWith("after ")) { String afterPrincipalName = order.substring(6); //find the index of the ACE of the 'after' principal for (int i = accessControlEntries.length - 1; i >= 0; i--) { if (afterPrincipalName.equals(accessControlEntries[i].getPrincipal().getName())) { //found it! // the 'before' ACE is the next one after the 'after' ACE if (i >= accessControlEntries.length - 1) { //the after is the last one in the list beforeEntry = null; } else { beforeEntry = accessControlEntries[i + 1]; } break; } } if (beforeEntry == null) { //didn't find an ACE that matched the 'after' principal throw new IllegalArgumentException("No ACE was found for the specified principal: " + afterPrincipalName); } } else { try { int index = Integer.parseInt(order); if (index > accessControlEntries.length) { //invalid index throw new IndexOutOfBoundsException("Index value is too large: " + index); } if (index == 0) { beforeEntry = accessControlEntries[0]; } else { //the index value is the index of the principal. A principal may have more // than one ACEs (deny + grant), so we need to compensate. Set<Principal> processedPrincipals = new HashSet<Principal>(); for (int i = 0; i < accessControlEntries.length; i++) { Principal principal2 = accessControlEntries[i].getPrincipal(); if (processedPrincipals.size() == index && !processedPrincipals.contains(principal2)) { //we are now at the correct position in the list beforeEntry = accessControlEntries[i]; break; } processedPrincipals.add(principal2); } } } catch (NumberFormatException nfe) { //not a number. throw new IllegalArgumentException("Illegal value for the order parameter: " + order); } } //now loop through the entries to move the affected ACEs to the specified // position. for (int i = accessControlEntries.length - 1; i >= 0; i--) { AccessControlEntry ace = accessControlEntries[i]; if (principal.equals(ace.getPrincipal())) { //this ACE is for the specified principal. jacl.orderBefore(ace, beforeEntry); } } } else { throw new IllegalArgumentException("The acl must be an instance of JackrabbitAccessControlList"); } } }