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