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