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