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