001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Thomas Roger
016 */
017
018package org.nuxeo.ecm.permissions;
019
020import static org.nuxeo.ecm.core.api.event.CoreEventConstants.CHANGED_ACL_NAME;
021import static org.nuxeo.ecm.core.api.event.CoreEventConstants.NEW_ACE;
022import static org.nuxeo.ecm.core.api.event.CoreEventConstants.NEW_ACP;
023import static org.nuxeo.ecm.core.api.event.CoreEventConstants.OLD_ACE;
024import static org.nuxeo.ecm.core.api.event.CoreEventConstants.OLD_ACP;
025import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_SECURITY_UPDATED;
026import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_DIRECTORY;
027import static org.nuxeo.ecm.permissions.Constants.ACE_KEY;
028import static org.nuxeo.ecm.permissions.Constants.ACL_NAME_KEY;
029import static org.nuxeo.ecm.permissions.Constants.COMMENT_KEY;
030import static org.nuxeo.ecm.permissions.Constants.NOTIFY_KEY;
031import static org.nuxeo.ecm.permissions.Constants.PERMISSION_NOTIFICATION_EVENT;
032import static org.nuxeo.ecm.permissions.PermissionHelper.computeDirectoryId;
033
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.List;
037import java.util.Map;
038
039import org.nuxeo.ecm.core.api.DocumentModel;
040import org.nuxeo.ecm.core.api.security.ACE;
041import org.nuxeo.ecm.core.api.security.ACL;
042import org.nuxeo.ecm.core.api.security.ACP;
043import org.nuxeo.ecm.core.event.Event;
044import org.nuxeo.ecm.core.event.EventContext;
045import org.nuxeo.ecm.core.event.EventListener;
046import org.nuxeo.ecm.core.event.EventService;
047import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
048import org.nuxeo.ecm.directory.Session;
049import org.nuxeo.ecm.directory.api.DirectoryService;
050import org.nuxeo.runtime.api.Framework;
051
052/**
053 * Listener filling the 'aceinfo' directory when an ACP is updated.
054 *
055 * @since 7.4
056 */
057public class PermissionListener implements EventListener {
058
059    @Override
060    public void handleEvent(Event event) {
061        EventContext ctx = event.getContext();
062        if (!(ctx instanceof DocumentEventContext)) {
063            return;
064        }
065
066        if (DOCUMENT_SECURITY_UPDATED.equals(event.getName())) {
067            updateDirectory((DocumentEventContext) ctx);
068        }
069    }
070
071    protected void updateDirectory(DocumentEventContext docCtx) {
072        ACE oldACE = (ACE) docCtx.getProperty(OLD_ACE);
073        ACE newACE = (ACE) docCtx.getProperty(NEW_ACE);
074        String changedACLName = (String) docCtx.getProperty(CHANGED_ACL_NAME);
075        if (oldACE != null && newACE != null && changedACLName != null) {
076            handleReplaceACE(docCtx, changedACLName, oldACE, newACE);
077        } else {
078            ACP oldACP = (ACP) docCtx.getProperty(OLD_ACP);
079            ACP newACP = (ACP) docCtx.getProperty(NEW_ACP);
080            if (oldACP != null && newACP != null) {
081                handleUpdateACP(docCtx, oldACP, newACP);
082            }
083        }
084    }
085
086    protected void handleUpdateACP(DocumentEventContext docCtx, ACP oldACP, ACP newACP) {
087        DocumentModel doc = docCtx.getSourceDocument();
088
089        List<ACLDiff> aclDiffs = extractACLDiffs(oldACP, newACP);
090        DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
091        for (ACLDiff diff : aclDiffs) {
092            try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
093                for (ACE ace : diff.removedACEs) {
094                    String id = computeDirectoryId(doc, diff.aclName, ace.getId());
095                    session.deleteEntry(id);
096                }
097
098                for (ACE ace : diff.addedACEs) {
099                    String id = computeDirectoryId(doc, diff.aclName, ace.getId());
100                    // remove it if it exists
101                    if (session.hasEntry(id)) {
102                        session.deleteEntry(id);
103                    }
104
105                    Boolean notify = (Boolean) ace.getContextData(NOTIFY_KEY);
106                    String comment = (String) ace.getContextData(Constants.COMMENT_KEY);
107                    notify = notify != null ? notify : false;
108                    Map<String, Object> m = PermissionHelper.createDirectoryEntry(doc, diff.aclName, ace, notify,
109                            comment);
110                    session.createEntry(m);
111
112                    if (notify && ace.isGranted() && ace.isEffective()) {
113                        firePermissionNotificationEvent(docCtx, diff.aclName, ace);
114                    }
115                }
116            }
117        }
118    }
119
120    protected void handleReplaceACE(DocumentEventContext docCtx, String changedACLName, ACE oldACE, ACE newACE) {
121        DocumentModel doc = docCtx.getSourceDocument();
122
123        DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
124        try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
125            Boolean notify = (Boolean) newACE.getContextData(NOTIFY_KEY);
126            String comment = (String) newACE.getContextData(COMMENT_KEY);
127
128            String oldId = computeDirectoryId(doc, changedACLName, oldACE.getId());
129            DocumentModel oldEntry = session.getEntry(oldId);
130            if (oldEntry != null) {
131                // remove the old entry
132                session.deleteEntry(oldId);
133            }
134
135            // add the new entry
136            notify = notify != null ? notify : false;
137            Map<String, Object> m = PermissionHelper.createDirectoryEntry(doc, changedACLName, newACE, notify, comment);
138            session.createEntry(m);
139
140            if (notify && newACE.isGranted() && newACE.isEffective()) {
141                firePermissionNotificationEvent(docCtx, changedACLName, newACE);
142            }
143        }
144    }
145
146    protected List<ACLDiff> extractACLDiffs(ACP oldACP, ACP newACP) {
147        List<ACLDiff> aclDiffs = new ArrayList<>();
148
149        List<String> oldACLNames = toACLNames(oldACP);
150        List<String> newACLNames = toACLNames(newACP);
151        List<String> addedACLNames = toACLNames(newACP);
152        List<String> removedACLNames = toACLNames(oldACP);
153
154        addedACLNames.removeAll(oldACLNames);
155        removedACLNames.removeAll(newACLNames);
156
157        for (String name : addedACLNames) {
158            aclDiffs.add(new ACLDiff(name, new ArrayList<>(newACP.getACL(name)), null));
159        }
160
161        for (String name : removedACLNames) {
162            aclDiffs.add(new ACLDiff(name, null, new ArrayList<>(oldACP.getACL(name))));
163        }
164
165        for (ACL newACL : newACP.getACLs()) {
166            ACL oldACL = oldACP.getACL(newACL.getName());
167            if (oldACL != null) {
168                List<ACE> addedACEs = new ArrayList<>(newACL);
169                List<ACE> removedACEs = new ArrayList<>(oldACL);
170
171                addedACEs.removeAll(oldACL);
172                removedACEs.removeAll(newACL);
173                aclDiffs.add(new ACLDiff(newACL.getName(), addedACEs, removedACEs));
174            }
175        }
176        return aclDiffs;
177    }
178
179    protected List<String> toACLNames(ACP acp) {
180        List<String> aclNames = new ArrayList<>();
181        for (ACL acl : acp.getACLs()) {
182            aclNames.add(acl.getName());
183        }
184        return aclNames;
185    }
186
187    protected void firePermissionNotificationEvent(DocumentEventContext docCtx, String aclName, ACE ace) {
188        docCtx.setProperty(ACE_KEY, ace);
189        docCtx.setProperty(ACL_NAME_KEY, aclName);
190        EventService eventService = Framework.getService(EventService.class);
191        eventService.fireEvent(PERMISSION_NOTIFICATION_EVENT, docCtx);
192
193    }
194
195    private static class ACLDiff {
196        public final String aclName;
197
198        public final List<ACE> addedACEs;
199
200        public final List<ACE> removedACEs;
201
202        private ACLDiff(String aclName, List<ACE> addedACEs, List<ACE> removedACEs) {
203            this.aclName = aclName;
204            this.addedACEs = addedACEs != null ? addedACEs : Collections.emptyList();
205            this.removedACEs = removedACEs != null ? removedACEs : Collections.emptyList();
206        }
207    }
208}