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.common.utils.DateUtils.formatISODateTime;
023import static org.nuxeo.ecm.core.io.registry.reflect.Instantiations.SINGLETON;
024import static org.nuxeo.ecm.core.io.registry.reflect.Priorities.REFERENCE;
025import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_COMMENT;
026import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_DIRECTORY;
027import static org.nuxeo.ecm.permissions.Constants.ACE_INFO_NOTIFY;
028
029import java.io.Closeable;
030import java.io.IOException;
031import java.io.Serializable;
032import java.util.HashMap;
033import java.util.Map;
034
035import org.nuxeo.ecm.core.api.DocumentModel;
036import org.nuxeo.ecm.core.api.NuxeoPrincipal;
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.api.security.SecurityConstants;
041import org.nuxeo.ecm.core.io.marshallers.json.enrichers.AbstractJsonEnricher;
042import org.nuxeo.ecm.core.io.registry.context.MaxDepthReachedException;
043import org.nuxeo.ecm.core.io.registry.reflect.Setup;
044import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolver;
045import org.nuxeo.ecm.core.schema.types.resolver.ObjectResolverService;
046import org.nuxeo.ecm.directory.Session;
047import org.nuxeo.ecm.directory.api.DirectoryService;
048import org.nuxeo.ecm.platform.usermanager.UserManagerResolver;
049import org.nuxeo.runtime.api.Framework;
050import org.nuxeo.runtime.services.config.ConfigurationService;
051
052import com.fasterxml.jackson.core.JsonGenerator;
053
054/**
055 * Enrich {@link DocumentModel} Json.
056 * <p>
057 * Add {@link DocumentModel}'s ACP as json attachment.
058 * <p>
059 * Enable if parameter enrichers-document=acls is present.
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 * }
091 * </pre>
092 *
093 * <p>
094 * {@code username} and {@code creator} property can be fetched with fetch.acls=username or fetch.acls=creator.
095 * <p>
096 * Additional ACE fields (such as notify and notification comment) can be written by using fetch.acls=extended.
097 *
098 * @see org.nuxeo.ecm.platform.usermanager.io.NuxeoPrincipalJsonWriter
099 * @see org.nuxeo.ecm.platform.usermanager.io.NuxeoGroupJsonWriter
100 * @since 7.2
101 */
102@Setup(mode = SINGLETON, priority = REFERENCE)
103public class ACLJsonEnricher extends AbstractJsonEnricher<DocumentModel> {
104
105    public static final String NAME = "acls";
106
107    public static final String USERNAME_PROPERTY = "username";
108
109    public static final String CREATOR_PROPERTY = "creator";
110
111    public static final String EXTENDED_ACLS_PROPERTY = "extended";
112
113    public static final String COMPATIBILITY_CONFIGURATION_PARAM = "nuxeo.permissions.acl.enricher.compatibility";
114
115    public ACLJsonEnricher() {
116        super(NAME);
117    }
118
119    @Override
120    public void write(JsonGenerator jg, DocumentModel document) throws IOException {
121        ACP item = document.getACP();
122        jg.writeArrayFieldStart(NAME);
123        for (ACL acl : item.getACLs()) {
124            jg.writeStartObject();
125            jg.writeStringField("name", acl.getName());
126            writeACEsField(jg, "aces", acl, document);
127
128            ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
129            if (configurationService.isBooleanTrue(COMPATIBILITY_CONFIGURATION_PARAM)) {
130                writeACEsField(jg, "ace", acl, document);
131            }
132            jg.writeEndObject();
133        }
134        jg.writeEndArray();
135    }
136
137    protected void writeACEsField(JsonGenerator jg, String fieldName, ACL acl, DocumentModel document)
138            throws IOException {
139        jg.writeArrayFieldStart(fieldName);
140        for (ACE ace : acl.getACEs()) {
141            jg.writeStartObject();
142            jg.writeStringField("id", ace.getId());
143            String username = ace.getUsername();
144            writePrincipalOrGroup(USERNAME_PROPERTY, username, jg);
145            jg.writeBooleanField("externalUser", NuxeoPrincipal.isTransientUsername(username));
146            jg.writeStringField("permission", ace.getPermission());
147            jg.writeBooleanField("granted", ace.isGranted());
148            writePrincipalOrGroup(CREATOR_PROPERTY, ace.getCreator(), jg);
149            jg.writeStringField("begin", formatISODateTime(ace.getBegin()));
150            jg.writeStringField("end", formatISODateTime(ace.getEnd()));
151            jg.writeStringField("status", ace.getStatus().toString().toLowerCase());
152
153            if (ctx.getFetched(NAME).contains(EXTENDED_ACLS_PROPERTY)) {
154                Map<String, Serializable> m = computeAdditionalFields(document, acl.getName(), ace.getId());
155                for (Map.Entry<String, Serializable> entry : m.entrySet()) {
156                    jg.writeObjectField(entry.getKey(), entry.getValue());
157                }
158            }
159            jg.writeEndObject();
160        }
161        jg.writeEndArray();
162    }
163
164    protected void writePrincipalOrGroup(String propertyName, String value, JsonGenerator jg) throws IOException {
165        if (value != null && !SecurityConstants.SYSTEM_USERNAME.equals(value)
166                && ctx.getFetched(NAME).contains(propertyName)) {
167            try (Closeable resource = ctx.wrap().controlDepth().open()) {
168                ObjectResolver resolver = Framework.getService(ObjectResolverService.class)
169                        .getResolver(UserManagerResolver.NAME, Map.of());
170                Object entity = resolver.fetch(value);
171                if (entity != null) {
172                    writeEntityField(propertyName, entity, jg);
173                    return;
174                }
175            } catch (MaxDepthReachedException e) {
176                // do nothing
177            }
178        }
179        jg.writeStringField(propertyName, value);
180    }
181
182    protected Map<String, Serializable> computeAdditionalFields(DocumentModel doc, String aclName, String aceId) {
183        Map<String, Serializable> m = new HashMap<>();
184
185        DirectoryService directoryService = Framework.getService(DirectoryService.class);
186        Framework.doPrivileged(() -> {
187            try (Session session = directoryService.open(ACE_INFO_DIRECTORY)) {
188                String id = computeDirectoryId(doc, aclName, aceId);
189                DocumentModel entry = session.getEntry(id);
190                if (entry != null) {
191                    m.put("notify", entry.getPropertyValue(ACE_INFO_NOTIFY));
192                    m.put("comment", entry.getPropertyValue(ACE_INFO_COMMENT));
193                }
194            }
195        });
196
197        return m;
198    }
199
200    protected String computeDirectoryId(DocumentModel doc, String aclName, String aceId) {
201        return String.format("%s:%s:%s:%s", doc.getId(), doc.getRepositoryName(), aclName, aceId);
202    }
203
204}