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}