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