001/*
002 * (C) Copyright 2006-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 *     George Lefter
018 *     Florent Guillaume
019 *     Anahide Tchertchian
020 *     Gagnavarslan ehf
021 */
022package org.nuxeo.ecm.platform.usermanager;
023
024import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
025import static org.nuxeo.ecm.platform.usermanager.UserConfig.GROUPS_COLUMN;
026
027import java.io.Serializable;
028import java.security.Principal;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.Map;
037import java.util.Map.Entry;
038import java.util.Set;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041
042import org.apache.commons.codec.digest.DigestUtils;
043import org.apache.commons.lang3.StringUtils;
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046import org.nuxeo.ecm.core.api.DocumentModel;
047import org.nuxeo.ecm.core.api.DocumentModelComparator;
048import org.nuxeo.ecm.core.api.DocumentModelList;
049import org.nuxeo.ecm.core.api.NuxeoException;
050import org.nuxeo.ecm.core.api.NuxeoGroup;
051import org.nuxeo.ecm.core.api.NuxeoPrincipal;
052import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
053import org.nuxeo.ecm.core.api.model.Property;
054import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
055import org.nuxeo.ecm.core.api.security.ACE;
056import org.nuxeo.ecm.core.api.security.ACL;
057import org.nuxeo.ecm.core.api.security.ACP;
058import org.nuxeo.ecm.core.api.security.AdministratorGroupsProvider;
059import org.nuxeo.ecm.core.api.security.PermissionProvider;
060import org.nuxeo.ecm.core.api.security.SecurityConstants;
061import org.nuxeo.ecm.core.cache.Cache;
062import org.nuxeo.ecm.core.cache.CacheManagement;
063import org.nuxeo.ecm.core.cache.CacheService;
064import org.nuxeo.ecm.core.event.EventProducer;
065import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
066import org.nuxeo.ecm.core.event.impl.UnboundEventContext;
067import org.nuxeo.ecm.core.query.sql.model.MultiExpression;
068import org.nuxeo.ecm.core.query.sql.model.Operator;
069import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
070import org.nuxeo.ecm.core.query.sql.model.OrderByExprs;
071import org.nuxeo.ecm.core.query.sql.model.OrderByList;
072import org.nuxeo.ecm.core.query.sql.model.Predicate;
073import org.nuxeo.ecm.core.query.sql.model.Predicates;
074import org.nuxeo.ecm.core.query.sql.model.QueryBuilder;
075import org.nuxeo.ecm.directory.AbstractDirectory;
076import org.nuxeo.ecm.directory.BaseDirectoryDescriptor.SubstringMatchType;
077import org.nuxeo.ecm.directory.BaseSession;
078import org.nuxeo.ecm.directory.Directory;
079import org.nuxeo.ecm.directory.DirectoryException;
080import org.nuxeo.ecm.directory.Session;
081import org.nuxeo.ecm.directory.api.DirectoryService;
082import org.nuxeo.ecm.directory.memory.MemoryDirectoryExpressionEvaluator;
083import org.nuxeo.ecm.platform.usermanager.exceptions.GroupAlreadyExistsException;
084import org.nuxeo.ecm.platform.usermanager.exceptions.InvalidPasswordException;
085import org.nuxeo.ecm.platform.usermanager.exceptions.UserAlreadyExistsException;
086import org.nuxeo.runtime.api.Framework;
087import org.nuxeo.runtime.services.config.ConfigurationService;
088import org.nuxeo.runtime.services.event.Event;
089import org.nuxeo.runtime.services.event.EventService;
090
091/**
092 * Standard implementation of the Nuxeo UserManager.
093 */
094public class UserManagerImpl implements UserManager, MultiTenantUserManager, AdministratorGroupsProvider {
095
096    private static final String VALIDATE_PASSWORD_PARAM = "nuxeo.usermanager.check.password";
097
098    /** @since 11.1 */
099    protected static final String SEARCH_ESCAPE_COMPAT_PARAM = "nuxeo.usermanager.search.escape.compat";
100
101    private static final long serialVersionUID = 1L;
102
103    private static final Log log = LogFactory.getLog(UserManagerImpl.class);
104
105    public static final String USERMANAGER_TOPIC = "usermanager";
106
107    /** Used by JaasCacheFlusher. */
108    public static final String USERCHANGED_EVENT_ID = "user_changed";
109
110    public static final String USERCREATED_EVENT_ID = "user_created";
111
112    public static final String USERDELETED_EVENT_ID = "user_deleted";
113
114    public static final String USERMODIFIED_EVENT_ID = "user_modified";
115
116    /** Used by JaasCacheFlusher. */
117    public static final String GROUPCHANGED_EVENT_ID = "group_changed";
118
119    public static final String GROUPCREATED_EVENT_ID = "group_created";
120
121    public static final String GROUPDELETED_EVENT_ID = "group_deleted";
122
123    public static final String GROUPMODIFIED_EVENT_ID = "group_modified";
124
125    public static final String DEFAULT_ANONYMOUS_USER_ID = "Anonymous";
126
127    public static final String VIRTUAL_FIELD_FILTER_PREFIX = "__";
128
129    public static final String INVALIDATE_PRINCIPAL_EVENT_ID = "invalidatePrincipal";
130
131    public static final String INVALIDATE_ALL_PRINCIPALS_EVENT_ID = "invalidateAllPrincipals";
132
133    /**
134     * Possible value for the {@link DocumentEventContext#CATEGORY_PROPERTY_KEY} key of a core event context.
135     *
136     * @since 9.2
137     */
138    public static final String USER_GROUP_CATEGORY = "userGroup";
139
140    /**
141     * Key for the id of a user or a group in a core event context.
142     *
143     * @since 9.2
144     */
145    public static final String ID_PROPERTY_KEY = "id";
146
147    /**
148     * Key for the ancestor group names of a group in a core event context.
149     *
150     * @since 9.2
151     */
152    public static final String ANCESTOR_GROUPS_PROPERTY_KEY = "ancestorGroups";
153
154    /**
155     * @since 11.4
156     */
157    public static final String USER_HAS_PARTIAL_CONTENT = "userHasPartialContent";
158
159    protected final DirectoryService dirService;
160
161    protected final CacheService cacheService;
162
163    protected Cache principalCache = null;
164
165    public UserMultiTenantManagement multiTenantManagement = new DefaultUserMultiTenantManagement();
166
167    /**
168     * A structure used to inject field name configuration of users schema into a NuxeoPrincipalImpl instance. TODO not
169     * all fields inside are configurable for now - they will use default values
170     */
171    protected UserConfig userConfig;
172
173    /**
174     * @since 9.3
175     */
176    protected GroupConfig groupConfig;
177
178    protected String userDirectoryName;
179
180    protected String userSchemaName;
181
182    protected String userIdField;
183
184    protected String userEmailField;
185
186    protected Map<String, MatchType> userSearchFields;
187
188    protected String groupDirectoryName;
189
190    protected String groupSchemaName;
191
192    protected String groupIdField;
193
194    protected String groupLabelField;
195
196    protected String groupMembersField;
197
198    protected String groupSubGroupsField;
199
200    protected String groupParentGroupsField;
201
202    protected String groupSortField;
203
204    protected Map<String, MatchType> groupSearchFields;
205
206    protected String defaultGroup;
207
208    protected List<String> administratorIds;
209
210    protected List<String> administratorGroups;
211
212    protected Boolean disableDefaultAdministratorsGroup;
213
214    protected String userSortField;
215
216    protected String userListingMode;
217
218    protected String groupListingMode;
219
220    protected Pattern userPasswordPattern;
221
222    protected VirtualUser anonymousUser;
223
224    protected String digestAuthDirectory;
225
226    protected String digestAuthRealm;
227
228    protected final Map<String, VirtualUserDescriptor> virtualUsers;
229
230    public UserManagerImpl() {
231        dirService = Framework.getService(DirectoryService.class);
232        cacheService = Framework.getService(CacheService.class);
233        virtualUsers = new HashMap<>();
234        userConfig = new UserConfig();
235    }
236
237    @Override
238    public void setConfiguration(UserManagerDescriptor descriptor) {
239        defaultGroup = descriptor.defaultGroup;
240        administratorIds = descriptor.defaultAdministratorIds;
241        disableDefaultAdministratorsGroup = false;
242        if (descriptor.disableDefaultAdministratorsGroup != null) {
243            disableDefaultAdministratorsGroup = descriptor.disableDefaultAdministratorsGroup;
244        }
245        administratorGroups = new ArrayList<>();
246        if (!disableDefaultAdministratorsGroup) {
247            administratorGroups.add(SecurityConstants.ADMINISTRATORS);
248        }
249        if (descriptor.administratorsGroups != null) {
250            administratorGroups.addAll(descriptor.administratorsGroups);
251        }
252        if (administratorGroups.isEmpty()) {
253            log.warn("No administrators group has been defined: at least one should be set"
254                    + " to avoid lockups when blocking rights for instance");
255        }
256        userSortField = descriptor.userSortField;
257        groupSortField = descriptor.groupSortField;
258        userListingMode = descriptor.userListingMode;
259        groupListingMode = descriptor.groupListingMode;
260        userEmailField = descriptor.userEmailField;
261        userSearchFields = descriptor.userSearchFields;
262        userPasswordPattern = descriptor.userPasswordPattern;
263        groupLabelField = descriptor.groupLabelField;
264        groupMembersField = descriptor.groupMembersField;
265        groupSubGroupsField = descriptor.groupSubGroupsField;
266        groupParentGroupsField = descriptor.groupParentGroupsField;
267        groupSearchFields = descriptor.groupSearchFields;
268        anonymousUser = descriptor.anonymousUser;
269
270        setUserDirectoryName(descriptor.userDirectoryName);
271        setGroupDirectoryName(descriptor.groupDirectoryName);
272        setVirtualUsers(descriptor.virtualUsers);
273
274        digestAuthDirectory = descriptor.digestAuthDirectory;
275        digestAuthRealm = descriptor.digestAuthRealm;
276
277        userConfig = new UserConfig();
278        userConfig.emailKey = userEmailField;
279        userConfig.schemaName = userSchemaName;
280        userConfig.nameKey = userIdField;
281
282        groupConfig = new GroupConfig();
283        groupConfig.schemaName = groupSchemaName;
284        groupConfig.idField = groupIdField;
285        groupConfig.labelField = groupLabelField;
286        groupConfig.membersField = groupMembersField;
287        groupConfig.subGroupsField = groupSubGroupsField;
288        groupConfig.parentGroupsField = groupParentGroupsField;
289
290        if (cacheService != null && descriptor.userCacheName != null) {
291            principalCache = cacheService.getCache(descriptor.userCacheName);
292            invalidateAllPrincipals();
293        }
294
295    }
296
297    protected void setUserDirectoryName(String userDirectoryName) {
298        this.userDirectoryName = userDirectoryName;
299        userSchemaName = dirService.getDirectorySchema(userDirectoryName);
300        userIdField = dirService.getDirectoryIdField(userDirectoryName);
301    }
302
303    @Override
304    public String getUserDirectoryName() {
305        return userDirectoryName;
306    }
307
308    @Override
309    public String getUserIdField() {
310        return userIdField;
311    }
312
313    @Override
314    public String getUserSchemaName() {
315        return userSchemaName;
316    }
317
318    @Override
319    public String getUserEmailField() {
320        return userEmailField;
321    }
322
323    @Override
324    public Set<String> getUserSearchFields() {
325        return Collections.unmodifiableSet(userSearchFields.keySet());
326    }
327
328    @Override
329    public Set<String> getGroupSearchFields() {
330        return Collections.unmodifiableSet(groupSearchFields.keySet());
331    }
332
333    protected void setGroupDirectoryName(String groupDirectoryName) {
334        this.groupDirectoryName = groupDirectoryName;
335        groupSchemaName = dirService.getDirectorySchema(groupDirectoryName);
336        groupIdField = dirService.getDirectoryIdField(groupDirectoryName);
337    }
338
339    @Override
340    public String getGroupDirectoryName() {
341        return groupDirectoryName;
342    }
343
344    @Override
345    public String getGroupIdField() {
346        return groupIdField;
347    }
348
349    @Override
350    public String getGroupLabelField() {
351        return groupLabelField;
352    }
353
354    @Override
355    public String getGroupSchemaName() {
356        return groupSchemaName;
357    }
358
359    @Override
360    public String getGroupMembersField() {
361        return groupMembersField;
362    }
363
364    @Override
365    public String getGroupSubGroupsField() {
366        return groupSubGroupsField;
367    }
368
369    @Override
370    public String getGroupParentGroupsField() {
371        return groupParentGroupsField;
372    }
373
374    @Override
375    public String getUserListingMode() {
376        return userListingMode;
377    }
378
379    @Override
380    public String getGroupListingMode() {
381        return groupListingMode;
382    }
383
384    @Override
385    public String getDefaultGroup() {
386        return defaultGroup;
387    }
388
389    @Override
390    public Pattern getUserPasswordPattern() {
391        return userPasswordPattern;
392    }
393
394    @Override
395    public String getAnonymousUserId() {
396        if (anonymousUser == null) {
397            return null;
398        }
399        String anonymousUserId = anonymousUser.getId();
400        if (anonymousUserId == null) {
401            return DEFAULT_ANONYMOUS_USER_ID;
402        }
403        return anonymousUserId;
404    }
405
406    protected void setVirtualUsers(Map<String, VirtualUserDescriptor> virtualUsers) {
407        this.virtualUsers.clear();
408        if (virtualUsers != null) {
409            this.virtualUsers.putAll(virtualUsers);
410        }
411    }
412
413    @Override
414    public boolean checkUsernamePassword(String username, String password) {
415
416        if (username == null || password == null) {
417            log.warn("Trying to authenticate against null username or password");
418            return false;
419        }
420
421        // deal with anonymous user
422        String anonymousUserId = getAnonymousUserId();
423        if (username.equals(anonymousUserId)) {
424            log.warn(String.format("Trying to authenticate anonymous user (%s)", anonymousUserId));
425            return false;
426        }
427
428        // deal with virtual users
429        if (virtualUsers.containsKey(username)) {
430            VirtualUser user = virtualUsers.get(username);
431            String expected = user.getPassword();
432            if (expected == null) {
433                return false;
434            }
435            return expected.equals(password);
436        }
437
438        String userDirName;
439        // BBB backward compat for userDirectory + userAuthentication
440        if ("userDirectory".equals(userDirectoryName) && dirService.getDirectory("userAuthentication") != null) {
441            userDirName = "userAuthentication";
442        } else {
443            userDirName = userDirectoryName;
444        }
445        try (Session userDir = dirService.open(userDirName)) {
446            if (!userDir.isAuthenticating()) {
447                log.error("Trying to authenticate against a non authenticating " + "directory: " + userDirName);
448                return false;
449            }
450
451            boolean authenticated = userDir.authenticate(username, password);
452            if (authenticated) {
453                Framework.doPrivileged(() -> syncDigestAuthPassword(username, password));
454            }
455            return authenticated;
456        }
457    }
458
459    protected void syncDigestAuthPassword(String username, String password) {
460        if (StringUtils.isEmpty(digestAuthDirectory) || StringUtils.isEmpty(digestAuthRealm) || username == null
461                || password == null) {
462            return;
463        }
464
465        String ha1 = encodeDigestAuthPassword(username, digestAuthRealm, password);
466        try (Session dir = dirService.open(digestAuthDirectory)) {
467            dir.setReadAllColumns(true); // needed to read digest password
468            String schema = dirService.getDirectorySchema(digestAuthDirectory);
469            DocumentModel entry = dir.getEntry(username, true);
470            if (entry == null) {
471                entry = getDigestAuthModel();
472                entry.setProperty(schema, dir.getIdField(), username);
473                entry.setProperty(schema, dir.getPasswordField(), ha1);
474                dir.createEntry(entry);
475                log.debug("Created digest auth password for user:" + username);
476            } else {
477                String storedHa1 = (String) entry.getProperty(schema, dir.getPasswordField());
478                if (!ha1.equals(storedHa1)) {
479                    entry.setProperty(schema, dir.getPasswordField(), ha1);
480                    dir.updateEntry(entry);
481                    log.debug("Updated digest auth password for user:" + username);
482                }
483            }
484        } catch (DirectoryException e) {
485            log.warn("Digest auth password not synchronized, check your configuration", e);
486        }
487    }
488
489    protected DocumentModel getDigestAuthModel() {
490        String schema = dirService.getDirectorySchema(digestAuthDirectory);
491        return BaseSession.createEntryModel(null, schema, null, null);
492    }
493
494    public static String encodeDigestAuthPassword(String username, String realm, String password) {
495        String a1 = username + ":" + realm + ":" + password;
496        return DigestUtils.md5Hex(a1);
497    }
498
499    @Override
500    public String getDigestAuthDirectory() {
501        return digestAuthDirectory;
502    }
503
504    @Override
505    public String getDigestAuthRealm() {
506        return digestAuthRealm;
507    }
508
509    @Override
510    public boolean validatePassword(String password) {
511        if (userPasswordPattern == null) {
512            return true;
513        } else {
514            Matcher userPasswordMatcher = userPasswordPattern.matcher(password);
515            return userPasswordMatcher.find();
516        }
517    }
518
519    protected NuxeoPrincipal makeAnonymousPrincipal() {
520        DocumentModel userEntry = makeVirtualUserEntry(getAnonymousUserId(), anonymousUser);
521        // XXX: pass anonymous user groups, but they will be ignored
522        return makePrincipal(userEntry, true, anonymousUser.getGroups());
523    }
524
525    protected NuxeoPrincipal makeVirtualPrincipal(VirtualUser user) {
526        DocumentModel userEntry = makeVirtualUserEntry(user.getId(), user);
527        return makePrincipal(userEntry, false, user.getGroups());
528    }
529
530    protected NuxeoPrincipal makeTransientPrincipal(String username) {
531        DocumentModel userEntry = BaseSession.createEntryModel(null, userSchemaName, username, null);
532        userEntry.setProperty(userSchemaName, userIdField, username);
533        NuxeoPrincipal principal = makePrincipal(userEntry, false, true, null);
534        String[] parts = username.split("/");
535        String email = parts[1];
536        principal.setFirstName(email);
537        principal.setEmail(email);
538        return principal;
539    }
540
541    protected DocumentModel makeVirtualUserEntry(String id, VirtualUser user) {
542        final DocumentModel userEntry = BaseSession.createEntryModel(null, userSchemaName, id, null);
543        // at least fill id field
544        userEntry.setProperty(userSchemaName, userIdField, id);
545        for (Entry<String, Serializable> prop : user.getProperties().entrySet()) {
546            try {
547                userEntry.setProperty(userSchemaName, prop.getKey(), prop.getValue());
548            } catch (PropertyNotFoundException ce) {
549                log.error("Property: " + prop.getKey() + " does not exists. Check your " + "UserService configuration.",
550                        ce);
551            }
552        }
553        return userEntry;
554    }
555
556    protected NuxeoPrincipal makePrincipal(DocumentModel userEntry) {
557        return makePrincipal(userEntry, false, null);
558    }
559
560    protected NuxeoPrincipal makePrincipal(DocumentModel userEntry, boolean anonymous, List<String> groups) {
561        return makePrincipal(userEntry, anonymous, false, groups);
562    }
563
564    protected NuxeoPrincipal makePrincipal(DocumentModel userEntry, boolean anonymous, boolean isTransient,
565            List<String> groups) {
566        boolean admin = false;
567        String username = userEntry.getId();
568
569        List<String> virtualGroups = new LinkedList<>();
570        // Add preconfigured groups: useful for LDAP, not for anonymous users
571        if (defaultGroup != null && !anonymous && !isTransient) {
572            virtualGroups.add(defaultGroup);
573        }
574        // Add additional groups: useful for virtual users
575        if (groups != null && !isTransient) {
576            virtualGroups.addAll(groups);
577        }
578        // Create a default admin if needed
579        if (administratorIds != null && administratorIds.contains(username)) {
580            admin = true;
581            if (administratorGroups != null) {
582                virtualGroups.addAll(administratorGroups);
583            }
584        }
585
586        NuxeoPrincipalImpl principal = new NuxeoPrincipalImpl(username, anonymous, admin, false);
587        principal.setConfig(userConfig);
588
589        principal.setModel(userEntry, false);
590        principal.setVirtualGroups(virtualGroups, true);
591
592        // TODO: reenable roles initialization once we have a use case for
593        // a role directory. In the mean time we only set the JBOSS role
594        // that is required to login
595        List<String> roles = Collections.singletonList("regular");
596        principal.setRoles(roles);
597
598        return principal;
599    }
600
601    protected boolean useCache() {
602        return principalCache != null;
603    }
604
605    @Override
606    public NuxeoPrincipal getPrincipal(String username, boolean fetchReferences) {
607        // Caching is not needed for a principal without references because this object is only a reference on an
608        // already cached User Model
609        if (useCache() && fetchReferences) {
610            return getPrincipalUsingCache(username);
611        }
612        return getPrincipal(username, null, fetchReferences);
613    }
614
615    protected NuxeoPrincipal getPrincipalUsingCache(String username) {
616        NuxeoPrincipal ret = (NuxeoPrincipal) principalCache.get(username);
617        if (ret == null) {
618            ret = getPrincipal(username, null);
619            if (ret == null) {
620                return ret;
621            }
622            ((CacheManagement) principalCache).putLocal(username, ret);
623        }
624        return ((NuxeoPrincipalImpl) ret).cloneTransferable(); // should not return cached principal
625    }
626
627    @Override
628    public DocumentModel getUserModel(String userName) {
629        return getUserModel(userName, null);
630    }
631
632    @Override
633    public DocumentModel getBareUserModel() {
634        String schema = dirService.getDirectorySchema(userDirectoryName);
635        return BaseSession.createEntryModel(null, schema, null, null);
636    }
637
638    @Override
639    public NuxeoGroup getGroup(String groupName) {
640        return getGroup(groupName, null);
641    }
642
643    protected NuxeoGroup getGroup(String groupName, DocumentModel context) {
644        DocumentModel groupEntry = getGroupModel(groupName, context);
645        if (groupEntry != null) {
646            return makeGroup(groupEntry);
647        }
648        return null;
649
650    }
651
652    @Override
653    public DocumentModel getGroupModel(String groupName) {
654        return getGroupModel(groupName, null);
655    }
656
657    @SuppressWarnings("unchecked")
658    protected NuxeoGroup makeGroup(DocumentModel groupEntry) {
659        return new NuxeoGroupImpl(groupEntry, groupConfig);
660    }
661
662    @Override
663    public List<String> getTopLevelGroups() {
664        return getTopLevelGroups(null);
665    }
666
667    @Override
668    public List<String> getGroupsInGroup(String parentId) {
669        NuxeoGroup group = getGroup(parentId, null);
670        if (group != null) {
671            return group.getMemberGroups();
672        } else {
673            return Collections.emptyList();
674        }
675    }
676
677    @Override
678    public List<String> getUsersInGroup(String groupId) {
679        return getGroup(groupId).getMemberUsers();
680    }
681
682    @Override
683    public List<String> getUsersInGroupAndSubGroups(String groupId) {
684        return getUsersInGroupAndSubGroups(groupId, null);
685    }
686
687    protected void appendSubgroups(String groupId, Set<String> groups, DocumentModel context) {
688        List<String> groupsToAppend = getDescendantGroups(groupId);
689        groups.addAll(groupsToAppend);
690
691    }
692
693    protected boolean isAnonymousMatching(Map<String, Serializable> filter, Set<String> fulltext) {
694        String anonymousUserId = getAnonymousUserId();
695        if (anonymousUserId == null) {
696            return false;
697        }
698        if (filter == null || filter.isEmpty()) {
699            return true;
700        }
701        Map<String, Serializable> anonymousUserMap = anonymousUser.getProperties();
702        anonymousUserMap.put(userIdField, anonymousUserId);
703        for (Entry<String, Serializable> e : filter.entrySet()) {
704            String fieldName = e.getKey();
705            Object expected = e.getValue();
706            Object value = anonymousUserMap.get(fieldName);
707            if (value == null) {
708                if (expected != null) {
709                    return false;
710                }
711            } else {
712                if (fulltext != null && fulltext.contains(fieldName)) {
713                    if (!value.toString().toLowerCase().startsWith(expected.toString().toLowerCase())) {
714                        return false;
715                    }
716                } else {
717                    if (!value.equals(expected)) {
718                        return false;
719                    }
720                }
721            }
722        }
723        return true;
724    }
725
726    protected boolean isAnonymousMatching(QueryBuilder queryBuilder, Directory dir) {
727        String anonymousUserId = getAnonymousUserId();
728        if (anonymousUserId == null) {
729            return false;
730        }
731        MultiExpression expression = queryBuilder.predicate();
732        if (expression.predicates.isEmpty()) {
733            return true;
734        }
735        @SuppressWarnings({ "rawtypes", "unchecked" })
736        Map<String, Object> entry = (Map) anonymousUser.getProperties();
737        entry.put(userIdField, anonymousUserId);
738        return new MemoryDirectoryExpressionEvaluator(dir).matchesEntry(expression, entry);
739    }
740
741    @Override
742    public List<NuxeoPrincipal> searchPrincipals(String pattern) {
743        DocumentModelList entries = searchUsers(pattern);
744        List<NuxeoPrincipal> principals = new ArrayList<>(entries.size());
745        for (DocumentModel entry : entries) {
746            principals.add(makePrincipal(entry));
747        }
748        return principals;
749    }
750
751    @Override
752    public DocumentModelList searchGroups(String pattern) {
753        return searchGroups(pattern, null);
754    }
755
756    @Override
757    public String getUserSortField() {
758        return userSortField;
759    }
760
761    protected Map<String, String> getUserSortMap() {
762        return getDirectorySortMap(userSortField, userIdField);
763    }
764
765    protected OrderByExpr getUserOrderBy() {
766        String sortField = StringUtils.defaultString(userSortField, userIdField);
767        return OrderByExprs.asc(sortField);
768    }
769
770    protected Map<String, String> getGroupSortMap() {
771        return getDirectorySortMap(groupSortField, groupIdField);
772    }
773
774    protected OrderByExpr getGroupOrderBy() {
775        String sortField = StringUtils.defaultString(groupSortField, groupIdField);
776        return OrderByExprs.asc(sortField);
777    }
778
779    protected Map<String, String> getDirectorySortMap(String descriptorSortField, String fallBackField) {
780        String sortField = descriptorSortField != null ? descriptorSortField : fallBackField;
781        Map<String, String> orderBy = new HashMap<>();
782        orderBy.put(sortField, DocumentModelComparator.ORDER_ASC);
783        return orderBy;
784    }
785
786    /**
787     * @since 8.2
788     */
789    protected void notifyCore(String userOrGroupId, String eventId) {
790        notifyCore(userOrGroupId, eventId, null);
791    }
792
793    /**
794     * @since 9.2
795     */
796    protected void notifyCore(String userOrGroupId, String eventId, List<String> ancestorGroupIds) {
797        Map<String, Serializable> eventProperties = new HashMap<>();
798        eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY, USER_GROUP_CATEGORY);
799        eventProperties.put(ID_PROPERTY_KEY, userOrGroupId);
800        if (ancestorGroupIds != null) {
801            eventProperties.put(ANCESTOR_GROUPS_PROPERTY_KEY, (Serializable) ancestorGroupIds);
802        }
803        NuxeoPrincipal principal = NuxeoPrincipal.getCurrent();
804        UnboundEventContext envContext = new UnboundEventContext(principal, eventProperties);
805        envContext.setProperties(eventProperties);
806        EventProducer eventProducer = Framework.getService(EventProducer.class);
807        eventProducer.fireEvent(envContext.newEvent(eventId));
808    }
809
810    protected void notifyRuntime(String userOrGroupName, String eventId) {
811        EventService eventService = Framework.getService(EventService.class);
812        eventService.sendEvent(new Event(USERMANAGER_TOPIC, eventId, this, userOrGroupName));
813    }
814
815    @Override
816    public void notifyUserChanged(String userName, String eventId) {
817        invalidatePrincipal(userName);
818        notifyRuntime(userName, USERCHANGED_EVENT_ID);
819        if (eventId != null) {
820            notifyRuntime(userName, eventId);
821            notifyCore(userName, eventId);
822        }
823    }
824
825    protected void invalidatePrincipal(String userName) {
826        if (useCache()) {
827            principalCache.invalidate(userName);
828        }
829    }
830
831    @Override
832    public void notifyGroupChanged(String groupName, String eventId, List<String> ancestorGroupNames) {
833        invalidateAllPrincipals();
834        notifyRuntime(groupName, GROUPCHANGED_EVENT_ID);
835        if (eventId != null) {
836            notifyRuntime(groupName, eventId);
837            notifyCore(groupName, eventId, ancestorGroupNames);
838        }
839    }
840
841    protected void invalidateAllPrincipals() {
842        if (useCache()) {
843            principalCache.invalidateAll();
844        }
845    }
846
847    @Override
848    public Boolean areGroupsReadOnly() {
849        try (Session groupDir = dirService.open(groupDirectoryName)) {
850            return groupDir.isReadOnly();
851        } catch (DirectoryException e) {
852            log.error(e);
853            return false;
854        }
855    }
856
857    @Override
858    public Boolean areUsersReadOnly() {
859        try (Session userDir = dirService.open(userDirectoryName)) {
860            return userDir.isReadOnly();
861        } catch (DirectoryException e) {
862            log.error(e);
863            return false;
864        }
865    }
866
867    protected void checkGrouId(DocumentModel groupModel) {
868        // be sure the name does not contains trailing spaces
869        Object groupIdValue = groupModel.getProperty(groupSchemaName, groupIdField);
870        if (groupIdValue != null) {
871            groupModel.setProperty(groupSchemaName, groupIdField, groupIdValue.toString().trim());
872        }
873    }
874
875    protected String getGroupId(DocumentModel groupModel) {
876        Object groupIdValue = groupModel.getProperty(groupSchemaName, groupIdField);
877        if (groupIdValue != null && !(groupIdValue instanceof String)) {
878            throw new NuxeoException("Invalid group id " + groupIdValue);
879        }
880        return (String) groupIdValue;
881    }
882
883    protected void checkUserId(DocumentModel userModel) {
884        Object userIdValue = userModel.getProperty(userSchemaName, userIdField);
885        if (userIdValue != null) {
886            userModel.setProperty(userSchemaName, userIdField, userIdValue.toString().trim());
887        }
888    }
889
890    protected String getUserId(DocumentModel userModel) {
891        Object userIdValue = userModel.getProperty(userSchemaName, userIdField);
892        if (userIdValue != null && !(userIdValue instanceof String)) {
893            throw new NuxeoException("Invalid user id " + userIdValue);
894        }
895        return (String) userIdValue;
896    }
897
898    @Override
899    public DocumentModel createGroup(DocumentModel groupModel) {
900        return createGroup(groupModel, null);
901    }
902
903    @Override
904    public DocumentModel createUser(DocumentModel userModel) {
905        return createUser(userModel, null);
906    }
907
908    @Override
909    public void deleteGroup(String groupId) {
910        deleteGroup(groupId, null);
911    }
912
913    @Override
914    public void deleteGroup(DocumentModel groupModel) {
915        deleteGroup(groupModel, null);
916    }
917
918    @Override
919    public void deleteUser(String userId) {
920        deleteUser(userId, null);
921    }
922
923    @Override
924    public void deleteUser(DocumentModel userModel) {
925        String userId = getUserId(userModel);
926        deleteUser(userId);
927    }
928
929    @Override
930    public List<String> getGroupIds() {
931        try (Session groupDir = dirService.open(groupDirectoryName)) {
932            List<String> groupIds = groupDir.getProjection(Collections.<String, Serializable> emptyMap(),
933                    groupDir.getIdField());
934            Collections.sort(groupIds);
935            return groupIds;
936        }
937    }
938
939    @Override
940    public List<String> getUserIds() {
941        return getUserIds(null);
942    }
943
944    protected void removeVirtualFilters(Map<String, Serializable> filter) {
945        if (filter == null) {
946            return;
947        }
948        List<String> keys = new ArrayList<>(filter.keySet());
949        for (String key : keys) {
950            if (key.startsWith(VIRTUAL_FIELD_FILTER_PREFIX)) {
951                filter.remove(key);
952            }
953        }
954    }
955
956    protected QueryBuilder getQueryForPattern(String pattern, String dirName, Map<String, MatchType> searchFields,
957            OrderByExpr orderBy) {
958        QueryBuilder queryBuilder = new QueryBuilder();
959        if (!StringUtils.isBlank(pattern)) {
960            // build query
961            pattern = pattern.trim().toLowerCase();
962            String likePattern;
963            if (!useSearchEscapeCompat()) {
964                // replace characters that are meaningful in a LIKE pattern
965                likePattern = pattern.replace("\\", "\\\\");
966                likePattern = likePattern.replace("%", "\\%");
967                likePattern = likePattern.replace("_", "\\_");
968            } else {
969                // compat: don't do any escaping
970                likePattern = pattern;
971            }
972            List<Predicate> predicates = new ArrayList<>();
973            for (Entry<String, MatchType> fieldEntry : searchFields.entrySet()) {
974                String key = fieldEntry.getKey();
975                Predicate predicate;
976                if (fieldEntry.getValue() == MatchType.SUBSTRING) {
977                    Directory dir = dirService.getDirectory(dirName);
978                    SubstringMatchType substringMatchType = dir.getDescriptor().getSubstringMatchType();
979                    String value;
980                    switch (substringMatchType) {
981                    case subany:
982                        value = '%' + likePattern + '%';
983                        break;
984                    case subinitial:
985                        value = likePattern + '%';
986                        break;
987                    case subfinal:
988                        value = '%' + likePattern;
989                        break;
990                    default:
991                        throw new IllegalStateException(substringMatchType.toString());
992                    }
993                    predicate = Predicates.ilike(key, value);
994                } else { // MatchType.EXACT
995                    predicate = Predicates.eq(key, pattern);
996                }
997                predicates.add(predicate);
998            }
999            queryBuilder.filter(new MultiExpression(Operator.OR, predicates));
1000        }
1001        queryBuilder.order(orderBy);
1002        return queryBuilder;
1003    }
1004
1005    @Override
1006    public DocumentModelList searchGroups(Map<String, Serializable> filter, Set<String> fulltext) {
1007        return searchGroups(filter, fulltext, null);
1008    }
1009
1010    @Override
1011    public DocumentModelList searchGroups(QueryBuilder queryBuilder) {
1012        return searchGroups(queryBuilder, null);
1013    }
1014
1015    @Override
1016    public DocumentModelList searchUsers(String pattern) {
1017        return searchUsers(pattern, null);
1018    }
1019
1020    @Override
1021    public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext) {
1022        return searchUsers(filter, fulltext, getUserSortMap(), null);
1023    }
1024
1025    @Override
1026    public DocumentModelList searchUsers(QueryBuilder queryBuilder) {
1027        return searchUsers(queryBuilder, null);
1028    }
1029
1030    @Override
1031    public void updateGroup(DocumentModel groupModel) {
1032        updateGroup(groupModel, null);
1033    }
1034
1035    @Override
1036    public void updateUser(DocumentModel userModel) {
1037        updateUser(userModel, null);
1038    }
1039
1040    @Override
1041    public DocumentModel getBareGroupModel() {
1042        String schema = dirService.getDirectorySchema(groupDirectoryName);
1043        return BaseSession.createEntryModel(null, schema, null, null);
1044    }
1045
1046    @Override
1047    public List<String> getAdministratorsGroups() {
1048        return administratorGroups;
1049    }
1050
1051    protected List<String> getLeafPermissions(String perm) {
1052        ArrayList<String> permissions = new ArrayList<>();
1053        PermissionProvider permissionProvider = Framework.getService(PermissionProvider.class);
1054        String[] subpermissions = permissionProvider.getSubPermissions(perm);
1055        if (subpermissions == null || subpermissions.length <= 0) {
1056            // it's a leaf
1057            permissions.add(perm);
1058            return permissions;
1059        }
1060        for (String subperm : subpermissions) {
1061            permissions.addAll(getLeafPermissions(subperm));
1062        }
1063        return permissions;
1064    }
1065
1066    @Override
1067    public String[] getUsersForPermission(String perm, ACP acp) {
1068        return getUsersForPermission(perm, acp, null);
1069    }
1070
1071    @Override
1072    public Principal authenticate(String name, String password) {
1073        return checkUsernamePassword(name, password) ? getPrincipal(name) : null;
1074    }
1075
1076    /*************** MULTI-TENANT-IMPLEMENTATION ************************/
1077
1078    public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext,
1079            Map<String, String> orderBy, DocumentModel context) {
1080        try (Session userDir = dirService.open(userDirectoryName, context)) {
1081            removeVirtualFilters(filter);
1082
1083            // XXX: do not fetch references, can be costly
1084            DocumentModelList entries = userDir.query(filter, fulltext, null, false);
1085            if (isAnonymousMatching(filter, fulltext)) {
1086                entries.add(makeVirtualUserEntry(getAnonymousUserId(), anonymousUser));
1087            }
1088
1089            // TODO: match searchable virtual users
1090
1091            if (orderBy != null && !orderBy.isEmpty()) {
1092                // sort: cannot sort before virtual users are added
1093                entries.sort(new DocumentModelComparator(userSchemaName, orderBy));
1094            }
1095
1096            return entries;
1097        }
1098    }
1099
1100    @Override
1101    public List<String> getUsersInGroup(String groupId, DocumentModel context) {
1102        String storeGroupId = multiTenantManagement.groupnameTranformer(this, groupId, context);
1103        return getGroup(storeGroupId).getMemberUsers();
1104    }
1105
1106    @Override
1107    public DocumentModelList searchUsers(String pattern, DocumentModel context) {
1108        QueryBuilder queryBuilder = getQueryForPattern(pattern, userDirectoryName, userSearchFields, getUserOrderBy());
1109        return searchUsers(queryBuilder, context);
1110    }
1111
1112    @Override
1113    public DocumentModelList searchUsers(QueryBuilder queryBuilder, DocumentModel context) {
1114        Directory dir = dirService.getDirectory(userDirectoryName, context);
1115        try (Session session = dir.getSession()) {
1116            if (isAnonymousMatching(queryBuilder, dir)) {
1117                DocumentModel anonymousEntry = makeVirtualUserEntry(getAnonymousUserId(), anonymousUser);
1118                List<DocumentModel> virtualEntries = Collections.singletonList(anonymousEntry);
1119                return queryWithVirtualEntries(session, queryBuilder, virtualEntries);
1120            } else {
1121                return session.query(queryBuilder, false);
1122            }
1123        }
1124    }
1125
1126    /**
1127     * Executes a query then adds virtual entries (already supposed to match the query). Then does
1128     * limit/offset/order/countTotal.
1129     *
1130     * @since 10.3
1131     */
1132    protected DocumentModelList queryWithVirtualEntries(Session session, QueryBuilder queryBuilder,
1133            List<DocumentModel> virtualEntries) {
1134        AbstractDirectory dir = (AbstractDirectory) ((BaseSession) session).getDirectory();
1135
1136        // do the basic query
1137        DocumentModelList entries = session.query(queryBuilder, false);
1138
1139        int limit = Math.max(0, (int) queryBuilder.limit());
1140        int offset = Math.max(0, (int) queryBuilder.offset());
1141        boolean countTotal = queryBuilder.countTotal();
1142        OrderByList orders = queryBuilder.orders();
1143        int size = entries.size();
1144        long totalSize = entries.totalSize();
1145
1146        // if we have all the results or a page small enough that all virtual entries fit, just add them
1147        if (offset == 0 && (limit == 0 || size + virtualEntries.size() <= limit)) {
1148            // add virtual entries
1149            entries.addAll(virtualEntries);
1150            if (totalSize >= 0) {
1151                ((DocumentModelListImpl) entries).setTotalSize(totalSize + virtualEntries.size());
1152            }
1153            // re-sort
1154            dir.orderEntries(entries, AbstractDirectory.makeOrderBy(orders));
1155            return entries;
1156        }
1157
1158        // else we have to do a manual paging/sorting...
1159        // re-do the full query with limit/offset
1160        queryBuilder = new QueryBuilder(queryBuilder).limit(0).offset(0).orders(Collections.emptyList());
1161        entries = session.query(queryBuilder, false);
1162        // add virtual entries
1163        entries.addAll(virtualEntries);
1164        // sort
1165        if (!orders.isEmpty()) {
1166            dir.orderEntries(entries, AbstractDirectory.makeOrderBy(orders));
1167        }
1168        // manual limit/offset
1169        entries = ((BaseSession) session).applyQueryLimits(entries, limit, offset);
1170        if (!countTotal) { // offset != 0 always here
1171            // compat with other directories
1172            ((DocumentModelListImpl) entries).setTotalSize(-2);
1173        }
1174        return entries;
1175    }
1176
1177    @Override
1178    public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext,
1179            DocumentModel context) {
1180        throw new UnsupportedOperationException();
1181    }
1182
1183    @Override
1184    public List<String> getGroupIds(DocumentModel context) {
1185        throw new UnsupportedOperationException();
1186    }
1187
1188    @Override
1189    public DocumentModelList searchGroups(Map<String, Serializable> filter, Set<String> fulltext,
1190            DocumentModel context) {
1191        filter = filter != null ? cloneMap(filter) : new HashMap<>();
1192        HashSet<String> fulltextClone = fulltext != null ? cloneSet(fulltext) : new HashSet<>();
1193        multiTenantManagement.queryTransformer(this, filter, fulltextClone, context);
1194
1195        try (Session groupDir = dirService.open(groupDirectoryName, context)) {
1196            removeVirtualFilters(filter);
1197            return groupDir.query(filter, fulltextClone, getGroupSortMap(), false);
1198        }
1199    }
1200
1201    @Override
1202    public DocumentModelList searchGroups(QueryBuilder queryBuilder, DocumentModel context) {
1203        queryBuilder = multiTenantManagement.groupQueryTransformer(this, queryBuilder, context);
1204        try (Session session = dirService.open(groupDirectoryName, context)) {
1205            return session.query(queryBuilder, false);
1206        }
1207    }
1208
1209    @Override
1210    public DocumentModel createGroup(DocumentModel groupModel, DocumentModel context)
1211            throws GroupAlreadyExistsException {
1212        groupModel = multiTenantManagement.groupTransformer(this, groupModel, context);
1213
1214        // be sure the name does not contains trailing spaces
1215        checkGrouId(groupModel);
1216
1217        try (Session groupDir = dirService.open(groupDirectoryName, context)) {
1218            String groupId = getGroupId(groupModel);
1219
1220            // check the group does not exist
1221            if (groupDir.hasEntry(groupId)) {
1222                throw new GroupAlreadyExistsException();
1223            }
1224            groupModel = groupDir.createEntry(groupModel);
1225            notifyGroupChanged(groupId, GROUPCREATED_EVENT_ID);
1226            return groupModel;
1227
1228        }
1229    }
1230
1231    @Override
1232    public DocumentModel getGroupModel(String groupIdValue, DocumentModel context) {
1233        String groupName = multiTenantManagement.groupnameTranformer(this, groupIdValue, context);
1234        if (groupName != null) {
1235            groupName = groupName.trim();
1236        }
1237
1238        try (Session groupDir = dirService.open(groupDirectoryName, context)) {
1239            return groupDir.getEntry(groupName);
1240        }
1241    }
1242
1243    @Override
1244    public DocumentModel getUserModel(String userName, DocumentModel context) {
1245        return getUserModel(userName, context, true);
1246    }
1247
1248    protected DocumentModel getUserModel(String userName, DocumentModel context, boolean fetchReferences) {
1249        if (userName == null) {
1250            return null;
1251        }
1252
1253        userName = userName.trim();
1254        // return anonymous model
1255        if (anonymousUser != null && userName.equals(anonymousUser.getId())) {
1256            return makeVirtualUserEntry(getAnonymousUserId(), anonymousUser);
1257        }
1258
1259        try (Session userDir = dirService.open(userDirectoryName, context)) {
1260            DocumentModel userModel = userDir.getEntry(userName, fetchReferences);
1261            if (userModel != null && !fetchReferences) {
1262                userModel.putContextData(USER_HAS_PARTIAL_CONTENT, true);
1263            }
1264            return userModel;
1265        }
1266    }
1267
1268    protected Map<String, Serializable> cloneMap(Map<String, Serializable> map) {
1269        Map<String, Serializable> result = new HashMap<>();
1270        for (String key : map.keySet()) {
1271            result.put(key, map.get(key));
1272        }
1273        return result;
1274    }
1275
1276    protected HashSet<String> cloneSet(Set<String> set) {
1277        HashSet<String> result = new HashSet<>();
1278        for (String key : set) {
1279            result.add(key);
1280        }
1281        return result;
1282    }
1283
1284    @Override
1285    public NuxeoPrincipal getPrincipal(String username, DocumentModel context) {
1286        return getPrincipal(username, context, true);
1287    }
1288
1289    protected NuxeoPrincipal getPrincipal(String username, DocumentModel context, boolean fetchReferences) {
1290        if (username == null) {
1291            return null;
1292        }
1293        String anonymousUserId = getAnonymousUserId();
1294        if (username.equals(anonymousUserId)) {
1295            return makeAnonymousPrincipal();
1296        }
1297        if (virtualUsers.containsKey(username)) {
1298            return makeVirtualPrincipal(virtualUsers.get(username));
1299        }
1300        if (NuxeoPrincipal.isTransientUsername(username)) {
1301            return makeTransientPrincipal(username);
1302        }
1303        DocumentModel userModel = getUserModel(username, context, fetchReferences);
1304        if (userModel != null) {
1305            return makePrincipal(userModel);
1306        }
1307        return null;
1308    }
1309
1310    @Override
1311    public DocumentModelList searchGroups(String pattern, DocumentModel context) {
1312        QueryBuilder queryBuilder = getQueryForPattern(pattern, groupDirectoryName, groupSearchFields,
1313                getGroupOrderBy());
1314        return searchGroups(queryBuilder, context);
1315    }
1316
1317    @Override
1318    public List<String> getUserIds(DocumentModel context) {
1319        try (Session userDir = dirService.open(userDirectoryName, context)) {
1320            List<String> userIds = userDir.getProjection(Collections.<String, Serializable> emptyMap(),
1321                    userDir.getIdField());
1322            Collections.sort(userIds);
1323            return userIds;
1324        }
1325    }
1326
1327    @Override
1328    public DocumentModel createUser(DocumentModel userModel, DocumentModel context) throws UserAlreadyExistsException {
1329        // be sure UserId does not contains any trailing spaces
1330        checkUserId(userModel);
1331
1332        try (Session userDir = dirService.open(userDirectoryName, context)) {
1333            String userId = getUserId(userModel);
1334
1335            // check the user does not exist
1336            if (userDir.hasEntry(userId)) {
1337                throw new UserAlreadyExistsException();
1338            }
1339
1340            checkPasswordValidity(userModel);
1341
1342            String schema = dirService.getDirectorySchema(userDirectoryName);
1343            // If trying to create the user with groups, check that the groups exist
1344            checkGroupsExistence(userModel, schema, context);
1345
1346            String clearUsername = (String) userModel.getProperty(schema, userDir.getIdField());
1347            String clearPassword = (String) userModel.getProperty(schema, userDir.getPasswordField());
1348
1349            userModel = userDir.createEntry(userModel);
1350
1351            syncDigestAuthPassword(clearUsername, clearPassword);
1352
1353            notifyUserChanged(userId, USERCREATED_EVENT_ID);
1354            return userModel;
1355
1356        }
1357    }
1358
1359    @SuppressWarnings("unchecked")
1360    protected void checkGroupsExistence(DocumentModel userModel, String schema, DocumentModel context) {
1361        List<String> groups = (List<String>) userModel.getProperty(schema, GROUPS_COLUMN);
1362        if (groups == null || groups.isEmpty()) {
1363            return;
1364        }
1365        try (Session groupDir = dirService.open(groupDirectoryName, context)) {
1366            for (String group : groups) {
1367                if (!groupDir.hasEntry(group)) {
1368                    throw new NuxeoException("group does not exist: " + group, SC_FORBIDDEN);
1369                }
1370            }
1371        }
1372    }
1373
1374    protected void checkPasswordValidity(DocumentModel userModel) throws InvalidPasswordException {
1375        if (!mustCheckPasswordValidity()) {
1376            return;
1377        }
1378        String schema = dirService.getDirectorySchema(userDirectoryName);
1379        String passwordField = dirService.getDirectory(userDirectoryName).getPasswordField();
1380
1381        Property passwordProperty = userModel.getPropertyObject(schema, passwordField);
1382
1383        if (passwordProperty.isDirty()) {
1384            String clearPassword = (String) passwordProperty.getValue();
1385            if (StringUtils.isNotBlank(clearPassword) && !validatePassword(clearPassword)) {
1386                throw new InvalidPasswordException();
1387            }
1388        }
1389    }
1390
1391    @Override
1392    public void updateUser(DocumentModel userModel, DocumentModel context) {
1393        try (Session userDir = dirService.open(userDirectoryName, context)) {
1394            String userId = getUserId(userModel);
1395
1396            if (!userDir.hasEntry(userId)) {
1397                throw new DirectoryException("user does not exist: " + userId);
1398            }
1399
1400            String schema = dirService.getDirectorySchema(userDirectoryName);
1401            // If trying to update the user with groups, check that the groups exist
1402            checkGroupsExistence(userModel, schema, context);
1403
1404            checkPasswordValidity(userModel);
1405
1406            String clearUsername = (String) userModel.getProperty(schema, userDir.getIdField());
1407            String clearPassword = (String) userModel.getProperty(schema, userDir.getPasswordField());
1408
1409            userDir.updateEntry(userModel);
1410
1411            syncDigestAuthPassword(clearUsername, clearPassword);
1412
1413            notifyUserChanged(userId, USERMODIFIED_EVENT_ID);
1414        }
1415    }
1416
1417    private boolean mustCheckPasswordValidity() {
1418        return Framework.getService(ConfigurationService.class).isBooleanTrue(VALIDATE_PASSWORD_PARAM);
1419    }
1420
1421    protected boolean useSearchEscapeCompat() {
1422        return Framework.getService(ConfigurationService.class).isBooleanTrue(SEARCH_ESCAPE_COMPAT_PARAM);
1423    }
1424
1425    @Override
1426    public void deleteUser(DocumentModel userModel, DocumentModel context) {
1427        String userId = getUserId(userModel);
1428        deleteUser(userId, context);
1429    }
1430
1431    @Override
1432    public void deleteUser(String userId, DocumentModel context) {
1433        try (Session userDir = dirService.open(userDirectoryName, context)) {
1434            if (!userDir.hasEntry(userId)) {
1435                throw new DirectoryException("User does not exist: " + userId);
1436            }
1437            userDir.deleteEntry(userId);
1438            notifyUserChanged(userId, USERDELETED_EVENT_ID);
1439
1440        } finally {
1441            notifyUserChanged(userId, null);
1442        }
1443    }
1444
1445    @Override
1446    public void updateGroup(DocumentModel groupModel, DocumentModel context) {
1447        try (Session groupDir = dirService.open(groupDirectoryName, context)) {
1448            String groupId = getGroupId(groupModel);
1449
1450            if (!groupDir.hasEntry(groupId)) {
1451                throw new DirectoryException("group does not exist: " + groupId);
1452            }
1453            groupDir.updateEntry(groupModel);
1454            notifyGroupChanged(groupId, GROUPMODIFIED_EVENT_ID);
1455        }
1456    }
1457
1458    @Override
1459    public void deleteGroup(DocumentModel groupModel, DocumentModel context) {
1460        String groupId = getGroupId(groupModel);
1461        deleteGroup(groupId, context);
1462    }
1463
1464    @Override
1465    public void deleteGroup(String groupId, DocumentModel context) {
1466        try (Session groupDir = dirService.open(groupDirectoryName, context)) {
1467            if (!groupDir.hasEntry(groupId)) {
1468                throw new DirectoryException("Group does not exist: " + groupId);
1469            }
1470            // Get ancestor group names before deletion to pass them as a property of the core event
1471            List<String> ancestorGroupNames = getAncestorGroups(groupId);
1472            groupDir.deleteEntry(groupId);
1473            notifyGroupChanged(groupId, GROUPDELETED_EVENT_ID, ancestorGroupNames);
1474        }
1475    }
1476
1477    @Override
1478    public List<String> getGroupsInGroup(String parentId, DocumentModel context) {
1479        return getGroup(parentId, null).getMemberGroups();
1480    }
1481
1482    @Override
1483    public List<String> getTopLevelGroups(DocumentModel context) {
1484        try (Session groupDir = dirService.open(groupDirectoryName, context)) {
1485            List<String> topLevelGroups = new LinkedList<>();
1486            // XXX retrieve all entries with references, can be costly.
1487            DocumentModelList groups = groupDir.query(Collections.<String, Serializable> emptyMap(), null, null, true);
1488            for (DocumentModel group : groups) {
1489                @SuppressWarnings("unchecked")
1490                List<String> parents = (List<String>) group.getProperty(groupSchemaName, groupParentGroupsField);
1491
1492                if (parents == null || parents.isEmpty()) {
1493                    topLevelGroups.add(group.getId());
1494                }
1495            }
1496            return topLevelGroups;
1497        }
1498    }
1499
1500    @Override
1501    public List<String> getUsersInGroupAndSubGroups(String groupId, DocumentModel context) {
1502        Set<String> groups = new HashSet<>();
1503        groups.add(groupId);
1504        appendSubgroups(groupId, groups, context);
1505
1506        Set<String> users = new HashSet<>();
1507        for (String groupid : groups) {
1508            users.addAll(getGroup(groupid, context).getMemberUsers());
1509        }
1510
1511        return new ArrayList<>(users);
1512    }
1513
1514    @Override
1515    public String[] getUsersForPermission(String perm, ACP acp, DocumentModel context) {
1516        PermissionProvider permissionProvider = Framework.getService(PermissionProvider.class);
1517        // using a hashset to avoid duplicates
1518        HashSet<String> usernames = new HashSet<>();
1519
1520        ACL merged = acp.getMergedACLs("merged");
1521        // The list of permission that is has "perm" as its (compound)
1522        // permission
1523        ArrayList<ACE> filteredACEbyPerm = new ArrayList<>();
1524
1525        List<String> currentPermissions = getLeafPermissions(perm);
1526
1527        for (ACE ace : merged.getACEs()) {
1528            // Checking if the permission contains the permission we want to
1529            // check (we use the security service method for coumpound
1530            // permissions)
1531            List<String> acePermissions = getLeafPermissions(ace.getPermission());
1532
1533            // Everything is a special permission (not compound)
1534            if (SecurityConstants.EVERYTHING.equals(ace.getPermission())) {
1535                acePermissions = Arrays.asList(permissionProvider.getPermissions());
1536            }
1537
1538            if (acePermissions.containsAll(currentPermissions)) {
1539                // special case: everybody perm grant false, don't take in
1540                // account the previous ace
1541                if (SecurityConstants.EVERYONE.equals(ace.getUsername()) && !ace.isGranted()) {
1542                    break;
1543                }
1544                filteredACEbyPerm.add(ace);
1545            }
1546        }
1547
1548        for (ACE ace : filteredACEbyPerm) {
1549            String aceUsername = ace.getUsername();
1550            List<String> users = null;
1551            // If everyone, add/remove all the users
1552            if (SecurityConstants.EVERYONE.equals(aceUsername)) {
1553                users = getUserIds();
1554            }
1555            // if a group, add/remove all the user from the group (and
1556            // subgroups)
1557            if (users == null) {
1558                NuxeoGroup group;
1559                group = getGroup(aceUsername, context);
1560                if (group != null) {
1561                    users = getUsersInGroupAndSubGroups(aceUsername, context);
1562                }
1563
1564            }
1565            // otherwise, add the user
1566            if (users == null) {
1567                users = new ArrayList<>();
1568                users.add(aceUsername);
1569            }
1570            if (ace.isGranted()) {
1571                usernames.addAll(users);
1572            } else {
1573                usernames.removeAll(users);
1574            }
1575        }
1576        return usernames.toArray(new String[usernames.size()]);
1577    }
1578
1579    @Override
1580    public List<String> getAncestorGroups(String groupId) {
1581        List<String> ancestorGroups = new ArrayList<>();
1582        populateAncestorGroups(groupId, ancestorGroups);
1583        return ancestorGroups;
1584    }
1585
1586    protected void populateAncestorGroups(String groupId, List<String> ancestorGroups) {
1587        NuxeoGroup group = getGroup(groupId);
1588        if (group != null) {
1589            List<String> parentGroups = group.getParentGroups();
1590            // Avoid infinite loop in case a group has one of its parents as a subgroup
1591            parentGroups.stream().filter(parentGroup -> !ancestorGroups.contains(parentGroup)).forEach(parentGroup -> {
1592                ancestorGroups.add(parentGroup);
1593                populateAncestorGroups(parentGroup, ancestorGroups);
1594            });
1595        }
1596    }
1597
1598    @Override
1599    public List<String> getDescendantGroups(String groupId) {
1600        List<String> descendantGroups = new ArrayList<>();
1601        populateDescendantGroups(groupId, descendantGroups);
1602        return descendantGroups;
1603    }
1604
1605    protected void populateDescendantGroups(String groupId, List<String> descendantGroups) {
1606        NuxeoGroup group = getGroup(groupId);
1607        if (group != null) {
1608            List<String> subGroups = group.getMemberGroups();
1609            subGroups.stream().filter(subGroup -> !descendantGroups.contains(subGroup)).forEach(subGroup -> {
1610                descendantGroups.add(subGroup);
1611                populateDescendantGroups(subGroup, descendantGroups);
1612            });
1613        }
1614    }
1615
1616    @Override
1617    public GroupConfig getGroupConfig() {
1618        return groupConfig;
1619    }
1620
1621    @Override
1622    public void handleEvent(Event event) {
1623        String id = event.getId();
1624        if (INVALIDATE_PRINCIPAL_EVENT_ID.equals(id)) {
1625            invalidatePrincipal((String) event.getData());
1626        } else if (INVALIDATE_ALL_PRINCIPALS_EVENT_ID.equals(id)) {
1627            invalidateAllPrincipals();
1628        }
1629    }
1630
1631}