LdapGroupProvider.java 17.4 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile$
 * $Revision: 3191 $
 * $Date: 2005-12-12 13:41:22 -0300 (Mon, 12 Dec 2005) $
 *
6
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
7
 *
8 9 10 11 12 13 14 15 16 17 18
 * 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.
19 20
 */

21
package org.jivesoftware.openfire.ldap;
22

23 24 25 26 27 28 29
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
30 31

import javax.naming.NamingEnumeration;
32
import javax.naming.NamingException;
33 34 35 36 37 38
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
39 40 41 42 43 44 45 46 47 48 49

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.JiveConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID;
50 51

/**
52 53
 * 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.
54
 *
55
 * @author Matt Tucker, Greg Ferguson and Cameron Moore
56 57 58
 */
public class LdapGroupProvider implements GroupProvider {

59 60
	private static final Logger Log = LoggerFactory.getLogger(LdapGroupProvider.class);

61 62 63
    private LdapManager manager;
    private UserManager userManager;
    private String[] standardAttributes;
64 65
    private int groupCount = -1;
    private long expiresStamp = System.currentTimeMillis();
66 67

    /**
68
     * Constructs a new LDAP group provider.
69 70 71 72 73 74 75 76 77 78 79
     */
    public LdapGroupProvider() {
        manager = LdapManager.getInstance();
        userManager = UserManager.getInstance();
        standardAttributes = new String[3];
        standardAttributes[0] = manager.getGroupNameField();
        standardAttributes[1] = manager.getGroupDescriptionField();
        standardAttributes[2] = manager.getGroupMemberField();
    }

    /**
80
     * Always throws an UnsupportedOperationException because LDAP groups are read-only.
81 82 83 84 85 86 87 88 89
     *
     * @param name the name of the group to create.
     * @throws UnsupportedOperationException when called.
     */
    public Group createGroup(String name) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

    /**
90
     * Always throws an UnsupportedOperationException because LDAP groups are read-only.
91 92 93 94 95 96 97 98
     *
     * @param name the name of the group to delete
     * @throws UnsupportedOperationException when called.
     */
    public void deleteGroup(String name) throws UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }

99 100
    public Group getGroup(String groupName) throws GroupNotFoundException {
        LdapContext ctx = null;
101
        try {
102 103 104 105
            String groupDN = manager.findGroupDN(groupName);
            // Load record.
            ctx = manager.getContext(manager.getGroupsBaseDN(groupName));
            Attributes attrs = ctx.getAttributes(groupDN, standardAttributes);
106

107
            return processGroup(ctx, attrs);
108
        }
109
        catch (Exception e) {
110
            Log.error(e.getMessage(), e);
111
            throw new GroupNotFoundException("Group with name " + groupName + " not found.", e);
Matt Tucker's avatar
Matt Tucker committed
112
        }
113 114 115 116 117 118 119 120 121 122
        finally {
            try {
                if (ctx != null) {
                    ctx.setRequestControls(null);
                    ctx.close();
                }
            }
            catch (Exception ignored) {
                // Ignore.
            }
123 124 125 126
        }
    }

    /**
127
     * Always throws an UnsupportedOperationException because LDAP groups are read-only.
128 129 130 131 132 133 134 135 136 137
     *
     * @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();
    }

    /**
138
     * Always throws an UnsupportedOperationException because LDAP groups are read-only.
139
     *
140
     * @param name the group name.
141 142 143
     * @param description the group description.
     * @throws UnsupportedOperationException when called.
     */
144
    public void setDescription(String name, String description)
145 146
            throws UnsupportedOperationException
    {
147 148 149 150 151
        throw new UnsupportedOperationException();
    }

    public int getGroupCount() {
        if (manager.isDebugEnabled()) {
152
            Log.debug("LdapGroupProvider: Trying to get the number of groups in the system.");
153
        }
154 155 156 157
        // Cache user count for 5 minutes.
        if (groupCount != -1 && System.currentTimeMillis() < expiresStamp) {
            return groupCount;
        }
158
        this.groupCount = manager.retrieveListCount(
159
                manager.getGroupNameField(),
160
                MessageFormat.format(manager.getGroupSearchFilter(), "*")
161
        );
162
        this.expiresStamp = System.currentTimeMillis() + JiveConstants.MINUTE *5;
163
        return this.groupCount;
164 165
    }

166 167 168 169 170
    public Collection<String> getSharedGroupsNames() {
        // Get the list of shared groups from the database
        return Group.getSharedGroupsNames();
    }

171
    public Collection<String> getGroupNames() {
172
        return getGroupNames(-1, -1);
173 174
    }

175
    public Collection<String> getGroupNames(int startIndex, int numResults) {
176 177 178 179 180 181 182
        return manager.retrieveList(
                manager.getGroupNameField(),
                MessageFormat.format(manager.getGroupSearchFilter(), "*"),
                startIndex,
                numResults,
                null
        );
183 184
    }

185
    public Collection<String> getGroupNames(JID user) {
186 187
        // Get DN of specified user
        XMPPServer server = XMPPServer.getInstance();
188
        String username;
189
        if (!manager.isPosixMode()) {
190 191 192 193 194
            // Check if the user exists (only if user is a local user)
            if (!server.isLocal(user)) {
                return Collections.emptyList();
            }
            username = JID.unescapeNode(user.getNode());
195
            try {
196
                username = manager.findUserDN(username) + "," + manager.getUsersBaseDN(username);
197 198
            }
            catch (Exception e) {
199
                Log.error("Could not find user in LDAP " + username);
200
                return Collections.emptyList();
201 202
            }
        }
203 204 205
        else {
            username = server.isLocal(user) ? JID.unescapeNode(user.getNode()) : user.toString();
        }
206 207 208 209
        // Do nothing if the user is empty or null
        if (username == null || "".equals(username)) {
            return Collections.emptyList();
        }
210 211 212 213 214 215 216
        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());
217
        }
218 219 220 221 222 223 224 225
        // Perform the LDAP query
        return manager.retrieveList(
                manager.getGroupNameField(),
                filter.toString(),
                -1,
                -1,
                null
        );
226 227 228
    }

    /**
229
     * Always throws an UnsupportedOperationException because LDAP groups are read-only.
230
     *
231 232
     * @param groupName name of a group.
     * @param user the JID of the user to add
233 234 235 236
     * @param administrator true if is an administrator.
     * @throws UnsupportedOperationException when called.
     */
    public void addMember(String groupName, JID user, boolean administrator)
237 238
            throws UnsupportedOperationException
    {
239 240 241 242
        throw new UnsupportedOperationException();
    }

    /**
243
     * Always throws an UnsupportedOperationException because LDAP groups are read-only.
244
     *
245 246
     * @param groupName the naame of a group.
     * @param user the JID of the user with new privileges
247 248 249 250
     * @param administrator true if is an administrator.
     * @throws UnsupportedOperationException when called.
     */
    public void updateMember(String groupName, JID user, boolean administrator)
251
            throws UnsupportedOperationException {
252 253 254 255
        throw new UnsupportedOperationException();
    }

    /**
256
     * Always throws an UnsupportedOperationException because LDAP groups are read-only.
257 258
     *
     * @param groupName the name of a group.
259
     * @param user the JID of the user to delete.
260 261
     * @throws UnsupportedOperationException when called.
     */
Matt Tucker's avatar
Matt Tucker committed
262
    public void deleteMember(String groupName, JID user) throws UnsupportedOperationException {
263 264 265 266
        throw new UnsupportedOperationException();
    }

    /**
Matt Tucker's avatar
Matt Tucker committed
267
     * Returns true because LDAP groups are read-only.
268 269 270 271 272 273 274
     *
     * @return true because all LDAP functions are read-only.
     */
    public boolean isReadOnly() {
        return true;
    }

275
    public Collection<String> search(String query) {
276
        return search(query, -1, -1);
277 278
    }

279
    public Collection<String> search(String query, int startIndex, int numResults) {
280 281 282 283 284 285 286 287
        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 + "*";
        }
288 289 290 291 292 293 294 295 296 297
        StringBuilder filter = new StringBuilder();
        filter.append("(").append(manager.getGroupNameField()).append("=").append(query).append(")");
        // Perform the LDAP query
        return manager.retrieveList(
                manager.getGroupNameField(),
                filter.toString(),
                startIndex,
                numResults,
                null
        );
298 299 300 301 302 303
    }

    public boolean isSearchSupported() {
        return true;
    }

304 305 306
    private Group processGroup(LdapContext ctx, Attributes a) throws NamingException {
        XMPPServer server = XMPPServer.getInstance();
        String serverName = server.getServerInfo().getXMPPDomain();
307
        // Build `3 groups.
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
        // 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() + "=)([^,]+)(.+)");

        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 name;
        String description;
        try {
            name = ((String)((a.get(manager.getGroupNameField())).get()));
        }
        catch (Exception e) {
            name = "";
        }
        try {
            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.
362 363 364 365 366 367
                            StringBuilder userFilter = new StringBuilder();
                            userFilter.append("(&(");
                            userFilter.append(ldapName.get(ldapName.size() - 1));
                            userFilter.append(")");
                            userFilter.append(MessageFormat.format(manager.getSearchFilter(), "*"));
                            userFilter.append(")");
368
                            NamingEnumeration usrAnswer = ctx.search("",
369
                                    userFilter.toString(), searchControls);
370
                            if (usrAnswer != null && usrAnswer.hasMoreElements()) {
371 372 373 374
                                Attribute usernameAttr = ((SearchResult)usrAnswer.next()).getAttributes().get(manager.getUsernameField());
                                if (usernameAttr != null) {
                                    username = (String)usernameAttr.get();
                                }
375 376 377 378 379 380
                            }
                            // Close the enumeration.
                            usrAnswer.close();
                        }
                    }
                    catch (Exception e) {
381
                        // TODO: A NPE is occuring here
382
                        Log.error(e.getMessage(), e);
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
                    }
                }
                // 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();
        return new Group(name, description, members, admins);
    }
431
}