/*
* (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Contributors:
* George Lefter
* Florent Guillaume
* Anahide Tchertchian
* Gagnavarslan ehf
*/
package org.nuxeo.ecm.platform.usermanager;
import java.io.Serializable;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.DocumentModel;
import org.nuxeo.ecm.core.api.DocumentModelComparator;
import org.nuxeo.ecm.core.api.DocumentModelList;
import org.nuxeo.ecm.core.api.NuxeoException;
import org.nuxeo.ecm.core.api.NuxeoGroup;
import org.nuxeo.ecm.core.api.NuxeoPrincipal;
import org.nuxeo.ecm.core.api.PropertyException;
import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
import org.nuxeo.ecm.core.api.impl.NuxeoGroupImpl;
import org.nuxeo.ecm.core.api.local.ClientLoginModule;
import org.nuxeo.ecm.core.api.model.Property;
import org.nuxeo.ecm.core.api.model.PropertyNotFoundException;
import org.nuxeo.ecm.core.api.security.ACE;
import org.nuxeo.ecm.core.api.security.ACL;
import org.nuxeo.ecm.core.api.security.ACP;
import org.nuxeo.ecm.core.api.security.AdministratorGroupsProvider;
import org.nuxeo.ecm.core.api.security.PermissionProvider;
import org.nuxeo.ecm.core.api.security.SecurityConstants;
import org.nuxeo.ecm.core.cache.Cache;
import org.nuxeo.ecm.core.cache.CacheService;
import org.nuxeo.ecm.core.event.EventProducer;
import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
import org.nuxeo.ecm.core.event.impl.UnboundEventContext;
import org.nuxeo.ecm.directory.BaseSession;
import org.nuxeo.ecm.directory.DirectoryException;
import org.nuxeo.ecm.directory.Session;
import org.nuxeo.ecm.directory.api.DirectoryService;
import org.nuxeo.ecm.platform.usermanager.exceptions.GroupAlreadyExistsException;
import org.nuxeo.ecm.platform.usermanager.exceptions.InvalidPasswordException;
import org.nuxeo.ecm.platform.usermanager.exceptions.UserAlreadyExistsException;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.services.config.ConfigurationService;
import org.nuxeo.runtime.services.event.Event;
import org.nuxeo.runtime.services.event.EventService;
/**
* Standard implementation of the Nuxeo UserManager.
*/
public class UserManagerImpl implements UserManager, MultiTenantUserManager, AdministratorGroupsProvider {
private static final String VALIDATE_PASSWORD_PARAM = "nuxeo.usermanager.check.password";
private static final long serialVersionUID = 1L;
private static final Log log = LogFactory.getLog(UserManagerImpl.class);
public static final String USERMANAGER_TOPIC = "usermanager";
/** Used by JaasCacheFlusher. */
public static final String USERCHANGED_EVENT_ID = "user_changed";
public static final String USERCREATED_EVENT_ID = "user_created";
public static final String USERDELETED_EVENT_ID = "user_deleted";
public static final String USERMODIFIED_EVENT_ID = "user_modified";
/** Used by JaasCacheFlusher. */
public static final String GROUPCHANGED_EVENT_ID = "group_changed";
public static final String GROUPCREATED_EVENT_ID = "group_created";
public static final String GROUPDELETED_EVENT_ID = "group_deleted";
public static final String GROUPMODIFIED_EVENT_ID = "group_modified";
public static final String DEFAULT_ANONYMOUS_USER_ID = "Anonymous";
public static final String VIRTUAL_FIELD_FILTER_PREFIX = "__";
public static final String INVALIDATE_PRINCIPAL_EVENT_ID = "invalidatePrincipal";
public static final String INVALIDATE_ALL_PRINCIPALS_EVENT_ID = "invalidateAllPrincipals";
/**
* Possible value for the {@link DocumentEventContext#CATEGORY_PROPERTY_KEY} key of a core event context.
*
* @since 9.2
*/
public static final String USER_GROUP_CATEGORY = "userGroup";
/**
* Key for the id of a user or a group in a core event context.
*
* @since 9.2
*/
public static final String ID_PROPERTY_KEY = "id";
/**
* Key for the ancestor group names of a group in a core event context.
*
* @since 9.2
*/
public static final String ANCESTOR_GROUPS_PROPERTY_KEY = "ancestorGroups";
protected final DirectoryService dirService;
protected final CacheService cacheService;
protected Cache principalCache = null;
public UserMultiTenantManagement multiTenantManagement = new DefaultUserMultiTenantManagement();
/**
* A structure used to inject field name configuration of users schema into a NuxeoPrincipalImpl instance. TODO not
* all fields inside are configurable for now - they will use default values
*/
protected UserConfig userConfig;
protected String userDirectoryName;
protected String userSchemaName;
protected String userIdField;
protected String userEmailField;
protected Map<String, MatchType> userSearchFields;
protected String groupDirectoryName;
protected String groupSchemaName;
protected String groupIdField;
protected String groupLabelField;
protected String groupMembersField;
protected String groupSubGroupsField;
protected String groupParentGroupsField;
protected String groupSortField;
protected Map<String, MatchType> groupSearchFields;
protected String defaultGroup;
protected List<String> administratorIds;
protected List<String> administratorGroups;
protected Boolean disableDefaultAdministratorsGroup;
protected String userSortField;
protected String userListingMode;
protected String groupListingMode;
protected Pattern userPasswordPattern;
protected VirtualUser anonymousUser;
protected String digestAuthDirectory;
protected String digestAuthRealm;
protected final Map<String, VirtualUserDescriptor> virtualUsers;
public UserManagerImpl() {
dirService = Framework.getLocalService(DirectoryService.class);
cacheService = Framework.getLocalService(CacheService.class);
virtualUsers = new HashMap<>();
userConfig = new UserConfig();
}
@Override
public void setConfiguration(UserManagerDescriptor descriptor) {
defaultGroup = descriptor.defaultGroup;
administratorIds = descriptor.defaultAdministratorIds;
disableDefaultAdministratorsGroup = false;
if (descriptor.disableDefaultAdministratorsGroup != null) {
disableDefaultAdministratorsGroup = descriptor.disableDefaultAdministratorsGroup;
}
administratorGroups = new ArrayList<>();
if (!disableDefaultAdministratorsGroup) {
administratorGroups.add(SecurityConstants.ADMINISTRATORS);
}
if (descriptor.administratorsGroups != null) {
administratorGroups.addAll(descriptor.administratorsGroups);
}
if (administratorGroups.isEmpty()) {
log.warn("No administrators group has been defined: at least one should be set"
+ " to avoid lockups when blocking rights for instance");
}
userSortField = descriptor.userSortField;
groupSortField = descriptor.groupSortField;
userListingMode = descriptor.userListingMode;
groupListingMode = descriptor.groupListingMode;
userEmailField = descriptor.userEmailField;
userSearchFields = descriptor.userSearchFields;
userPasswordPattern = descriptor.userPasswordPattern;
groupLabelField = descriptor.groupLabelField;
groupMembersField = descriptor.groupMembersField;
groupSubGroupsField = descriptor.groupSubGroupsField;
groupParentGroupsField = descriptor.groupParentGroupsField;
groupSearchFields = descriptor.groupSearchFields;
anonymousUser = descriptor.anonymousUser;
setUserDirectoryName(descriptor.userDirectoryName);
setGroupDirectoryName(descriptor.groupDirectoryName);
setVirtualUsers(descriptor.virtualUsers);
digestAuthDirectory = descriptor.digestAuthDirectory;
digestAuthRealm = descriptor.digestAuthRealm;
userConfig = new UserConfig();
userConfig.emailKey = userEmailField;
userConfig.schemaName = userSchemaName;
userConfig.nameKey = userIdField;
if (cacheService != null && descriptor.userCacheName != null) {
principalCache = cacheService.getCache(descriptor.userCacheName);
invalidateAllPrincipals();
}
}
protected void setUserDirectoryName(String userDirectoryName) {
this.userDirectoryName = userDirectoryName;
userSchemaName = dirService.getDirectorySchema(userDirectoryName);
userIdField = dirService.getDirectoryIdField(userDirectoryName);
}
@Override
public String getUserDirectoryName() {
return userDirectoryName;
}
@Override
public String getUserIdField() {
return userIdField;
}
@Override
public String getUserSchemaName() {
return userSchemaName;
}
@Override
public String getUserEmailField() {
return userEmailField;
}
@Override
public Set<String> getUserSearchFields() {
return Collections.unmodifiableSet(userSearchFields.keySet());
}
@Override
public Set<String> getGroupSearchFields() {
return Collections.unmodifiableSet(groupSearchFields.keySet());
}
protected void setGroupDirectoryName(String groupDirectoryName) {
this.groupDirectoryName = groupDirectoryName;
groupSchemaName = dirService.getDirectorySchema(groupDirectoryName);
groupIdField = dirService.getDirectoryIdField(groupDirectoryName);
}
@Override
public String getGroupDirectoryName() {
return groupDirectoryName;
}
@Override
public String getGroupIdField() {
return groupIdField;
}
@Override
public String getGroupLabelField() {
return groupLabelField;
}
@Override
public String getGroupSchemaName() {
return groupSchemaName;
}
@Override
public String getGroupMembersField() {
return groupMembersField;
}
@Override
public String getGroupSubGroupsField() {
return groupSubGroupsField;
}
@Override
public String getGroupParentGroupsField() {
return groupParentGroupsField;
}
@Override
public String getUserListingMode() {
return userListingMode;
}
@Override
public String getGroupListingMode() {
return groupListingMode;
}
@Override
public String getDefaultGroup() {
return defaultGroup;
}
@Override
public Pattern getUserPasswordPattern() {
return userPasswordPattern;
}
@Override
public String getAnonymousUserId() {
if (anonymousUser == null) {
return null;
}
String anonymousUserId = anonymousUser.getId();
if (anonymousUserId == null) {
return DEFAULT_ANONYMOUS_USER_ID;
}
return anonymousUserId;
}
protected void setVirtualUsers(Map<String, VirtualUserDescriptor> virtualUsers) {
this.virtualUsers.clear();
if (virtualUsers != null) {
this.virtualUsers.putAll(virtualUsers);
}
}
@Override
public boolean checkUsernamePassword(String username, String password) {
if (username == null || password == null) {
log.warn("Trying to authenticate against null username or password");
return false;
}
// deal with anonymous user
String anonymousUserId = getAnonymousUserId();
if (username.equals(anonymousUserId)) {
log.warn(String.format("Trying to authenticate anonymous user (%s)", anonymousUserId));
return false;
}
// deal with virtual users
if (virtualUsers.containsKey(username)) {
VirtualUser user = virtualUsers.get(username);
String expected = user.getPassword();
if (expected == null) {
return false;
}
return expected.equals(password);
}
String userDirName;
// BBB backward compat for userDirectory + userAuthentication
if ("userDirectory".equals(userDirectoryName) && dirService.getDirectory("userAuthentication") != null) {
userDirName = "userAuthentication";
} else {
userDirName = userDirectoryName;
}
try (Session userDir = dirService.open(userDirName)) {
if (!userDir.isAuthenticating()) {
log.error("Trying to authenticate against a non authenticating " + "directory: " + userDirName);
return false;
}
boolean authenticated = userDir.authenticate(username, password);
if (authenticated) {
syncDigestAuthPassword(username, password);
}
return authenticated;
}
}
protected void syncDigestAuthPassword(String username, String password) {
if (StringUtils.isEmpty(digestAuthDirectory) || StringUtils.isEmpty(digestAuthRealm) || username == null
|| password == null) {
return;
}
String ha1 = encodeDigestAuthPassword(username, digestAuthRealm, password);
try (Session dir = dirService.open(digestAuthDirectory)) {
dir.setReadAllColumns(true); // needed to read digest password
String schema = dirService.getDirectorySchema(digestAuthDirectory);
DocumentModel entry = dir.getEntry(username, true);
if (entry == null) {
entry = getDigestAuthModel();
entry.setProperty(schema, dir.getIdField(), username);
entry.setProperty(schema, dir.getPasswordField(), ha1);
dir.createEntry(entry);
log.debug("Created digest auth password for user:" + username);
} else {
String storedHa1 = (String) entry.getProperty(schema, dir.getPasswordField());
if (!ha1.equals(storedHa1)) {
entry.setProperty(schema, dir.getPasswordField(), ha1);
dir.updateEntry(entry);
log.debug("Updated digest auth password for user:" + username);
}
}
} catch (DirectoryException e) {
log.warn("Digest auth password not synchronized, check your configuration", e);
}
}
protected DocumentModel getDigestAuthModel() {
String schema = dirService.getDirectorySchema(digestAuthDirectory);
return BaseSession.createEntryModel(null, schema, null, null);
}
public static String encodeDigestAuthPassword(String username, String realm, String password) {
String a1 = username + ":" + realm + ":" + password;
return DigestUtils.md5Hex(a1);
}
@Override
public String getDigestAuthDirectory() {
return digestAuthDirectory;
}
@Override
public String getDigestAuthRealm() {
return digestAuthRealm;
}
@Override
public boolean validatePassword(String password) {
if (userPasswordPattern == null) {
return true;
} else {
Matcher userPasswordMatcher = userPasswordPattern.matcher(password);
return userPasswordMatcher.find();
}
}
protected NuxeoPrincipal makeAnonymousPrincipal() {
DocumentModel userEntry = makeVirtualUserEntry(getAnonymousUserId(), anonymousUser);
// XXX: pass anonymous user groups, but they will be ignored
return makePrincipal(userEntry, true, anonymousUser.getGroups());
}
protected NuxeoPrincipal makeVirtualPrincipal(VirtualUser user) {
DocumentModel userEntry = makeVirtualUserEntry(user.getId(), user);
return makePrincipal(userEntry, false, user.getGroups());
}
protected NuxeoPrincipal makeTransientPrincipal(String username) {
DocumentModel userEntry = BaseSession.createEntryModel(null, userSchemaName, username, null);
userEntry.setProperty(userSchemaName, userIdField, username);
NuxeoPrincipal principal = makePrincipal(userEntry, false, true, null);
String[] parts = username.split("/");
String email = parts[1];
principal.setFirstName(email);
principal.setEmail(email);
return principal;
}
protected DocumentModel makeVirtualUserEntry(String id, VirtualUser user) {
final DocumentModel userEntry = BaseSession.createEntryModel(null, userSchemaName, id, null);
// at least fill id field
userEntry.setProperty(userSchemaName, userIdField, id);
for (Entry<String, Serializable> prop : user.getProperties().entrySet()) {
try {
userEntry.setProperty(userSchemaName, prop.getKey(), prop.getValue());
} catch (PropertyNotFoundException ce) {
log.error("Property: " + prop.getKey() + " does not exists. Check your " + "UserService configuration.",
ce);
}
}
return userEntry;
}
protected NuxeoPrincipal makePrincipal(DocumentModel userEntry) {
return makePrincipal(userEntry, false, null);
}
protected NuxeoPrincipal makePrincipal(DocumentModel userEntry, boolean anonymous, List<String> groups) {
return makePrincipal(userEntry, anonymous, false, groups);
}
protected NuxeoPrincipal makePrincipal(DocumentModel userEntry, boolean anonymous, boolean isTransient,
List<String> groups) {
boolean admin = false;
String username = userEntry.getId();
List<String> virtualGroups = new LinkedList<>();
// Add preconfigured groups: useful for LDAP, not for anonymous users
if (defaultGroup != null && !anonymous && !isTransient) {
virtualGroups.add(defaultGroup);
}
// Add additional groups: useful for virtual users
if (groups != null && !isTransient) {
virtualGroups.addAll(groups);
}
// Create a default admin if needed
if (administratorIds != null && administratorIds.contains(username)) {
admin = true;
if (administratorGroups != null) {
virtualGroups.addAll(administratorGroups);
}
}
NuxeoPrincipalImpl principal = new NuxeoPrincipalImpl(username, anonymous, admin, false);
principal.setConfig(userConfig);
principal.setModel(userEntry, false);
principal.setVirtualGroups(virtualGroups, true);
// TODO: reenable roles initialization once we have a use case for
// a role directory. In the mean time we only set the JBOSS role
// that is required to login
List<String> roles = Collections.singletonList("regular");
principal.setRoles(roles);
return principal;
}
protected boolean useCache() {
return principalCache != null;
}
@Override
public NuxeoPrincipal getPrincipal(String username) {
if (useCache()) {
return getPrincipalUsingCache(username);
}
return getPrincipal(username, null);
}
protected NuxeoPrincipal getPrincipalUsingCache(String username) {
NuxeoPrincipal ret = (NuxeoPrincipal) principalCache.get(username);
if (ret == null) {
ret = getPrincipal(username, null);
if (ret == null) {
return ret;
}
principalCache.put(username, ret);
}
return ((NuxeoPrincipalImpl) ret).cloneTransferable(); // should not return cached principal
}
@Override
public DocumentModel getUserModel(String userName) {
return getUserModel(userName, null);
}
@Override
public DocumentModel getBareUserModel() {
String schema = dirService.getDirectorySchema(userDirectoryName);
return BaseSession.createEntryModel(null, schema, null, null);
}
@Override
public NuxeoGroup getGroup(String groupName) {
return getGroup(groupName, null);
}
protected NuxeoGroup getGroup(String groupName, DocumentModel context) {
DocumentModel groupEntry = getGroupModel(groupName, context);
if (groupEntry != null) {
return makeGroup(groupEntry);
}
return null;
}
@Override
public DocumentModel getGroupModel(String groupName) {
return getGroupModel(groupName, null);
}
@SuppressWarnings("unchecked")
protected NuxeoGroup makeGroup(DocumentModel groupEntry) {
NuxeoGroup group = new NuxeoGroupImpl(groupEntry.getId());
List<String> list;
try {
list = (List<String>) groupEntry.getProperty(groupSchemaName, groupMembersField);
} catch (PropertyException e) {
list = null;
}
if (list != null) {
group.setMemberUsers(list);
}
try {
list = (List<String>) groupEntry.getProperty(groupSchemaName, groupSubGroupsField);
} catch (PropertyException e) {
list = null;
}
if (list != null) {
group.setMemberGroups(list);
}
try {
list = (List<String>) groupEntry.getProperty(groupSchemaName, groupParentGroupsField);
} catch (PropertyException e) {
list = null;
}
if (list != null) {
group.setParentGroups(list);
}
try {
String label = (String) groupEntry.getProperty(groupSchemaName, groupLabelField);
if (label != null) {
group.setLabel(label);
}
} catch (PropertyException e) {
// Nothing to do.
}
return group;
}
@Override
public List<String> getTopLevelGroups() {
return getTopLevelGroups(null);
}
@Override
public List<String> getGroupsInGroup(String parentId) {
NuxeoGroup group = getGroup(parentId, null);
if (group != null) {
return group.getMemberGroups();
} else {
return Collections.emptyList();
}
}
@Override
public List<String> getUsersInGroup(String groupId) {
return getGroup(groupId).getMemberUsers();
}
@Override
public List<String> getUsersInGroupAndSubGroups(String groupId) {
return getUsersInGroupAndSubGroups(groupId, null);
}
protected void appendSubgroups(String groupId, Set<String> groups, DocumentModel context) {
List<String> groupsToAppend = getGroupsInGroup(groupId, context);
groups.addAll(groupsToAppend);
for (String subgroupId : groupsToAppend) {
groups.add(subgroupId);
// avoiding infinite loop
if (!groups.contains(subgroupId)) {
appendSubgroups(subgroupId, groups, context);
}
}
}
protected boolean isAnonymousMatching(Map<String, Serializable> filter, Set<String> fulltext) {
String anonymousUserId = getAnonymousUserId();
if (anonymousUserId == null) {
return false;
}
if (filter == null || filter.isEmpty()) {
return true;
}
Map<String, Serializable> anonymousUserMap = anonymousUser.getProperties();
anonymousUserMap.put(userIdField, anonymousUserId);
for (Entry<String, Serializable> e : filter.entrySet()) {
String fieldName = e.getKey();
Object expected = e.getValue();
Object value = anonymousUserMap.get(fieldName);
if (value == null) {
if (expected != null) {
return false;
}
} else {
if (fulltext != null && fulltext.contains(fieldName)) {
if (!value.toString().toLowerCase().startsWith(expected.toString().toLowerCase())) {
return false;
}
} else {
if (!value.equals(expected)) {
return false;
}
}
}
}
return true;
}
@Override
public List<NuxeoPrincipal> searchPrincipals(String pattern) {
DocumentModelList entries = searchUsers(pattern);
List<NuxeoPrincipal> principals = new ArrayList<>(entries.size());
for (DocumentModel entry : entries) {
principals.add(makePrincipal(entry));
}
return principals;
}
@Override
public DocumentModelList searchGroups(String pattern) {
return searchGroups(pattern, null);
}
@Override
public String getUserSortField() {
return userSortField;
}
protected Map<String, String> getUserSortMap() {
return getDirectorySortMap(userSortField, userIdField);
}
protected Map<String, String> getGroupSortMap() {
return getDirectorySortMap(groupSortField, groupIdField);
}
protected Map<String, String> getDirectorySortMap(String descriptorSortField, String fallBackField) {
String sortField = descriptorSortField != null ? descriptorSortField : fallBackField;
Map<String, String> orderBy = new HashMap<>();
orderBy.put(sortField, DocumentModelComparator.ORDER_ASC);
return orderBy;
}
/**
* @since 8.2
*/
protected void notifyCore(String userOrGroupId, String eventId) {
notifyCore(userOrGroupId, eventId, null);
}
/**
* @since 9.2
*/
protected void notifyCore(String userOrGroupId, String eventId, List<String> ancestorGroupIds) {
Map<String, Serializable> eventProperties = new HashMap<>();
eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY, USER_GROUP_CATEGORY);
eventProperties.put(ID_PROPERTY_KEY, userOrGroupId);
if (ancestorGroupIds != null) {
eventProperties.put(ANCESTOR_GROUPS_PROPERTY_KEY, (Serializable) ancestorGroupIds);
}
NuxeoPrincipal principal = ClientLoginModule.getCurrentPrincipal();
UnboundEventContext envContext = new UnboundEventContext(principal, eventProperties);
envContext.setProperties(eventProperties);
EventProducer eventProducer = Framework.getService(EventProducer.class);
eventProducer.fireEvent(envContext.newEvent(eventId));
}
protected void notifyRuntime(String userOrGroupName, String eventId) {
EventService eventService = Framework.getService(EventService.class);
eventService.sendEvent(new Event(USERMANAGER_TOPIC, eventId, this, userOrGroupName));
}
@Override
public void notifyUserChanged(String userName, String eventId) {
invalidatePrincipal(userName);
notifyRuntime(userName, USERCHANGED_EVENT_ID);
if (eventId != null) {
notifyRuntime(userName, eventId);
notifyCore(userName, eventId);
}
}
protected void invalidatePrincipal(String userName) {
if (useCache()) {
principalCache.invalidate(userName);
}
}
@Override
public void notifyGroupChanged(String groupName, String eventId, List<String> ancestorGroupNames) {
invalidateAllPrincipals();
notifyRuntime(groupName, GROUPCHANGED_EVENT_ID);
if (eventId != null) {
notifyRuntime(groupName, eventId);
notifyCore(groupName, eventId, ancestorGroupNames);
}
}
protected void invalidateAllPrincipals() {
if (useCache()) {
principalCache.invalidateAll();
}
}
@Override
public Boolean areGroupsReadOnly() {
try (Session groupDir = dirService.open(groupDirectoryName)) {
return groupDir.isReadOnly();
} catch (DirectoryException e) {
log.error(e);
return false;
}
}
@Override
public Boolean areUsersReadOnly() {
try (Session userDir = dirService.open(userDirectoryName)) {
return userDir.isReadOnly();
} catch (DirectoryException e) {
log.error(e);
return false;
}
}
protected void checkGrouId(DocumentModel groupModel) {
// be sure the name does not contains trailing spaces
Object groupIdValue = groupModel.getProperty(groupSchemaName, groupIdField);
if (groupIdValue != null) {
groupModel.setProperty(groupSchemaName, groupIdField, groupIdValue.toString().trim());
}
}
protected String getGroupId(DocumentModel groupModel) {
Object groupIdValue = groupModel.getProperty(groupSchemaName, groupIdField);
if (groupIdValue != null && !(groupIdValue instanceof String)) {
throw new NuxeoException("Invalid group id " + groupIdValue);
}
return (String) groupIdValue;
}
protected void checkUserId(DocumentModel userModel) {
Object userIdValue = userModel.getProperty(userSchemaName, userIdField);
if (userIdValue != null) {
userModel.setProperty(userSchemaName, userIdField, userIdValue.toString().trim());
}
}
protected String getUserId(DocumentModel userModel) {
Object userIdValue = userModel.getProperty(userSchemaName, userIdField);
if (userIdValue != null && !(userIdValue instanceof String)) {
throw new NuxeoException("Invalid user id " + userIdValue);
}
return (String) userIdValue;
}
@Override
public DocumentModel createGroup(DocumentModel groupModel) {
return createGroup(groupModel, null);
}
@Override
public DocumentModel createUser(DocumentModel userModel) {
return createUser(userModel, null);
}
@Override
public void deleteGroup(String groupId) {
deleteGroup(groupId, null);
}
@Override
public void deleteGroup(DocumentModel groupModel) {
deleteGroup(groupModel, null);
}
@Override
public void deleteUser(String userId) {
deleteUser(userId, null);
}
@Override
public void deleteUser(DocumentModel userModel) {
String userId = getUserId(userModel);
deleteUser(userId);
}
@Override
public List<String> getGroupIds() {
try (Session groupDir = dirService.open(groupDirectoryName)) {
List<String> groupIds = groupDir.getProjection(Collections.<String, Serializable> emptyMap(),
groupDir.getIdField());
Collections.sort(groupIds);
return groupIds;
}
}
@Override
public List<String> getUserIds() {
return getUserIds(null);
}
protected void removeVirtualFilters(Map<String, Serializable> filter) {
if (filter == null) {
return;
}
List<String> keys = new ArrayList<>(filter.keySet());
for (String key : keys) {
if (key.startsWith(VIRTUAL_FIELD_FILTER_PREFIX)) {
filter.remove(key);
}
}
}
@Override
public DocumentModelList searchGroups(Map<String, Serializable> filter, Set<String> fulltext) {
return searchGroups(filter, fulltext, null);
}
@Override
public DocumentModelList searchUsers(String pattern) {
return searchUsers(pattern, null);
}
@Override
public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext) {
return searchUsers(filter, fulltext, getUserSortMap(), null);
}
@Override
public void updateGroup(DocumentModel groupModel) {
updateGroup(groupModel, null);
}
@Override
public void updateUser(DocumentModel userModel) {
updateUser(userModel, null);
}
@Override
public DocumentModel getBareGroupModel() {
String schema = dirService.getDirectorySchema(groupDirectoryName);
return BaseSession.createEntryModel(null, schema, null, null);
}
@Override
public List<String> getAdministratorsGroups() {
return administratorGroups;
}
protected List<String> getLeafPermissions(String perm) {
ArrayList<String> permissions = new ArrayList<>();
PermissionProvider permissionProvider = Framework.getService(PermissionProvider.class);
String[] subpermissions = permissionProvider.getSubPermissions(perm);
if (subpermissions == null || subpermissions.length <= 0) {
// it's a leaf
permissions.add(perm);
return permissions;
}
for (String subperm : subpermissions) {
permissions.addAll(getLeafPermissions(subperm));
}
return permissions;
}
@Override
public String[] getUsersForPermission(String perm, ACP acp) {
return getUsersForPermission(perm, acp, null);
}
@Override
public Principal authenticate(String name, String password) {
return checkUsernamePassword(name, password) ? getPrincipal(name) : null;
}
/*************** MULTI-TENANT-IMPLEMENTATION ************************/
public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext,
Map<String, String> orderBy, DocumentModel context) {
try (Session userDir = dirService.open(userDirectoryName, context)) {
removeVirtualFilters(filter);
// XXX: do not fetch references, can be costly
DocumentModelList entries = userDir.query(filter, fulltext, null, false);
if (isAnonymousMatching(filter, fulltext)) {
entries.add(makeVirtualUserEntry(getAnonymousUserId(), anonymousUser));
}
// TODO: match searchable virtual users
if (orderBy != null && !orderBy.isEmpty()) {
// sort: cannot sort before virtual users are added
Collections.sort(entries, new DocumentModelComparator(userSchemaName, orderBy));
}
return entries;
}
}
@Override
public List<String> getUsersInGroup(String groupId, DocumentModel context) {
String storeGroupId = multiTenantManagement.groupnameTranformer(this, groupId, context);
return getGroup(storeGroupId).getMemberUsers();
}
@Override
public DocumentModelList searchUsers(String pattern, DocumentModel context) {
DocumentModelList entries = new DocumentModelListImpl();
if (pattern == null || pattern.length() == 0) {
entries = searchUsers(Collections.<String, Serializable> emptyMap(), null);
} else {
pattern = pattern.trim();
Map<String, DocumentModel> uniqueEntries = new HashMap<>();
for (Entry<String, MatchType> fieldEntry : userSearchFields.entrySet()) {
Map<String, Serializable> filter = new HashMap<>();
filter.put(fieldEntry.getKey(), pattern);
DocumentModelList fetchedEntries;
if (fieldEntry.getValue() == MatchType.SUBSTRING) {
fetchedEntries = searchUsers(filter, filter.keySet(), null, context);
} else {
fetchedEntries = searchUsers(filter, null, null, context);
}
for (DocumentModel entry : fetchedEntries) {
uniqueEntries.put(entry.getId(), entry);
}
}
log.debug(String.format("found %d unique entries", uniqueEntries.size()));
entries.addAll(uniqueEntries.values());
}
// sort
Collections.sort(entries, new DocumentModelComparator(userSchemaName, getUserSortMap()));
return entries;
}
@Override
public DocumentModelList searchUsers(Map<String, Serializable> filter, Set<String> fulltext,
DocumentModel context) {
throw new UnsupportedOperationException();
}
@Override
public List<String> getGroupIds(DocumentModel context) {
throw new UnsupportedOperationException();
}
@Override
public DocumentModelList searchGroups(Map<String, Serializable> filter, Set<String> fulltext,
DocumentModel context) {
filter = filter != null ? cloneMap(filter) : new HashMap<>();
HashSet<String> fulltextClone = fulltext != null ? cloneSet(fulltext) : new HashSet<>();
multiTenantManagement.queryTransformer(this, filter, fulltextClone, context);
try (Session groupDir = dirService.open(groupDirectoryName, context)) {
removeVirtualFilters(filter);
return groupDir.query(filter, fulltextClone, getGroupSortMap(), false);
}
}
@Override
public DocumentModel createGroup(DocumentModel groupModel, DocumentModel context)
throws GroupAlreadyExistsException {
groupModel = multiTenantManagement.groupTransformer(this, groupModel, context);
// be sure the name does not contains trailing spaces
checkGrouId(groupModel);
try (Session groupDir = dirService.open(groupDirectoryName, context)) {
String groupId = getGroupId(groupModel);
// check the group does not exist
if (groupDir.hasEntry(groupId)) {
throw new GroupAlreadyExistsException();
}
groupModel = groupDir.createEntry(groupModel);
notifyGroupChanged(groupId, GROUPCREATED_EVENT_ID);
return groupModel;
}
}
@Override
public DocumentModel getGroupModel(String groupIdValue, DocumentModel context) {
String groupName = multiTenantManagement.groupnameTranformer(this, groupIdValue, context);
if (groupName != null) {
groupName = groupName.trim();
}
try (Session groupDir = dirService.open(groupDirectoryName, context)) {
return groupDir.getEntry(groupName);
}
}
@Override
public DocumentModel getUserModel(String userName, DocumentModel context) {
if (userName == null) {
return null;
}
userName = userName.trim();
// return anonymous model
if (anonymousUser != null && userName.equals(anonymousUser.getId())) {
return makeVirtualUserEntry(getAnonymousUserId(), anonymousUser);
}
try (Session userDir = dirService.open(userDirectoryName, context)) {
return userDir.getEntry(userName);
}
}
protected Map<String, Serializable> cloneMap(Map<String, Serializable> map) {
Map<String, Serializable> result = new HashMap<>();
for (String key : map.keySet()) {
result.put(key, map.get(key));
}
return result;
}
protected HashSet<String> cloneSet(Set<String> set) {
HashSet<String> result = new HashSet<>();
for (String key : set) {
result.add(key);
}
return result;
}
@Override
public NuxeoPrincipal getPrincipal(String username, DocumentModel context) {
if (username == null) {
return null;
}
String anonymousUserId = getAnonymousUserId();
if (username.equals(anonymousUserId)) {
return makeAnonymousPrincipal();
}
if (virtualUsers.containsKey(username)) {
return makeVirtualPrincipal(virtualUsers.get(username));
}
if (NuxeoPrincipal.isTransientUsername(username)) {
return makeTransientPrincipal(username);
}
DocumentModel userModel = getUserModel(username, context);
if (userModel != null) {
return makePrincipal(userModel);
}
return null;
}
@Override
public DocumentModelList searchGroups(String pattern, DocumentModel context) {
DocumentModelList entries = new DocumentModelListImpl();
if (pattern == null || pattern.length() == 0) {
entries = searchGroups(Collections.<String, Serializable> emptyMap(), null);
} else {
pattern = pattern.trim();
Map<String, DocumentModel> uniqueEntries = new HashMap<>();
for (Entry<String, MatchType> fieldEntry : groupSearchFields.entrySet()) {
Map<String, Serializable> filter = new HashMap<>();
filter.put(fieldEntry.getKey(), pattern);
DocumentModelList fetchedEntries;
if (fieldEntry.getValue() == MatchType.SUBSTRING) {
fetchedEntries = searchGroups(filter, filter.keySet(), context);
} else {
fetchedEntries = searchGroups(filter, null, context);
}
for (DocumentModel entry : fetchedEntries) {
uniqueEntries.put(entry.getId(), entry);
}
}
log.debug(String.format("found %d unique group entries", uniqueEntries.size()));
entries.addAll(uniqueEntries.values());
}
// sort
Collections.sort(entries, new DocumentModelComparator(groupSchemaName, getGroupSortMap()));
return entries;
}
@Override
public List<String> getUserIds(DocumentModel context) {
try (Session userDir = dirService.open(userDirectoryName, context)) {
List<String> userIds = userDir.getProjection(Collections.<String, Serializable> emptyMap(),
userDir.getIdField());
Collections.sort(userIds);
return userIds;
}
}
@Override
public DocumentModel createUser(DocumentModel userModel, DocumentModel context) throws UserAlreadyExistsException {
// be sure UserId does not contains any trailing spaces
checkUserId(userModel);
try (Session userDir = dirService.open(userDirectoryName, context)) {
String userId = getUserId(userModel);
// check the user does not exist
if (userDir.hasEntry(userId)) {
throw new UserAlreadyExistsException();
}
checkPasswordValidity(userModel);
String schema = dirService.getDirectorySchema(userDirectoryName);
String clearUsername = (String) userModel.getProperty(schema, userDir.getIdField());
String clearPassword = (String) userModel.getProperty(schema, userDir.getPasswordField());
userModel = userDir.createEntry(userModel);
syncDigestAuthPassword(clearUsername, clearPassword);
notifyUserChanged(userId, USERCREATED_EVENT_ID);
return userModel;
}
}
protected void checkPasswordValidity(DocumentModel userModel) throws InvalidPasswordException {
if (!mustCheckPasswordValidity()) {
return;
}
String schema = dirService.getDirectorySchema(userDirectoryName);
String passwordField = dirService.getDirectory(userDirectoryName).getPasswordField();
Property passwordProperty = userModel.getPropertyObject(schema, passwordField);
if (passwordProperty.isDirty()) {
String clearPassword = (String) passwordProperty.getValue();
if (StringUtils.isNotBlank(clearPassword) && !validatePassword(clearPassword)) {
throw new InvalidPasswordException();
}
}
}
@Override
public void updateUser(DocumentModel userModel, DocumentModel context) {
try (Session userDir = dirService.open(userDirectoryName, context)) {
String userId = getUserId(userModel);
if (!userDir.hasEntry(userId)) {
throw new DirectoryException("user does not exist: " + userId);
}
String schema = dirService.getDirectorySchema(userDirectoryName);
checkPasswordValidity(userModel);
String clearUsername = (String) userModel.getProperty(schema, userDir.getIdField());
String clearPassword = (String) userModel.getProperty(schema, userDir.getPasswordField());
userDir.updateEntry(userModel);
syncDigestAuthPassword(clearUsername, clearPassword);
notifyUserChanged(userId, USERMODIFIED_EVENT_ID);
}
}
private boolean mustCheckPasswordValidity() {
return Framework.getService(ConfigurationService.class).isBooleanPropertyTrue(VALIDATE_PASSWORD_PARAM);
}
@Override
public void deleteUser(DocumentModel userModel, DocumentModel context) {
String userId = getUserId(userModel);
deleteUser(userId, context);
}
@Override
public void deleteUser(String userId, DocumentModel context) {
try (Session userDir = dirService.open(userDirectoryName, context)) {
if (!userDir.hasEntry(userId)) {
throw new DirectoryException("User does not exist: " + userId);
}
userDir.deleteEntry(userId);
notifyUserChanged(userId, USERDELETED_EVENT_ID);
} finally {
notifyUserChanged(userId, null);
}
}
@Override
public void updateGroup(DocumentModel groupModel, DocumentModel context) {
try (Session groupDir = dirService.open(groupDirectoryName, context)) {
String groupId = getGroupId(groupModel);
if (!groupDir.hasEntry(groupId)) {
throw new DirectoryException("group does not exist: " + groupId);
}
groupDir.updateEntry(groupModel);
notifyGroupChanged(groupId, GROUPMODIFIED_EVENT_ID);
}
}
@Override
public void deleteGroup(DocumentModel groupModel, DocumentModel context) {
String groupId = getGroupId(groupModel);
deleteGroup(groupId, context);
}
@Override
public void deleteGroup(String groupId, DocumentModel context) {
try (Session groupDir = dirService.open(groupDirectoryName, context)) {
if (!groupDir.hasEntry(groupId)) {
throw new DirectoryException("Group does not exist: " + groupId);
}
// Get ancestor group names before deletion to pass them as a property of the core event
List<String> ancestorGroupNames = getAncestorGroups(groupId);
groupDir.deleteEntry(groupId);
notifyGroupChanged(groupId, GROUPDELETED_EVENT_ID, ancestorGroupNames);
}
}
@Override
public List<String> getGroupsInGroup(String parentId, DocumentModel context) {
return getGroup(parentId, null).getMemberGroups();
}
@Override
public List<String> getTopLevelGroups(DocumentModel context) {
try (Session groupDir = dirService.open(groupDirectoryName, context)) {
List<String> topLevelGroups = new LinkedList<>();
// XXX retrieve all entries with references, can be costly.
DocumentModelList groups = groupDir.query(Collections.<String, Serializable> emptyMap(), null, null, true);
for (DocumentModel group : groups) {
@SuppressWarnings("unchecked")
List<String> parents = (List<String>) group.getProperty(groupSchemaName, groupParentGroupsField);
if (parents == null || parents.isEmpty()) {
topLevelGroups.add(group.getId());
}
}
return topLevelGroups;
}
}
@Override
public List<String> getUsersInGroupAndSubGroups(String groupId, DocumentModel context) {
Set<String> groups = new HashSet<>();
groups.add(groupId);
appendSubgroups(groupId, groups, context);
Set<String> users = new HashSet<>();
for (String groupid : groups) {
users.addAll(getGroup(groupid, context).getMemberUsers());
}
return new ArrayList<>(users);
}
@Override
public String[] getUsersForPermission(String perm, ACP acp, DocumentModel context) {
PermissionProvider permissionProvider = Framework.getService(PermissionProvider.class);
// using a hashset to avoid duplicates
HashSet<String> usernames = new HashSet<>();
ACL merged = acp.getMergedACLs("merged");
// The list of permission that is has "perm" as its (compound)
// permission
ArrayList<ACE> filteredACEbyPerm = new ArrayList<>();
List<String> currentPermissions = getLeafPermissions(perm);
for (ACE ace : merged.getACEs()) {
// Checking if the permission contains the permission we want to
// check (we use the security service method for coumpound
// permissions)
List<String> acePermissions = getLeafPermissions(ace.getPermission());
// Everything is a special permission (not compound)
if (SecurityConstants.EVERYTHING.equals(ace.getPermission())) {
acePermissions = Arrays.asList(permissionProvider.getPermissions());
}
if (acePermissions.containsAll(currentPermissions)) {
// special case: everybody perm grant false, don't take in
// account the previous ace
if (SecurityConstants.EVERYONE.equals(ace.getUsername()) && !ace.isGranted()) {
break;
}
filteredACEbyPerm.add(ace);
}
}
for (ACE ace : filteredACEbyPerm) {
String aceUsername = ace.getUsername();
List<String> users = null;
// If everyone, add/remove all the users
if (SecurityConstants.EVERYONE.equals(aceUsername)) {
users = getUserIds();
}
// if a group, add/remove all the user from the group (and
// subgroups)
if (users == null) {
NuxeoGroup group;
group = getGroup(aceUsername, context);
if (group != null) {
users = getUsersInGroupAndSubGroups(aceUsername, context);
}
}
// otherwise, add the user
if (users == null) {
users = new ArrayList<>();
users.add(aceUsername);
}
if (ace.isGranted()) {
usernames.addAll(users);
} else {
usernames.removeAll(users);
}
}
return usernames.toArray(new String[usernames.size()]);
}
@Override
public List<String> getAncestorGroups(String groupId) {
List<String> ancestorGroups = new ArrayList<>();
populateAncestorGroups(groupId, ancestorGroups);
return ancestorGroups;
}
protected void populateAncestorGroups(String groupId, List<String> ancestorGroups) {
NuxeoGroup group = getGroup(groupId);
if (group != null) {
List<String> parentGroups = group.getParentGroups();
// Avoid infinite loop in case a group has one of its parents as a subgroup
parentGroups.stream().filter(parentGroup -> !ancestorGroups.contains(parentGroup)).forEach(parentGroup -> {
ancestorGroups.add(parentGroup);
populateAncestorGroups(parentGroup, ancestorGroups);
});
}
}
@Override
public void handleEvent(Event event) {
String id = event.getId();
if (INVALIDATE_PRINCIPAL_EVENT_ID.equals(id)) {
invalidatePrincipal((String) event.getData());
} else if (INVALIDATE_ALL_PRINCIPALS_EVENT_ID.equals(id)) {
invalidateAllPrincipals();
}
}
}