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