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.commons.logging.Log;
026import org.apache.commons.logging.LogFactory;
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 Log log = LogFactory.getLog(NuxeoDriveFileSystemDeletionListener.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            if (log.isDebugEnabled()) {
078                log.debug(String.format("NuxeoDriveGroupUpdateListener handling %s event for group %s", event.getName(),
079                        groupName));
080            }
081            List<String> groupNames = getAllGroupNames(groupName, context);
082            handleUpdatedGroups(groupNames);
083        }
084    }
085
086    /**
087     * Returns a list containing the names of the given group and all its ancestor groups.
088     */
089    @SuppressWarnings("unchecked")
090    protected List<String> getAllGroupNames(String groupName, EventContext context) {
091        List<String> groupNames = new ArrayList<>();
092        groupNames.add(groupName);
093        // Get ancestor groups from the event context or compute them if not provided
094        // and do it as system user in the local thread to access group directory
095        LoginStack loginStack = ClientLoginModule.getThreadLocalLogin();
096        loginStack.push(new SystemPrincipal(null), null, null);
097        try {
098            List<String> ancestorGroups = (List<String>) context.getProperty(
099                    UserManagerImpl.ANCESTOR_GROUPS_PROPERTY_KEY);
100            if (ancestorGroups != null) {
101                groupNames.addAll(ancestorGroups);
102            } else {
103                groupNames.addAll(Framework.getService(UserManager.class).getAncestorGroups(groupName));
104            }
105        } finally {
106            loginStack.pop();
107        }
108        return groupNames;
109    }
110
111    protected void handleUpdatedGroups(List<String> groupNames) {
112        RepositoryManager repositoryManager = Framework.getService(RepositoryManager.class);
113        for (String repositoryName : repositoryManager.getRepositoryNames()) {
114            CoreInstance.doPrivileged(repositoryName, (CoreSession session) -> {
115                DocumentModelList impactedDocuments = getImpactedDocuments(session, groupNames);
116                impactedDocuments.forEach(doc -> fireGroupUpdatedEvent(session, doc));
117            });
118        }
119    }
120
121    /**
122     * Returns the list of documents carrying an ACL impacted by one of the given group names.
123     */
124    protected DocumentModelList getImpactedDocuments(CoreSession session, List<String> groupNames) {
125        String groups = groupNames.stream().map(NXQL::escapeString).collect(Collectors.joining(","));
126        String query = "SELECT * FROM Document WHERE ecm:isTrashed = 0 AND ecm:isVersion = 0 AND ecm:acl/*/principal IN ("
127                + groups + ")";
128        return session.query(query);
129    }
130
131    protected void fireGroupUpdatedEvent(CoreSession session, DocumentModel source) {
132        EventContext context = new DocumentEventContext(session, session.getPrincipal(), source);
133        Event event = context.newEvent(NuxeoDriveEvents.GROUP_UPDATED);
134        Framework.getService(EventService.class).fireEvent(event);
135    }
136
137}