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}