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 handleUpdateACP(DocumentEventContext docCtx, ACP oldACP, ACP newACP) {
083        Framework.doPrivileged(() -> {
084            DocumentModel doc = docCtx.getSourceDocument();
085            List<ACLDiff> aclDiffs = extractACLDiffs(oldACP, newACP);
086            DirectoryService directoryService = Framework.getService(DirectoryService.class);
087            for (ACLDiff diff : aclDiffs) {
088                try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
089                    for (ACE ace : diff.removedACEs) {
090                        String id = computeDirectoryId(doc, diff.aclName, ace.getId());
091                        session.deleteEntry(id);
092
093                        removeToken(doc, ace);
094                    }
095
096                    for (ACE ace : diff.addedACEs) {
097                        String id = computeDirectoryId(doc, diff.aclName, ace.getId());
098                        // remove it if it exists
099                        if (session.hasEntry(id)) {
100                            session.deleteEntry(id);
101                        }
102
103                        Boolean notify = (Boolean) ace.getContextData(NOTIFY_KEY);
104                        String comment = (String) ace.getContextData(Constants.COMMENT_KEY);
105                        notify = notify != null ? notify : false;
106                        Map<String, Object> m = PermissionHelper.createDirectoryEntry(doc, diff.aclName, ace, notify,
107                                comment);
108                        session.createEntry(m);
109
110                        addToken(doc, ace);
111
112                        if (notify && ace.isGranted() && ace.isEffective()) {
113                            firePermissionNotificationEvent(docCtx, diff.aclName, ace);
114                        }
115                    }
116                }
117            }
118        });
119    }
120
121    /**
122     * @deprecated since 8.1. Not used anymore.
123     */
124    @Deprecated
125    protected void handleReplaceACE(DocumentEventContext docCtx, String changedACLName, ACE oldACE, ACE newACE) {
126        Framework.doPrivileged(() -> {
127            DocumentModel doc = docCtx.getSourceDocument();
128
129            DirectoryService directoryService = Framework.getService(DirectoryService.class);
130            try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
131                Boolean notify = (Boolean) newACE.getContextData(NOTIFY_KEY);
132                String comment = (String) newACE.getContextData(COMMENT_KEY);
133
134                String oldId = computeDirectoryId(doc, changedACLName, oldACE.getId());
135                DocumentModel oldEntry = session.getEntry(oldId);
136                if (oldEntry != null) {
137                    // remove the old entry
138                    session.deleteEntry(oldId);
139                }
140
141                // add the new entry
142                notify = notify != null ? notify : false;
143                Map<String, Object> m = PermissionHelper.createDirectoryEntry(doc, changedACLName, newACE, notify,
144                        comment);
145                session.createEntry(m);
146
147                if (notify && newACE.isGranted() && newACE.isEffective()) {
148                    firePermissionNotificationEvent(docCtx, changedACLName, newACE);
149                }
150            }
151        });
152    }
153
154    protected List<ACLDiff> extractACLDiffs(ACP oldACP, ACP newACP) {
155        List<ACLDiff> aclDiffs = new ArrayList<>();
156
157        List<String> oldACLNames = toACLNames(oldACP);
158        List<String> newACLNames = toACLNames(newACP);
159        List<String> addedACLNames = toACLNames(newACP);
160        List<String> removedACLNames = toACLNames(oldACP);
161
162        addedACLNames.removeAll(oldACLNames);
163        removedACLNames.removeAll(newACLNames);
164
165        for (String name : addedACLNames) {
166            aclDiffs.add(new ACLDiff(name, new ArrayList<>(newACP.getACL(name)), null));
167        }
168
169        for (String name : removedACLNames) {
170            aclDiffs.add(new ACLDiff(name, null, new ArrayList<>(oldACP.getACL(name))));
171        }
172
173        for (ACL newACL : newACP.getACLs()) {
174            ACL oldACL = oldACP.getACL(newACL.getName());
175            if (oldACL != null) {
176                List<ACE> addedACEs = new ArrayList<>(newACL);
177                List<ACE> removedACEs = new ArrayList<>(oldACL);
178
179                addedACEs.removeAll(oldACL);
180                removedACEs.removeAll(newACL);
181                aclDiffs.add(new ACLDiff(newACL.getName(), addedACEs, removedACEs));
182            }
183        }
184        return aclDiffs;
185    }
186
187    protected List<String> toACLNames(ACP acp) {
188        List<String> aclNames = new ArrayList<>();
189        for (ACL acl : acp.getACLs()) {
190            aclNames.add(acl.getName());
191        }
192        return aclNames;
193    }
194
195    protected void firePermissionNotificationEvent(DocumentEventContext docCtx, String aclName, ACE ace) {
196        docCtx.setProperty(ACE_KEY, ace);
197        docCtx.setProperty(ACL_NAME_KEY, aclName);
198        EventService eventService = Framework.getService(EventService.class);
199        eventService.fireEvent(PERMISSION_NOTIFICATION_EVENT, docCtx);
200    }
201
202    protected void addToken(DocumentModel doc, ACE ace) {
203        if (!ace.isArchived()) {
204            TransientUserPermissionHelper.acquireToken(ace.getUsername(), doc, ace.getPermission());
205        }
206    }
207
208    protected void removeToken(DocumentModel doc, ACE deletedAce) {
209        TransientUserPermissionHelper.revokeToken(deletedAce.getUsername(), doc);
210    }
211
212    private static class ACLDiff {
213        public final String aclName;
214
215        public final List<ACE> addedACEs;
216
217        public final List<ACE> removedACEs;
218
219        private ACLDiff(String aclName, List<ACE> addedACEs, List<ACE> removedACEs) {
220            this.aclName = aclName;
221            this.addedACEs = addedACEs != null ? addedACEs : Collections.emptyList();
222            this.removedACEs = removedACEs != null ? removedACEs : Collections.emptyList();
223        }
224    }
225}