1 /* 2 * Copyright (C) 2004-2008 Jive Software. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire.ldap; 18 19 import org.jivesoftware.admin.LdapUserTester; 20 import org.jivesoftware.openfire.XMPPServer; 21 import org.jivesoftware.openfire.group.Group; 22 import org.jivesoftware.openfire.group.GroupManager; 23 import org.jivesoftware.openfire.user.*; 24 import org.jivesoftware.util.JiveGlobals; 25 import org.slf4j.Logger; 26 import org.slf4j.LoggerFactory; 27 import org.xmpp.packet.JID; 28 29 import javax.naming.NamingEnumeration; 30 import javax.naming.directory.Attribute; 31 import javax.naming.directory.Attributes; 32 import javax.naming.directory.DirContext; 33 import javax.naming.ldap.Rdn; 34 import java.text.MessageFormat; 35 import java.text.SimpleDateFormat; 36 import java.time.Instant; 37 import java.time.temporal.ChronoUnit; 38 import java.util.*; 39 import java.util.stream.Collectors; 40 41 /** 42 * LDAP implementation of the UserProvider interface. All data in the directory is 43 * treated as read-only so any set operations will result in an exception. 44 * 45 * @author Matt Tucker 46 */ 47 public class LdapUserProvider implements UserProvider { 48 49 private static final Logger Log = LoggerFactory.getLogger(LdapUserProvider.class); 50 51 // LDAP date format parser. 52 private static final SimpleDateFormat ldapDateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); 53 54 private final LdapManager manager; 55 private Map<String, String> searchFields; 56 private Instant allUserCacheExpires = Instant.now(); 57 private int userCount = -1; 58 private List<String> allUsernames = null; 59 private Collection<User> allUsers = null; 60 LdapUserProvider()61 public LdapUserProvider() { 62 // Convert XML based provider setup to Database based 63 JiveGlobals.migrateProperty("ldap.searchFields"); 64 65 manager = LdapManager.getInstance(); 66 searchFields = new LinkedHashMap<>(); 67 String fieldList = JiveGlobals.getProperty("ldap.searchFields"); 68 // If the value isn't present, default to to username, name, and email. 69 if (fieldList == null) { 70 searchFields.put("Username", manager.getUsernameField()); 71 int i = 0; 72 for ( final String nameField : manager.getNameField().getFields() ) { 73 searchFields.put((i == 0 ? "Name" : "Name (" + i + ")"), nameField); 74 i++; 75 } 76 searchFields.put("Email", manager.getEmailField()); 77 } 78 else { 79 try { 80 for (StringTokenizer i=new StringTokenizer(fieldList, ","); i.hasMoreTokens(); ) { 81 String[] field = i.nextToken().split("/"); 82 searchFields.put(field[0], field[1]); 83 } 84 } 85 catch (Exception e) { 86 Log.error("Error parsing LDAP search fields: " + fieldList, e); 87 } 88 } 89 } 90 91 @Override loadUser(String username)92 public User loadUser(String username) throws UserNotFoundException { 93 if(username.contains("@")) { 94 if (!XMPPServer.getInstance().isLocal(new JID(username))) { 95 throw new UserNotFoundException("Cannot load user of remote server: " + username); 96 } 97 username = username.substring(0,username.lastIndexOf("@")); 98 } 99 // Un-escape username. 100 username = JID.unescapeNode(username); 101 DirContext ctx = null; 102 try { 103 Rdn[] userRDN = manager.findUserRDN(username); 104 // Load record. 105 final List<String> attributes = new ArrayList<>(); 106 attributes.add( manager.getUsernameField() ); 107 attributes.addAll( manager.getNameField().getFields() ); 108 attributes.add( manager.getEmailField() ); 109 attributes.add( "createTimestamp" ); 110 attributes.add( "modifyTimestamp" ); 111 112 ctx = manager.getContext(manager.getUsersBaseDN(username)); 113 Attributes attrs = ctx.getAttributes(LdapManager.escapeForJNDI(userRDN), attributes.toArray(new String[0])); 114 String name = LdapUserTester.getPropertyValue(manager.getNameField(), attrs); 115 String email = null; 116 Attribute emailField = attrs.get(manager.getEmailField()); 117 if (emailField != null) { 118 email = (String)emailField.get(); 119 } 120 Date creationDate = new Date(); 121 Attribute creationDateField = attrs.get("createTimestamp"); 122 if (creationDateField != null && "".equals(((String) creationDateField.get()).trim())) { 123 creationDate = parseLDAPDate((String) creationDateField.get()); 124 } 125 Date modificationDate = new Date(); 126 Attribute modificationDateField = attrs.get("modifyTimestamp"); 127 if (modificationDateField != null && "".equals(((String) modificationDateField.get()).trim())) { 128 modificationDate = parseLDAPDate((String)modificationDateField.get()); 129 } 130 // Escape the username so that it can be used as a JID. 131 username = JID.escapeNode(username); 132 133 // As defined by RFC5803. 134 Attribute authPassword = attrs.get("authPassword"); 135 User user = new User(username, name, email, creationDate, modificationDate); 136 if (manager.isFindUsersFromGroupsEnabled() && GroupManager.getInstance().getGroups(user).isEmpty()) { 137 throw new UserNotFoundException("User exists in LDAP but is not a member of any Openfire groups"); 138 } 139 if (authPassword != null) { 140 // The authPassword attribute can be multivalued. 141 // Not sure if this is the right API to loop through them. 142 NamingEnumeration values = authPassword.getAll(); 143 while (values.hasMore()) { 144 Attribute authPasswordValue = (Attribute) values.next(); 145 String[] parts = ((String) authPasswordValue.get()).split("$"); 146 String[] authInfo = parts[1].split(":"); 147 String[] authValue = parts[2].split(":"); 148 149 String scheme = parts[0].trim(); 150 151 // We only support SCRAM-SHA-1 at the moment. 152 if ("SCRAM-SHA-1".equals(scheme)) { 153 int iterations = Integer.valueOf(authInfo[0].trim()); 154 String salt = authInfo[1].trim(); 155 String storedKey = authValue[0].trim(); 156 String serverKey = authValue[1].trim(); 157 158 user.setSalt(salt); 159 user.setStoredKey(storedKey); 160 user.setServerKey(serverKey); 161 user.setIterations(iterations); 162 163 break; 164 } 165 } 166 } 167 return user; 168 } 169 catch (Exception e) { 170 throw new UserNotFoundException(e); 171 } 172 finally { 173 try { 174 if (ctx != null) { 175 ctx.close(); 176 } 177 } 178 catch (Exception ex) { 179 Log.debug( "An exception occurred while closing the LDAP context after attempting to load user {}", username, ex); 180 } 181 } 182 } 183 184 @Override createUser(String username, String password, String name, String email)185 public User createUser(String username, String password, String name, String email) 186 throws UserAlreadyExistsException 187 { 188 throw new UnsupportedOperationException(); 189 } 190 191 @Override deleteUser(String username)192 public void deleteUser(String username) { 193 throw new UnsupportedOperationException(); 194 } 195 196 @Override getUserCount()197 public int getUserCount() { 198 // Cache user count for 5 minutes. 199 if (userCount != -1 && allUserCacheExpires.isAfter(Instant.now())) { 200 return userCount; 201 } 202 // Refresh the cache 203 getUsers(); 204 return this.userCount; 205 } 206 207 @Override getUsernames()208 public Collection<String> getUsernames() { 209 // Cache usernames for 5 minutes. 210 if (allUsernames != null && allUserCacheExpires.isAfter(Instant.now())) { 211 return allUsernames; 212 } 213 // Refresh the cache 214 getUsers(); 215 return this.allUsernames; 216 } 217 218 @Override getUsers()219 public synchronized Collection<User> getUsers() { 220 if (allUsers != null && allUserCacheExpires.isAfter(Instant.now())) { 221 return allUsers; 222 } 223 this.allUsers = getUsers( -1, -1 ); 224 // When all user have been fetched, we can update various other cached values. 225 this.userCount = this.allUsers.size(); 226 this.allUsernames = allUsers.stream().map(User::getUsername).collect(Collectors.toList()); 227 this.allUserCacheExpires = Instant.now().plus(5, ChronoUnit.MINUTES); 228 return allUsers; 229 } 230 231 @Override getUsers(int startIndex, int numResults)232 public Collection<User> getUsers(int startIndex, int numResults) { 233 final List<String> userlist; 234 if (manager.isFindUsersFromGroupsEnabled()) { 235 final Set<String> allUsers = GroupManager.getInstance().getGroups() 236 .stream() 237 .map(Group::getAll) 238 .flatMap(Collection::stream) 239 .map(JID::getNode) 240 .collect(Collectors.toSet()); 241 userlist = LdapManager.sortAndPaginate(allUsers, startIndex, numResults); 242 } else { 243 userlist = manager.retrieveList( 244 manager.getUsernameField(), 245 MessageFormat.format(manager.getSearchFilter(), "*"), 246 startIndex, 247 numResults, 248 manager.getUsernameSuffix(), 249 true 250 ); 251 } 252 return new UserCollection(userlist.toArray(new String[userlist.size()])); 253 } 254 255 @Override setName(String username, String name)256 public void setName(String username, String name) throws UserNotFoundException { 257 throw new UnsupportedOperationException(); 258 } 259 260 @Override setEmail(String username, String email)261 public void setEmail(String username, String email) throws UserNotFoundException { 262 throw new UnsupportedOperationException(); 263 } 264 265 @Override setCreationDate(String username, Date creationDate)266 public void setCreationDate(String username, Date creationDate) throws UserNotFoundException { 267 throw new UnsupportedOperationException(); 268 } 269 270 @Override setModificationDate(String username, Date modificationDate)271 public void setModificationDate(String username, Date modificationDate) throws UserNotFoundException { 272 throw new UnsupportedOperationException(); 273 } 274 275 @Override getSearchFields()276 public Set<String> getSearchFields() throws UnsupportedOperationException { 277 return Collections.unmodifiableSet(searchFields.keySet()); 278 } 279 setSearchFields(String fieldList)280 public void setSearchFields(String fieldList) { 281 this.searchFields = new LinkedHashMap<>(); 282 // If the value isn't present, default to to username, name, and email. 283 if (fieldList == null) { 284 searchFields.put("Username", manager.getUsernameField()); 285 int i = 0; 286 for ( final String nameField : manager.getNameField().getFields() ) { 287 searchFields.put((i == 0 ? "Name" : "Name (" + i + ")"), nameField); 288 i++; 289 } 290 searchFields.put("Email", manager.getEmailField()); 291 } 292 else { 293 try { 294 for (StringTokenizer i=new StringTokenizer(fieldList, ","); i.hasMoreTokens(); ) { 295 String[] field = i.nextToken().split("/"); 296 searchFields.put(field[0], field[1]); 297 } 298 } 299 catch (Exception e) { 300 Log.error("Error parsing LDAP search fields: " + fieldList, e); 301 } 302 } 303 JiveGlobals.setProperty("ldap.searchFields", fieldList); 304 } 305 306 @Override findUsers(Set<String> fields, String query)307 public Collection<User> findUsers(Set<String> fields, String query) 308 throws UnsupportedOperationException 309 { 310 return findUsers(fields, query, -1, -1); 311 } 312 313 @Override findUsers(Set<String> fields, String query, int startIndex, int numResults)314 public Collection<User> findUsers(Set<String> fields, String query, int startIndex, 315 int numResults) throws UnsupportedOperationException 316 { 317 if (fields.isEmpty() || query == null || "".equals(query)) { 318 return Collections.emptyList(); 319 } 320 321 query = LdapManager.sanitizeSearchFilter(query, true); 322 323 // Make the query be a wildcard search by default. So, if the user searches for 324 // "John", make the search be "John*" instead. 325 if (!query.endsWith("*")) { 326 query = query + "*"; 327 } 328 329 if (!searchFields.keySet().containsAll(fields)) { 330 throw new IllegalArgumentException("Search fields " + fields + " are not valid."); 331 } 332 StringBuilder filter = new StringBuilder(); 333 //Add the global search filter so only those users the directory administrator wants to include 334 //are returned from the directory 335 filter.append("(&("); 336 filter.append(MessageFormat.format(manager.getSearchFilter(),"*")); 337 filter.append(')'); 338 if (fields.size() > 1) { 339 filter.append("(|"); 340 } 341 for (String field:fields) { 342 String attribute = searchFields.get(field); 343 filter.append('(').append(attribute).append('=') 344 .append( query ).append(")"); 345 } 346 if (fields.size() > 1) { 347 filter.append(')'); 348 } 349 filter.append(')'); 350 List<String> userlist = manager.retrieveList( 351 manager.getUsernameField(), 352 filter.toString(), 353 startIndex, 354 numResults, 355 manager.getUsernameSuffix(), 356 true 357 ); 358 if (manager.isFindUsersFromGroupsEnabled()) { 359 userlist = userlist.stream() 360 .filter(user -> 361 !GroupManager.getInstance().getGroups( 362 XMPPServer.getInstance().createJID(user, null)) 363 .isEmpty()) 364 .collect(Collectors.toList()); 365 } 366 return new UserCollection(userlist.toArray(new String[userlist.size()])); 367 } 368 369 @Override isReadOnly()370 public boolean isReadOnly() { 371 return true; 372 } 373 374 @Override isNameRequired()375 public boolean isNameRequired() { 376 return false; 377 } 378 379 @Override isEmailRequired()380 public boolean isEmailRequired() { 381 return false; 382 } 383 384 /** 385 * Parses dates/time stamps stored in LDAP. Some possible values: 386 * 387 * <ul> 388 * <li>20020228150820</li> 389 * <li>20030228150820Z</li> 390 * <li>20050228150820.12</li> 391 * <li>20060711011740.0Z</li> 392 * </ul> 393 * 394 * @param dateText the date string. 395 * @return the Date. 396 */ parseLDAPDate(String dateText)397 private static Date parseLDAPDate(String dateText) { 398 // If the date ends with a "Z", that means that it's in the UTC time zone. Otherwise, 399 // Use the default time zone. 400 boolean useUTC = false; 401 if (dateText.endsWith("Z")) { 402 useUTC = true; 403 } 404 Date date = new Date(); 405 try { 406 if (useUTC) { 407 ldapDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 408 } 409 else { 410 ldapDateFormat.setTimeZone(TimeZone.getDefault()); 411 } 412 date = ldapDateFormat.parse(dateText); 413 } 414 catch (Exception e) { 415 Log.error(e.getMessage(), e); 416 } 417 return date; 418 } 419 } 420