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.io.registry.reflect.Instantiations.SINGLETON;
021import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
022import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_COMMENT;
023import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_DIRECTORY;
024import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_NOTIFY;
025
026import java.io.Closeable;
027import java.io.IOException;
028import java.io.Serializable;
029import java.util.HashMap;
030import java.util.Map;
031
032import org.codehaus.jackson.JsonGenerator;
033import org.joda.time.DateTime;
034import org.joda.time.format.DateTimeFormatter;
035import org.joda.time.format.ISODateTimeFormat;
036import org.nuxeo.ecm.core.api.DocumentModel;
037import org.nuxeo.ecm.core.api.security.ACE;
038import org.nuxeo.ecm.core.api.security.ACL;
039import org.nuxeo.ecm.core.api.security.ACP;
040import org.nuxeo.ecm.core.io.marshallers.json.enrichers.AbstractJsonEnricher;
041import org.nuxeo.ecm.core.io.registry.context.MaxDepthReachedException;
042import org.nuxeo.ecm.core.io.registry.reflect.Setup;
043import org.nuxeo.ecm.directory.Session;
044import org.nuxeo.ecm.directory.api.DirectoryService;
045import org.nuxeo.ecm.platform.usermanager.UserManager;
046import org.nuxeo.runtime.api.Framework;
047
048/**
049 * Enrich {@link DocumentModel} Json.
050 * <p>
051 * Add {@link DocumentModel}'s ACP as json attachment.
052 * </p>
053 * <p>
054 * Enable if parameter enrichers.document=acls is present.
055 * </p>
056 * <p>
057 * Format is:
058 *
059 * <pre>
060 * {@code
061 * {
062 *   "entity-type":"document",
063 *   ...
064 *   "contextParameters": {
065 *     "acls": [
066 *       {
067 *         "name": "inherited",
068 *         "aces" :[
069 *           {
070 *             "username": "administrators",
071 *             "permission": "Everything",
072 *             "granted": true,
073 *             "creator": "Administrator",
074 *             "begin": "2014-10-19T09:16:30.291Z",
075 *             "end": "2016-10-19T09:16:30.291Z"
076 *             "notify": true // optional
077 *             "comment": "" // optional
078 *           },
079 *           ...
080 *         ]
081 *       },
082 *       ...
083 *     ]
084 *   }
085 * }
086 * </pre>
087 *
088 * </p>
089 * <p>
090 * {@code username} and {@code creator} property can be fetched with fetch.acls=username or fetch.acls=creator.
091 * </p>
092 * <p>
093 * Additional ACE fields (such as notify and notification comment) can be written by using fetch.acls=extended.
094 * </p>
095 *
096 * @see org.nuxeo.ecm.platform.usermanager.io.NuxeoPrincipalJsonWriter
097 * @see org.nuxeo.ecm.platform.usermanager.io.NuxeoGroupJsonWriter
098 * @since 7.2
099 */
100@Setup(mode = SINGLETON, priority = REFERENCE)
101public class ACLJsonEnricher extends AbstractJsonEnricher<DocumentModel> {
102
103    public static final String NAME = "acls";
104
105    public static final String USERNAME_PROPERTY = "username";
106
107    public static final String CREATOR_PROPERTY = "creator";
108
109    public static final String EXTENDED_ACLS_PROPERTY = "extended";
110
111    public ACLJsonEnricher() {
112        super(NAME);
113    }
114
115    @Override
116    public void write(JsonGenerator jg, DocumentModel document) throws IOException {
117        ACP item = document.getACP();
118        jg.writeArrayFieldStart(NAME);
119        for (ACL acl : item.getACLs()) {
120            jg.writeStartObject();
121            jg.writeStringField("name", acl.getName());
122            jg.writeArrayFieldStart("aces");
123            for (ACE ace : acl.getACEs()) {
124                jg.writeStartObject();
125                jg.writeStringField("id", ace.getId());
126                writePrincipalOrGroup(USERNAME_PROPERTY, ace.getUsername(), jg);
127                jg.writeStringField("permission", ace.getPermission());
128                jg.writeBooleanField("granted", ace.isGranted());
129                writePrincipalOrGroup(CREATOR_PROPERTY, ace.getCreator(), jg);
130                DateTimeFormatter dateTimeFormatter = ISODateTimeFormat.dateTime();
131                jg.writeStringField("begin",
132                        ace.getBegin() != null ? dateTimeFormatter.print(new DateTime(ace.getBegin())) : null);
133                jg.writeStringField("end", ace.getEnd() != null ? dateTimeFormatter.print(new DateTime(ace.getEnd()))
134                        : null);
135                jg.writeStringField("status", ace.getStatus().toString().toLowerCase());
136
137                if (ctx.getFetched(NAME).contains(EXTENDED_ACLS_PROPERTY)) {
138                    Map<String, Serializable> m = computeAdditionalFields(document, acl.getName(), ace.getId());
139                    for (Map.Entry<String, Serializable> entry : m.entrySet()) {
140                        jg.writeObjectField(entry.getKey(), entry.getValue());
141                    }
142                }
143
144                jg.writeEndObject();
145            }
146            jg.writeEndArray();
147            jg.writeEndObject();
148        }
149        jg.writeEndArray();
150    }
151
152    protected void writePrincipalOrGroup(String propertyName, String value, JsonGenerator jg) throws IOException {
153        if (ctx.getFetched(NAME).contains(propertyName)) {
154            try (Closeable resource = ctx.wrap().controlDepth().open()) {
155                UserManager userManager = Framework.getService(UserManager.class);
156                Object entity = userManager.getPrincipal(value);
157                if (entity == null) {
158                    entity = userManager.getGroup(value);
159                }
160
161                if (entity != null) {
162                    writeEntityField(propertyName, entity, jg);
163                    return;
164                }
165            } catch (MaxDepthReachedException e) {
166                // do nothing
167            }
168        }
169        jg.writeStringField(propertyName, value);
170    }
171
172    protected Map<String, Serializable> computeAdditionalFields(DocumentModel doc, String aclName, String aceId) {
173        Map<String, Serializable> m = new HashMap<>();
174
175        DirectoryService directoryService = Framework.getLocalService(DirectoryService.class);
176        Session session = null;
177        try {
178            session = directoryService.open(ACE_INFO_DIRECTORY);
179            String id = computeDirectoryId(doc, aclName, aceId);
180            DocumentModel entry = session.getEntry(id);
181            if (entry != null) {
182                m.put("notify", entry.getPropertyValue(ACE_INFO_NOTIFY));
183                m.put("comment", entry.getPropertyValue(ACE_INFO_COMMENT));
184            }
185        } finally {
186            if (session != null) {
187                session.close();
188            }
189        }
190
191        return m;
192    }
193
194    protected String computeDirectoryId(DocumentModel doc, String aclName, String aceId) {
195        return String.format("%s:%s:%s:%s", doc.getId(), doc.getRepositoryName(), aclName, aceId);
196    }
197
198}