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