001/*
002 * (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Antoine Taillefer <ataillefer@nuxeo.com>
018 */
019package org.nuxeo.drive.listener;
020
021import java.util.ArrayList;
022import java.util.List;
023import java.util.stream.Collectors;
024
025import org.apache.logging.log4j.LogManager;
026import org.apache.logging.log4j.Logger;
027import org.nuxeo.drive.service.NuxeoDriveEvents;
028import org.nuxeo.ecm.core.api.CoreInstance;
029import org.nuxeo.ecm.core.api.CoreSession;
030import org.nuxeo.ecm.core.api.DocumentModel;
031import org.nuxeo.ecm.core.api.DocumentModelList;
032import org.nuxeo.ecm.core.api.SystemPrincipal;
033import org.nuxeo.ecm.core.api.repository.RepositoryManager;
034import org.nuxeo.ecm.core.event.Event;
035import org.nuxeo.ecm.core.event.EventBundle;
036import org.nuxeo.ecm.core.event.EventContext;
037import org.nuxeo.ecm.core.event.EventService;
038import org.nuxeo.ecm.core.event.PostCommitFilteringEventListener;
039import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
040import org.nuxeo.ecm.core.query.sql.NXQL;
041import org.nuxeo.ecm.platform.usermanager.UserManager;
042import org.nuxeo.ecm.platform.usermanager.UserManagerImpl;
043import org.nuxeo.runtime.api.Framework;
044import org.nuxeo.runtime.api.login.LoginComponent;
045
046/**
047 * Post-commit asynchronous listener that handles group change events fired by the {@link UserManager}.
048 * <p>
049 * For all the documents carrying an ACL impacted by a changed group or one of its ancestors it fires the
050 * {@link NuxeoDriveEvents#GROUP_UPDATED} event that is handled by the synchronous
051 * {@link NuxeoDriveFileSystemDeletionListener}.
052 *
053 * @since 9.2
054 */
055public class NuxeoDriveGroupUpdateListener implements PostCommitFilteringEventListener {
056
057    protected static final Logger log = LogManager.getLogger(NuxeoDriveGroupUpdateListener.class);
058
059    @Override
060    public boolean acceptEvent(Event event) {
061        return event.getContext() != null && UserManagerImpl.USER_GROUP_CATEGORY.equals(
062                event.getContext().getProperty(DocumentEventContext.CATEGORY_PROPERTY_KEY));
063    }
064
065    @Override
066    public void handleEvent(EventBundle events) {
067        for (Event event : events) {
068            EventContext context = event.getContext();
069            if (context == null) {
070                continue;
071            }
072            String groupName = (String) context.getProperty(UserManagerImpl.ID_PROPERTY_KEY);
073            if (groupName == null) {
074                continue;
075            }
076            log.debug("NuxeoDriveGroupUpdateListener handling {} event for group {}", event::getName, () -> groupName);
077            List<String> groupNames = getAllGroupNames(groupName, context);
078            handleUpdatedGroups(groupNames);
079        }
080    }
081
082    /**
083     * Returns a list containing the names of the given group and all its ancestor groups.
084     */
085    @SuppressWarnings("unchecked")
086    protected List<String> getAllGroupNames(String groupName, EventContext context) {
087        List<String> groupNames = new ArrayList<>();
088        groupNames.add(groupName);
089        // Get ancestor groups from the event context or compute them if not provided
090        // and do it as system user in the local thread to access group directory
091        LoginComponent.pushPrincipal(new SystemPrincipal(null));
092        try {
093            List<String> ancestorGroups = (List<String>) context.getProperty(
094                    UserManagerImpl.ANCESTOR_GROUPS_PROPERTY_KEY);
095            if (ancestorGroups != null) {
096                groupNames.addAll(ancestorGroups);
097            } else {
098                groupNames.addAll(Framework.getService(UserManager.class).getAncestorGroups(groupName));
099            }
100        } finally {
101            LoginComponent.popPrincipal();
102        }
103        return groupNames;
104    }
105
106    protected void handleUpdatedGroups(List<String> groupNames) {
107        RepositoryManager repositoryManager = Framework.getService(RepositoryManager.class);
108        for (String repositoryName : repositoryManager.getRepositoryNames()) {
109            CoreInstance.doPrivileged(repositoryName, (CoreSession session) -> {
110                DocumentModelList impactedDocuments = getImpactedDocuments(session, groupNames);
111                impactedDocuments.forEach(doc -> fireGroupUpdatedEvent(session, doc));
112            });
113        }
114    }
115
116    /**
117     * Returns the list of documents carrying an ACL impacted by one of the given group names.
118     */
119    protected DocumentModelList getImpactedDocuments(CoreSession session, List<String> groupNames) {
120        String groups = groupNames.stream().map(NXQL::escapeString).collect(Collectors.joining(","));
121        String query = "SELECT * FROM Document WHERE ecm:isTrashed = 0 AND ecm:isVersion = 0 AND ecm:acl/*/principal IN ("
122                + groups + ")";
123        return session.query(query);
124    }
125
126    protected void fireGroupUpdatedEvent(CoreSession session, DocumentModel source) {
127        EventContext context = new DocumentEventContext(session, session.getPrincipal(), source);
128        Event event = context.newEvent(NuxeoDriveEvents.GROUP_UPDATED);
129        Framework.getService(EventService.class).fireEvent(event);
130    }
131
132}