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