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 javax.security.auth.login.LoginContext;
039import javax.security.auth.login.LoginException;
040
041import org.nuxeo.ecm.core.api.DocumentModel;
042import org.nuxeo.ecm.core.api.NuxeoException;
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        ACP oldACP = (ACP) docCtx.getProperty(OLD_ACP);
076        ACP newACP = (ACP) docCtx.getProperty(NEW_ACP);
077        if (oldACP != null && newACP != null) {
078            handleUpdateACP(docCtx, oldACP, newACP);
079        }
080    }
081
082    protected void doAsSystemUser(Runnable runnable) {
083        LoginContext loginContext;
084        try {
085            loginContext = Framework.login();
086        } catch (LoginException e) {
087            throw new NuxeoException(e);
088        }
089
090        try {
091            runnable.run();
092        } finally {
093            try {
094                // Login context may be null in tests
095                if (loginContext != null) {
096                    loginContext.logout();
097                }
098            } catch (LoginException e) {
099                throw new NuxeoException("Cannot log out system user", e);
100            }
101        }
102    }
103
104    protected void handleUpdateACP(DocumentEventContext docCtx, ACP oldACP, ACP newACP) {
105        doAsSystemUser(() -> {
106            DocumentModel doc = docCtx.getSourceDocument();
107            List<ACLDiff> aclDiffs = extractACLDiffs(oldACP, newACP);
108            DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
109            for (ACLDiff diff : aclDiffs) {
110                try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
111                    for (ACE ace : diff.removedACEs) {
112                        String id = computeDirectoryId(doc, diff.aclName, ace.getId());
113                        session.deleteEntry(id);
114
115                        removeToken(doc, ace);
116                    }
117
118                    for (ACE ace : diff.addedACEs) {
119                        String id = computeDirectoryId(doc, diff.aclName, ace.getId());
120                        // remove it if it exists
121                        if (session.hasEntry(id)) {
122                            session.deleteEntry(id);
123                        }
124
125                        Boolean notify = (Boolean) ace.getContextData(NOTIFY_KEY);
126                        String comment = (String) ace.getContextData(Constants.COMMENT_KEY);
127                        notify = notify != null ? notify : false;
128                        Map<String, Object> m = PermissionHelper.createDirectoryEntry(doc, diff.aclName, ace, notify,
129                                comment);
130                        session.createEntry(m);
131
132                        addToken(doc, ace);
133
134                        if (notify && ace.isGranted() && ace.isEffective()) {
135                            firePermissionNotificationEvent(docCtx, diff.aclName, ace);
136                        }
137                    }
138                }
139            }
140        });
141    }
142
143    /**
144     * @deprecated since 8.1. Not used anymore.
145     */
146    @Deprecated
147    protected void handleReplaceACE(DocumentEventContext docCtx, String changedACLName, ACE oldACE, ACE newACE) {
148        doAsSystemUser(() -> {
149            DocumentModel doc = docCtx.getSourceDocument();
150
151            DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
152            try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
153                Boolean notify = (Boolean) newACE.getContextData(NOTIFY_KEY);
154                String comment = (String) newACE.getContextData(COMMENT_KEY);
155
156                String oldId = computeDirectoryId(doc, changedACLName, oldACE.getId());
157                DocumentModel oldEntry = session.getEntry(oldId);
158                if (oldEntry != null) {
159                    // remove the old entry
160                    session.deleteEntry(oldId);
161                }
162
163                // add the new entry
164                notify = notify != null ? notify : false;
165                Map<String, Object> m = PermissionHelper.createDirectoryEntry(doc, changedACLName, newACE, notify,
166                        comment);
167                session.createEntry(m);
168
169                if (notify && newACE.isGranted() && newACE.isEffective()) {
170                    firePermissionNotificationEvent(docCtx, changedACLName, newACE);
171                }
172            }
173        });
174    }
175
176    protected List<ACLDiff> extractACLDiffs(ACP oldACP, ACP newACP) {
177        List<ACLDiff> aclDiffs = new ArrayList<>();
178
179        List<String> oldACLNames = toACLNames(oldACP);
180        List<String> newACLNames = toACLNames(newACP);
181        List<String> addedACLNames = toACLNames(newACP);
182        List<String> removedACLNames = toACLNames(oldACP);
183
184        addedACLNames.removeAll(oldACLNames);
185        removedACLNames.removeAll(newACLNames);
186
187        for (String name : addedACLNames) {
188            aclDiffs.add(new ACLDiff(name, new ArrayList<>(newACP.getACL(name)), null));
189        }
190
191        for (String name : removedACLNames) {
192            aclDiffs.add(new ACLDiff(name, null, new ArrayList<>(oldACP.getACL(name))));
193        }
194
195        for (ACL newACL : newACP.getACLs()) {
196            ACL oldACL = oldACP.getACL(newACL.getName());
197            if (oldACL != null) {
198                List<ACE> addedACEs = new ArrayList<>(newACL);
199                List<ACE> removedACEs = new ArrayList<>(oldACL);
200
201                addedACEs.removeAll(oldACL);
202                removedACEs.removeAll(newACL);
203                aclDiffs.add(new ACLDiff(newACL.getName(), addedACEs, removedACEs));
204            }
205        }
206        return aclDiffs;
207    }
208
209    protected List<String> toACLNames(ACP acp) {
210        List<String> aclNames = new ArrayList<>();
211        for (ACL acl : acp.getACLs()) {
212            aclNames.add(acl.getName());
213        }
214        return aclNames;
215    }
216
217    protected void firePermissionNotificationEvent(DocumentEventContext docCtx, String aclName, ACE ace) {
218        docCtx.setProperty(ACE_KEY, ace);
219        docCtx.setProperty(ACL_NAME_KEY, aclName);
220        EventService eventService = Framework.getService(EventService.class);
221        eventService.fireEvent(PERMISSION_NOTIFICATION_EVENT, docCtx);
222    }
223
224    protected void addToken(DocumentModel doc, ACE ace) {
225        if (!ace.isArchived()) {
226            TransientUserPermissionHelper.acquireToken(ace.getUsername(), doc, ace.getPermission());
227        }
228    }
229
230    protected void removeToken(DocumentModel doc, ACE deletedAce) {
231        TransientUserPermissionHelper.revokeToken(deletedAce.getUsername(), doc);
232    }
233
234    private static class ACLDiff {
235        public final String aclName;
236
237        public final List<ACE> addedACEs;
238
239        public final List<ACE> removedACEs;
240
241        private ACLDiff(String aclName, List<ACE> addedACEs, List<ACE> removedACEs) {
242            this.aclName = aclName;
243            this.addedACEs = addedACEs != null ? addedACEs : Collections.emptyList();
244            this.removedACEs = removedACEs != null ? removedACEs : Collections.emptyList();
245        }
246    }
247}