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