/*
* (C) Copyright 2016 Netcentric AG.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package biz.netcentric.cq.tools.actool.aceinstaller;
import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
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.AccessControlManager;
import javax.jcr.security.Privilege;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Service;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.oak.spi.security.principal.PrincipalImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.day.cq.security.util.CqActions;
import biz.netcentric.cq.tools.actool.configmodel.AceBean;
import biz.netcentric.cq.tools.actool.helper.AccessControlUtils;
import biz.netcentric.cq.tools.actool.helper.RestrictionsHolder;
import biz.netcentric.cq.tools.actool.history.AcInstallationLog;
/** The way ACEs were installed in version one is still available and can be configured in "global_config" section by setting
* "installAclsIncrementally=false". */
@Service
@Component
public class AceBeanInstallerClassic extends BaseAceBeanInstaller implements AceBeanInstaller {
private static final Logger LOG = LoggerFactory.getLogger(AceBeanInstallerClassic.class);
/** Installs a full set of ACE beans that form an ACL for the path
*
* @throws RepositoryException */
protected void installAcl(Set<AceBean> aceBeanSetFromConfig, String path, Set<String> principalsToRemoveAcesFor, Session session,
AcInstallationLog installLog) throws RepositoryException {
// Remove all config contained authorizables from ACL of this path
int countRemoved = AccessControlUtils.deleteAllEntriesForPrincipalsFromACL(session,
path, principalsToRemoveAcesFor.toArray(new String[principalsToRemoveAcesFor.size()]));
installLog.addVerboseMessage(LOG, "Deleted " + countRemoved + " ACEs for configured principals from path " + path);
// Set ACL in repo with permissions from merged config
for (final AceBean bean : aceBeanSetFromConfig) {
LOG.debug("Writing bean to repository {}", bean);
Principal currentPrincipal = new PrincipalImpl(bean.getPrincipalName());
installAce(bean, session, currentPrincipal, installLog);
}
installLog.incCountAclsChanged();
}
/** Installs the AccessControlEntry being represented by this bean in the repository
*
* @throws NoSuchMethodException */
private void installAce(AceBean aceBean, final Session session, Principal principal,
AcInstallationLog installLog) throws RepositoryException {
if (aceBean.isInitialContentOnlyConfig()) {
return;
}
final AccessControlManager acMgr = session.getAccessControlManager();
JackrabbitAccessControlList acl = AccessControlUtils.getModifiableAcl(acMgr, aceBean.getJcrPathForPolicyApi());
if (acl == null) {
installLog.addMessage(LOG, "Skipped installing privileges/actions for non existing path: " + aceBean.getJcrPath());
return;
}
// first install actions
final JackrabbitAccessControlList newAcl = installActions(aceBean, principal, acl, session, acMgr, installLog);
if (acl != newAcl) {
installLog.addVerboseMessage(LOG, "Added action(s) for path: " + aceBean.getJcrPath()
+ ", principal: " + principal.getName() + ", actions: "
+ aceBean.getActionsString() + ", allow: " + aceBean.isAllow());
removeRedundantPrivileges(aceBean, session);
acl = newAcl;
}
// then install (remaining) privileges
if (installPrivileges(aceBean, principal, acl, session, acMgr)) {
installLog.addVerboseMessage(LOG, "Added privilege(s) for path: " + aceBean.getJcrPath()
+ ", principal: " + principal.getName() + ", privileges: "
+ aceBean.getPrivilegesString() + ", allow: " + aceBean.isAllow());
}
if (!acl.isEmpty()) {
acMgr.setPolicy(aceBean.getJcrPathForPolicyApi(), acl);
} else {
acMgr.removePolicy(aceBean.getJcrPathForPolicyApi(), acl);
}
}
/** Installs the CQ actions in the repository.
*
* @return either the same acl as given in the parameter {@code acl} if no actions have been installed otherwise the new
* AccessControlList (comprising the entres being installed for the actions).
* @throws RepositoryException */
private JackrabbitAccessControlList installActions(AceBean aceBean, Principal principal, JackrabbitAccessControlList acl,
Session session, AccessControlManager acMgr, AcInstallationLog installLog) throws RepositoryException {
final Map<String, Boolean> actionMap = aceBean.getActionMap();
if (actionMap.isEmpty()) {
return acl;
}
final CqActions cqActions = new CqActions(session);
final Collection<String> inheritedAllows = cqActions.getAllowedActions(
aceBean.getJcrPathForPolicyApi(), Collections.singleton(principal));
// this does always install new entries
cqActions.installActions(aceBean.getJcrPathForPolicyApi(), principal, actionMap, inheritedAllows);
// since the aclist has been modified, retrieve it again
final JackrabbitAccessControlList newAcl = AccessControlUtils.getAccessControlList(session, aceBean.getJcrPath());
final RestrictionsHolder restrictions = getRestrictions(aceBean, session, acl);
if (!aceBean.getRestrictions().isEmpty()) {
// additionally set restrictions on the installed actions (this is not supported by CQ Security API)
addAdditionalRestriction(aceBean, acl, newAcl, restrictions);
}
return newAcl;
}
private void addAdditionalRestriction(AceBean aceBean, JackrabbitAccessControlList oldAcl, JackrabbitAccessControlList newAcl,
RestrictionsHolder restrictions)
throws RepositoryException {
final List<AccessControlEntry> changedAces = getModifiedAces(oldAcl, newAcl);
if (!changedAces.isEmpty()) {
for (final AccessControlEntry newAce : changedAces) {
addRestrictionIfNotSet(newAcl, restrictions, newAce);
}
} else {
// check cornercase: yaml file contains 2 ACEs with same action same principal same path but one with additional restriction
// (e.g. read and repGlob: '')
// in that case old and new acl contain the same elements (equals == true) and in both lists the last ace contains the action
// without restriction
// for that group
final AccessControlEntry lastOldAce = oldAcl.getAccessControlEntries()[oldAcl.getAccessControlEntries().length - 1];
final AccessControlEntry lastNewAce = newAcl.getAccessControlEntries()[newAcl.getAccessControlEntries().length - 1];
if (lastOldAce.equals(lastNewAce) && lastNewAce.getPrincipal().getName().equals(aceBean.getPrincipalName())) {
addRestrictionIfNotSet(newAcl, restrictions, lastNewAce);
} else {
throw new IllegalStateException("No new entries have been set for AccessControlList at " + aceBean.getJcrPath());
}
}
}
private void addRestrictionIfNotSet(JackrabbitAccessControlList newAcl, RestrictionsHolder restrictions,
AccessControlEntry newAce)
throws RepositoryException, AccessControlException, UnsupportedRepositoryOperationException, SecurityException {
if (!(newAce instanceof JackrabbitAccessControlEntry)) {
throw new IllegalStateException(
"Can not deal with non JackrabbitAccessControlEntrys, but entry is of type " + newAce.getClass().getName());
}
final JackrabbitAccessControlEntry ace = (JackrabbitAccessControlEntry) newAce;
// only extend those AccessControlEntries which do not yet have a restriction
if (ace.getRestrictionNames().length == 0) {
// modify this AccessControlEntry by adding the restriction
extendExistingAceWithRestrictions(newAcl, ace, restrictions);
}
}
@SuppressWarnings("unchecked")
private List<AccessControlEntry> getModifiedAces(final JackrabbitAccessControlList oldAcl, JackrabbitAccessControlList newAcl)
throws RepositoryException {
final List<AccessControlEntry> oldAces = Arrays.asList(oldAcl.getAccessControlEntries());
final List<AccessControlEntry> newAces = Arrays.asList(newAcl.getAccessControlEntries());
return (List<AccessControlEntry>) CollectionUtils.subtract(newAces, oldAces);
}
private void removeRedundantPrivileges(AceBean aceBean, Session session) throws RepositoryException {
final Set<String> cleanedPrivileges = removeRedundantPrivileges(session, aceBean.getPrivileges(), aceBean.getActions());
aceBean.setPrivilegesString(StringUtils.join(cleanedPrivileges, ","));
}
/** Modifies the privileges so that privileges already covered by actions are removed. This is only a best effort operation as one
* action can lead to privileges on multiple nodes.
*
* @throws RepositoryException */
private static Set<String> removeRedundantPrivileges(Session session, String[] privileges, String[] actions)
throws RepositoryException {
final CqActions cqActions = new CqActions(session);
final Set<String> cleanedPrivileges = new HashSet<String>();
if (privileges == null) {
return cleanedPrivileges;
}
cleanedPrivileges.addAll(Arrays.asList(privileges));
if (actions == null) {
return cleanedPrivileges;
}
for (final String action : actions) {
@SuppressWarnings("deprecation")
final Set<Privilege> coveredPrivileges = cqActions.getPrivileges(action);
for (final Privilege coveredPrivilege : coveredPrivileges) {
cleanedPrivileges.remove(coveredPrivilege.getName());
}
}
return cleanedPrivileges;
}
private void extendExistingAceWithRestrictions(JackrabbitAccessControlList accessControlList,
JackrabbitAccessControlEntry accessControlEntry, RestrictionsHolder restrictions)
throws SecurityException, UnsupportedRepositoryOperationException, RepositoryException {
// 1. add new entry
if (!accessControlList.addEntry(accessControlEntry.getPrincipal(), accessControlEntry.getPrivileges(), accessControlEntry.isAllow(),
restrictions.getSingleValuedRestrictionsMap(), restrictions.getMultiValuedRestrictionsMap())) {
throw new IllegalStateException("Could not add entry, probably because it was already there!");
}
// we assume the entry being added is the last one
final AccessControlEntry newAccessControlEntry = accessControlList.getAccessControlEntries()[accessControlList.size() - 1];
// 2. put it to the right position now!
accessControlList.orderBefore(newAccessControlEntry, accessControlEntry);
// 3. remove old entry
accessControlList.removeAccessControlEntry(accessControlEntry);
}
}