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}