biz.netcentric.cq.tools.actool.acls.AceBeanInstallerImpl.java Source code

Java tutorial

Introduction

Here is the source code for biz.netcentric.cq.tools.actool.acls.AceBeanInstallerImpl.java

Source

/*
 * (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.acls;

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 java.util.TreeSet;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.ValueFormatException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.day.cq.security.util.CqActions;

import biz.netcentric.cq.tools.actool.comparators.AcePermissionComparator;
import biz.netcentric.cq.tools.actool.configmodel.AceBean;
import biz.netcentric.cq.tools.actool.configmodel.Restriction;
import biz.netcentric.cq.tools.actool.helper.AcHelper;
import biz.netcentric.cq.tools.actool.helper.AccessControlUtils;
import biz.netcentric.cq.tools.actool.helper.ContentHelper;
import biz.netcentric.cq.tools.actool.helper.RestrictionsHolder;
import biz.netcentric.cq.tools.actool.installationhistory.AcInstallationHistoryPojo;

@Service
@Component
public class AceBeanInstallerImpl implements AceBeanInstaller {

    private static final Logger LOG = LoggerFactory.getLogger(AceBeanInstallerImpl.class);

    @Override
    public void installPathBasedACEs(final Map<String, Set<AceBean>> pathBasedAceMapFromConfig,
            final Session session, final AcInstallationHistoryPojo history,
            Set<String> authorizablesToRemoveAcesFor, boolean intermediateSaves) throws Exception {

        final Set<String> paths = pathBasedAceMapFromConfig.keySet();

        final String msg = "Found " + paths.size() + "  paths in config";
        LOG.debug(msg);
        history.addVerboseMessage(msg);
        LOG.trace("Paths with ACEs: {}", paths);

        if (intermediateSaves) {
            final String messageSave = "Will save ACL for each path to session due to configuration option intermediateSaves=true - rollback functionality is disabled.";
            LOG.info(messageSave);
            history.addMessage(messageSave);
        }

        // loop through all nodes from config
        for (final String path : paths) {

            final Set<AceBean> aceBeanSetFromConfig = pathBasedAceMapFromConfig.get(path); // Set which holds the AceBeans of the current path in configuration

            // check if the path even exists
            final boolean pathExits = AccessControlUtils.getModifiableAcl(session.getAccessControlManager(),
                    path) != null;
            if (!pathExits) {
                if (!ContentHelper.createInitialContent(session, history, path, aceBeanSetFromConfig)) {
                    final String msgNonExistingPath = "Skipped installing privileges/actions for non existing path: "
                            + path;
                    LOG.debug(msgNonExistingPath);
                    history.addMessage(msgNonExistingPath);
                    continue;
                }
            }

            // order entries (denies in front of allows)
            final Set<AceBean> orderedAceBeanSetFromConfig = new TreeSet<AceBean>(new AcePermissionComparator());
            orderedAceBeanSetFromConfig.addAll(aceBeanSetFromConfig);

            // Remove all config contained auhtorizables from ACL of this path
            int countRemoved = AccessControlUtils.deleteAllEntriesForAuthorizableFromACL(session, path,
                    authorizablesToRemoveAcesFor.toArray(new String[authorizablesToRemoveAcesFor.size()]));
            final String message = "Deleted " + countRemoved + " ACEs for configured authorizables from path "
                    + path;
            LOG.debug(message);
            history.addVerboseMessage(message);

            writeAcBeansToRepository(session, history, orderedAceBeanSetFromConfig);

            if (intermediateSaves) {
                final String messageSave = "Saved session for path " + path;
                LOG.debug(messageSave);
                history.addVerboseMessage(messageSave);
                session.save();
            }
        }
    }

    private void writeAcBeansToRepository(final Session session, final AcInstallationHistoryPojo history,
            final Set<AceBean> aceBeanSetFromConfig) throws RepositoryException,
            UnsupportedRepositoryOperationException, NoSuchMethodException, SecurityException {

        // reset ACL in repo with permissions from merged ACL
        for (final AceBean bean : aceBeanSetFromConfig) {

            LOG.debug("Writing bean to repository {}", bean);

            final Principal currentPrincipal = AcHelper.getPrincipal(session, bean);

            if (currentPrincipal == null) {
                final String errMessage = "Could not find definition for authorizable " + bean.getPrincipalName()
                        + " in groups config while installing ACE for: " + bean.getJcrPath()
                        + "! Skipped installation of ACEs for this authorizable!\n";
                LOG.error(errMessage);
                history.addError(errMessage);
                continue;

            } else {
                history.addVerboseMessage("starting installation of bean: \n" + bean);
                install(bean, session, currentPrincipal, history);
            }
        }
    }

    /** Installs the CQ actions in the repository.
     *
     * @param aceBean
     * @param principal
     * @param acl
     * @param session
     * @param acMgr
     * @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
     * @throws SecurityException
     * @throws NoSuchMethodException */
    private JackrabbitAccessControlList installActions(AceBean aceBean, Principal principal,
            JackrabbitAccessControlList acl, Session session, AccessControlManager acMgr,
            AcInstallationHistoryPojo history) throws RepositoryException, SecurityException {
        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.getJcrPath(),
                Collections.singleton(principal));
        // this does always install new entries
        cqActions.installActions(aceBean.getJcrPath(), 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,
            AccessControlException, UnsupportedRepositoryOperationException, SecurityException {
        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 boolean installPrivileges(AceBean aceBean, Principal principal, JackrabbitAccessControlList acl,
            Session session, AccessControlManager acMgr) throws RepositoryException {
        // then install remaining privileges
        final Set<Privilege> privileges = AccessControlUtils.getPrivilegeSet(aceBean.getPrivileges(), acMgr);
        if (!privileges.isEmpty()) {
            final RestrictionsHolder restrictions = getRestrictions(aceBean, session, acl);
            if (!restrictions.isEmpty()) {
                acl.addEntry(principal, privileges.toArray(new Privilege[privileges.size()]), aceBean.isAllow(),
                        restrictions.getSingleValuedRestrictionsMap(),
                        restrictions.getMultiValuedRestrictionsMap());
            } else {
                acl.addEntry(principal, privileges.toArray(new Privilege[privileges.size()]), aceBean.isAllow());
            }
            return true;
        }
        return false;
    }

    /** Installs the AccessControlEntry being represented by this bean in the repository
     *
     * @throws SecurityException
     * @throws NoSuchMethodException */
    private void install(AceBean aceBean, final Session session, Principal principal,
            AcInstallationHistoryPojo history) throws RepositoryException, SecurityException {

        if (aceBean.isInitialContentOnlyConfig()) {
            return;
        }

        final AccessControlManager acMgr = session.getAccessControlManager();

        JackrabbitAccessControlList acl = AccessControlUtils.getModifiableAcl(acMgr, aceBean.getJcrPath());
        if (acl == null) {
            final String msg = "Skipped installing privileges/actions for non existing path: "
                    + aceBean.getJcrPath();
            LOG.debug(msg);
            history.addMessage(msg);
            return;
        }

        // first install actions
        final JackrabbitAccessControlList newAcl = installActions(aceBean, principal, acl, session, acMgr, history);
        if (acl != newAcl) {
            history.addVerboseMessage(
                    "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)) {
            history.addVerboseMessage(
                    "added privilege(s) for path: " + aceBean.getJcrPath() + ", principal: " + principal.getName()
                            + ", privileges: " + aceBean.getPrivilegesString() + ", allow: " + aceBean.isAllow());
        }
        acMgr.setPolicy(aceBean.getJcrPath(), acl);
    }

    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;
    }

    /** Creates a RestrictionHolder object containing 2 restriction maps being used in
     * {@link JackrabbitAccessControlList#addEntry(Principal, Privilege[], boolean, Map, Map)} out of the set actions on this bean.
     *
     * @param session the session
     * @param acl the access control list for which this restriction map should be used
     * @return RestrictionMapsHolder containing 2 maps with restriction names as keys and restriction values as values
     *         (singleValuedRestrictionsMap) and values[] (multiValuedRestrictionsMap).
     * @throws ValueFormatException
     * @throws UnsupportedRepositoryOperationException
     * @throws RepositoryException */
    private RestrictionsHolder getRestrictions(AceBean aceBean, Session session, JackrabbitAccessControlList acl)
            throws ValueFormatException, UnsupportedRepositoryOperationException, RepositoryException {

        final Collection<String> supportedRestrictionNames = Arrays.asList(acl.getRestrictionNames());

        if (aceBean.getRestrictions().isEmpty()) {
            return RestrictionsHolder.empty();
        }

        List<Restriction> restrictions = aceBean.getRestrictions();
        for (Restriction restriction : restrictions) {
            if (!supportedRestrictionNames.contains(restriction.getName())) {
                throw new IllegalStateException("The AccessControlList at " + acl.getPath()
                        + " does not support setting " + restriction.getName() + " restrictions!");
            }
        }

        RestrictionsHolder restrictionsHolder = new RestrictionsHolder(restrictions, session.getValueFactory(),
                acl);
        return restrictionsHolder;
    }

    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);
    }

}