001/**************************************************************** 002 * Licensed to the Apache Software Foundation (ASF) under one * 003 * or more contributor license agreements. See the NOTICE file * 004 * distributed with this work for additional information * 005 * regarding copyright ownership. The ASF licenses this file * 006 * to you under the Apache License, Version 2.0 (the * 007 * "License"); you may not use this file except in compliance * 008 * with the License. You may obtain a copy of the License at * 009 * * 010 * http://www.apache.org/licenses/LICENSE-2.0 * 011 * * 012 * Unless required by applicable law or agreed to in writing, * 013 * software distributed under the License is distributed on an * 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 015 * KIND, either express or implied. See the License for the * 016 * specific language governing permissions and limitations * 017 * under the License. * 018 ****************************************************************/ 019 020package org.apache.james.user.ldap; 021 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.HashSet; 025import java.util.Iterator; 026import java.util.List; 027import java.util.Map; 028import java.util.Properties; 029import java.util.Set; 030 031import javax.annotation.PostConstruct; 032import javax.naming.Context; 033import javax.naming.NamingEnumeration; 034import javax.naming.NamingException; 035import javax.naming.directory.Attribute; 036import javax.naming.directory.Attributes; 037import javax.naming.directory.SearchControls; 038import javax.naming.directory.SearchResult; 039import javax.naming.ldap.InitialLdapContext; 040import javax.naming.ldap.LdapContext; 041 042import org.apache.commons.configuration.ConfigurationException; 043import org.apache.commons.configuration.HierarchicalConfiguration; 044import org.apache.commons.lang.StringUtils; 045import org.apache.james.lifecycle.api.Configurable; 046import org.apache.james.lifecycle.api.LogEnabled; 047import org.apache.james.user.api.UsersRepository; 048import org.apache.james.user.api.UsersRepositoryException; 049import org.apache.james.user.api.model.User; 050import org.apache.james.user.ldap.api.LdapConstants; 051import org.apache.james.util.retry.DoublingRetrySchedule; 052import org.apache.james.util.retry.api.RetrySchedule; 053import org.apache.james.util.retry.naming.ldap.RetryingLdapContext; 054import org.apache.mailet.MailAddress; 055import org.slf4j.Logger; 056 057import com.google.common.base.Optional; 058 059/** 060 * <p> 061 * This repository implementation serves as a bridge between Apache James and 062 * LDAP. It allows James to authenticate users against an LDAP compliant server 063 * such as Apache DS or Microsoft AD. It also enables role/group based access 064 * restriction based on LDAP groups. 065 * </p> 066 * <p> 067 * It is intended for organisations that already have a user-authentication and 068 * authorisation mechanism in place, and want to leverage this when deploying 069 * James. The assumption inherent here is that such organisations would not want 070 * to manage user details via James, but will do so externally using whatever 071 * mechanism provided by, or built on top off, their LDAP implementation. 072 * </p> 073 * <p> 074 * Based on this assumption, this repository is strictly <b>read-only</b>. As a 075 * consequence, user modification, deletion and creation requests will be 076 * ignored when using this repository. 077 * </p> 078 * <p> 079 * The following fragment of XML provides an example configuration to enable 080 * this repository: </br> 081 * 082 * <pre> 083 * <users-store> 084 * <repository name="LDAPUsers" 085 * class="org.apache.james.userrepository.ReadOnlyUsersLDAPRepository" 086 * ldapHost="ldap://myldapserver:389" 087 * principal="uid=ldapUser,ou=system" 088 * credentials="password" 089 * userBase="ou=People,o=myorg.com,ou=system" 090 * userIdAttribute="uid" 091 * userObjectClass="inetOrgPerson" 092 * maxRetries="20" 093 * retryStartInterval="0" 094 * retryMaxInterval="30" 095 * retryIntervalScale="1000" 096 * administratorId="ldapAdmin" 097 * </users-store> 098 * </pre> 099 * 100 * </br> 101 * 102 * Its constituent attributes are defined as follows: 103 * <ul> 104 * <li><b>ldapHost:</b> The URL of the LDAP server to connect to.</li> 105 * <li> 106 * <b>principal:</b> (optional) The name (DN) of the user with which to 107 * initially bind to the LDAP server.</li> 108 * <li> 109 * <b>credentials:</b> (optional) The password with which to initially bind to 110 * the LDAP server.</li> 111 * <li> 112 * <b>userBase:</b>The context within which to search for user entities.</li> 113 * <li> 114 * <b>userIdAttribute:</b>The name of the LDAP attribute which holds user ids. 115 * For example "uid" for Apache DS, or "sAMAccountName" for 116 * Microsoft Active Directory.</li> 117 * <li> 118 * <b>userObjectClass:</b>The objectClass value for user nodes below the 119 * userBase. For example "inetOrgPerson" for Apache DS, or 120 * "user" for Microsoft Active Directory.</li> 121 ** 122 * <li> 123 * <b>maxRetries:</b> (optional, default = 0) The maximum number of times to 124 * retry a failed operation. -1 means retry forever.</li> 125 * <li> 126 * <b>retryStartInterval:</b> (optional, default = 0) The interval in 127 * milliseconds to wait before the first retry. If > 0, subsequent retries are 128 * made at double the proceeding one up to the <b>retryMaxInterval</b> described 129 * below. If = 0, the next retry is 1 and subsequent retries proceed as above.</li> 130 * <li> 131 * <b>retryMaxInterval:</b> (optional, default = 60) The maximum interval in 132 * milliseconds to wait between retries</li> 133 * <li> 134 * <b>retryIntervalScale:</b> (optional, default = 1000) The amount by which to 135 * multiply each retry interval. The default value of 1000 (milliseconds) is 1 136 * second, so the default <b>retryMaxInterval</b> of 60 is 60 seconds, or 1 137 * minute. 138 * </ul> 139 * </p> 140 * <p> 141 * <em>Example Schedules</em> 142 * <ul> 143 * <li> 144 * Retry after 1000 milliseconds, doubling the interval for each retry up to 145 * 30000 milliseconds, subsequent retry intervals are 30000 milliseconds until 146 * 10 retries have been attempted, after which the <code>Exception</code> 147 * causing the fault is thrown: 148 * <ul> 149 * <li>maxRetries = 10 150 * <li>retryStartInterval = 1000 151 * <li>retryMaxInterval = 30000 152 * <li>retryIntervalScale = 1 153 * </ul> 154 * <li> 155 * Retry immediately, then retry after 1 * 1000 milliseconds, doubling the 156 * interval for each retry up to 30 * 1000 milliseconds, subsequent retry 157 * intervals are 30 * 1000 milliseconds until 20 retries have been attempted, 158 * after which the <code>Exception</code> causing the fault is thrown: 159 * <ul> 160 * <li>maxRetries = 20 161 * <li>retryStartInterval = 0 162 * <li>retryMaxInterval = 30 163 * <li>retryIntervalScale = 1000 164 * </ul> 165 * <li> 166 * Retry after 5000 milliseconds, subsequent retry intervals are 5000 167 * milliseconds. Retry forever: 168 * <ul> 169 * <li>maxRetries = -1 170 * <li>retryStartInterval = 5000 171 * <li>retryMaxInterval = 5000 172 * <li>retryIntervalScale = 1 173 * </ul> 174 * </ul> 175 * </p> 176 * 177 * <p> 178 * In order to enable group/role based access restrictions, you can use the 179 * "<restriction>" configuration element. An example of this is 180 * shown below: <br> 181 * 182 * <pre> 183 * <restriction 184 * memberAttribute="uniqueMember"> 185 * <group>cn=PermanentStaff,ou=Groups,o=myorg.co.uk,ou=system</group> 186 * <group>cn=TemporaryStaff,ou=Groups,o=myorg.co.uk,ou=system</group> 187 * </restriction> 188 * </pre> 189 * 190 * Its constituent attributes and elements are defined as follows: 191 * <ul> 192 * <li> 193 * <b>memberAttribute:</b> The LDAP attribute whose values indicate the DNs of 194 * the users which belong to the group or role.</li> 195 * <li> 196 * <b>group:</b> A valid group or role DN. A user is only authenticated 197 * (permitted access) if they belong to at least one of the groups listed under 198 * the "<restriction>" sections.</li> 199 * </ul> 200 * </p> 201 * 202 * <p> 203 * The following parameters may be used to adjust the underlying 204 * <code>com.sun.jndi.ldap.LdapCtxFactory</code>. See <a href= 205 * "http://docs.oracle.com/javase/1.5.0/docs/guide/jndi/jndi-ldap.html#SPIPROPS" 206 * > LDAP Naming Service Provider for the Java Naming and Directory InterfaceTM 207 * (JNDI) : Provider-specific Properties</a> for details. 208 * <ul> 209 * <li> 210 * <b>useConnectionPool:</b> (optional, default = true) Sets property 211 * <code>com.sun.jndi.ldap.connect.pool</code> to the specified boolean value 212 * <li> 213 * <b>connectionTimeout:</b> (optional) Sets property 214 * <code>com.sun.jndi.ldap.connect.timeout</code> to the specified integer value 215 * <li> 216 * <b>readTimeout:</b> (optional) Sets property 217 * <code>com.sun.jndi.ldap.read.timeout</code> to the specified integer value. 218 * Applicable to Java 6 and above. 219 * <li> 220 * <b>administratorId:</b> (optional) User identifier of the administrator user. 221 * The administrator user is allowed to authenticate as other users. 222 * </ul> 223 * </p> 224 * 225 * <p> 226 * The <b>supportsVirtualHosting</b> tag allows you to define this repository as supporing 227 * virtual hosting. For this LDAP repository, it means users will be looked for by their email 228 * address instead of their unique identifier. 229 * Generally to make it work, you need to configure <b>userIdAttribute</b> attribute to map 230 * to a mail attribute such as <code>mail</code> instead of an unique id identifier. 231 * </p> 232 * 233 * @see ReadOnlyLDAPUser 234 * @see ReadOnlyLDAPGroupRestriction 235 * 236 */ 237public class ReadOnlyUsersLDAPRepository implements UsersRepository, Configurable, LogEnabled { 238 239 // The name of the factory class which creates the initial context 240 // for the LDAP service provider 241 private static final String INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; 242 243 private static final String PROPERTY_NAME_CONNECTION_POOL = "com.sun.jndi.ldap.connect.pool"; 244 private static final String PROPERTY_NAME_CONNECT_TIMEOUT = "com.sun.jndi.ldap.connect.timeout"; 245 private static final String PROPERTY_NAME_READ_TIMEOUT = "com.sun.jndi.ldap.read.timeout"; 246 public static final String SUPPORTS_VIRTUAL_HOSTING = "supportsVirtualHosting"; 247 248 /** 249 * The URL of the LDAP server against which users are to be authenticated. 250 * Note that users are actually authenticated by binding against the LDAP 251 * server using the users "dn" and "credentials".The 252 * value of this field is taken from the value of the configuration 253 * attribute "ldapHost". 254 */ 255 private String ldapHost; 256 257 /** 258 * The value of this field is taken from the configuration attribute 259 * "userIdAttribute". This is the LDAP attribute type which holds 260 * the userId value. Note that this is not the same as the email address 261 * attribute. 262 */ 263 private String userIdAttribute; 264 265 /** 266 * The value of this field is taken from the configuration attribute 267 * "userObjectClass". This is the LDAP object class to use in the 268 * search filter for user nodes under the userBase value. 269 */ 270 private String userObjectClass; 271 272 /** 273 * The value of this field is taken from the configuration attribute "filter". 274 * This is the search filter to use to find the desired user. 275 */ 276 private String filter; 277 278 /** 279 * This is the LDAP context/sub-context within which to search for user 280 * entities. The value of this field is taken from the configuration 281 * attribute "userBase". 282 */ 283 private String userBase; 284 285 /** 286 * The user with which to initially bind to the LDAP server. The value of 287 * this field is taken from the configuration attribute 288 * "principal". 289 */ 290 private String principal; 291 292 /** 293 * The password/credentials with which to initially bind to the LDAP server. 294 * The value of this field is taken from the configuration attribute 295 * "credentials". 296 */ 297 private String credentials; 298 299 /** 300 * Encapsulates the information required to restrict users to LDAP groups or 301 * roles. This object is populated from the contents of the configuration 302 * element <restriction>. 303 */ 304 private ReadOnlyLDAPGroupRestriction restriction; 305 306 /** 307 * The context for the LDAP server. This is the connection that is built 308 * from the configuration attributes "ldapHost", 309 * "principal" and "credentials". 310 */ 311 private LdapContext ldapContext; 312 private boolean supportsVirtualHosting; 313 314 /** 315 * UserId of the administrator 316 * The administrator is allowed to log in as other users 317 */ 318 private Optional<String> administratorId; 319 320 // Use a connection pool. Default is true. 321 private boolean useConnectionPool = true; 322 323 // The connection timeout in milliseconds. 324 // A value of less than or equal to zero means to use the network protocol's 325 // (i.e., TCP's) timeout value. 326 private int connectionTimeout = -1; 327 328 // The LDAP read timeout in milliseconds. 329 private int readTimeout = -1; 330 331 // The schedule for retry attempts 332 private RetrySchedule schedule = null; 333 334 // Maximum number of times to retry a connection attempts. Default is no 335 // retries. 336 private int maxRetries = 0; 337 338 private Logger log; 339 340 /** 341 * Creates a new instance of ReadOnlyUsersLDAPRepository. 342 * 343 */ 344 public ReadOnlyUsersLDAPRepository() { 345 super(); 346 } 347 348 /** 349 * Extracts the parameters required by the repository instance from the 350 * James server configuration data. The fields extracted include 351 * {@link #ldapHost}, {@link #userIdAttribute}, {@link #userBase}, 352 * {@link #principal}, {@link #credentials} and {@link #restriction}. 353 * 354 * @param configuration 355 * An encapsulation of the James server configuration data. 356 */ 357 public void configure(HierarchicalConfiguration configuration) throws ConfigurationException { 358 ldapHost = configuration.getString("[@ldapHost]", ""); 359 principal = configuration.getString("[@principal]", ""); 360 credentials = configuration.getString("[@credentials]", ""); 361 userBase = configuration.getString("[@userBase]"); 362 userIdAttribute = configuration.getString("[@userIdAttribute]"); 363 userObjectClass = configuration.getString("[@userObjectClass]"); 364 // Default is to use connection pooling 365 useConnectionPool = configuration.getBoolean("[@useConnectionPool]", true); 366 connectionTimeout = configuration.getInt("[@connectionTimeout]", -1); 367 readTimeout = configuration.getInt("[@readTimeout]", -1); 368 // Default maximum retries is 1, which allows an alternate connection to 369 // be found in a multi-homed environment 370 maxRetries = configuration.getInt("[@maxRetries]", 1); 371 supportsVirtualHosting = configuration.getBoolean(SUPPORTS_VIRTUAL_HOSTING, false); 372 // Default retry start interval is 0 second 373 long retryStartInterval = configuration.getLong("[@retryStartInterval]", 0); 374 // Default maximum retry interval is 60 seconds 375 long retryMaxInterval = configuration.getLong("[@retryMaxInterval]", 60); 376 int scale = configuration.getInt("[@retryIntervalScale]", 1000); // seconds 377 schedule = new DoublingRetrySchedule(retryStartInterval, retryMaxInterval, scale); 378 379 HierarchicalConfiguration restrictionConfig = null; 380 // Check if we have a restriction we can use 381 // See JAMES-1204 382 if (configuration.containsKey("restriction[@memberAttribute]")) { 383 restrictionConfig = configuration.configurationAt("restriction"); 384 } 385 restriction = new ReadOnlyLDAPGroupRestriction(restrictionConfig); 386 387 //see if there is a filter argument 388 filter = configuration.getString("[@filter]"); 389 390 administratorId = Optional.fromNullable(configuration.getString("[@administratorId]")); 391 392 checkState(); 393 } 394 395 private void checkState() throws ConfigurationException { 396 if (userBase == null) { 397 throw new ConfigurationException("[@userBase] is mandatory"); 398 } 399 if (userIdAttribute == null) { 400 throw new ConfigurationException("[@userIdAttribute] is mandatory"); 401 } 402 if (userObjectClass == null) { 403 throw new ConfigurationException("[@userObjectClass] is mandatory"); 404 } 405 } 406 407 /** 408 * Initialises the user-repository instance. It will create a connection to 409 * the LDAP host using the supplied configuration. 410 * 411 * @throws Exception 412 * If an error occurs authenticating or connecting to the 413 * specified LDAP host. 414 */ 415 @PostConstruct 416 public void init() throws Exception { 417 if (log.isDebugEnabled()) { 418 log.debug(this.getClass().getName() + ".init()" + '\n' + "LDAP host: " + ldapHost + '\n' + "User baseDN: " + userBase + '\n' + "userIdAttribute: " + userIdAttribute + '\n' + "Group restriction: " + restriction + '\n' + "UseConnectionPool: " + useConnectionPool + '\n' + "connectionTimeout: " + connectionTimeout + '\n' + "readTimeout: " + readTimeout + '\n' + "retrySchedule: " + schedule + '\n' + "maxRetries: " + maxRetries + '\n'); 419 } 420 // Setup the initial LDAP context 421 updateLdapContext(); 422 } 423 424 /** 425 * Answer the LDAP context used to connect with the LDAP server. 426 * 427 * @return an <code>LdapContext</code> 428 * @throws NamingException 429 */ 430 protected LdapContext getLdapContext() throws NamingException { 431 if (null == ldapContext) { 432 updateLdapContext(); 433 } 434 return ldapContext; 435 } 436 437 protected void updateLdapContext() throws NamingException { 438 ldapContext = computeLdapContext(); 439 } 440 441 /** 442 * Answers a new LDAP/JNDI context using the specified user credentials. 443 * 444 * @return an LDAP directory context 445 * @throws NamingException 446 * Propagated from underlying LDAP communication API. 447 */ 448 protected LdapContext computeLdapContext() throws NamingException { 449 return new RetryingLdapContext(schedule, maxRetries, log) { 450 451 @Override 452 public Context newDelegate() throws NamingException { 453 return new InitialLdapContext(getContextEnvironment(), null); 454 } 455 }; 456 } 457 458 protected Properties getContextEnvironment() 459 { 460 final Properties props = new Properties(); 461 props.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY); 462 props.put(Context.PROVIDER_URL, null == ldapHost ? "" : ldapHost); 463 if (null == credentials || credentials.isEmpty()) { 464 props.put(Context.SECURITY_AUTHENTICATION, LdapConstants.SECURITY_AUTHENTICATION_NONE); 465 } else { 466 props.put(Context.SECURITY_AUTHENTICATION, LdapConstants.SECURITY_AUTHENTICATION_SIMPLE); 467 props.put(Context.SECURITY_PRINCIPAL, null == principal ? "" : principal); 468 props.put(Context.SECURITY_CREDENTIALS, credentials); 469 } 470 // The following properties are specific to com.sun.jndi.ldap.LdapCtxFactory 471 props.put(PROPERTY_NAME_CONNECTION_POOL, Boolean.toString(useConnectionPool)); 472 if (connectionTimeout > -1) 473 { 474 props.put(PROPERTY_NAME_CONNECT_TIMEOUT, Integer.toString(connectionTimeout)); 475 } 476 if (readTimeout > -1) 477 { 478 props.put(PROPERTY_NAME_READ_TIMEOUT, Integer.toString(readTimeout)); 479 } 480 return props; 481 } 482 483 /** 484 * Indicates if the user with the specified DN can be found in the group 485 * membership map-as encapsulated by the specified parameter map. 486 * 487 * @param userDN 488 * The DN of the user to search for. 489 * @param groupMembershipList 490 * A map containing the entire group membership lists for the 491 * configured groups. This is organised as a map of 492 * 493 * <code>"<groupDN>=<[userDN1,userDN2,...,userDNn]>"</code> 494 * pairs. In essence, each <code>groupDN</code> string is 495 * associated to a list of <code>userDNs</code>. 496 * @return <code>True</code> if the specified userDN is associated with at 497 * least one group in the parameter map, and <code>False</code> 498 * otherwise. 499 */ 500 private boolean userInGroupsMembershipList(String userDN, 501 Map<String, Collection<String>> groupMembershipList) { 502 boolean result = false; 503 504 Collection<Collection<String>> memberLists = groupMembershipList.values(); 505 Iterator<Collection<String>> memberListsIterator = memberLists.iterator(); 506 507 while (memberListsIterator.hasNext() && !result) { 508 Collection<String> groupMembers = memberListsIterator.next(); 509 result = groupMembers.contains(userDN); 510 } 511 512 return result; 513 } 514 515 /** 516 * Gets all the user entities taken from the LDAP server, as taken from the 517 * search-context given by the value of the attribute {@link #userBase}. 518 * 519 * @return A set containing all the relevant users found in the LDAP 520 * directory. 521 * @throws NamingException 522 * Propagated from the LDAP communication layer. 523 */ 524 private Set<String> getAllUsersFromLDAP() throws NamingException { 525 Set<String> result = new HashSet<String>(); 526 527 SearchControls sc = new SearchControls(); 528 sc.setSearchScope(SearchControls.SUBTREE_SCOPE); 529 sc.setReturningAttributes(new String[] { "distinguishedName" }); 530 NamingEnumeration<SearchResult> sr = ldapContext.search(userBase, "(objectClass=" 531 + userObjectClass + ")", sc); 532 while (sr.hasMore()) { 533 SearchResult r = sr.next(); 534 result.add(r.getNameInNamespace()); 535 } 536 537 return result; 538 } 539 540 /** 541 * Extract the user attributes for the given collection of userDNs, and 542 * encapsulates the user list as a collection of {@link ReadOnlyLDAPUser}s. 543 * This method delegates the extraction of a single user's details to the 544 * method {@link #buildUser(String)}. 545 * 546 * @param userDNs 547 * The distinguished-names (DNs) of the users whose information 548 * is to be extracted from the LDAP repository. 549 * @return A collection of {@link ReadOnlyLDAPUser}s as taken from the LDAP 550 * server. 551 * @throws NamingException 552 * Propagated from the underlying LDAP communication layer. 553 */ 554 private Collection<ReadOnlyLDAPUser> buildUserCollection(Collection<String> userDNs) 555 throws NamingException { 556 List<ReadOnlyLDAPUser> results = new ArrayList<ReadOnlyLDAPUser>(); 557 558 for (String userDN : userDNs) { 559 ReadOnlyLDAPUser user = buildUser(userDN); 560 results.add(user); 561 } 562 563 return results; 564 } 565 566 567 /** 568 * For a given name, this method makes ldap search in userBase with filter {@link #userIdAttribute}=name and objectClass={@link #userObjectClass} 569 * and builds {@link User} based on search result. 570 * 571 * @param name 572 * The userId which should be value of the field {@link #userIdAttribute} 573 * @return A {@link ReadOnlyLDAPUser} instance which is initialized with the 574 * userId of this user and ldap connection information with which 575 * the user was searched. Return null if such a user was not found. 576 * @throws NamingException 577 * Propagated by the underlying LDAP communication layer. 578 */ 579 private ReadOnlyLDAPUser searchAndBuildUser(String name) throws NamingException { 580 SearchControls sc = new SearchControls(); 581 sc.setSearchScope(SearchControls.SUBTREE_SCOPE); 582 sc.setReturningAttributes(new String[] { userIdAttribute }); 583 sc.setCountLimit(1); 584 585 StringBuilder builderFilter = new StringBuilder("(&("); 586 builderFilter.append(userIdAttribute).append("=").append(name).append(")") 587 .append("(objectClass=").append(userObjectClass).append(")"); 588 589 if(StringUtils.isNotEmpty(filter)){ 590 builderFilter.append(filter).append(")"); 591 } 592 else{ 593 builderFilter.append(")"); 594 } 595 596 NamingEnumeration<SearchResult> sr = ldapContext.search(userBase, builderFilter.toString(), 597 sc); 598 599 if (!sr.hasMore()) 600 return null; 601 602 SearchResult r = sr.next(); 603 Attribute userName = r.getAttributes().get(userIdAttribute); 604 605 if (!restriction.isActivated() 606 || userInGroupsMembershipList(r.getNameInNamespace(), restriction.getGroupMembershipLists(ldapContext))) 607 return new ReadOnlyLDAPUser(userName.get().toString(), r.getNameInNamespace(), ldapContext); 608 609 return null; 610 } 611 612 /** 613 * Given a userDN, this method retrieves the user attributes from the LDAP 614 * server, so as to extract the items that are of interest to James. 615 * Specifically it extracts the userId, which is extracted from the LDAP 616 * attribute whose name is given by the value of the field 617 * {@link #userIdAttribute}. 618 * 619 * @param userDN 620 * The distinguished-name of the user whose details are to be 621 * extracted from the LDAP repository. 622 * @return A {@link ReadOnlyLDAPUser} instance which is initialized with the 623 * userId of this user and ldap connection information with which 624 * the userDN and attributes were obtained. 625 * @throws NamingException 626 * Propagated by the underlying LDAP communication layer. 627 */ 628 private ReadOnlyLDAPUser buildUser(String userDN) throws NamingException { 629 Attributes userAttributes = ldapContext.getAttributes(userDN); 630 Attribute userName = userAttributes.get(userIdAttribute); 631 return new ReadOnlyLDAPUser(userName.get().toString(), userDN, ldapContext); 632 } 633 634 /** 635 * @see UsersRepository#contains(java.lang.String) 636 */ 637 public boolean contains(String name) throws UsersRepositoryException { 638 return getUserByName(name) != null; 639 } 640 641 /* 642 * TODO Should this be deprecated? At least the method isn't declared in the 643 * interface anymore 644 * 645 * @see UsersRepository#containsCaseInsensitive(java.lang.String) 646 */ 647 public boolean containsCaseInsensitive(String name) throws UsersRepositoryException { 648 return getUserByNameCaseInsensitive(name) != null; 649 } 650 651 /** 652 * @see UsersRepository#countUsers() 653 */ 654 public int countUsers() throws UsersRepositoryException { 655 try { 656 return getValidUsers().size(); 657 } catch (NamingException e) { 658 log.error("Unable to retrieve user count from ldap", e); 659 throw new UsersRepositoryException("Unable to retrieve user count from ldap", e); 660 661 } 662 } 663 664 /* 665 * TODO Should this be deprecated? At least the method isn't declared in the 666 * interface anymore 667 * 668 * @see UsersRepository#getRealName(java.lang.String) 669 */ 670 public String getRealName(String name) throws UsersRepositoryException { 671 User u = getUserByNameCaseInsensitive(name); 672 if (u != null) { 673 return u.getUserName(); 674 } 675 676 return null; 677 } 678 679 /** 680 * @see UsersRepository#getUserByName(java.lang.String) 681 */ 682 public User getUserByName(String name) throws UsersRepositoryException { 683 try { 684 return searchAndBuildUser(name); 685 } catch (NamingException e) { 686 log.error("Unable to retrieve user from ldap", e); 687 throw new UsersRepositoryException("Unable to retrieve user from ldap", e); 688 689 } 690 } 691 692 /* 693 * TODO Should this be deprecated? At least the method isn't declared in the 694 * interface anymore 695 * 696 * @see UsersRepository#getUserByNameCaseInsensitive(java.lang.String) 697 */ 698 public User getUserByNameCaseInsensitive(String name) throws UsersRepositoryException { 699 try { 700 for (ReadOnlyLDAPUser u : buildUserCollection(getValidUsers())) { 701 if (u.getUserName().equalsIgnoreCase(name)) { 702 return u; 703 } 704 } 705 706 } catch (NamingException e) { 707 log.error("Unable to retrieve user from ldap", e); 708 throw new UsersRepositoryException("Unable to retrieve user from ldap", e); 709 710 } 711 return null; 712 } 713 714 /** 715 * @see UsersRepository#list() 716 */ 717 public Iterator<String> list() throws UsersRepositoryException { 718 List<String> result = new ArrayList<String>(); 719 try { 720 721 for (ReadOnlyLDAPUser readOnlyLDAPUser : buildUserCollection(getValidUsers())) { 722 result.add(readOnlyLDAPUser.getUserName()); 723 } 724 } catch (NamingException namingException) { 725 throw new UsersRepositoryException( 726 "Unable to retrieve users list from LDAP due to unknown naming error.", 727 namingException); 728 } 729 730 return result.iterator(); 731 } 732 733 private Collection<String> getValidUsers() throws NamingException { 734 Set<String> userDNs = getAllUsersFromLDAP(); 735 Collection<String> validUserDNs; 736 737 if (restriction.isActivated()) { 738 Map<String, Collection<String>> groupMembershipList = restriction 739 .getGroupMembershipLists(ldapContext); 740 validUserDNs = new ArrayList<String>(); 741 742 Iterator<String> userDNIterator = userDNs.iterator(); 743 String userDN; 744 while (userDNIterator.hasNext()) { 745 userDN = userDNIterator.next(); 746 if (userInGroupsMembershipList(userDN, groupMembershipList)) 747 validUserDNs.add(userDN); 748 } 749 } else { 750 validUserDNs = userDNs; 751 } 752 return validUserDNs; 753 } 754 755 /** 756 * @see UsersRepository#removeUser(java.lang.String) 757 */ 758 public void removeUser(String name) throws UsersRepositoryException { 759 log.warn("This user-repository is read-only. Modifications are not permitted."); 760 throw new UsersRepositoryException( 761 "This user-repository is read-only. Modifications are not permitted."); 762 763 } 764 765 /** 766 * @see UsersRepository#test(java.lang.String, java.lang.String) 767 */ 768 public boolean test(String name, String password) throws UsersRepositoryException { 769 User u = getUserByName(name); 770 return u != null && u.verifyPassword(password); 771 } 772 773 /** 774 * @see UsersRepository#addUser(java.lang.String, java.lang.String) 775 */ 776 public void addUser(String username, String password) throws UsersRepositoryException { 777 log.error("This user-repository is read-only. Modifications are not permitted."); 778 throw new UsersRepositoryException( 779 "This user-repository is read-only. Modifications are not permitted."); 780 } 781 782 /** 783 */ 784 public void updateUser(User user) throws UsersRepositoryException { 785 log.error("This user-repository is read-only. Modifications are not permitted."); 786 throw new UsersRepositoryException( 787 "This user-repository is read-only. Modifications are not permitted."); 788 } 789 790 /** 791 * @see org.apache.james.lifecycle.api.LogEnabled#setLog(org.slf4j.Logger) 792 */ 793 public void setLog(Logger log) { 794 this.log = log; 795 } 796 797 /** 798 * VirtualHosting not supported 799 */ 800 public boolean supportVirtualHosting() { 801 return supportsVirtualHosting; 802 } 803 804 805 @Override 806 public String getUser(MailAddress mailAddress) throws UsersRepositoryException { 807 if (supportVirtualHosting()) { 808 return mailAddress.asString(); 809 } else { 810 return mailAddress.getLocalPart(); 811 } 812 } 813 814 @Override 815 public boolean isAdministrator(String username) throws UsersRepositoryException { 816 if (administratorId.isPresent()) { 817 return administratorId.get().equals(username); 818 } 819 return false; 820 } 821}