/** * $RCSfile$ * $Revision: 3191 $ * $Date: 2005-12-12 13:41:22 -0300 (Mon, 12 Dec 2005) $ * * Copyright (C) 2005 Jive Software. All rights reserved. * * This software is published under the terms of the GNU Public License (GPL), * a copy of which is included in this distribution. */ package org.jivesoftware.openfire.ldap; import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.openfire.group.Group; import org.jivesoftware.openfire.group.GroupNotFoundException; import org.jivesoftware.openfire.group.GroupProvider; import org.jivesoftware.openfire.user.UserManager; import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.Log; import org.xmpp.packet.JID; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.*; import javax.naming.ldap.Control; import javax.naming.ldap.LdapContext; import javax.naming.ldap.LdapName; import javax.naming.ldap.SortControl; import java.text.MessageFormat; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * LDAP implementation of the GroupProvider interface. All data in the directory is treated as * read-only so any set operations will result in an exception. * * @author Matt Tucker, Greg Ferguson and Cameron Moore */ public class LdapGroupProvider implements GroupProvider { private LdapManager manager; private UserManager userManager; private String[] standardAttributes; /** * Constructs a new LDAP group provider. */ public LdapGroupProvider() { manager = LdapManager.getInstance(); userManager = UserManager.getInstance(); standardAttributes = new String[3]; standardAttributes[0] = manager.getGroupNameField(); standardAttributes[1] = manager.getGroupDescriptionField(); standardAttributes[2] = manager.getGroupMemberField(); } /** * Always throws an UnsupportedOperationException because LDAP groups are read-only. * * @param name the name of the group to create. * @throws UnsupportedOperationException when called. */ public Group createGroup(String name) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Always throws an UnsupportedOperationException because LDAP groups are read-only. * * @param name the name of the group to delete * @throws UnsupportedOperationException when called. */ public void deleteGroup(String name) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } public Group getGroup(String groupName) throws GroupNotFoundException { Collection<Group> groups; LdapContext ctx = null; try { ctx = manager.getContext(); // Search for the dn based on the group name. SearchControls searchControls = new SearchControls(); // See if recursive searching is enabled. Otherwise, only search one level. if (manager.isSubTreeSearch()) { searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); } else { searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); } searchControls.setReturningAttributes(standardAttributes); String filter = MessageFormat.format(manager.getGroupSearchFilter(), groupName); NamingEnumeration<SearchResult> answer = ctx.search("", filter, searchControls); groups = populateGroups(answer); // Close the enumeration. answer.close(); if (groups.size() == 1) { return groups.iterator().next(); } } catch (Exception e) { Log.error(e); throw new GroupNotFoundException(e); } finally { try { if (ctx != null) { ctx.setRequestControls(null); ctx.close(); } } catch (Exception ignored) { // Ignore. } } if (groups.size() > 1) { // If multiple groups found, throw exception. throw new GroupNotFoundException( "Too many groups with name " + groupName + " were found."); } throw new GroupNotFoundException("Group with name " + groupName + " not found."); } /** * Always throws an UnsupportedOperationException because LDAP groups are read-only. * * @param oldName the current name of the group. * @param newName the desired new name of the group. * @throws UnsupportedOperationException when called. */ public void setName(String oldName, String newName) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Always throws an UnsupportedOperationException because LDAP groups are read-only. * * @param name the group name. * @param description the group description. * @throws UnsupportedOperationException when called. */ public void setDescription(String name, String description) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } public int getGroupCount() { if (manager.isDebugEnabled()) { Log.debug("LdapGroupProvider: Trying to get the number of groups in the system."); } int count = 0; LdapContext ctx = null; try { ctx = manager.getContext(); SearchControls searchControls = new SearchControls(); // See if recursive searching is enabled. Otherwise, only search one level. if (manager.isSubTreeSearch()) { searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); } else { searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); } searchControls.setReturningAttributes(new String[] { manager.getGroupNameField() }); String filter = MessageFormat.format(manager.getGroupSearchFilter(), "*"); NamingEnumeration answer = ctx.search("", filter, searchControls); while (answer.hasMoreElements()) { answer.next(); count++; } // Close the enumeration. answer.close(); } catch (Exception e) { Log.error(e); } finally { try { if (ctx != null) { ctx.setRequestControls(null); ctx.close(); } } catch (Exception ignored) { // Ignore. } } return count; } public Collection<String> getGroupNames() { List<String> groupNames = new ArrayList<String>(); LdapContext ctx = null; try { ctx = manager.getContext(); // Sort on group name field. Control[] searchControl = new Control[]{ new SortControl(new String[]{manager.getGroupNameField()}, Control.NONCRITICAL) }; ctx.setRequestControls(searchControl); SearchControls searchControls = new SearchControls(); // See if recursive searching is enabled. Otherwise, only search one level. if (manager.isSubTreeSearch()) { searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); } else { searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); } searchControls.setReturningAttributes(new String[] { manager.getGroupNameField() }); String filter = MessageFormat.format(manager.getGroupSearchFilter(), "*"); NamingEnumeration answer = ctx.search("", filter, searchControls); while (answer.hasMoreElements()) { // Get the next group. String groupName = (String)((SearchResult)answer.next()).getAttributes().get( manager.getGroupNameField()).get(); // Escape group name and add to results. groupNames.add(JID.escapeNode(groupName)); } // Close the enumeration. answer.close(); // If client-side sorting is enabled, sort. if (Boolean.valueOf(JiveGlobals.getXMLProperty("ldap.clientSideSorting"))) { Collections.sort(groupNames); } } catch (Exception e) { Log.error(e); } finally { try { if (ctx != null) { ctx.setRequestControls(null); ctx.close(); } } catch (Exception ignored) { // Ignore. } } return groupNames; } public Collection<String> getGroupNames(int startIndex, int numResults) { List<String> groupNames = new ArrayList<String>(); LdapContext ctx = null; try { ctx = manager.getContext(); // Sort on group name field. Control[] searchControl = new Control[]{ new SortControl(new String[]{manager.getGroupNameField()}, Control.NONCRITICAL) }; ctx.setRequestControls(searchControl); SearchControls searchControls = new SearchControls(); // See if recursive searching is enabled. Otherwise, only search one level. if (manager.isSubTreeSearch()) { searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); } else { searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); } searchControls.setReturningAttributes(new String[] { manager.getGroupNameField() }); String filter = MessageFormat.format(manager.getGroupSearchFilter(), "*"); // TODO: used paged results if supported by LDAP server. NamingEnumeration answer = ctx.search("", filter, searchControls); for (int i=0; i < startIndex; i++) { if (answer.hasMoreElements()) { answer.next(); } else { return Collections.emptyList(); } } // Now read in desired number of results (or stop if we run out of results). for (int i = 0; i < numResults; i++) { if (answer.hasMoreElements()) { // Get the next group. String groupName = (String)((SearchResult)answer.next()).getAttributes().get( manager.getGroupNameField()).get(); // Escape group name and add to results. groupNames.add(JID.escapeNode(groupName)); } else { break; } } // Close the enumeration. answer.close(); // If client-side sorting is enabled, sort. if (Boolean.valueOf(JiveGlobals.getXMLProperty("ldap.clientSideSorting"))) { Collections.sort(groupNames); } } catch (Exception e) { Log.error(e); } finally { try { if (ctx != null) { ctx.setRequestControls(null); ctx.close(); } } catch (Exception ignored) { // Ignore. } } return groupNames; } public Collection<String> getGroupNames(JID user) { // Get DN of specified user XMPPServer server = XMPPServer.getInstance(); String username; if (!manager.isPosixMode()) { // Check if the user exists (only if user is a local user) if (!server.isLocal(user)) { return Collections.emptyList(); } username = JID.unescapeNode(user.getNode()); try { username = manager.findUserDN(username) + "," + manager.getBaseDN(); } catch (Exception e) { Log.error("Could not find user in LDAP " + username); return Collections.emptyList(); } } else { username = server.isLocal(user) ? JID.unescapeNode(user.getNode()) : user.toString(); } // Do nothing if the user is empty or null if (username == null || "".equals(username)) { return Collections.emptyList(); } // Perform the LDAP query List<String> groupNames = new ArrayList<String>(); LdapContext ctx = null; try { ctx = manager.getContext(); // Search for the dn based on the group name. SearchControls searchControls = new SearchControls(); // See if recursive searching is enabled. Otherwise, only search one level. if (manager.isSubTreeSearch()) { searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); } else { searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); } searchControls.setReturningAttributes(new String[] { manager.getGroupNameField() }); StringBuilder filter = new StringBuilder(); filter.append("(&"); filter.append(MessageFormat.format(manager.getGroupSearchFilter(), "*")); filter.append("(").append(manager.getGroupMemberField()).append("=").append(username); filter.append("))"); if (Log.isDebugEnabled()) { Log.debug("Trying to find group names for user: " + user + " using query: " + filter.toString()); } NamingEnumeration answer = ctx.search("", filter.toString(), searchControls); while (answer.hasMoreElements()) { // Get the next group. String groupName = (String)((SearchResult)answer.next()).getAttributes().get( manager.getGroupNameField()).get(); // Escape group name and add to results. groupNames.add(JID.escapeNode(groupName)); } // Close the enumeration. answer.close(); // If client-side sorting is enabled, sort. if (Boolean.valueOf(JiveGlobals.getXMLProperty("ldap.clientSideSorting"))) { Collections.sort(groupNames); } } catch (Exception e) { Log.error("Error getting groups for user: " + user, e); return Collections.emptyList(); } finally { try { if (ctx != null) { ctx.setRequestControls(null); ctx.close(); } } catch (Exception ignored) { // Ignore. } } return groupNames; } /** * Always throws an UnsupportedOperationException because LDAP groups are read-only. * * @param groupName name of a group. * @param user the JID of the user to add * @param administrator true if is an administrator. * @throws UnsupportedOperationException when called. */ public void addMember(String groupName, JID user, boolean administrator) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Always throws an UnsupportedOperationException because LDAP groups are read-only. * * @param groupName the naame of a group. * @param user the JID of the user with new privileges * @param administrator true if is an administrator. * @throws UnsupportedOperationException when called. */ public void updateMember(String groupName, JID user, boolean administrator) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Always throws an UnsupportedOperationException because LDAP groups are read-only. * * @param groupName the name of a group. * @param user the JID of the user to delete. * @throws UnsupportedOperationException when called. */ public void deleteMember(String groupName, JID user) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } /** * Returns true because LDAP groups are read-only. * * @return true because all LDAP functions are read-only. */ public boolean isReadOnly() { return true; } public Collection<String> search(String query) { if (query == null || "".equals(query)) { return Collections.emptyList(); } // Make the query be a wildcard search by default. So, if the user searches for // "Test", make the search be "Test*" instead. if (!query.endsWith("*")) { query = query + "*"; } List<String> groupNames = new ArrayList<String>(); LdapContext ctx = null; try { ctx = manager.getContext(); // Sort on username field. Control[] searchControl = new Control[]{ new SortControl(new String[]{manager.getGroupNameField()}, Control.NONCRITICAL) }; ctx.setRequestControls(searchControl); // Search for the dn based on the group name. SearchControls searchControls = new SearchControls(); // See if recursive searching is enabled. Otherwise, only search one level. if (manager.isSubTreeSearch()) { searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); } else { searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); } searchControls.setReturningAttributes(new String[] { manager.getGroupNameField() }); StringBuilder filter = new StringBuilder(); filter.append("(").append(manager.getGroupNameField()).append("=").append(query).append(")"); NamingEnumeration answer = ctx.search("", filter.toString(), searchControls); while (answer.hasMoreElements()) { // Get the next group. String groupName = (String)((SearchResult)answer.next()).getAttributes().get( manager.getGroupNameField()).get(); // Escape group name and add to results. groupNames.add(JID.escapeNode(groupName)); } // Close the enumeration. answer.close(); // If client-side sorting is enabled, sort. if (Boolean.valueOf(JiveGlobals.getXMLProperty("ldap.clientSideSorting"))) { Collections.sort(groupNames); } } catch (Exception e) { Log.error(e); } finally { try { if (ctx != null) { ctx.setRequestControls(null); ctx.close(); } } catch (Exception ignored) { // Ignore. } } return groupNames; } public Collection<String> search(String query, int startIndex, int numResults) { if (query == null || "".equals(query)) { return Collections.emptyList(); } // Make the query be a wildcard search by default. So, if the user searches for // "Test", make the search be "Test*" instead. if (!query.endsWith("*")) { query = query + "*"; } List<String> groupNames = new ArrayList<String>(); LdapContext ctx = null; try { ctx = manager.getContext(); // Sort on username field. Control[] searchControl = new Control[]{ new SortControl(new String[]{manager.getGroupNameField()}, Control.NONCRITICAL) }; ctx.setRequestControls(searchControl); // Search for the dn based on the group name. SearchControls searchControls = new SearchControls(); // See if recursive searching is enabled. Otherwise, only search one level. if (manager.isSubTreeSearch()) { searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); } else { searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); } searchControls.setReturningAttributes(new String[] { manager.getGroupNameField() }); StringBuilder filter = new StringBuilder(); filter.append("(").append(manager.getGroupNameField()).append("=").append(query).append(")"); // TODO: used paged results if supported by LDAP server. NamingEnumeration answer = ctx.search("", filter.toString(), searchControls); for (int i=0; i < startIndex; i++) { if (answer.hasMoreElements()) { answer.next(); } else { return Collections.emptyList(); } } // Now read in desired number of results (or stop if we run out of results). for (int i = 0; i < numResults; i++) { if (answer.hasMoreElements()) { // Get the next group. String groupName = (String)((SearchResult)answer.next()).getAttributes().get( manager.getGroupNameField()).get(); // Escape group name and add to results. groupNames.add(JID.escapeNode(groupName)); } else { break; } } // Close the enumeration. answer.close(); // If client-side sorting is enabled, sort. if (Boolean.valueOf(JiveGlobals.getXMLProperty("ldap.clientSideSorting"))) { Collections.sort(groupNames); } } catch (Exception e) { Log.error(e); } finally { try { if (ctx != null) { ctx.setRequestControls(null); ctx.close(); } } catch (Exception ignored) { // Ignore. } } return groupNames; } public boolean isSearchSupported() { return true; } /** * An auxilary method used to populate LDAP groups based on a provided LDAP search result. * * @param answer LDAP search result. * @return a collection of groups. * @throws javax.naming.NamingException */ private Collection<Group> populateGroups(Enumeration<SearchResult> answer) throws NamingException { if (manager.isDebugEnabled()) { Log.debug("LdapGroupProvider: Starting to populate groups with users."); } DirContext ctx = null; try { TreeMap<String, Group> groups = new TreeMap<String, Group>(); ctx = manager.getContext(); SearchControls searchControls = new SearchControls(); searchControls.setReturningAttributes(new String[] { manager.getUsernameField() }); // See if recursive searching is enabled. Otherwise, only search one level. if (manager.isSubTreeSearch()) { searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); } else { searchControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); } XMPPServer server = XMPPServer.getInstance(); String serverName = server.getServerInfo().getXMPPDomain(); // Build 3 groups. // group 1: uid= // group 2: rest of the text until first comma // group 3: rest of the text Pattern pattern = Pattern.compile("(?i)(^" + manager.getUsernameField() + "=)([^,]+)(.+)"); while (answer.hasMoreElements()) { String name = ""; try { Attributes a = answer.nextElement().getAttributes(); String description; try { name = ((String)((a.get(manager.getGroupNameField())).get())); description = ((String)((a.get(manager.getGroupDescriptionField())).get())); } catch (Exception e) { description = ""; } Set<JID> members = new TreeSet<JID>(); Attribute memberField = a.get(manager.getGroupMemberField()); if (memberField != null) { NamingEnumeration ne = memberField.getAll(); while (ne.hasMore()) { String username = (String) ne.next(); // If not posix mode, each group member is stored as a full DN. if (!manager.isPosixMode()) { try { // Try to find the username with a regex pattern match. Matcher matcher = pattern.matcher(username); if (matcher.matches() && matcher.groupCount() == 3) { // The username is in the DN, no additional search needed username = matcher.group(2); } // The regex pattern match failed. This will happen if the // the member DN's don't use the standard username field. For // example, Active Directory has a username field of // sAMAccountName, but stores group members as "CN=...". else { // Create an LDAP name with the full DN. LdapName ldapName = new LdapName(username); // Turn the LDAP name into something we can use in a // search by stripping off the comma. String userDNPart = ldapName.get(ldapName.size() - 1); NamingEnumeration usrAnswer = ctx.search("", userDNPart, searchControls); if (usrAnswer != null && usrAnswer.hasMoreElements()) { username = (String) ((SearchResult) usrAnswer.next()) .getAttributes().get( manager.getUsernameField()).get(); } // Close the enumeration. usrAnswer.close(); } } catch (Exception e) { Log.error(e); } } // A search filter may have been defined in the LdapUserProvider. // Therefore, we have to try to load each user we found to see if // it passes the filter. try { JID userJID; int position = username.indexOf("@" + serverName); // Create JID of local user if JID does not match a component's JID if (position == -1) { // In order to lookup a username from the manager, the username // must be a properly escaped JID node. String escapedUsername = JID.escapeNode(username); if (!escapedUsername.equals(username)) { // Check if escaped username is valid userManager.getUser(escapedUsername); } // No exception, so the user must exist. Add the user as a group // member using the escaped username. userJID = server.createJID(escapedUsername, null); } else { // This is a JID of a component or node of a server's component String node = username.substring(0, position); String escapedUsername = JID.escapeNode(node); userJID = new JID(escapedUsername + "@" + serverName); } members.add(userJID); } catch (UserNotFoundException e) { // We can safely ignore this error. It likely means that // the user didn't pass the search filter that's defined. // So, we want to simply ignore the user as a group member. if (manager.isDebugEnabled()) { Log.debug("LdapGroupProvider: User not found: " + username); } } } // Close the enumeration. ne.close(); } if (manager.isDebugEnabled()) { Log.debug("LdapGroupProvider: Adding group \"" + name + "\" with " + members.size() + " members."); } Collection<JID> admins = Collections.emptyList(); Group group = new Group(name, description, members, admins); groups.put(name, group); } catch (Exception e) { e.printStackTrace(); if (manager.isDebugEnabled()) { Log.debug("LdapGroupProvider: Error while populating group, " + name + ".", e); } } } if (manager.isDebugEnabled()) { Log.debug("LdapGroupProvider: Finished populating group(s) with users."); } return groups.values(); } finally { try { if (ctx != null) { ctx.close(); } } catch (Exception e) { // Ignore. } } } }