/** * $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.wildfire.ldap; import org.jivesoftware.util.JiveConstants; import org.jivesoftware.util.Log; import org.jivesoftware.wildfire.XMPPServer; import org.jivesoftware.wildfire.group.Group; import org.jivesoftware.wildfire.group.GroupNotFoundException; import org.jivesoftware.wildfire.group.GroupProvider; import org.jivesoftware.wildfire.user.UserManager; import org.jivesoftware.wildfire.user.UserNotFoundException; import org.xmpp.packet.JID; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.*; import javax.naming.ldap.LdapName; 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 Greg Ferguson and Cameron Moore */ public class LdapGroupProvider implements GroupProvider { private LdapManager manager; private UserManager userManager; private int groupCount; private long expiresStamp; private String[] standardAttributes; /** * Constructor of the LdapGroupProvider class. Gets an LdapManager instance from the LdapManager class. */ public LdapGroupProvider() { manager = LdapManager.getInstance(); userManager = UserManager.getInstance(); groupCount = -1; expiresStamp = System.currentTimeMillis(); 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 group) throws GroupNotFoundException { String filter = MessageFormat.format(manager.getGroupSearchFilter(), "*"); String searchFilter = "(&" + filter + "(" + manager.getGroupNameField() + "=" + group + "))"; Collection<Group> groups; try { groups = populateGroups(searchForGroups(searchFilter, standardAttributes)); } catch (NamingException e) { Log.error("Error populating groups from LDAP", e); throw new GroupNotFoundException("Error populating groups from LDAP", e); } if (groups.size() > 1) { // If multiple groups found, throw exception. throw new GroupNotFoundException("Too many groups with name " + group + " were found."); } else if (groups.isEmpty()) { throw new GroupNotFoundException("Group with name " + group + " not found."); } else { return groups.iterator().next(); } } /** * 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() { // Cache group count for 5 minutes. if (groupCount != -1 && System.currentTimeMillis() < expiresStamp) { return groupCount; } int count = 0; if (manager.isDebugEnabled()) { Log.debug("Trying to get the number of groups in the system."); } String searchFilter = MessageFormat.format(manager.getGroupSearchFilter(), "*"); String returningAttributes[] = {manager.getGroupNameField()}; try { NamingEnumeration<SearchResult> answer = searchForGroups(searchFilter, returningAttributes); for (; answer.hasMoreElements(); count++) { try { answer.next(); } catch (Exception e) { // Ignore. } } this.groupCount = count; this.expiresStamp = System.currentTimeMillis() + JiveConstants.MINUTE * 5; } catch (NamingException ex) { Log.error("Error searching for groups in LDAP", ex); } return count; } public Collection<Group> getGroups() { String filter = MessageFormat.format(manager.getGroupSearchFilter(), "*"); try { return populateGroups(searchForGroups(filter, standardAttributes)); } catch (NamingException ex) { return Collections.emptyList(); } } public Collection<Group> getGroups(Set<String> groupNames) { if (groupNames.isEmpty()) { return Collections.emptyList(); } Collection<Group> groups = new ArrayList<Group>(groupNames.size()); String filter = MessageFormat.format(manager.getGroupSearchFilter(), "*"); // Instead of loading all groups at once which may not work for super big collections // of group names, we are going to make many queries and load by 10 groups at onces Collection<String> searchFilters = new ArrayList<String>(groupNames.size()); List<String> names = new ArrayList<String>(groupNames); int i = 0; int range = 10; do { List<String> subset = names.subList(i, Math.min(i + range, groupNames.size())); if (subset.size() == 1) { String searchFilter = "(&" + filter + "(" + manager.getGroupNameField() + "=" + subset.get(0) + "))"; searchFilters.add(searchFilter); } else { StringBuilder sb = new StringBuilder(300); sb.append("(&").append(filter).append("(|"); for (String groupName : subset) { sb.append("(").append(manager.getGroupNameField()).append("="); sb.append(groupName).append(")"); } sb.append("))"); searchFilters.add(sb.toString()); } // Increment counter to get next range i = i + range; } while (groupNames.size() > i); // Perform all required queries to load all requested groups for (String searchFilter : searchFilters) { try { groups.addAll(populateGroups(searchForGroups(searchFilter, standardAttributes))); } catch (NamingException e) { Log.error("Error populating groups from LDAP", e); return Collections.emptyList(); } } return new ArrayList<Group>(groups); } public Collection<Group> getGroups(int start, int num) { // Get an enumeration of all groups in the system String searchFilter = MessageFormat.format(manager.getGroupSearchFilter(), "*"); NamingEnumeration<SearchResult> answer; try { answer = searchForGroups(searchFilter, standardAttributes); } catch (NamingException e) { Log.error("Error searching for groups in LDAP", e); return Collections.emptyList(); } // Place all groups that are wanted into an enumeration Vector<SearchResult> v = new Vector<SearchResult>(); for (int i = 1; answer.hasMoreElements() && i <= (start + num); i++) { try { SearchResult sr = answer.next(); if (i >= start) { v.add(sr); } } catch (Exception e) { // Ignore. } } try { return populateGroups(v.elements()); } catch (NamingException e) { Log.error("Error populating groups recieved from LDAP", e); return Collections.emptyList(); } } public Collection<Group> getGroups(JID 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(); } String filter = MessageFormat.format(manager.getGroupSearchFilter(), username); try { return populateGroups(searchForGroups(filter, standardAttributes)); } catch (NamingException e) { Log.error("Error populating groups recieved from LDAP", e); return Collections.emptyList(); } } /** * 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; } /** * An auxilary method used to perform LDAP queries based on a provided LDAP search filter. * * @param searchFilter LDAP search filter used to query. * @return an enumeration of SearchResult. */ private NamingEnumeration<SearchResult> searchForGroups(String searchFilter, String[] returningAttributes) throws NamingException { if (manager.isDebugEnabled()) { Log.debug("Trying to find all groups in the system."); } DirContext ctx = null; NamingEnumeration<SearchResult> answer; try { ctx = manager.getContext(); if (manager.isDebugEnabled()) { Log.debug("Starting LDAP search..."); Log.debug("Using groupSearchFilter: " + searchFilter); } // Search for the dn based on the groupname. SearchControls searchControls = new SearchControls(); searchControls.setReturningAttributes(returningAttributes); // 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); } answer = ctx.search("", searchFilter, searchControls); if (manager.isDebugEnabled()) { Log.debug("... search finished"); } return answer; } finally { if (ctx != null) { try { ctx.close(); } catch (Exception ex) { /* do nothing */ } } } } /** * 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. */ private Collection<Group> populateGroups(Enumeration<SearchResult> answer) throws NamingException { if (manager.isDebugEnabled()) { Log.debug("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); } String userSearchFilter = MessageFormat.format(manager.getSearchFilter(), "*"); XMPPServer server = XMPPServer.getInstance(); String serverName = server.getServerInfo().getName(); // 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 = ""; } TreeSet<JID> members = new TreeSet<JID>(); Attribute member = a.get(manager.getGroupMemberField()); NamingEnumeration ne = member.getAll(); while (ne.hasMore()) { String username = (String) ne.next(); if (!manager.isPosixMode()) { //userName is full dn if not posix try { // LdapName will not generate spaces around an '=' // (according to the docs) 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); } else { // We have to do a new search to find the username field // Get the CN using LDAP LdapName ldapname = new LdapName(username); String ldapcn = ldapname.get(ldapname.size() - 1); String combinedFilter = "(&(" + ldapcn + ")" + userSearchFilter + ")"; NamingEnumeration usrAnswer = ctx.search("", combinedFilter, searchControls); if (usrAnswer.hasMoreElements()) { username = (String) ((SearchResult) usrAnswer.next()) .getAttributes().get( manager.getUsernameField()).get(); } else { throw new UserNotFoundException(); } } } catch (Exception e) { if (manager.isDebugEnabled()) { Log.debug("Error populating user with DN: " + username, 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) { if (manager.isDebugEnabled()) { Log.debug("User not found: " + username); } } } if (manager.isDebugEnabled()) { Log.debug("Adding group \"" + name + "\" with " + members.size() + " members."); } Group g = new Group(name, description, members, new ArrayList<JID>()); groups.put(name, g); } catch (Exception e) { if (manager.isDebugEnabled()) { Log.debug("Error while populating group, " + name + ".", e); } } } if (manager.isDebugEnabled()) { Log.debug("Finished populating group(s) with users."); } return groups.values(); } finally { try { if (ctx != null) { ctx.close(); } } catch (Exception e) { // Ignore. } } } }