Java tutorial
/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.xwiki.contrib.ldap; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Pattern; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.text.StrSubstitutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.cache.Cache; import org.xwiki.cache.CacheException; import org.xwiki.cache.CacheManager; import org.xwiki.cache.config.CacheConfiguration; import org.xwiki.cache.config.LRUCacheConfiguration; import org.xwiki.model.reference.DocumentReference; import org.xwiki.rendering.syntax.Syntax; import com.novell.ldap.LDAPAttribute; import com.novell.ldap.LDAPConnection; import com.novell.ldap.LDAPDN; import com.novell.ldap.LDAPEntry; import com.novell.ldap.LDAPException; import com.novell.ldap.LDAPSearchResults; import com.novell.ldap.rfc2251.RfcFilter; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.objects.BaseObject; import com.xpn.xwiki.objects.BaseProperty; import com.xpn.xwiki.objects.classes.BaseClass; import com.xpn.xwiki.objects.classes.ListClass; import com.xpn.xwiki.objects.classes.PropertyClass; import com.xpn.xwiki.objects.classes.StringClass; import com.xpn.xwiki.web.Utils; /** * LDAP communication tool. * * @version $Id$ * @since 8.3 */ public class XWikiLDAPUtils { /** * Logging tool. */ private static final Logger LOGGER = LoggerFactory.getLogger(XWikiLDAPUtils.class); /** * LDAP objectClass parameter. */ private static final String LDAP_OBJECTCLASS = "objectClass"; /** * The name of the LDAP groups cache. */ private static final String CACHE_NAME_GROUPS = "ldap.groups"; /** * The name of the XWiki group member field. */ private static final String XWIKI_GROUP_MEMBERFIELD = "member"; /** * The XWiki space where users are stored. */ private static final String XWIKI_USER_SPACE = "XWiki"; /** * The configuration of the LDAP group cache. */ private static LRUCacheConfiguration cacheConfigurationGroups; /** * Default unique user field name. */ private static final String LDAP_DEFAULT_UID = "cn"; /** * The name of the LDAP object field "dn". */ private static final String LDAP_FIELD_DN = "dn"; /** * Contains caches for each LDAP host:port. */ private static Map<String, Map<String, Cache<Map<String, String>>>> cachePool = new HashMap<String, Map<String, Cache<Map<String, String>>>>(); /** * The LDAP connection. */ private final XWikiLDAPConnection connection; private final XWikiLDAPConfig configuration; /** * The LDAP attribute containing the identifier for a user. */ private String uidAttributeName = LDAP_DEFAULT_UID; /** * Different LDAP implementations groups classes names. */ private Collection<String> groupClasses = XWikiLDAPConfig.DEFAULT_GROUP_CLASSES; /** * Different LDAP implementations groups member property name. */ private Collection<String> groupMemberFields = XWikiLDAPConfig.DEFAULT_GROUP_MEMBERFIELDS; /** * The LDAP base DN from where to executes LDAP queries. */ private String baseDN = ""; /** * LDAP search format string. */ private String userSearchFormatString = "({0}={1})"; /** * @see #isResolveSubgroups() */ private boolean resolveSubgroups = true; /** * Create an instance of {@link XWikiLDAPUtils}. * * @param connection the XWiki LDAP connection tool. * @deprecated since 8.5, use {@link #XWikiLDAPUtils(XWikiLDAPConnection, XWikiLDAPConfig)} instead */ @Deprecated public XWikiLDAPUtils(XWikiLDAPConnection connection) { this(connection, new XWikiLDAPConfig(null)); } /** * @param connection the XWiki LDAP connection tool. * @since 9.0 */ public XWikiLDAPUtils(XWikiLDAPConnection connection, XWikiLDAPConfig configuration) { this.connection = connection; this.configuration = configuration; } /** * @param uidAttributeName the LDAP attribute containing the identifier for a user. */ public void setUidAttributeName(String uidAttributeName) { this.uidAttributeName = uidAttributeName; } /** * @return the LDAP attribute containing the identifier for a user. */ public String getUidAttributeName() { return this.uidAttributeName; } /** * @param baseDN the LDAP base DN from where to executes LDAP queries. */ public void setBaseDN(String baseDN) { this.baseDN = baseDN; } /** * @return the LDAP base DN from where to executes LDAP queries. */ public String getBaseDN() { return this.baseDN; } /** * @param fmt the user search format string. */ public void setUserSearchFormatString(String fmt) { this.userSearchFormatString = fmt; } /** * @return the user search format string. */ public String getUserSearchFormatString() { return this.userSearchFormatString; } /** * @param groupClasses the different LDAP implementations groups classes names. */ public void setGroupClasses(Collection<String> groupClasses) { this.groupClasses = groupClasses; } /** * @return the different LDAP implementations groups classes names. */ public Collection<String> getGroupClasses() { return this.groupClasses; } /** * @param groupMemberFields the different LDAP implementations groups member property name. */ public void setGroupMemberFields(Collection<String> groupMemberFields) { this.groupMemberFields = groupMemberFields; } /** * @return the different LDAP implementations groups member property name. */ public Collection<String> getGroupMemberFields() { return this.groupMemberFields; } /** * @return true if sub groups should be resolved too */ public boolean isResolveSubgroups() { return this.resolveSubgroups; } /** * @param resolveSubgroups true if sub groups should be resolved too */ public void setResolveSubgroups(boolean resolveSubgroups) { this.resolveSubgroups = resolveSubgroups; } /** * Get the cache with the provided name for a particular LDAP server. * * @param configuration the configuration to use to create the cache and to find it if it's already created. * @param context the XWiki context. * @return the cache. * @throws CacheException error when creating the cache. * @deprecated use {@link #getGroupCache(CacheConfiguration, XWikiContext)} instead since 4.1M1 */ @Deprecated public Cache<Map<String, String>> getCache(CacheConfiguration configuration, XWikiContext context) throws CacheException { return getGroupCache(configuration, context); } /** * Get the cache with the provided name for a particular LDAP server. * * @param configuration the configuration to use to create the cache and to find it if it's already created. * @param context the XWiki context. * @return the cache. * @throws CacheException error when creating the cache. * @since 4.1M1 */ public Cache<Map<String, String>> getGroupCache(CacheConfiguration configuration, XWikiContext context) throws CacheException { Cache<Map<String, String>> cache; String cacheKey = getUidAttributeName() + "." + getConnection().getConnection().getHost() + ":" + getConnection().getConnection().getPort(); synchronized (cachePool) { Map<String, Cache<Map<String, String>>> cacheMap; if (cachePool.containsKey(cacheKey)) { cacheMap = cachePool.get(cacheKey); } else { cacheMap = new HashMap<String, Cache<Map<String, String>>>(); cachePool.put(cacheKey, cacheMap); } cache = cacheMap.get(configuration.getConfigurationId()); if (cache == null) { cache = Utils.getComponent(CacheManager.class).createNewCache(configuration); cacheMap.put(configuration.getConfigurationId(), cache); } } return cache; } /** * Force to empty the group cache. * * @since 4.1M1 */ public static void resetGroupCache() { synchronized (cachePool) { for (Map<String, Cache<Map<String, String>>> caches : cachePool.values()) { for (Cache<Map<String, String>> cache : caches.values()) { cache.dispose(); } } } cachePool.clear(); } /** * @return get {@link XWikiLDAPConnection}. */ public XWikiLDAPConnection getConnection() { return this.connection; } /** * @return get {@link XWikiLDAPConfig} */ public XWikiLDAPConfig getConfiguration() { return this.configuration; } /** * Execute LDAP query to get all group's members. * * @param groupDN the group to retrieve the members of and scan for subgroups. * @return the LDAP search result. * @throws LDAPException failed to execute LDAP query */ private LDAPSearchResults searchGroupsMembersByDN(String groupDN) throws LDAPException { String[] attrs = new String[2 + getGroupMemberFields().size()]; int i = 0; attrs[i++] = LDAP_OBJECTCLASS; for (String groupMember : getGroupMemberFields()) { attrs[i++] = groupMember; } // in case it's a organization unit get the users ids attrs[i++] = getUidAttributeName(); return getConnection().search(groupDN, null, attrs, LDAPConnection.SCOPE_SUB); } /** * Execute LDAP query to get all group's members. * * @param filter the LDAP filter to search with. * @return the LDAP search result. * @throws LDAPException failed to execute LDAP query */ private LDAPSearchResults searchGroupsMembersByFilter(String filter) throws LDAPException { String[] attrs = new String[2 + getGroupMemberFields().size()]; int i = 0; attrs[i++] = LDAP_OBJECTCLASS; for (String groupMember : getGroupMemberFields()) { attrs[i++] = groupMember; } // in case it's a organization unit get the users ids attrs[i++] = getUidAttributeName(); return getConnection().search(getBaseDN(), filter, attrs, LDAPConnection.SCOPE_SUB); } /** * Extract group's members from provided LDAP search result. * * @param searchAttributeList the LDAP search result. * @param memberMap the result: maps DN to member id. * @param subgroups return all the subgroups identified. * @param context the XWiki context. */ private void getGroupMembersFromSearchResult(List<XWikiLDAPSearchAttribute> searchAttributeList, Map<String, String> memberMap, List<String> subgroups, XWikiContext context) { for (XWikiLDAPSearchAttribute searchAttribute : searchAttributeList) { String key = searchAttribute.name; if (getGroupMemberFields().contains(key.toLowerCase())) { // or subgroup String member = searchAttribute.value; // we check for subgroups recursive call to scan all subgroups and identify members // and their uid getGroupMembers(member, memberMap, subgroups, context); } } } /** * Extract group's members from provided LDAP search result. * * @param ldapEntry the LDAP search result. * @param memberMap the result: maps DN to member id. * @param subgroups return all the subgroups identified. * @param context the XWiki context. */ private void getGroupMembersFromLDAPEntry(LDAPEntry ldapEntry, Map<String, String> memberMap, List<String> subgroups, XWikiContext context) { for (String memberField : getGroupMemberFields()) { LDAPAttribute attribute = ldapEntry.getAttribute(memberField); if (attribute != null) { Enumeration<String> values = attribute.getStringValues(); while (values.hasMoreElements()) { String member = values.nextElement(); LOGGER.debug(" |- Member value [{}] found. Trying to resolve it.", member); // we check for subgroups recursive call to scan all subgroups and identify members // and their uid getGroupMembers(member, memberMap, subgroups, context); } } } } /** * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well. * Retrieve an identifier for each member. * * @param groupDN the group to retrieve the members of and scan for subgroups. * @param memberMap the result: maps DN to member id. * @param subgroups all the subgroups identified. * @param searchAttributeList the groups members found in LDAP search. * @param context the XWiki context. * @return whether the groupDN is actually a group. */ public boolean getGroupMembers(String groupDN, Map<String, String> memberMap, List<String> subgroups, List<XWikiLDAPSearchAttribute> searchAttributeList, XWikiContext context) { boolean isGroup = false; String id = null; for (XWikiLDAPSearchAttribute searchAttribute : searchAttributeList) { String key = searchAttribute.name; if (key.equalsIgnoreCase(LDAP_OBJECTCLASS)) { String objectName = searchAttribute.value; if (getGroupClasses().contains(objectName.toLowerCase())) { isGroup = true; } } else if (key.equalsIgnoreCase(getUidAttributeName())) { id = searchAttribute.value; } } if (!isGroup) { if (id == null) { LOGGER.error("Could not find attribute [{}] for LDAP dn [{}]", getUidAttributeName(), groupDN); } if (!memberMap.containsKey(groupDN.toLowerCase())) { memberMap.put(groupDN.toLowerCase(), id == null ? "" : id.toLowerCase()); } } else { // remember this group if (subgroups != null) { subgroups.add(groupDN.toLowerCase()); } getGroupMembersFromSearchResult(searchAttributeList, memberMap, subgroups, context); } return isGroup; } /** * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well. * Retrieve an identifier for each member. * * @param memberMap the result: maps DN to member id. * @param subgroups all the subgroups identified. * @param ldapEntry the ldap entry returned by a search members found in LDAP search. * @param context the XWiki context. * @return whether the groupDN is actually a group. * @throws LDAPException error when parsing the provided LDAP entry * @since 3.3M1 */ public boolean getGroupMembers(Map<String, String> memberMap, List<String> subgroups, LDAPEntry ldapEntry, XWikiContext context) throws LDAPException { boolean isGroup = false; // Check if the entry is a group LDAPAttribute classAttribute = ldapEntry.getAttribute(LDAP_OBJECTCLASS); if (classAttribute != null) { Enumeration<String> values = classAttribute.getStringValues(); Collection<String> groupClasses = getGroupClasses(); while (values.hasMoreElements()) { String value = values.nextElement(); if (groupClasses.contains(value.toLowerCase())) { isGroup = true; } } } // Get members or user id if it's a user if (isGroup) { // remember this group if (subgroups != null) { subgroups.add(ldapEntry.getDN().toLowerCase()); } getGroupMembersFromLDAPEntry(ldapEntry, memberMap, subgroups, context); } else { LDAPAttribute uidAttribute = ldapEntry.getAttribute(getUidAttributeName()); if (uidAttribute != null) { String uid = uidAttribute.getStringValue(); if (!memberMap.containsKey(ldapEntry.getDN().toLowerCase())) { memberMap.put(ldapEntry.getDN().toLowerCase(), uid.toLowerCase()); } } else { LOGGER.debug("Probably a organization unit or a search"); } } return isGroup; } /** * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well. * Retrieve an identifier for each member. * * @param userOrGroup the group to retrieve the members of and scan for subgroups. Can be * <ul> * <li>a group DN</li> * <li>a user DN</li> * <li>a group id</li> * <li>a user id</li> * </ul> * @param memberMap the result: maps DN to member id. * @param subgroups all the subgroups identified. * @param context the XWiki context. * @return whether the identifier is actually a group. */ public boolean getGroupMembers(String userOrGroup, Map<String, String> memberMap, List<String> subgroups, XWikiContext context) { boolean isGroup = false; int nbMembers = memberMap.size(); if (LDAPDN.isValid(userOrGroup)) { LOGGER.debug("[{}] is a valid DN, lets try to get corresponding entry.", userOrGroup); // Stop there if passed used is already a resolved member if (memberMap.containsKey(userOrGroup.toLowerCase())) { LOGGER.debug("[{}] is already resolved", userOrGroup); return false; } // Stop there if subgroup resolution is disabled if (!subgroups.isEmpty() && !isResolveSubgroups()) { LOGGER.debug("Group members resolve is disabled to add [{}] as group member directly", userOrGroup); memberMap.put(userOrGroup.toLowerCase(), userOrGroup); return false; } isGroup = getGroupMembersFromDN(userOrGroup, memberMap, subgroups, context); } if (!isGroup && nbMembers == memberMap.size()) { // Probably not a DN, lets try as filter or id LOGGER.debug("Looks like [{}] is not a DN, lets try filter or id", userOrGroup); try { // Test if it's valid LDAP filter syntax new RfcFilter(userOrGroup); isGroup = getGroupMembersFromFilter(userOrGroup, memberMap, subgroups, context); } catch (LDAPException e) { LOGGER.debug("[{}] is not a valid LDAP filter, lets try id", userOrGroup, e); // Not a valid filter, try as uid List<XWikiLDAPSearchAttribute> searchAttributeList = searchUserAttributesByUid(userOrGroup, new String[] { LDAP_FIELD_DN }); if (searchAttributeList != null && !searchAttributeList.isEmpty()) { String dn = searchAttributeList.get(0).value; // Stop there if passed used is already a resolved member if (memberMap.containsKey(dn.toLowerCase())) { LOGGER.debug("[{}] is already resolved", dn); return false; } // Stop there if subgroup resolution is disabled if (!subgroups.isEmpty() && !isResolveSubgroups()) { LOGGER.debug("Group members resolve is disabled to add [{}] as group member directly", dn); memberMap.put(dn.toLowerCase(), dn); return false; } isGroup = getGroupMembers(dn, memberMap, subgroups, context); } } } return isGroup; } /** * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well. * Retrieve an identifier for each member. * * @param userOrGroupDN the group DN to retrieve the members from or the user DN to add in the members map. * @param memberMap the result: maps DN to member id. * @param subgroups all the subgroups identified. * @param context the XWiki context. * @return whether the provided DN is actually a group or not. */ public boolean getGroupMembersFromDN(String userOrGroupDN, Map<String, String> memberMap, List<String> subgroups, XWikiContext context) { boolean isGroup = false; // break out if there is a loop of groups if (subgroups != null && subgroups.contains(userOrGroupDN.toLowerCase())) { LOGGER.debug("[{}] groups already resolved.", userOrGroupDN); return true; } LDAPSearchResults result; try { result = searchGroupsMembersByDN(userOrGroupDN); } catch (LDAPException e) { LOGGER.debug("Failed to search for [{}]", userOrGroupDN, e); return false; } try { isGroup = getGroupMembersSearchResult(result, memberMap, subgroups, context); } finally { if (result.hasMore()) { try { getConnection().getConnection().abandon(result); } catch (LDAPException e) { LOGGER.debug("LDAP Search clean up failed", e); } } } return isGroup; } /** * Get all members of a given group based on the groupDN. If the group contains subgroups get these members as well. * Retrieve an identifier for each member. * * @param filter the LDAP filter to search with. * @param memberMap the result: maps DN to member id. * @param subgroups all the subgroups identified. * @param context the XWiki context. * @return whether the provided DN is actually a group or not. */ public boolean getGroupMembersFromFilter(String filter, Map<String, String> memberMap, List<String> subgroups, XWikiContext context) { boolean isGroup = false; LDAPSearchResults result; try { result = searchGroupsMembersByFilter(filter); } catch (LDAPException e) { LOGGER.debug("Failed to search for [{}]", filter, e); return false; } try { isGroup = getGroupMembersSearchResult(result, memberMap, subgroups, context); } finally { if (result.hasMore()) { try { getConnection().getConnection().abandon(result); } catch (LDAPException e) { LOGGER.debug("LDAP Search clean up failed", e); } } } return isGroup; } /** * Get all members of a given group based on the the result of a LDAP search. If the group contains subgroups get * these members as well. Retrieve an identifier for each member. * * @param result the result of a LDAP search. * @param memberMap the result: maps DN to member id. * @param subgroups all the subgroups identified. * @param context the XWiki context. * @return whether the provided DN is actually a group or not. */ public boolean getGroupMembersSearchResult(LDAPSearchResults result, Map<String, String> memberMap, List<String> subgroups, XWikiContext context) { boolean isGroup = false; LDAPEntry resultEntry = null; // For some weird reason result.hasMore() is always true before the first call to next() even if nothing is // found if (result.hasMore()) { try { resultEntry = result.next(); } catch (LDAPException e) { LOGGER.debug("Failed to get group members", e); } } if (resultEntry != null) { do { try { isGroup |= getGroupMembers(memberMap, subgroups, resultEntry, context); resultEntry = result.hasMore() ? result.next() : null; } catch (LDAPException e) { LOGGER.debug("Failed to get group members", e); } } while (resultEntry != null); } return isGroup; } /** * Get group members from cache or update it from LDAP if it is not already cached. * * @param groupDN the name of the group. * @param context the XWiki context. * @return the members of the group. * @throws XWikiException error when getting the group cache. */ public Map<String, String> getGroupMembers(String groupDN, XWikiContext context) throws XWikiException { Map<String, String> groupMembers = null; Cache<Map<String, String>> cache; try { cache = getGroupCache(getGroupCacheConfiguration(context), context); synchronized (cache) { groupMembers = cache.get(groupDN); if (groupMembers == null) { Map<String, String> members = new HashMap<String, String>(); LOGGER.debug("Retrieving Members of the group [{}]", groupDN); boolean isGroup = getGroupMembers(groupDN, members, new ArrayList<String>(), context); if (isGroup || !members.isEmpty()) { groupMembers = members; cache.set(groupDN, groupMembers); } } else { LOGGER.debug("Found cache entry for group [{}]", groupDN); } } } catch (CacheException e) { LOGGER.error("Unknown error with cache", e); } LOGGER.debug("Found group [{}] members [{}]", groupDN, groupMembers); return groupMembers; } /** * Check if provided DN is in provided LDAP group. * * @param memberDN the DN to find in the provided group. * @param groupDN the DN of the group where to search. * @param context the XWiki context. * @return true if provided members in the provided group. * @throws XWikiException error when searching for group members. */ public boolean isMemberOfGroup(String memberDN, String groupDN, XWikiContext context) throws XWikiException { Map<String, String> groupMembers = getGroupMembers(groupDN, context); if (groupMembers != null) { for (String memberDNEntry : groupMembers.keySet()) { if (memberDNEntry.equals(memberDN.toLowerCase())) { return true; } } } return false; } /** * Check if provided DN is in one of the provided LDAP groups. * * @param memberDN the DN to find in the provided groups. * @param groupDNList the list of DN of the groups where to search. * @param context the XWiki context. * @return true if provided members in one of the provided groups. * @throws XWikiException error when searching for group members. */ public boolean isMemberOfGroups(String memberDN, Collection<String> groupDNList, XWikiContext context) throws XWikiException { for (String groupDN : groupDNList) { if (isMemberOfGroup(memberDN, groupDN, context)) { return true; } } return false; } /** * @param context the XWiki context used to get cache configuration. * @return the configuration for the LDAP groups cache. */ public static CacheConfiguration getGroupCacheConfiguration(XWikiContext context) { if (cacheConfigurationGroups == null) { XWikiLDAPConfig config = XWikiLDAPConfig.getInstance(); cacheConfigurationGroups = new LRUCacheConfiguration(CACHE_NAME_GROUPS); cacheConfigurationGroups.getLRUEvictionConfiguration().setLifespan(config.getCacheExpiration()); } return cacheConfigurationGroups; } /** * Locates the user in the Map: either the user is a value or the key starts with the LDAP syntax. * * @param userName the name of the user. * @param groupMembers the members of LDAP group. * @param context the XWiki context. * @return the full user name. * @deprecated since 8.4, use {@link #findInGroup(String, Map)} instead */ @Deprecated protected String findInGroup(String userName, Map<String, String> groupMembers, XWikiContext context) { return findUidInGroup(userName, groupMembers); } /** * Locates the user in the Map: either the user is a value or the key starts with the LDAP syntax. * * @param userName the name of the user. * @param groupMembers the members of LDAP group. * @return the full user name. */ protected String findUidInGroup(String userName, Map<String, String> groupMembers) { Pattern ldapuserPattern = Pattern.compile( "^" + Pattern.quote(getUidAttributeName()) + "=" + Pattern.quote(userName.toLowerCase()) + " *,"); for (Map.Entry<String, String> entry : groupMembers.entrySet()) { // implementing it case-insensitive for now if (userName.equalsIgnoreCase(entry.getValue()) || ldapuserPattern.matcher(entry.getKey()).find()) { return entry.getKey(); } } return null; } /** * Locates the user in the Map: either the user is a value or the key starts with the LDAP syntax. * * @param userDN the name of the user. * @param groupMembers the members of LDAP group. * @return the full user name. */ protected String findDNInGroup(String userDN, Map<String, String> groupMembers) { if (groupMembers.containsKey(userDN.toLowerCase())) { return userDN; } return null; } /** * Check if user is in provided LDAP group and return source DN. * * @param uid the user name. * @param dn the user dn. * @param groupDN the LDAP group DN. * @param context the XWiki context. * @return LDAP user's DN if the user is in the LDAP group, null otherwise. * @throws XWikiException error when getting the group cache. */ public String isInGroup(String uid, String dn, String groupDN, XWikiContext context) throws XWikiException { String userDN = null; if (groupDN.length() > 0) { Map<String, String> groupMembers = null; try { groupMembers = getGroupMembers(groupDN, context); } catch (Exception e) { // Ignore exception to allow negative match for exclusion LOGGER.debug("Unable to retrieve group members of group [{}]", groupDN, e); } // no match when a user does not have access to the group if (groupMembers != null) { // check if user is in the list if (dn == null) { userDN = findUidInGroup(uid, groupMembers); } else { userDN = findDNInGroup(dn, groupMembers); } LOGGER.debug("Found user dn in user group [{}]", userDN); } } return userDN; } /** * Check if user is in provided LDAP group and return source DN. * * @param userName the user name. * @param groupDN the LDAP group DN. * @param context the XWiki context. * @return LDAP user's DN if the user is in the LDAP group, null otherwise. * @throws XWikiException error when getting the group cache. */ public String isUidInGroup(String userName, String groupDN, XWikiContext context) throws XWikiException { return isInGroup(userName, null, groupDN, context); } /** * Check if the DN is in provided LDAP group and return source DN. * * @param dn the user DN. * @param groupDN the LDAP group DN. * @param context the XWiki context. * @return LDAP user's DN if the user is in the LDAP group, null otherwise. * @throws XWikiException error when getting the group cache. * @since 8.4 */ public String isDNInGroup(String dn, String groupDN, XWikiContext context) throws XWikiException { return isInGroup(null, dn, groupDN, context); } /** * @param uid the unique identifier of the user in the LDAP server. * @param attributeNameTable the names of the LDAP user attributes to query. * @return the found LDAP attributes. * @since 1.6M2 */ public List<XWikiLDAPSearchAttribute> searchUserAttributesByUid(String uid, String[] attributeNameTable) { // search for the user in LDAP String filter = MessageFormat.format(this.userSearchFormatString, new Object[] { XWikiLDAPConnection.escapeLDAPSearchFilter(this.uidAttributeName), XWikiLDAPConnection.escapeLDAPSearchFilter(uid) }); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Searching for the user in LDAP: user [{}] base [{}] query [{}] uid [{}]", new Object[] { uid, this.baseDN, filter, this.uidAttributeName }); } return getConnection().searchLDAP(this.baseDN, filter, attributeNameTable, LDAPConnection.SCOPE_SUB); } /** * @param uid the unique identifier of the user in the LDAP server. * @return the user DN, return null if no user was found. * @since 1.6M2 */ public String searchUserDNByUid(String uid) { String userDN = null; List<XWikiLDAPSearchAttribute> searchAttributes = searchUserAttributesByUid(uid, new String[] { LDAP_FIELD_DN }); if (searchAttributes != null && !searchAttributes.isEmpty()) { userDN = searchAttributes.get(0).value; } return userDN; } /** * Update or create XWiki user base on LDAP. * * @param userProfile the name of the user. * @param ldapDn the LDAP user DN. * @param authInput the input used to identify the user * @param attributes the attributes of the LDAP user. * @param context the XWiki context. * @return the XWiki user document * @throws XWikiException error when updating or creating XWiki user. */ public XWikiDocument syncUser(XWikiDocument userProfile, List<XWikiLDAPSearchAttribute> attributes, String ldapDn, String authInput, XWikiContext context) throws XWikiException { // check if we have to create the user if (userProfile == null || userProfile.isNew() || this.configuration.getLDAPParam("ldap_update_user", "0", context).equals("1")) { LOGGER.debug("LDAP attributes will be used to update XWiki attributes."); // Get attributes from LDAP if we don't already have them if (attributes == null) { // didn't get attributes before, so do it now attributes = getConnection().searchLDAP(ldapDn, null, getAttributeNameTable(context), LDAPConnection.SCOPE_BASE); } if (attributes == null) { LOGGER.error("Can't find any attributes for user [{}]", ldapDn); } // Load XWiki user document if we don't already have them if (userProfile == null) { userProfile = getAvailableUserProfile(attributes, context); } if (userProfile.isNew()) { LOGGER.debug("Creating new XWiki user based on LDAP attribues located at [{}]", ldapDn); createUserFromLDAP(userProfile, attributes, ldapDn, authInput, context); LOGGER.debug("New XWiki user created: [{}]", userProfile.getDocumentReference()); } else { LOGGER.debug("Updating existing user with LDAP attribues located at [{}]", ldapDn); try { updateUserFromLDAP(userProfile, attributes, ldapDn, authInput, context); } catch (XWikiException e) { LOGGER.error("Failed to synchronise user's informations", e); } } } return userProfile; } /** * Not everything is supported in XWiki user page name so clean clean it a bit. * * @param pageName the name of the XWiki user page * @return the clean name of the XWiki user page * @since 9.0 */ public static String cleanXWikiUserPageName(String pageName) { // Protected from characters not well supported in user page name depending on the version of XWiki String cleanPageName = StringUtils.remove(pageName, '.'); cleanPageName = StringUtils.remove(cleanPageName, ' '); cleanPageName = StringUtils.remove(cleanPageName, '/'); return cleanPageName; } /** * Synchronize user XWiki membership with it's LDAP membership. * * @param xwikiUserName the name of the user. * @param userDN the LDAP DN of the user. * @param groupMappings the mapping between XWiki groups names and LDAP groups names. * @param context the XWiki context. * @throws XWikiException error when synchronizing user membership. */ public void syncGroupsMembership(String xwikiUserName, String userDN, Map<String, Set<String>> groupMappings, XWikiContext context) throws XWikiException { LOGGER.debug("Updating group membership for the user [{}]", xwikiUserName); Collection<String> xwikiUserGroupList = context.getWiki().getGroupService(context) .getAllGroupsNamesForMember(xwikiUserName, 0, 0, context); if (LOGGER.isDebugEnabled()) { LOGGER.debug("The user belongs to following XWiki groups: "); for (String userGroupName : xwikiUserGroupList) { LOGGER.debug(userGroupName); } } // go through mapped groups to locate the user for (Map.Entry<String, Set<String>> entry : groupMappings.entrySet()) { String xwikiGroupName = entry.getKey(); Set<String> groupDNSet = entry.getValue(); if (xwikiUserGroupList.contains(xwikiGroupName)) { if (!this.isMemberOfGroups(userDN, groupDNSet, context)) { removeUserFromXWikiGroup(xwikiUserName, xwikiGroupName, context); } } else { if (this.isMemberOfGroups(userDN, groupDNSet, context)) { addUserToXWikiGroup(xwikiUserName, xwikiGroupName, context); } } } } /** * @param context the XWiki context. * @return the LDAP user attributes names. */ public String[] getAttributeNameTable(XWikiContext context) { String[] attributeNameTable = null; List<String> attributeNameList = new ArrayList<String>(); this.configuration.getUserMappings(attributeNameList, context); int lsize = attributeNameList.size(); if (lsize > 0) { attributeNameTable = attributeNameList.toArray(new String[lsize]); } return attributeNameTable; } private void set(List<XWikiLDAPSearchAttribute> searchAttributes, Map<String, String> userMappings, BaseObject userObject, XWikiContext xcontext) throws XWikiException { if (searchAttributes != null) { // Convert LDAP attributes to a map usable with BaseClass#fromValueMap Map<String, Object> map = toMap(searchAttributes, userMappings, xcontext); // Set properties in the user object userObject.getXClass(xcontext).fromMap(map, userObject); } } private void setProperty(BaseObject userObject, String key, Object value, XWikiContext xcontext) { BaseClass bclass = userObject.getXClass(xcontext); PropertyClass pclass = (PropertyClass) bclass.get(key); BaseProperty prop = null; if (pclass != null) { if (value instanceof String) { // In case the LDAP side in a String go trough property class fromString to be safe prop = pclass.fromString((String) value); } else if (value instanceof Collection && pclass instanceof StringClass) { // In case the LDAP side is a list and XWiki size is a String, assume we want the first element prop = pclass.fromValue(((Collection) value).iterator().next()); } else { // Default behavior: try to put whatever we get as it is prop = pclass.fromValue(value); } } // TODO: else generate new property based on the type of the value instead of relying on what is already // there if (prop != null) { userObject.safeput(key, prop); } } private Map<String, Object> toMap(List<XWikiLDAPSearchAttribute> searchAttributes, Map<String, String> userMappings, XWikiContext xcontext) throws XWikiException { BaseClass userClass = xcontext.getWiki().getUserClass(xcontext); Map<String, Object> map = new HashMap<String, Object>(); if (searchAttributes != null) { for (XWikiLDAPSearchAttribute lattr : searchAttributes) { String lval = lattr.value; String xattr = userMappings.get(lattr.name.toLowerCase()); if (xattr == null) { continue; } PropertyClass pclass = (PropertyClass) userClass.get(xattr); if (pclass != null) { if (pclass instanceof ListClass) { Object mapValue = map.get(xattr); if (mapValue == null) { mapValue = new ArrayList<>(); map.put(xattr, mapValue); } ((List) mapValue).add(lval); } else { map.put(xattr, lval); } } } } return map; } /** * Create an XWiki user and set all mapped attributes from LDAP to XWiki attributes. * * @param userProfile the XWiki user profile. * @param attributes the attributes. * @param ldapDN the LDAP DN of the user. * @param ldapUid the LDAP unique id of the user. * @param context the XWiki context. * @throws XWikiException error when creating XWiki user. */ protected void createUserFromLDAP(XWikiDocument userProfile, List<XWikiLDAPSearchAttribute> attributes, String ldapDN, String ldapUid, XWikiContext context) throws XWikiException { Map<String, String> userMappings = this.configuration.getUserMappings(null, context); LOGGER.debug("Start first synchronization of LDAP profile [{}] with new user profile based on mapping [{}]", attributes, userMappings); Map<String, Object> map = toMap(attributes, userMappings, context); // Mark user active map.put("active", "1"); context.getWiki().createUser(userProfile.getDocumentReference().getName(), map, context); // Update ldap profile object XWikiDocument createdUserProfile = context.getWiki().getDocument(userProfile.getDocumentReference(), context); LDAPProfileXClass ldapXClass = new LDAPProfileXClass(context); if (this.configuration.getLDAPParam(XWikiLDAPConfig.PREF_LDAP_UPDATE_PHOTO, "0", context).equals("1")) { // Add user photo from LDAP updatePhotoFromLdap(ldapUid, createdUserProfile, context); } if (ldapXClass.updateLDAPObject(createdUserProfile, ldapDN, ldapUid)) { context.getWiki().saveDocument(createdUserProfile, "Created user profile from LDAP server", context); } } /** * Sets attributes on the user object based on attribute values provided by the LDAP. * * @param userProfile the XWiki user profile document. * @param attributes the attributes of the LDAP user to update. * @param ldapDN the DN of the LDAP user to update * @param ldapUid value of the unique identifier for the user to update. * @param context the XWiki context. * @throws XWikiException error when updating XWiki user. */ protected void updateUserFromLDAP(XWikiDocument userProfile, List<XWikiLDAPSearchAttribute> attributes, String ldapDN, String ldapUid, XWikiContext context) throws XWikiException { Map<String, String> userMappings = this.configuration.getUserMappings(null, context); BaseClass userClass = context.getWiki().getUserClass(context); BaseObject userObj = userProfile.getXObject(userClass.getDocumentReference()); LOGGER.debug("Start synchronization of LDAP profile [{}] with existing user profile based on mapping [{}]", attributes, userMappings); // Clone the user object BaseObject clonedUser = userObj.clone(); // Apply all attributes to the clone set(attributes, userMappings, clonedUser, context); // Let BaseObject#apply tell us if something changed or not boolean needsUpdate = userObj.apply(clonedUser, false); if (this.configuration.getLDAPParam(XWikiLDAPConfig.PREF_LDAP_UPDATE_PHOTO, "0", context).equals("1")) { // Sync user photo with LDAP needsUpdate = updatePhotoFromLdap(ldapUid, userProfile, context) || needsUpdate; } // Update ldap profile object LDAPProfileXClass ldaXClass = new LDAPProfileXClass(context); needsUpdate |= ldaXClass.updateLDAPObject(userProfile, ldapDN, ldapUid); if (needsUpdate) { context.getWiki().saveDocument(userProfile, "Synchronized user profile with LDAP server", true, context); } } /** * Sync user avatar with LDAP * * @param ldapUid value of the unique identifier for the user to update. * @param userProfile the XWiki user profile document. * @param context the XWiki context. * @return true if avatar was updated, false otherwise. * @throws XWikiException */ protected boolean updatePhotoFromLdap(String ldapUid, XWikiDocument userProfile, XWikiContext context) throws XWikiException { BaseClass userClass = context.getWiki().getUserClass(context); BaseObject userObj = userProfile.getXObject(userClass.getDocumentReference()); // Get current user avatar String userAvatar = userObj.getStringValue("avatar"); XWikiAttachment currentPhoto = null; if (userAvatar != null) { currentPhoto = userProfile.getAttachment(userAvatar); } // Get properties String photoAttachmentName = this.configuration .getLDAPParam(XWikiLDAPConfig.PREF_LDAP_PHOTO_ATTACHMENT_NAME, "ldapPhoto", context); String ldapPhotoAttribute = this.configuration.getLDAPParam(XWikiLDAPConfig.PREF_LDAP_PHOTO_ATTRIBUTE, XWikiLDAPConfig.DEFAULT_PHOTO_ATTRIBUTE, context); // Proceed only if any of conditions are true: // 1. User do not have avatar currently // 2. User have avatar and avatar file name is equals to PREF_LDAP_PHOTO_ATTACHMENT_NAME if (StringUtils.isEmpty(userAvatar) || photoAttachmentName.equals(FilenameUtils.getBaseName(userAvatar)) || currentPhoto == null) { // Obtain photo from LDAP byte[] ldapPhoto = null; List<XWikiLDAPSearchAttribute> ldapAttributes = searchUserAttributesByUid(ldapUid, new String[] { ldapPhotoAttribute }); if (ldapAttributes != null) { // searchUserAttributesByUid method may return dn as 1st element // Let's iterate over array and search ldapPhotoAttribute for (XWikiLDAPSearchAttribute attribute : ldapAttributes) { if (attribute.name.equals(ldapPhotoAttribute)) { ldapPhoto = attribute.byteValue; } } } if (ldapPhoto != null) { ByteArrayInputStream ldapPhotoInputStream = new ByteArrayInputStream(ldapPhoto); // Try to guess image type String ldapPhotoType = guessImageType(ldapPhotoInputStream); ldapPhotoInputStream.reset(); if (ldapPhotoType != null) { String photoAttachmentFullName = photoAttachmentName + "." + ldapPhotoType.toLowerCase(); if (!StringUtils.isEmpty(userAvatar) && currentPhoto != null) { try { // Compare current xwiki avatar and LDAP photo if (!IOUtils.contentEquals(currentPhoto.getContentInputStream(context), ldapPhotoInputStream)) { ldapPhotoInputStream.reset(); // Store photo return addPhotoToProfile(userProfile, context, ldapPhotoInputStream, ldapPhoto.length, photoAttachmentFullName); } } catch (IOException ex) { LOGGER.error(ex.getMessage()); } } else if (addPhotoToProfile(userProfile, context, ldapPhotoInputStream, ldapPhoto.length, photoAttachmentFullName)) { PropertyClass avatarProperty = (PropertyClass) userClass.getField("avatar"); userObj.safeput("avatar", avatarProperty.fromString(photoAttachmentFullName)); return true; } } else { LOGGER.info("Unable to determine LDAP photo image type."); } } else if (currentPhoto != null) { // Remove current avatar PropertyClass avatarProperty = (PropertyClass) userClass.getField("avatar"); userObj.safeput("avatar", avatarProperty.fromString("")); return true; } } return false; } /** * Add photo to user profile as attachment. * * @param userProfile the XWiki user profile document. * @param context the XWiki context. * @param photoInputStream InputStream containing photo. * @param streamLength size of provided InputStream. * @param attachmentName attachment name for provided photo. * @return true if photo was saved to user profile, false otherwise. */ protected boolean addPhotoToProfile(XWikiDocument userProfile, XWikiContext context, InputStream photoInputStream, int streamLength, String attachmentName) { XWikiAttachment attachment; try { attachment = userProfile.addAttachment(attachmentName, photoInputStream, context); } catch (IOException | XWikiException ex) { LOGGER.error(ex.getMessage()); return false; } attachment.resetMimeType(context); return true; } /** * Guess image type of InputStream. * * @param imageInputStream InputStream containing image. * @return type of image as String. */ protected String guessImageType(InputStream imageInputStream) { ImageInputStream imageStream; try { imageStream = ImageIO.createImageInputStream(imageInputStream); } catch (IOException ex) { LOGGER.error(ex.getMessage()); return null; } Iterator<ImageReader> it = ImageIO.getImageReaders(imageStream); if (!it.hasNext()) { LOGGER.warn("No image readers found for provided stream."); return null; } ImageReader imageReader = it.next(); imageReader.setInput(imageStream); try { return imageReader.getFormatName(); } catch (IOException ex) { LOGGER.error(ex.getMessage()); return null; } finally { imageReader.dispose(); } } /** * Add user name to provided XWiki group. * * @param xwikiUserName the full name of the user. * @param groupName the name of the group. * @param context the XWiki context. */ // TODO move this methods in a toolkit for all platform. protected void addUserToXWikiGroup(String xwikiUserName, String groupName, XWikiContext context) { try { LOGGER.debug("Adding user [{}] to xwiki group [{}]", xwikiUserName, groupName); BaseClass groupClass = context.getWiki().getGroupClass(context); // Get document representing group XWikiDocument groupDoc = context.getWiki().getDocument(groupName, context); synchronized (groupDoc) { // Make extra sure the group cannot contain duplicate (even if this method is not supposed to be called // in this case) List<BaseObject> xobjects = groupDoc.getXObjects(groupClass.getDocumentReference()); if (xobjects != null) { for (BaseObject memberObj : xobjects) { if (memberObj != null) { String existingMember = memberObj.getStringValue(XWIKI_GROUP_MEMBERFIELD); if (existingMember != null && existingMember.equals(xwikiUserName)) { LOGGER.warn("User [{}] already exist in group [{}]", xwikiUserName, groupDoc.getDocumentReference()); return; } } } } // Add a member object to document BaseObject memberObj = groupDoc.newXObject(groupClass.getDocumentReference(), context); Map<String, String> map = new HashMap<String, String>(); map.put(XWIKI_GROUP_MEMBERFIELD, xwikiUserName); groupClass.fromMap(map, memberObj); // If the document is new, set its content if (groupDoc.isNew()) { groupDoc.setSyntax(Syntax.XWIKI_2_0); groupDoc.setContent("{{include reference='XWiki.XWikiGroupSheet' /}}"); } // Save modifications context.getWiki().saveDocument(groupDoc, context); } LOGGER.debug("Finished adding user [{}] to xwiki group [{}]", xwikiUserName, groupName); } catch (Exception e) { LOGGER.error("Failed to add a user [{}] to a group [{}]", new Object[] { xwikiUserName, groupName, e }); } } /** * Remove user name from provided XWiki group. * * @param xwikiUserName the full name of the user. * @param groupName the name of the group. * @param context the XWiki context. */ // TODO move this methods in a toolkit for all platform. protected void removeUserFromXWikiGroup(String xwikiUserName, String groupName, XWikiContext context) { try { BaseClass groupClass = context.getWiki().getGroupClass(context); // Get the XWiki document holding the objects comprising the group membership list XWikiDocument groupDoc = context.getWiki().getDocument(groupName, context); synchronized (groupDoc) { // Get and remove the specific group membership object for the user BaseObject groupObj = groupDoc.getXObject(groupClass.getDocumentReference(), XWIKI_GROUP_MEMBERFIELD, xwikiUserName); groupDoc.removeXObject(groupObj); // Save modifications context.getWiki().saveDocument(groupDoc, context); } } catch (Exception e) { LOGGER.error("Failed to remove a user from a group " + xwikiUserName + " group: " + groupName, e); } } /** * @param validXWikiUserName the valid XWiki name of the user to get the profile for. Used for fast lookup relying * on the document cache before doing a database search. * @param userId the UID to get the profile for * @param context the XWiki context * @return the XWiki document of the user with the passed UID * @throws XWikiException when a problem occurs while retrieving the user profile */ public XWikiDocument getUserProfileByUid(String validXWikiUserName, String userId, XWikiContext context) throws XWikiException { LDAPProfileXClass ldapXClass = new LDAPProfileXClass(context); // Try default profile name (generally in the cache) XWikiDocument userProfile; if (validXWikiUserName != null) { userProfile = context.getWiki().getDocument( new DocumentReference(context.getWikiId(), XWIKI_USER_SPACE, validXWikiUserName), context); } else { userProfile = null; } if (!userId.equalsIgnoreCase(ldapXClass.getUid(userProfile))) { // Search for existing profile with provided uid userProfile = ldapXClass.searchDocumentByUid(userId); // Resolve default profile patch of an uid if (userProfile == null && validXWikiUserName != null) { userProfile = getAvailableUserProfile(validXWikiUserName, context); } } return userProfile; } /** * @param validXWikiUserName a valid XWiki username for which to get a profile document * @param context the XWiki context * @return a (new) XWiki document for the passed username * @throws XWikiException when a problem occurs while retrieving the user profile */ private XWikiDocument getAvailableUserProfile(String validXWikiUserName, XWikiContext context) throws XWikiException { DocumentReference userReference = new DocumentReference(context.getWikiId(), XWIKI_USER_SPACE, validXWikiUserName); // Check if the default profile document is available for (int i = 0; true; ++i) { if (i > 0) { userReference = new DocumentReference(context.getWikiId(), XWIKI_USER_SPACE, validXWikiUserName + "_" + i); } XWikiDocument doc = context.getWiki().getDocument(userReference, context); // Don't use non user existing document if (doc.isNew()) { return doc; } } } /** * @param attributes the LDAP attributes of the user * @param context the XWiki context * @return the name of the XWiki user profile page * @throws XWikiException when a problem occurs while retrieving the user profile */ private XWikiDocument getAvailableUserProfile(List<XWikiLDAPSearchAttribute> attributes, XWikiContext context) throws XWikiException { String pageName = getUserPageName(attributes, context); return getAvailableUserProfile(pageName, context); } /** * @param attributes the LDAP attributes of the user * @param context the XWiki context * @return the name of the XWiki user profile page */ private String getUserPageName(List<XWikiLDAPSearchAttribute> attributes, XWikiContext context) { String userPageName = getConfiguration().getLDAPParam("userPageName", "${uid}", context); Map<String, String> valueMap = new HashMap<>(getConfiguration().getMemoryConfiguration()); if (attributes != null) { for (XWikiLDAPSearchAttribute attribute : attributes) { valueMap.put("ldap." + attribute.name, attribute.value); if (attribute.name.equals(this.uidAttributeName)) { // Override the default uid value with the real one coming from LDAP valueMap.put("uid", attribute.value); } } } String pageName = StrSubstitutor.replace(userPageName, valueMap); pageName = cleanXWikiUserPageName(pageName); LOGGER.debug("UserPageName: {}", pageName); return pageName; } }