001/* 002 * (C) Copyright 2013-2018 Nuxeo (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 * <a href="mailto:grenard@nuxeo.com">Guillaume</a> 018 */ 019package org.nuxeo.ecm.automation.core.operations.users; 020 021import java.io.IOException; 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.Collections; 025import java.util.LinkedHashMap; 026import java.util.List; 027import java.util.Locale; 028import java.util.Map; 029import java.util.stream.Stream; 030 031import org.apache.commons.lang3.StringUtils; 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034import org.nuxeo.common.utils.i18n.I18NUtils; 035import org.nuxeo.ecm.automation.OperationContext; 036import org.nuxeo.ecm.automation.core.Constants; 037import org.nuxeo.ecm.automation.core.annotations.Context; 038import org.nuxeo.ecm.automation.core.annotations.Operation; 039import org.nuxeo.ecm.automation.core.annotations.OperationMethod; 040import org.nuxeo.ecm.automation.core.annotations.Param; 041import org.nuxeo.ecm.automation.features.SuggestConstants; 042import org.nuxeo.ecm.core.api.Blob; 043import org.nuxeo.ecm.core.api.Blobs; 044import org.nuxeo.ecm.core.api.DocumentModel; 045import org.nuxeo.ecm.core.api.DocumentModelList; 046import org.nuxeo.ecm.core.api.NuxeoGroup; 047import org.nuxeo.ecm.core.api.NuxeoPrincipal; 048import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 049import org.nuxeo.ecm.core.io.registry.MarshallingConstants; 050import org.nuxeo.ecm.core.schema.SchemaManager; 051import org.nuxeo.ecm.core.schema.types.Field; 052import org.nuxeo.ecm.core.schema.types.QName; 053import org.nuxeo.ecm.core.schema.types.Schema; 054import org.nuxeo.ecm.directory.Directory; 055import org.nuxeo.ecm.directory.SizeLimitExceededException; 056import org.nuxeo.ecm.directory.api.DirectoryService; 057import org.nuxeo.ecm.platform.usermanager.UserAdapter; 058import org.nuxeo.ecm.platform.usermanager.UserManager; 059import org.nuxeo.ecm.platform.usermanager.io.NuxeoGroupJsonWriter; 060import org.nuxeo.ecm.platform.usermanager.io.NuxeoPrincipalJsonWriter; 061 062/** 063 * SuggestUser Operation. 064 * 065 * @since 5.7.3 066 */ 067@Operation(id = SuggestUserEntries.ID, category = Constants.CAT_SERVICES, label = "Get user/group suggestion", description = "Get the user/group list of the running instance. This is returning a blob containing a serialized JSON array..", addToStudio = false) 068public class SuggestUserEntries { 069 070 @SuppressWarnings("unused") 071 private static final Log log = LogFactory.getLog(SuggestUserEntries.class); 072 073 public static final String ID = "UserGroup.Suggestion"; 074 075 public static final String POWERUSERS = "powerusers"; 076 077 @Context 078 protected OperationContext ctx; 079 080 @Context 081 protected SchemaManager schemaManager; 082 083 @Param(name = "searchTerm", alias = "prefix", required = false) 084 protected String prefix; 085 086 @Param(name = "searchType", required = false) 087 protected String searchType; 088 089 @Param(name = "groupRestriction", required = false, description = "Enter the id of a group to suggest only user from this group.") 090 protected String groupRestriction; 091 092 /** 093 * @since 7.10 094 */ 095 @Param(name = "hideAdminGroups", required = false, description = "If set, remove all administrator groups from the suggestions") 096 protected boolean hideAdminGroups; 097 098 /** 099 * @since 8.3 100 */ 101 @Param(name = "hidePowerUsersGroup", required = false, description = "If set, remove power users group from the suggestions") 102 protected boolean hidePowerUsersGroup; 103 104 @Param(name = "userSuggestionMaxSearchResults", required = false) 105 protected Integer userSuggestionMaxSearchResults; 106 107 @Param(name = "firstLabelField", required = false) 108 protected String firstLabelField; 109 110 @Param(name = "secondLabelField", required = false) 111 protected String secondLabelField; 112 113 @Param(name = "thirdLabelField", required = false) 114 protected String thirdLabelField; 115 116 @Param(name = "hideFirstLabel", required = false) 117 protected boolean hideFirstLabel = false; 118 119 @Param(name = "hideSecondLabel", required = false) 120 protected boolean hideSecondLabel = false; 121 122 @Param(name = "hideThirdLabel", required = false) 123 protected boolean hideThirdLabel; 124 125 @Param(name = "displayEmailInSuggestion", required = false) 126 protected boolean displayEmailInSuggestion; 127 128 @Param(name = "hideIcon", required = false) 129 protected boolean hideIcon; 130 131 @Context 132 protected UserManager userManager; 133 134 @Context 135 protected DirectoryService directoryService; 136 137 @Param(name = "lang", required = false) 138 protected String lang; 139 140 @OperationMethod 141 public Blob run() throws IOException { 142 List<Map<String, Object>> result = new ArrayList<>(); 143 boolean isGroupRestriction = !StringUtils.isBlank(groupRestriction); 144 boolean groupOnly = false; 145 boolean userOnly = isGroupRestriction; 146 147 if (!isGroupRestriction && searchType != null && !searchType.isEmpty()) { 148 if (searchType.equals(SuggestConstants.USER_TYPE)) { 149 userOnly = true; 150 } else if (searchType.equals(SuggestConstants.GROUP_TYPE)) { 151 groupOnly = true; 152 } 153 } 154 try { 155 DocumentModelList userList = null; 156 DocumentModelList groupList = null; 157 if (!groupOnly) { 158 userList = searchUsers(); 159 Schema schema = schemaManager.getSchema(userManager.getUserSchemaName()); 160 Directory userDir = directoryService.getDirectory(userManager.getUserDirectoryName()); 161 for (DocumentModel user : userList) { 162 Map<String, Object> obj = new LinkedHashMap<>(); 163 for (Field field : schema.getFields()) { 164 QName fieldName = field.getName(); 165 String key = fieldName.getLocalName(); 166 Serializable value = user.getPropertyValue(fieldName.getPrefixedName()); 167 if (key.equals(userDir.getPasswordField())) { 168 continue; 169 } 170 obj.put(key, value); 171 } 172 String userId = user.getId(); 173 obj.put(SuggestConstants.ID, userId); 174 obj.put(MarshallingConstants.ENTITY_FIELD_NAME, NuxeoPrincipalJsonWriter.ENTITY_TYPE); 175 obj.put(SuggestConstants.TYPE_KEY_NAME, SuggestConstants.USER_TYPE); 176 obj.put(SuggestConstants.PREFIXED_ID_KEY_NAME, NuxeoPrincipal.PREFIX + userId); 177 SuggestConstants.computeUserLabel(obj, firstLabelField, secondLabelField, thirdLabelField, 178 hideFirstLabel, hideSecondLabel, hideThirdLabel, displayEmailInSuggestion, userId); 179 SuggestConstants.computeUserGroupIcon(obj, hideIcon); 180 if (isGroupRestriction) { 181 // We need to load all data about the user particularly 182 // its 183 // groups. 184 user = userManager.getUserModel(userId); 185 UserAdapter userAdapter = user.getAdapter(UserAdapter.class); 186 List<String> groups = userAdapter.getGroups(); 187 if (groups != null && groups.contains(groupRestriction)) { 188 result.add(obj); 189 } 190 } else { 191 result.add(obj); 192 } 193 } 194 } 195 if (!userOnly) { 196 Schema schema = schemaManager.getSchema(userManager.getGroupSchemaName()); 197 groupList = userManager.searchGroups(prefix); 198 List<String> admins = new ArrayList<>(); 199 if (hideAdminGroups) { 200 admins = userManager.getAdministratorsGroups(); 201 } 202 groupLoop: for (DocumentModel group : groupList) { 203 if (hideAdminGroups) { 204 for (String adminGroupName : admins) { 205 if (adminGroupName.equals(group.getId())) { 206 break groupLoop; 207 } 208 } 209 } 210 if (hidePowerUsersGroup) { 211 if (POWERUSERS.equals(group.getId())) { 212 break groupLoop; 213 } 214 } 215 Map<String, Object> obj = new LinkedHashMap<>(); 216 for (Field field : schema.getFields()) { 217 QName fieldName = field.getName(); 218 String key = fieldName.getLocalName(); 219 Serializable value = group.getPropertyValue(fieldName.getPrefixedName()); 220 obj.put(key, value); 221 } 222 String groupId = group.getId(); 223 obj.put(SuggestConstants.ID, groupId); 224 obj.put(MarshallingConstants.ENTITY_FIELD_NAME, NuxeoGroupJsonWriter.ENTITY_TYPE); 225 // If the group hasn't an label, let's put the groupid 226 SuggestConstants.computeGroupLabel(obj, groupId, userManager.getGroupLabelField(), hideFirstLabel); 227 obj.put(SuggestConstants.TYPE_KEY_NAME, SuggestConstants.GROUP_TYPE); 228 obj.put(SuggestConstants.PREFIXED_ID_KEY_NAME, NuxeoGroup.PREFIX + groupId); 229 SuggestConstants.computeUserGroupIcon(obj, hideIcon); 230 result.add(obj); 231 } 232 } 233 234 // Limit size results. 235 int userSize = userList != null ? userList.size() : 0; 236 int groupSize = groupList != null ? groupList.size() : 0; 237 int totalSize = userSize + groupSize; 238 if (userSuggestionMaxSearchResults != null && userSuggestionMaxSearchResults > 0) { 239 if (userSize > userSuggestionMaxSearchResults || groupSize > userSuggestionMaxSearchResults 240 || totalSize > userSuggestionMaxSearchResults) { 241 throw new SizeLimitExceededException(); 242 } 243 } 244 245 } catch (SizeLimitExceededException e) { 246 return searchOverflowMessage(); 247 } 248 249 return Blobs.createJSONBlobFromValue(result); 250 } 251 252 /** 253 * Performs a full name user search, e.g. typing "John Do" returns the user with first name "John" and last name 254 * "Doe". Typing "John" returns the "John Doe" user and possibly other users such as "John Foo". Respectively, 255 * typing "Do" returns the "John Doe" user and possibly other users such as "Jack Donald". 256 */ 257 protected DocumentModelList searchUsers() { 258 if (StringUtils.isBlank(prefix)) { 259 // empty search term 260 return new DocumentModelListImpl(); 261 } 262 263 String trimmedPrefix = prefix.trim(); 264 // split search term around whitespace, e.g. "John Do" -> ["John", "Do"] 265 String[] searchTerms = trimmedPrefix.split("\\s", 2); 266 return Stream.of(searchTerms) 267 .map(userManager::searchUsers) // search on all terms, e.g. "John", "Do" 268 // intersection between all search results to handle full name 269 .reduce((a, b) -> { 270 a.retainAll(b); 271 return a; 272 }) 273 .filter(result -> !result.isEmpty()) 274 // search on whole term to handle a whitespace within the first or last name 275 .orElseGet(() -> userManager.searchUsers(trimmedPrefix)); 276 } 277 278 /** 279 * @return searchOverflowMessage 280 * @since 5.7.3 281 */ 282 private Blob searchOverflowMessage() throws IOException { 283 String label = I18NUtils.getMessageString("messages", "label.security.searchOverFlow", new Object[0], 284 getLocale()); 285 Map<String, Object> obj = Collections.singletonMap(SuggestConstants.LABEL, label); 286 return Blobs.createJSONBlobFromValue(Collections.singletonList(obj)); 287 } 288 289 protected String getLang() { 290 if (lang == null) { 291 lang = (String) ctx.get("lang"); 292 if (lang == null) { 293 lang = SuggestConstants.DEFAULT_LANG; 294 } 295 } 296 return lang; 297 } 298 299 protected Locale getLocale() { 300 return new Locale(getLang()); 301 } 302 303}