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.local.ClientLoginModule;
034import org.nuxeo.ecm.core.api.local.LoginStack;
035import org.nuxeo.ecm.core.api.repository.RepositoryManager;
036import org.nuxeo.ecm.core.event.Event;
037import org.nuxeo.ecm.core.event.EventBundle;
038import org.nuxeo.ecm.core.event.EventContext;
039import org.nuxeo.ecm.core.event.EventService;
040import org.nuxeo.ecm.core.event.PostCommitFilteringEventListener;
041import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
042import org.nuxeo.ecm.core.query.sql.NXQL;
043import org.nuxeo.ecm.platform.usermanager.UserManager;
044import org.nuxeo.ecm.platform.usermanager.UserManagerImpl;
045import org.nuxeo.runtime.api.Framework;
046
047/**
048 * Post-commit asynchronous listener that handles group change events fired by the {@link UserManager}.
049 * <p>
050 * For all the documents carrying an ACL impacted by a changed group or one of its ancestors it fires the
051 * {@link NuxeoDriveEvents#GROUP_UPDATED} event that is handled by the synchronous
052 * {@link NuxeoDriveFileSystemDeletionListener}.
053 *
054 * @since 9.2
055 */
056public class NuxeoDriveGroupUpdateListener implements PostCommitFilteringEventListener {
057
058    protected static final Logger log = LogManager.getLogger(NuxeoDriveGroupUpdateListener.class);
059
060    @Override
061    public boolean acceptEvent(Event event) {
062        return event.getContext() != null && UserManagerImpl.USER_GROUP_CATEGORY.equals(
063                event.getContext().getProperty(DocumentEventContext.CATEGORY_PROPERTY_KEY));
064    }
065
066    @Override
067    public void handleEvent(EventBundle events) {
068        for (Event event : events) {
069            EventContext context = event.getContext();
070            if (context == null) {
071                continue;
072            }
073            String groupName = (String) context.getProperty(UserManagerImpl.ID_PROPERTY_KEY);
074            if (groupName == null) {
075                continue;
076            }
077            log.debug("NuxeoDriveGroupUpdateListener handling {} event for group {}", event::getName, () -> groupName);
078            List<String> groupNames = getAllGroupNames(groupName, context);
079            handleUpdatedGroups(groupNames);
080        }
081    }
082
083    /**
084     * Returns a list containing the names of the given group and all its ancestor groups.
085     */
086    @SuppressWarnings("unchecked")
087    protected List<String> getAllGroupNames(String groupName, EventContext context) {
088        List<String> groupNames = new ArrayList<>();
089        groupNames.add(groupName);
090        // Get ancestor groups from the event context or compute them if not provided
091        // and do it as system user in the local thread to access group directory
092        LoginStack loginStack = ClientLoginModule.getThreadLocalLogin();
093        loginStack.push(new SystemPrincipal(null), null, null);
094        try {
095            List<String> ancestorGroups = (List<String>) context.getProperty(
096                    UserManagerImpl.ANCESTOR_GROUPS_PROPERTY_KEY);
097            if (ancestorGroups != null) {
098                groupNames.addAll(ancestorGroups);
099            } else {
100                groupNames.addAll(Framework.getService(UserManager.class).getAncestorGroups(groupName));
101            }
102        } finally {
103            loginStack.pop();
104        }
105        return groupNames;
106    }
107
108    protected void handleUpdatedGroups(List<String> groupNames) {
109        RepositoryManager repositoryManager = Framework.getService(RepositoryManager.class);
110        for (String repositoryName : repositoryManager.getRepositoryNames()) {
111            CoreInstance.doPrivileged(repositoryName, (CoreSession session) -> {
112                DocumentModelList impactedDocuments = getImpactedDocuments(session, groupNames);
113                impactedDocuments.forEach(doc -> fireGroupUpdatedEvent(session, doc));
114            });
115        }
116    }
117
118    /**
119     * Returns the list of documents carrying an ACL impacted by one of the given group names.
120     */
121    protected DocumentModelList getImpactedDocuments(CoreSession session, List<String> groupNames) {
122        String groups = groupNames.stream().map(NXQL::escapeString).collect(Collectors.joining(","));
123        String query = "SELECT * FROM Document WHERE ecm:isTrashed = 0 AND ecm:isVersion = 0 AND ecm:acl/*/principal IN ("
124                + groups + ")";
125        return session.query(query);
126    }
127
128    protected void fireGroupUpdatedEvent(CoreSession session, DocumentModel source) {
129        EventContext context = new DocumentEventContext(session, session.getPrincipal(), source);
130        Event event = context.newEvent(NuxeoDriveEvents.GROUP_UPDATED);
131        Framework.getService(EventService.class).fireEvent(event);
132    }
133
134}