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}