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_COMMENT;
027import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_DIRECTORY;
028import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_NOTIFIED;
029import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_NOTIFY;
030import static org.nuxeo.ecm.permissions.Constants.ACE_KEY;
031import static org.nuxeo.ecm.permissions.Constants.ACL_NAME_KEY;
032import static org.nuxeo.ecm.permissions.Constants.COMMENT_KEY;
033import static org.nuxeo.ecm.permissions.Constants.NOTIFY_KEY;
034import static org.nuxeo.ecm.permissions.Constants.PERMISSION_NOTIFICATION_EVENT;
035import static org.nuxeo.ecm.permissions.PermissionHelper.computeDirectoryId;
036
037import java.util.ArrayList;
038import java.util.Collections;
039import java.util.List;
040import java.util.Map;
041
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.security.ACE;
044import org.nuxeo.ecm.core.api.security.ACL;
045import org.nuxeo.ecm.core.api.security.ACP;
046import org.nuxeo.ecm.core.event.Event;
047import org.nuxeo.ecm.core.event.EventContext;
048import org.nuxeo.ecm.core.event.EventListener;
049import org.nuxeo.ecm.core.event.EventService;
050import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
051import org.nuxeo.ecm.directory.Session;
052import org.nuxeo.ecm.directory.api.DirectoryService;
053import org.nuxeo.runtime.api.Framework;
054
055/**
056 * Listener filling the 'aceinfo' directory when an ACP is updated.
057 *
058 * @since 7.4
059 */
060public class PermissionListener implements EventListener {
061
062    @Override
063    public void handleEvent(Event event) {
064        EventContext ctx = event.getContext();
065        if (!(ctx instanceof DocumentEventContext)) {
066            return;
067        }
068
069        if (DOCUMENT_SECURITY_UPDATED.equals(event.getName())) {
070            updateDirectory((DocumentEventContext) ctx);
071        }
072    }
073
074    protected void updateDirectory(DocumentEventContext docCtx) {
075        ACE oldACE = (ACE) docCtx.getProperty(OLD_ACE);
076        ACE newACE = (ACE) docCtx.getProperty(NEW_ACE);
077        String changedACLName = (String) docCtx.getProperty(CHANGED_ACL_NAME);
078        if (oldACE != null && newACE != null && changedACLName != null) {
079            handleReplaceACE(docCtx, changedACLName, oldACE, newACE);
080        } else {
081            ACP oldACP = (ACP) docCtx.getProperty(OLD_ACP);
082            ACP newACP = (ACP) docCtx.getProperty(NEW_ACP);
083            if (oldACP != null && newACP != null) {
084                handleUpdateACP(docCtx, oldACP, newACP);
085            }
086        }
087    }
088
089    protected void handleUpdateACP(DocumentEventContext docCtx, ACP oldACP, ACP newACP) {
090        DocumentModel doc = docCtx.getSourceDocument();
091
092        List<ACLDiff> aclDiffs = extractACLDiffs(oldACP, newACP);
093        DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
094        for (ACLDiff diff : aclDiffs) {
095            try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
096                for (ACE ace : diff.removedACEs) {
097                    String id = computeDirectoryId(doc, diff.aclName, ace.getId());
098                    session.deleteEntry(id);
099                }
100
101                for (ACE ace : diff.addedACEs) {
102                    String id = computeDirectoryId(doc, diff.aclName, ace.getId());
103                    // remove it if it exists
104                    if (session.hasEntry(id)) {
105                        session.deleteEntry(id);
106                    }
107
108                    Boolean notify = (Boolean) ace.getContextData(NOTIFY_KEY);
109                    String comment = (String) ace.getContextData(Constants.COMMENT_KEY);
110                    notify = notify != null ? notify : false;
111                    Map<String, Object> m = PermissionHelper.createDirectoryEntry(doc, diff.aclName, ace, notify, false,
112                            comment);
113                    session.createEntry(m);
114
115                    if (notify && ace.isGranted() && ace.isEffective()) {
116                        firePermissionNotificationEvent(docCtx, diff.aclName, ace);
117                    }
118                }
119            }
120        }
121    }
122
123    protected void handleReplaceACE(DocumentEventContext docCtx, String changedACLName, ACE oldACE, ACE newACE) {
124        DocumentModel doc = docCtx.getSourceDocument();
125
126        DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
127        try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
128            Boolean notify = (Boolean) newACE.getContextData(NOTIFY_KEY);
129            String comment = (String) newACE.getContextData(COMMENT_KEY);
130            boolean notified = false;
131
132            String oldId = computeDirectoryId(doc, changedACLName, oldACE.getId());
133            DocumentModel oldEntry = session.getEntry(oldId);
134            if (oldEntry != null) {
135                boolean oldNotified = (boolean) oldEntry.getPropertyValue(ACE_INFO_NOTIFIED);
136                boolean oldNotify = (boolean) oldEntry.getPropertyValue(ACE_INFO_NOTIFY);
137                String oldComment = (String) oldEntry.getPropertyValue(ACE_INFO_COMMENT);
138
139                // put back notified to false if notify has changed
140                notified = !(notify != null && notify != oldNotify) && oldNotified;
141                // only use old notify and comment if not updated
142                if (notify == null) {
143                    notify = oldNotify;
144                }
145                if (comment == null) {
146                    comment = oldComment;
147                }
148
149                // remove the old entry
150                session.deleteEntry(oldId);
151            }
152
153            // add the new entry
154            notify = notify != null ? notify : false;
155            Map<String, Object> m = PermissionHelper.createDirectoryEntry(doc, changedACLName, newACE, notify, notified,
156                    comment);
157            session.createEntry(m);
158
159            if (notify && newACE.isGranted() && newACE.isEffective() && !notified) {
160                firePermissionNotificationEvent(docCtx, changedACLName, newACE);
161            }
162        }
163    }
164
165    protected List<ACLDiff> extractACLDiffs(ACP oldACP, ACP newACP) {
166        List<ACLDiff> aclDiffs = new ArrayList<>();
167
168        List<String> oldACLNames = toACLNames(oldACP);
169        List<String> newACLNames = toACLNames(newACP);
170        List<String> addedACLNames = toACLNames(newACP);
171        List<String> removedACLNames = toACLNames(oldACP);
172
173        addedACLNames.removeAll(oldACLNames);
174        removedACLNames.removeAll(newACLNames);
175
176        for (String name : addedACLNames) {
177            aclDiffs.add(new ACLDiff(name, new ArrayList<>(newACP.getACL(name)), null));
178        }
179
180        for (String name : removedACLNames) {
181            aclDiffs.add(new ACLDiff(name, null, new ArrayList<>(oldACP.getACL(name))));
182        }
183
184        for (ACL newACL : newACP.getACLs()) {
185            ACL oldACL = oldACP.getACL(newACL.getName());
186            if (oldACL != null) {
187                List<ACE> addedACEs = new ArrayList<>(newACL);
188                List<ACE> removedACEs = new ArrayList<>(oldACL);
189
190                addedACEs.removeAll(oldACL);
191                removedACEs.removeAll(newACL);
192                aclDiffs.add(new ACLDiff(newACL.getName(), addedACEs, removedACEs));
193            }
194        }
195        return aclDiffs;
196    }
197
198    protected List<String> toACLNames(ACP acp) {
199        List<String> aclNames = new ArrayList<>();
200        for (ACL acl : acp.getACLs()) {
201            aclNames.add(acl.getName());
202        }
203        return aclNames;
204    }
205
206    protected void firePermissionNotificationEvent(DocumentEventContext docCtx, String aclName, ACE ace) {
207        docCtx.setProperty(ACE_KEY, ace);
208        docCtx.setProperty(ACL_NAME_KEY, aclName);
209        EventService eventService = Framework.getService(EventService.class);
210        eventService.fireEvent(PERMISSION_NOTIFICATION_EVENT, docCtx);
211
212    }
213
214    private static class ACLDiff {
215        public final String aclName;
216
217        public final List<ACE> addedACEs;
218
219        public final List<ACE> removedACEs;
220
221        private ACLDiff(String aclName, List<ACE> addedACEs, List<ACE> removedACEs) {
222            this.aclName = aclName;
223            this.addedACEs = addedACEs != null ? addedACEs : Collections.emptyList();
224            this.removedACEs = removedACEs != null ? removedACEs : Collections.emptyList();
225        }
226    }
227}