PageRenderTime 65ms CodeModel.GetById 24ms RepoModel.GetById 1ms app.codeStats 0ms

/java/org/apache/catalina/realm/JNDIRealm.java

https://github.com/dblevins/tomcat
Java | 2398 lines | 1027 code | 505 blank | 866 comment | 256 complexity | c4e8437cd8c3e624f7dc5cc76532fe12 MD5 | raw file
Possible License(s): Apache-2.0
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package org.apache.catalina.realm;
  18. import java.net.URI;
  19. import java.net.URISyntaxException;
  20. import java.security.Principal;
  21. import java.text.MessageFormat;
  22. import java.util.ArrayList;
  23. import java.util.Collections;
  24. import java.util.HashMap;
  25. import java.util.Hashtable;
  26. import java.util.Iterator;
  27. import java.util.List;
  28. import java.util.Map;
  29. import java.util.Map.Entry;
  30. import java.util.Set;
  31. import javax.naming.AuthenticationException;
  32. import javax.naming.CommunicationException;
  33. import javax.naming.CompositeName;
  34. import javax.naming.Context;
  35. import javax.naming.InvalidNameException;
  36. import javax.naming.Name;
  37. import javax.naming.NameNotFoundException;
  38. import javax.naming.NameParser;
  39. import javax.naming.NamingEnumeration;
  40. import javax.naming.NamingException;
  41. import javax.naming.PartialResultException;
  42. import javax.naming.ServiceUnavailableException;
  43. import javax.naming.directory.Attribute;
  44. import javax.naming.directory.Attributes;
  45. import javax.naming.directory.DirContext;
  46. import javax.naming.directory.InitialDirContext;
  47. import javax.naming.directory.SearchControls;
  48. import javax.naming.directory.SearchResult;
  49. import org.apache.catalina.LifecycleException;
  50. import org.ietf.jgss.GSSCredential;
  51. /**
  52. * <p>Implementation of <strong>Realm</strong> that works with a directory
  53. * server accessed via the Java Naming and Directory Interface (JNDI) APIs.
  54. * The following constraints are imposed on the data structure in the
  55. * underlying directory server:</p>
  56. * <ul>
  57. *
  58. * <li>Each user that can be authenticated is represented by an individual
  59. * element in the top level <code>DirContext</code> that is accessed
  60. * via the <code>connectionURL</code> property.</li>
  61. *
  62. * <li>If a socket connection can not be made to the <code>connectURL</code>
  63. * an attempt will be made to use the <code>alternateURL</code> if it
  64. * exists.</li>
  65. *
  66. * <li>Each user element has a distinguished name that can be formed by
  67. * substituting the presented username into a pattern configured by the
  68. * <code>userPattern</code> property.</li>
  69. *
  70. * <li>Alternatively, if the <code>userPattern</code> property is not
  71. * specified, a unique element can be located by searching the directory
  72. * context. In this case:
  73. * <ul>
  74. * <li>The <code>userSearch</code> pattern specifies the search filter
  75. * after substitution of the username.</li>
  76. * <li>The <code>userBase</code> property can be set to the element that
  77. * is the base of the subtree containing users. If not specified,
  78. * the search base is the top-level context.</li>
  79. * <li>The <code>userSubtree</code> property can be set to
  80. * <code>true</code> if you wish to search the entire subtree of the
  81. * directory context. The default value of <code>false</code>
  82. * requests a search of only the current level.</li>
  83. * </ul>
  84. * </li>
  85. *
  86. * <li>The user may be authenticated by binding to the directory with the
  87. * username and password presented. This method is used when the
  88. * <code>userPassword</code> property is not specified.</li>
  89. *
  90. * <li>The user may be authenticated by retrieving the value of an attribute
  91. * from the directory and comparing it explicitly with the value presented
  92. * by the user. This method is used when the <code>userPassword</code>
  93. * property is specified, in which case:
  94. * <ul>
  95. * <li>The element for this user must contain an attribute named by the
  96. * <code>userPassword</code> property.
  97. * <li>The value of the user password attribute is either a cleartext
  98. * String, or the result of passing a cleartext String through the
  99. * <code>RealmBase.digest()</code> method (using the standard digest
  100. * support included in <code>RealmBase</code>).
  101. * <li>The user is considered to be authenticated if the presented
  102. * credentials (after being passed through
  103. * <code>RealmBase.digest()</code>) are equal to the retrieved value
  104. * for the user password attribute.</li>
  105. * </ul></li>
  106. *
  107. * <li>Each group of users that has been assigned a particular role may be
  108. * represented by an individual element in the top level
  109. * <code>DirContext</code> that is accessed via the
  110. * <code>connectionURL</code> property. This element has the following
  111. * characteristics:
  112. * <ul>
  113. * <li>The set of all possible groups of interest can be selected by a
  114. * search pattern configured by the <code>roleSearch</code>
  115. * property.</li>
  116. * <li>The <code>roleSearch</code> pattern optionally includes pattern
  117. * replacements "{0}" for the distinguished name, and/or "{1}" for
  118. * the username, and/or "{2}" the value of an attribute from the
  119. * user's directory entry (the attribute is specified by the
  120. * <code>userRoleAttribute</code> property), of the authenticated user
  121. * for which roles will be retrieved.</li>
  122. * <li>The <code>roleBase</code> property can be set to the element that
  123. * is the base of the search for matching roles. If not specified,
  124. * the entire context will be searched.</li>
  125. * <li>The <code>roleSubtree</code> property can be set to
  126. * <code>true</code> if you wish to search the entire subtree of the
  127. * directory context. The default value of <code>false</code>
  128. * requests a search of only the current level.</li>
  129. * <li>The element includes an attribute (whose name is configured by
  130. * the <code>roleName</code> property) containing the name of the
  131. * role represented by this element.</li>
  132. * </ul></li>
  133. *
  134. * <li>In addition, roles may be represented by the values of an attribute
  135. * in the user's element whose name is configured by the
  136. * <code>userRoleName</code> property.</li>
  137. *
  138. * <li>A default role can be assigned to each user that was successfully
  139. * authenticated by setting the <code>commonRole</code> property to the
  140. * name of this role. The role doesn't have to exist in the directory.</li>
  141. *
  142. * <li>If the directory server contains nested roles, you can search for them
  143. * by setting <code>roleNested</code> to <code>true</code>.
  144. * The default value is <code>false</code>, so role searches will not find
  145. * nested roles.</li>
  146. *
  147. * <li>Note that the standard <code>&lt;security-role-ref&gt;</code> element in
  148. * the web application deployment descriptor allows applications to refer
  149. * to roles programmatically by names other than those used in the
  150. * directory server itself.</li>
  151. * </ul>
  152. *
  153. * <p><strong>TODO</strong> - Support connection pooling (including message
  154. * format objects) so that <code>authenticate()</code> does not have to be
  155. * synchronized.</p>
  156. *
  157. * <p><strong>WARNING</strong> - There is a reported bug against the Netscape
  158. * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to
  159. * successfully authenticated a non-existing user. The
  160. * report is here: http://issues.apache.org/bugzilla/show_bug.cgi?id=11210 .
  161. * With luck, Netscape has updated their provider code and this is not an
  162. * issue. </p>
  163. *
  164. * @author John Holman
  165. * @author Craig R. McClanahan
  166. */
  167. public class JNDIRealm extends RealmBase {
  168. // ----------------------------------------------------- Instance Variables
  169. /**
  170. * The type of authentication to use
  171. */
  172. protected String authentication = null;
  173. /**
  174. * The connection username for the server we will contact.
  175. */
  176. protected String connectionName = null;
  177. /**
  178. * The connection password for the server we will contact.
  179. */
  180. protected String connectionPassword = null;
  181. /**
  182. * The connection URL for the server we will contact.
  183. */
  184. protected String connectionURL = null;
  185. /**
  186. * The directory context linking us to our directory server.
  187. */
  188. protected DirContext context = null;
  189. /**
  190. * The JNDI context factory used to acquire our InitialContext. By
  191. * default, assumes use of an LDAP server using the standard JNDI LDAP
  192. * provider.
  193. */
  194. protected String contextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
  195. /**
  196. * How aliases should be dereferenced during search operations.
  197. */
  198. protected String derefAliases = null;
  199. /**
  200. * Constant that holds the name of the environment property for specifying
  201. * the manner in which aliases should be dereferenced.
  202. */
  203. public static final String DEREF_ALIASES = "java.naming.ldap.derefAliases";
  204. /**
  205. * Descriptive information about this Realm implementation.
  206. */
  207. protected static final String name = "JNDIRealm";
  208. /**
  209. * The protocol that will be used in the communication with the
  210. * directory server.
  211. */
  212. protected String protocol = null;
  213. /**
  214. * Should we ignore PartialResultExceptions when iterating over NamingEnumerations?
  215. * Microsoft Active Directory often returns referrals, which lead
  216. * to PartialResultExceptions. Unfortunately there's no stable way to detect,
  217. * if the Exceptions really come from an AD referral.
  218. * Set to true to ignore PartialResultExceptions.
  219. */
  220. protected boolean adCompat = false;
  221. /**
  222. * How should we handle referrals? Microsoft Active Directory often returns
  223. * referrals. If you need to follow them set referrals to "follow".
  224. * Caution: if your DNS is not part of AD, the LDAP client lib might try
  225. * to resolve your domain name in DNS to find another LDAP server.
  226. */
  227. protected String referrals = null;
  228. /**
  229. * The base element for user searches.
  230. */
  231. protected String userBase = "";
  232. /**
  233. * The message format used to search for a user, with "{0}" marking
  234. * the spot where the username goes.
  235. */
  236. protected String userSearch = null;
  237. /**
  238. * The MessageFormat object associated with the current
  239. * <code>userSearch</code>.
  240. */
  241. protected MessageFormat userSearchFormat = null;
  242. /**
  243. * Should we search the entire subtree for matching users?
  244. */
  245. protected boolean userSubtree = false;
  246. /**
  247. * The attribute name used to retrieve the user password.
  248. */
  249. protected String userPassword = null;
  250. /**
  251. * The name of the attribute inside the users
  252. * directory entry where the value will be
  253. * taken to search for roles
  254. * This attribute is not used during a nested search
  255. */
  256. protected String userRoleAttribute = null;
  257. /**
  258. * A string of LDAP user patterns or paths, ":"-separated
  259. * These will be used to form the distinguished name of a
  260. * user, with "{0}" marking the spot where the specified username
  261. * goes.
  262. * This is similar to userPattern, but allows for multiple searches
  263. * for a user.
  264. */
  265. protected String[] userPatternArray = null;
  266. /**
  267. * The message format used to form the distinguished name of a
  268. * user, with "{0}" marking the spot where the specified username
  269. * goes.
  270. */
  271. protected String userPattern = null;
  272. /**
  273. * An array of MessageFormat objects associated with the current
  274. * <code>userPatternArray</code>.
  275. */
  276. protected MessageFormat[] userPatternFormatArray = null;
  277. /**
  278. * The base element for role searches.
  279. */
  280. protected String roleBase = "";
  281. /**
  282. * The MessageFormat object associated with the current
  283. * <code>roleBase</code>.
  284. */
  285. protected MessageFormat roleBaseFormat = null;
  286. /**
  287. * The MessageFormat object associated with the current
  288. * <code>roleSearch</code>.
  289. */
  290. protected MessageFormat roleFormat = null;
  291. /**
  292. * The name of an attribute in the user's entry containing
  293. * roles for that user
  294. */
  295. protected String userRoleName = null;
  296. /**
  297. * The name of the attribute containing roles held elsewhere
  298. */
  299. protected String roleName = null;
  300. /**
  301. * The message format used to select roles for a user, with "{0}" marking
  302. * the spot where the distinguished name of the user goes. The "{1}"
  303. * and "{2}" are described in the Configuration Reference.
  304. */
  305. protected String roleSearch = null;
  306. /**
  307. * Should we search the entire subtree for matching memberships?
  308. */
  309. protected boolean roleSubtree = false;
  310. /**
  311. * Should we look for nested group in order to determine roles?
  312. */
  313. protected boolean roleNested = false;
  314. /**
  315. * When searching for user roles, should the search be performed as the user
  316. * currently being authenticated? If false, {@link #connectionName} and
  317. * {@link #connectionPassword} will be used if specified, else an anonymous
  318. * connection will be used.
  319. */
  320. protected boolean roleSearchAsUser = false;
  321. /**
  322. * An alternate URL, to which, we should connect if connectionURL fails.
  323. */
  324. protected String alternateURL;
  325. /**
  326. * The number of connection attempts. If greater than zero we use the
  327. * alternate url.
  328. */
  329. protected int connectionAttempt = 0;
  330. /**
  331. * Add this role to every authenticated user
  332. */
  333. protected String commonRole = null;
  334. /**
  335. * The timeout, in milliseconds, to use when trying to create a connection
  336. * to the directory. The default is 5000 (5 seconds).
  337. */
  338. protected String connectionTimeout = "5000";
  339. /**
  340. * The sizeLimit (also known as the countLimit) to use when the realm is
  341. * configured with {@link #userSearch}. Zero for no limit.
  342. */
  343. protected long sizeLimit = 0;
  344. /**
  345. * The timeLimit (in milliseconds) to use when the realm is configured with
  346. * {@link #userSearch}. Zero for no limit.
  347. */
  348. protected int timeLimit = 0;
  349. /**
  350. * Should delegated credentials from the SPNEGO authenticator be used if
  351. * available
  352. */
  353. protected boolean useDelegatedCredential = true;
  354. /**
  355. * The QOP that should be used for the connection to the LDAP server after
  356. * authentication. This value is used to set the
  357. * <code>javax.security.sasl.qop</code> environment property for the LDAP
  358. * connection.
  359. */
  360. protected String spnegoDelegationQop = "auth-conf";
  361. // ------------------------------------------------------------- Properties
  362. /**
  363. * Return the type of authentication to use.
  364. */
  365. public String getAuthentication() {
  366. return authentication;
  367. }
  368. /**
  369. * Set the type of authentication to use.
  370. *
  371. * @param authentication The authentication
  372. */
  373. public void setAuthentication(String authentication) {
  374. this.authentication = authentication;
  375. }
  376. /**
  377. * Return the connection username for this Realm.
  378. */
  379. public String getConnectionName() {
  380. return (this.connectionName);
  381. }
  382. /**
  383. * Set the connection username for this Realm.
  384. *
  385. * @param connectionName The new connection username
  386. */
  387. public void setConnectionName(String connectionName) {
  388. this.connectionName = connectionName;
  389. }
  390. /**
  391. * Return the connection password for this Realm.
  392. */
  393. public String getConnectionPassword() {
  394. return (this.connectionPassword);
  395. }
  396. /**
  397. * Set the connection password for this Realm.
  398. *
  399. * @param connectionPassword The new connection password
  400. */
  401. public void setConnectionPassword(String connectionPassword) {
  402. this.connectionPassword = connectionPassword;
  403. }
  404. /**
  405. * Return the connection URL for this Realm.
  406. */
  407. public String getConnectionURL() {
  408. return (this.connectionURL);
  409. }
  410. /**
  411. * Set the connection URL for this Realm.
  412. *
  413. * @param connectionURL The new connection URL
  414. */
  415. public void setConnectionURL(String connectionURL) {
  416. this.connectionURL = connectionURL;
  417. }
  418. /**
  419. * Return the JNDI context factory for this Realm.
  420. */
  421. public String getContextFactory() {
  422. return (this.contextFactory);
  423. }
  424. /**
  425. * Set the JNDI context factory for this Realm.
  426. *
  427. * @param contextFactory The new context factory
  428. */
  429. public void setContextFactory(String contextFactory) {
  430. this.contextFactory = contextFactory;
  431. }
  432. /**
  433. * Return the derefAliases setting to be used.
  434. */
  435. public java.lang.String getDerefAliases() {
  436. return derefAliases;
  437. }
  438. /**
  439. * Set the value for derefAliases to be used when searching the directory.
  440. *
  441. * @param derefAliases New value of property derefAliases.
  442. */
  443. public void setDerefAliases(java.lang.String derefAliases) {
  444. this.derefAliases = derefAliases;
  445. }
  446. /**
  447. * Return the protocol to be used.
  448. */
  449. public String getProtocol() {
  450. return protocol;
  451. }
  452. /**
  453. * Set the protocol for this Realm.
  454. *
  455. * @param protocol The new protocol.
  456. */
  457. public void setProtocol(String protocol) {
  458. this.protocol = protocol;
  459. }
  460. /**
  461. * Returns the current settings for handling PartialResultExceptions
  462. */
  463. public boolean getAdCompat () {
  464. return adCompat;
  465. }
  466. /**
  467. * How do we handle PartialResultExceptions?
  468. * True: ignore all PartialResultExceptions.
  469. */
  470. public void setAdCompat (boolean adCompat) {
  471. this.adCompat = adCompat;
  472. }
  473. /**
  474. * Returns the current settings for handling JNDI referrals.
  475. */
  476. public String getReferrals () {
  477. return referrals;
  478. }
  479. /**
  480. * How do we handle JNDI referrals? ignore, follow, or throw
  481. * (see javax.naming.Context.REFERRAL for more information).
  482. */
  483. public void setReferrals (String referrals) {
  484. this.referrals = referrals;
  485. }
  486. /**
  487. * Return the base element for user searches.
  488. */
  489. public String getUserBase() {
  490. return (this.userBase);
  491. }
  492. /**
  493. * Set the base element for user searches.
  494. *
  495. * @param userBase The new base element
  496. */
  497. public void setUserBase(String userBase) {
  498. this.userBase = userBase;
  499. }
  500. /**
  501. * Return the message format pattern for selecting users in this Realm.
  502. */
  503. public String getUserSearch() {
  504. return (this.userSearch);
  505. }
  506. /**
  507. * Set the message format pattern for selecting users in this Realm.
  508. *
  509. * @param userSearch The new user search pattern
  510. */
  511. public void setUserSearch(String userSearch) {
  512. this.userSearch = userSearch;
  513. if (userSearch == null)
  514. userSearchFormat = null;
  515. else
  516. userSearchFormat = new MessageFormat(userSearch);
  517. }
  518. /**
  519. * Return the "search subtree for users" flag.
  520. */
  521. public boolean getUserSubtree() {
  522. return (this.userSubtree);
  523. }
  524. /**
  525. * Set the "search subtree for users" flag.
  526. *
  527. * @param userSubtree The new search flag
  528. */
  529. public void setUserSubtree(boolean userSubtree) {
  530. this.userSubtree = userSubtree;
  531. }
  532. /**
  533. * Return the user role name attribute name for this Realm.
  534. */
  535. public String getUserRoleName() {
  536. return userRoleName;
  537. }
  538. /**
  539. * Set the user role name attribute name for this Realm.
  540. *
  541. * @param userRoleName The new userRole name attribute name
  542. */
  543. public void setUserRoleName(String userRoleName) {
  544. this.userRoleName = userRoleName;
  545. }
  546. /**
  547. * Return the base element for role searches.
  548. */
  549. public String getRoleBase() {
  550. return (this.roleBase);
  551. }
  552. /**
  553. * Set the base element for role searches.
  554. *
  555. * @param roleBase The new base element
  556. */
  557. public void setRoleBase(String roleBase) {
  558. this.roleBase = roleBase;
  559. if (roleBase == null)
  560. roleBaseFormat = null;
  561. else
  562. roleBaseFormat = new MessageFormat(roleBase);
  563. }
  564. /**
  565. * Return the role name attribute name for this Realm.
  566. */
  567. public String getRoleName() {
  568. return (this.roleName);
  569. }
  570. /**
  571. * Set the role name attribute name for this Realm.
  572. *
  573. * @param roleName The new role name attribute name
  574. */
  575. public void setRoleName(String roleName) {
  576. this.roleName = roleName;
  577. }
  578. /**
  579. * Return the message format pattern for selecting roles in this Realm.
  580. */
  581. public String getRoleSearch() {
  582. return (this.roleSearch);
  583. }
  584. /**
  585. * Set the message format pattern for selecting roles in this Realm.
  586. *
  587. * @param roleSearch The new role search pattern
  588. */
  589. public void setRoleSearch(String roleSearch) {
  590. this.roleSearch = roleSearch;
  591. if (roleSearch == null)
  592. roleFormat = null;
  593. else
  594. roleFormat = new MessageFormat(roleSearch);
  595. }
  596. public boolean isRoleSearchAsUser() {
  597. return roleSearchAsUser;
  598. }
  599. public void setRoleSearchAsUser(boolean roleSearchAsUser) {
  600. this.roleSearchAsUser = roleSearchAsUser;
  601. }
  602. /**
  603. * Return the "search subtree for roles" flag.
  604. */
  605. public boolean getRoleSubtree() {
  606. return (this.roleSubtree);
  607. }
  608. /**
  609. * Set the "search subtree for roles" flag.
  610. *
  611. * @param roleSubtree The new search flag
  612. */
  613. public void setRoleSubtree(boolean roleSubtree) {
  614. this.roleSubtree = roleSubtree;
  615. }
  616. /**
  617. * Return the "The nested group search flag" flag.
  618. */
  619. public boolean getRoleNested() {
  620. return (this.roleNested);
  621. }
  622. /**
  623. * Set the "search subtree for roles" flag.
  624. *
  625. * @param roleNested The nested group search flag
  626. */
  627. public void setRoleNested(boolean roleNested) {
  628. this.roleNested = roleNested;
  629. }
  630. /**
  631. * Return the password attribute used to retrieve the user password.
  632. */
  633. public String getUserPassword() {
  634. return (this.userPassword);
  635. }
  636. /**
  637. * Set the password attribute used to retrieve the user password.
  638. *
  639. * @param userPassword The new password attribute
  640. */
  641. public void setUserPassword(String userPassword) {
  642. this.userPassword = userPassword;
  643. }
  644. public String getUserRoleAttribute() {
  645. return userRoleAttribute;
  646. }
  647. public void setUserRoleAttribute(String userRoleAttribute) {
  648. this.userRoleAttribute = userRoleAttribute;
  649. }
  650. /**
  651. * Return the message format pattern for selecting users in this Realm.
  652. */
  653. public String getUserPattern() {
  654. return (this.userPattern);
  655. }
  656. /**
  657. * Set the message format pattern for selecting users in this Realm.
  658. * This may be one simple pattern, or multiple patterns to be tried,
  659. * separated by parentheses. (for example, either "cn={0}", or
  660. * "(cn={0})(cn={0},o=myorg)" Full LDAP search strings are also supported,
  661. * but only the "OR", "|" syntax, so "(|(cn={0})(cn={0},o=myorg))" is
  662. * also valid. Complex search strings with &, etc are NOT supported.
  663. *
  664. * @param userPattern The new user pattern
  665. */
  666. public void setUserPattern(String userPattern) {
  667. this.userPattern = userPattern;
  668. if (userPattern == null)
  669. userPatternArray = null;
  670. else {
  671. userPatternArray = parseUserPatternString(userPattern);
  672. int len = this.userPatternArray.length;
  673. userPatternFormatArray = new MessageFormat[len];
  674. for (int i=0; i < len; i++) {
  675. userPatternFormatArray[i] =
  676. new MessageFormat(userPatternArray[i]);
  677. }
  678. }
  679. }
  680. /**
  681. * Getter for property alternateURL.
  682. *
  683. * @return Value of property alternateURL.
  684. */
  685. public String getAlternateURL() {
  686. return this.alternateURL;
  687. }
  688. /**
  689. * Setter for property alternateURL.
  690. *
  691. * @param alternateURL New value of property alternateURL.
  692. */
  693. public void setAlternateURL(String alternateURL) {
  694. this.alternateURL = alternateURL;
  695. }
  696. /**
  697. * Return the common role
  698. */
  699. public String getCommonRole() {
  700. return commonRole;
  701. }
  702. /**
  703. * Set the common role
  704. *
  705. * @param commonRole The common role
  706. */
  707. public void setCommonRole(String commonRole) {
  708. this.commonRole = commonRole;
  709. }
  710. /**
  711. * Return the connection timeout.
  712. */
  713. public String getConnectionTimeout() {
  714. return connectionTimeout;
  715. }
  716. /**
  717. * Set the connection timeout.
  718. *
  719. * @param timeout The new connection timeout
  720. */
  721. public void setConnectionTimeout(String timeout) {
  722. this.connectionTimeout = timeout;
  723. }
  724. public long getSizeLimit() {
  725. return sizeLimit;
  726. }
  727. public void setSizeLimit(long sizeLimit) {
  728. this.sizeLimit = sizeLimit;
  729. }
  730. public int getTimeLimit() {
  731. return timeLimit;
  732. }
  733. public void setTimeLimit(int timeLimit) {
  734. this.timeLimit = timeLimit;
  735. }
  736. public boolean isUseDelegatedCredential() {
  737. return useDelegatedCredential;
  738. }
  739. public void setUseDelegatedCredential(boolean useDelegatedCredential) {
  740. this.useDelegatedCredential = useDelegatedCredential;
  741. }
  742. public String getSpnegoDelegationQop() {
  743. return spnegoDelegationQop;
  744. }
  745. public void setSpnegoDelegationQop(String spnegoDelegationQop) {
  746. this.spnegoDelegationQop = spnegoDelegationQop;
  747. }
  748. // ---------------------------------------------------------- Realm Methods
  749. /**
  750. * Return the Principal associated with the specified username and
  751. * credentials, if there is one; otherwise return <code>null</code>.
  752. *
  753. * If there are any errors with the JDBC connection, executing
  754. * the query or anything we return null (don't authenticate). This
  755. * event is also logged, and the connection will be closed so that
  756. * a subsequent request will automatically re-open it.
  757. *
  758. * @param username Username of the Principal to look up
  759. * @param credentials Password or other credentials to use in
  760. * authenticating this username
  761. */
  762. @Override
  763. public Principal authenticate(String username, String credentials) {
  764. DirContext context = null;
  765. Principal principal = null;
  766. try {
  767. // Ensure that we have a directory context available
  768. context = open();
  769. // Occassionally the directory context will timeout. Try one more
  770. // time before giving up.
  771. try {
  772. // Authenticate the specified username if possible
  773. principal = authenticate(context, username, credentials);
  774. } catch (NullPointerException e) {
  775. /* BZ 42449 - Kludge Sun's LDAP provider
  776. with broken SSL
  777. */
  778. // log the exception so we know it's there.
  779. containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
  780. // close the connection so we know it will be reopened.
  781. if (context != null)
  782. close(context);
  783. // open a new directory context.
  784. context = open();
  785. // Try the authentication again.
  786. principal = authenticate(context, username, credentials);
  787. } catch (CommunicationException e) {
  788. // log the exception so we know it's there.
  789. containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
  790. // close the connection so we know it will be reopened.
  791. if (context != null)
  792. close(context);
  793. // open a new directory context.
  794. context = open();
  795. // Try the authentication again.
  796. principal = authenticate(context, username, credentials);
  797. } catch (ServiceUnavailableException e) {
  798. // log the exception so we know it's there.
  799. containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
  800. // close the connection so we know it will be reopened.
  801. if (context != null)
  802. close(context);
  803. // open a new directory context.
  804. context = open();
  805. // Try the authentication again.
  806. principal = authenticate(context, username, credentials);
  807. }
  808. // Release this context
  809. release(context);
  810. // Return the authenticated Principal (if any)
  811. return (principal);
  812. } catch (NamingException e) {
  813. // Log the problem for posterity
  814. containerLog.error(sm.getString("jndiRealm.exception"), e);
  815. // Close the connection so that it gets reopened next time
  816. if (context != null)
  817. close(context);
  818. // Return "not authenticated" for this request
  819. if (containerLog.isDebugEnabled())
  820. containerLog.debug("Returning null principal.");
  821. return (null);
  822. }
  823. }
  824. // -------------------------------------------------------- Package Methods
  825. // ------------------------------------------------------ Protected Methods
  826. /**
  827. * Return the Principal associated with the specified username and
  828. * credentials, if there is one; otherwise return <code>null</code>.
  829. *
  830. * @param context The directory context
  831. * @param username Username of the Principal to look up
  832. * @param credentials Password or other credentials to use in
  833. * authenticating this username
  834. *
  835. * @exception NamingException if a directory server error occurs
  836. */
  837. public synchronized Principal authenticate(DirContext context,
  838. String username,
  839. String credentials)
  840. throws NamingException {
  841. if (username == null || username.equals("")
  842. || credentials == null || credentials.equals("")) {
  843. if (containerLog.isDebugEnabled())
  844. containerLog.debug("username null or empty: returning null principal.");
  845. return (null);
  846. }
  847. if (userPatternArray != null) {
  848. for (int curUserPattern = 0;
  849. curUserPattern < userPatternFormatArray.length;
  850. curUserPattern++) {
  851. // Retrieve user information
  852. User user = getUser(context, username, credentials, curUserPattern);
  853. if (user != null) {
  854. try {
  855. // Check the user's credentials
  856. if (checkCredentials(context, user, credentials)) {
  857. // Search for additional roles
  858. List<String> roles = getRoles(context, user);
  859. if (containerLog.isDebugEnabled()) {
  860. Iterator<String> it = roles.iterator();
  861. // TODO: Use a single log message
  862. while (it.hasNext()) {
  863. containerLog.debug("Found role: " + it.next());
  864. }
  865. }
  866. return (new GenericPrincipal(username,
  867. credentials,
  868. roles));
  869. }
  870. } catch (InvalidNameException ine) {
  871. // Log the problem for posterity
  872. containerLog.warn(sm.getString("jndiRealm.exception"), ine);
  873. // ignore; this is probably due to a name not fitting
  874. // the search path format exactly, as in a fully-
  875. // qualified name being munged into a search path
  876. // that already contains cn= or vice-versa
  877. }
  878. }
  879. }
  880. return null;
  881. } else {
  882. // Retrieve user information
  883. User user = getUser(context, username, credentials);
  884. if (user == null)
  885. return (null);
  886. // Check the user's credentials
  887. if (!checkCredentials(context, user, credentials))
  888. return (null);
  889. // Search for additional roles
  890. List<String> roles = getRoles(context, user);
  891. if (containerLog.isDebugEnabled()) {
  892. Iterator<String> it = roles.iterator();
  893. // TODO: Use a single log message
  894. while (it.hasNext()) {
  895. containerLog.debug("Found role: " + it.next());
  896. }
  897. }
  898. // Create and return a suitable Principal for this user
  899. return (new GenericPrincipal(username, credentials, roles));
  900. }
  901. }
  902. /**
  903. * Return a User object containing information about the user
  904. * with the specified username, if found in the directory;
  905. * otherwise return <code>null</code>.
  906. *
  907. * @param context The directory context
  908. * @param username Username to be looked up
  909. *
  910. * @exception NamingException if a directory server error occurs
  911. *
  912. * @see #getUser(DirContext, String, String, int)
  913. */
  914. protected User getUser(DirContext context, String username)
  915. throws NamingException {
  916. return getUser(context, username, null, -1);
  917. }
  918. /**
  919. * Return a User object containing information about the user
  920. * with the specified username, if found in the directory;
  921. * otherwise return <code>null</code>.
  922. *
  923. * @param context The directory context
  924. * @param username Username to be looked up
  925. * @param credentials User credentials (optional)
  926. *
  927. * @exception NamingException if a directory server error occurs
  928. *
  929. * @see #getUser(DirContext, String, String, int)
  930. */
  931. protected User getUser(DirContext context, String username, String credentials)
  932. throws NamingException {
  933. return getUser(context, username, credentials, -1);
  934. }
  935. /**
  936. * Return a User object containing information about the user
  937. * with the specified username, if found in the directory;
  938. * otherwise return <code>null</code>.
  939. *
  940. * If the <code>userPassword</code> configuration attribute is
  941. * specified, the value of that attribute is retrieved from the
  942. * user's directory entry. If the <code>userRoleName</code>
  943. * configuration attribute is specified, all values of that
  944. * attribute are retrieved from the directory entry.
  945. *
  946. * @param context The directory context
  947. * @param username Username to be looked up
  948. * @param credentials User credentials (optional)
  949. * @param curUserPattern Index into userPatternFormatArray
  950. *
  951. * @exception NamingException if a directory server error occurs
  952. */
  953. protected User getUser(DirContext context, String username,
  954. String credentials, int curUserPattern)
  955. throws NamingException {
  956. User user = null;
  957. // Get attributes to retrieve from user entry
  958. ArrayList<String> list = new ArrayList<>();
  959. if (userPassword != null)
  960. list.add(userPassword);
  961. if (userRoleName != null)
  962. list.add(userRoleName);
  963. if (userRoleAttribute != null) {
  964. list.add(userRoleAttribute);
  965. }
  966. String[] attrIds = new String[list.size()];
  967. list.toArray(attrIds);
  968. // Use pattern or search for user entry
  969. if (userPatternFormatArray != null && curUserPattern >= 0) {
  970. user = getUserByPattern(context, username, credentials, attrIds, curUserPattern);
  971. } else {
  972. user = getUserBySearch(context, username, attrIds);
  973. }
  974. return user;
  975. }
  976. /**
  977. * Use the distinguished name to locate the directory
  978. * entry for the user with the specified username and
  979. * return a User object; otherwise return <code>null</code>.
  980. *
  981. * @param context The directory context
  982. * @param username The username
  983. * @param attrIds String[]containing names of attributes to
  984. * @param dn Distinguished name of the user
  985. * retrieve.
  986. *
  987. * @exception NamingException if a directory server error occurs
  988. */
  989. protected User getUserByPattern(DirContext context,
  990. String username,
  991. String[] attrIds,
  992. String dn)
  993. throws NamingException {
  994. // If no attributes are requested, no need to look for them
  995. if (attrIds == null || attrIds.length == 0) {
  996. return new User(username, dn, null, null,null);
  997. }
  998. // Get required attributes from user entry
  999. Attributes attrs = null;
  1000. try {
  1001. attrs = context.getAttributes(dn, attrIds);
  1002. } catch (NameNotFoundException e) {
  1003. return (null);
  1004. }
  1005. if (attrs == null)
  1006. return (null);
  1007. // Retrieve value of userPassword
  1008. String password = null;
  1009. if (userPassword != null)
  1010. password = getAttributeValue(userPassword, attrs);
  1011. String userRoleAttrValue = null;
  1012. if (userRoleAttribute != null) {
  1013. userRoleAttrValue = getAttributeValue(userRoleAttribute, attrs);
  1014. }
  1015. // Retrieve values of userRoleName attribute
  1016. ArrayList<String> roles = null;
  1017. if (userRoleName != null)
  1018. roles = addAttributeValues(userRoleName, attrs, roles);
  1019. return new User(username, dn, password, roles, userRoleAttrValue);
  1020. }
  1021. /**
  1022. * Use the <code>UserPattern</code> configuration attribute to
  1023. * locate the directory entry for the user with the specified
  1024. * username and return a User object; otherwise return
  1025. * <code>null</code>.
  1026. *
  1027. * @param context The directory context
  1028. * @param username The username
  1029. * @param credentials User credentials (optional)
  1030. * @param attrIds String[]containing names of attributes to
  1031. * @param curUserPattern Index into userPatternFormatArray
  1032. *
  1033. * @exception NamingException if a directory server error occurs
  1034. * @see #getUserByPattern(DirContext, String, String[], String)
  1035. */
  1036. protected User getUserByPattern(DirContext context,
  1037. String username,
  1038. String credentials,
  1039. String[] attrIds,
  1040. int curUserPattern)
  1041. throws NamingException {
  1042. User user = null;
  1043. if (username == null || userPatternFormatArray[curUserPattern] == null)
  1044. return (null);
  1045. // Form the dn from the user pattern
  1046. String dn = userPatternFormatArray[curUserPattern].format(new String[] { username });
  1047. try {
  1048. user = getUserByPattern(context, username, attrIds, dn);
  1049. } catch (NameNotFoundException e) {
  1050. return (null);
  1051. } catch (NamingException e) {
  1052. // If the getUserByPattern() call fails, try it again with the
  1053. // credentials of the user that we're searching for
  1054. try {
  1055. userCredentialsAdd(context, dn, credentials);
  1056. user = getUserByPattern(context, username, attrIds, dn);
  1057. } finally {
  1058. userCredentialsRemove(context);
  1059. }
  1060. }
  1061. return user;
  1062. }
  1063. /**
  1064. * Search the directory to return a User object containing
  1065. * information about the user with the specified username, if
  1066. * found in the directory; otherwise return <code>null</code>.
  1067. *
  1068. * @param context The directory context
  1069. * @param username The username
  1070. * @param attrIds String[]containing names of attributes to retrieve.
  1071. *
  1072. * @exception NamingException if a directory server error occurs
  1073. */
  1074. protected User getUserBySearch(DirContext context,
  1075. String username,
  1076. String[] attrIds)
  1077. throws NamingException {
  1078. if (username == null || userSearchFormat == null)
  1079. return (null);
  1080. // Form the search filter
  1081. String filter = userSearchFormat.format(new String[] { username });
  1082. // Set up the search controls
  1083. SearchControls constraints = new SearchControls();
  1084. if (userSubtree) {
  1085. constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
  1086. }
  1087. else {
  1088. constraints.setSearchScope(SearchControls.ONELEVEL_SCOPE);
  1089. }
  1090. constraints.setCountLimit(sizeLimit);
  1091. constraints.setTimeLimit(timeLimit);
  1092. // Specify the attributes to be retrieved
  1093. if (attrIds == null)
  1094. attrIds = new String[0];
  1095. constraints.setReturningAttributes(attrIds);
  1096. NamingEnumeration<SearchResult> results =
  1097. context.search(userBase, filter, constraints);
  1098. // Fail if no entries found
  1099. try {
  1100. if (results == null || !results.hasMore()) {
  1101. return (null);
  1102. }
  1103. } catch (PartialResultException ex) {
  1104. if (!adCompat)
  1105. throw ex;
  1106. else
  1107. return (null);
  1108. }
  1109. // Get result for the first entry found
  1110. SearchResult result = results.next();
  1111. // Check no further entries were found
  1112. try {
  1113. if (results.hasMore()) {
  1114. if(containerLog.isInfoEnabled())
  1115. containerLog.info("username " + username + " has multiple entries");
  1116. return (null);
  1117. }
  1118. } catch (PartialResultException ex) {
  1119. if (!adCompat)
  1120. throw ex;
  1121. }
  1122. String dn = getDistinguishedName(context, userBase, result);
  1123. if (containerLog.isTraceEnabled())
  1124. containerLog.trace(" entry found for " + username + " with dn " + dn);
  1125. // Get the entry's attributes
  1126. Attributes attrs = result.getAttributes();
  1127. if (attrs == null)
  1128. return null;
  1129. // Retrieve value of userPassword
  1130. String password = null;
  1131. if (userPassword != null)
  1132. password = getAttributeValue(userPassword, attrs);
  1133. String userRoleAttrValue = null;
  1134. if (userRoleAttribute != null) {
  1135. userRoleAttrValue = getAttributeValue(userRoleAttribute, attrs);
  1136. }
  1137. // Retrieve values of userRoleName attribute
  1138. ArrayList<String> roles = null;
  1139. if (userRoleName != null)
  1140. roles = addAttributeValues(userRoleName, attrs, roles);
  1141. return new User(username, dn, password, roles, userRoleAttrValue);
  1142. }
  1143. /**
  1144. * Check whether the given User can be authenticated with the
  1145. * given credentials. If the <code>userPassword</code>
  1146. * configuration attribute is specified, the credentials
  1147. * previously retrieved from the directory are compared explicitly
  1148. * with those presented by the user. Otherwise the presented
  1149. * credentials are checked by binding to the directory as the
  1150. * user.
  1151. *
  1152. * @param context The directory context
  1153. * @param user The User to be authenticated
  1154. * @param credentials The credentials presented by the user
  1155. *
  1156. * @exception NamingException if a directory server error occurs
  1157. */
  1158. protected boolean checkCredentials(DirContext context,
  1159. User user,
  1160. String credentials)
  1161. throws NamingException {
  1162. boolean validated = false;
  1163. if (userPassword == null) {
  1164. validated = bindAsUser(context, user, credentials);
  1165. } else {
  1166. validated = compareCredentials(context, user, credentials);
  1167. }
  1168. if (containerLog.isTraceEnabled()) {
  1169. if (validated) {
  1170. containerLog.trace(sm.getString("jndiRealm.authenticateSuccess",
  1171. user.getUserName()));
  1172. } else {
  1173. containerLog.trace(sm.getString("jndiRealm.authenticateFailure",
  1174. user.getUserName()));
  1175. }
  1176. }
  1177. return (validated);
  1178. }
  1179. /**
  1180. * Check whether the credentials presented by the user match those
  1181. * retrieved from the directory.
  1182. *
  1183. * @param context The directory context
  1184. * @param info The User to be authenticated
  1185. * @param credentials Authentication credentials
  1186. *
  1187. * @exception NamingException if a directory server error occurs
  1188. */
  1189. protected boolean compareCredentials(DirContext context,
  1190. User info,
  1191. String credentials)
  1192. throws NamingException {
  1193. // Validate the credentials specified by the user
  1194. if (containerLog.isTraceEnabled())
  1195. containerLog.trace(" validating credentials");
  1196. if (info == null || credentials == null)
  1197. return (false);
  1198. String password = info.getPassword();
  1199. return compareCredentials(credentials, password);
  1200. }
  1201. /**
  1202. * Check credentials by binding to the directory as the user
  1203. *
  1204. * @param context The directory context
  1205. * @param user The User to be authenticated
  1206. * @param credentials Authentication credentials
  1207. *
  1208. * @exception NamingException if a directory server error occurs
  1209. */
  1210. protected boolean bindAsUser(DirContext context,
  1211. User user,
  1212. String credentials)
  1213. throws NamingException {
  1214. if (credentials == null || user == null)
  1215. return (false);
  1216. String dn = user.getDN();
  1217. if (dn == null)
  1218. return (false);
  1219. // Validate the credentials specified by the user
  1220. if (containerLog.isTraceEnabled()) {
  1221. containerLog.trace(" validating credentials by binding as the user");
  1222. }
  1223. userCredentialsAdd(context, dn, credentials);
  1224. // Elicit an LDAP bind operation
  1225. boolean validated = false;
  1226. try {
  1227. if (containerLog.isTraceEnabled()) {
  1228. containerLog.trace(" binding as " + dn);
  1229. }
  1230. context.getAttributes("", null);
  1231. validated = true;
  1232. }
  1233. catch (AuthenticationException e) {
  1234. if (containerLog.isTraceEnabled()) {
  1235. containerLog.trace(" bind attempt failed");
  1236. }
  1237. }
  1238. userCredentialsRemove(context);
  1239. return (validated);
  1240. }
  1241. /**
  1242. * Configure the context to use the provided credentials for
  1243. * authentication.
  1244. *
  1245. * @param context DirContext to configure
  1246. * @param dn Distinguished name of user
  1247. * @param credentials Credentials of user
  1248. */
  1249. private void userCredentialsAdd(DirContext context, String dn,
  1250. String credentials) throws NamingException {
  1251. // Set up security environment to bind as the user
  1252. context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn);
  1253. context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
  1254. }
  1255. /**
  1256. * Configure the context to use {@link #connectionName} and
  1257. * {@link #connectionPassword} if specified or an anonymous connection if
  1258. * those attributes are not specified.
  1259. *
  1260. * @param context DirContext to configure
  1261. */
  1262. private void userCredentialsRemove(DirContext context)
  1263. throws NamingException {
  1264. // Restore the original security environment
  1265. if (connectionName != null) {
  1266. context.addToEnvironment(Context.SECURITY_PRINCIPAL,
  1267. connectionName);
  1268. } else {
  1269. context.removeFromEnvironment(Context.SECURITY_PRINCIPAL);
  1270. }
  1271. if (connectionPassword != null) {
  1272. context.addToEnvironment(Context.SECURITY_CREDENTIALS,
  1273. connectionPassword);
  1274. }
  1275. else {
  1276. context.removeFromEnvironment(Context.SECURITY_CREDENTIALS);
  1277. }
  1278. }
  1279. /**
  1280. * Return a List of roles associated with the given User. Any
  1281. * roles present in the user's directory entry are supplemented by
  1282. * a directory search. If no roles are associated with this user,
  1283. * a zero-length List is returned.
  1284. *
  1285. * @param context The directory context we are searching
  1286. * @param user The User to be checked
  1287. *
  1288. * @exception NamingException if a directory server error occurs
  1289. */
  1290. protected List<String> getRoles(DirContext context, User user)
  1291. throws NamingException {
  1292. if (user == null)
  1293. return (null);
  1294. String dn = user.getDN();
  1295. String username = user.getUserName();
  1296. String userRoleId = user.getUserRoleId();
  1297. if (dn == null || username == null)
  1298. return (null);
  1299. if (containerLog.isTraceEnabled())
  1300. containerLog.trace(" getRoles(" + dn + ")");
  1301. // Start with roles retrieved from the user entry
  1302. List<String> list = new ArrayList<>();
  1303. List<String> userRoles = user.getRoles();
  1304. if (userRoles != null) {
  1305. list.addAll(userRoles);
  1306. }
  1307. if (commonRole != null)
  1308. list.add(commonRole);
  1309. if (containerLog.isTraceEnabled()) {
  1310. containerLog.trace(" Found " + list.size() + " user internal roles");
  1311. for (int i=0; i<list.size(); i++)
  1312. containerLog.trace( " Found user internal role " + list.get(i));
  1313. }
  1314. // Are we configured to do role searches?
  1315. if ((roleFormat == null) || (roleName == null))
  1316. return (list);
  1317. // Set up parameters for an appropriate search
  1318. String filter = roleFormat.format(new String[] { doRFC2254Encoding(dn), username, userRoleId });
  1319. SearchControls controls = new SearchControls();
  1320. if (roleSubtree)
  1321. controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
  1322. else
  1323. controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
  1324. controls.setReturningAttributes(new String[] {roleName});
  1325. String base = null;
  1326. if (roleBaseFormat != null) {
  1327. NameParser np = context.getNameParser("");
  1328. Name name = np.parse(dn);
  1329. String nameParts[] = new String[name.size()];
  1330. for (int i = 0; i < name.size(); i++) {
  1331. nameParts[i] = name.get(i);
  1332. }
  1333. base = roleBaseFormat.format(nameParts);
  1334. }
  1335. // Perform the configured search and process the results
  1336. NamingEnumeration<SearchResult> results = null;
  1337. try {
  1338. if (roleSearchAsUser) {
  1339. userCredentialsAdd(context, dn, user.getPassword());
  1340. }
  1341. results = context.search(base, filter, controls);
  1342. } finally {
  1343. if (roleSearchAsUser) {
  1344. userCredentialsRemove(context);
  1345. }
  1346. }
  1347. if (results == null)
  1348. return (list); // Should never happen, but just in case ...
  1349. HashMap<String, String> groupMap = new HashMap<>();
  1350. try {
  1351. while (results.hasMore()) {
  1352. SearchResult result = results.next();
  1353. Attributes attrs = result.getAttributes();
  1354. if (attrs == null)
  1355. continue;
  1356. String dname = getDistinguishedName(context, roleBase, result);
  1357. String name = getAttributeValue(roleName, attrs);
  1358. if (name != null && dname != null) {
  1359. groupMap.put(dname, name);
  1360. }
  1361. }
  1362. } catch (PartialResultException ex) {
  1363. if (!adCompat)
  1364. throw ex;
  1365. }
  1366. Set<String> keys = groupMap.keySet();
  1367. if (containerLog.isTraceEnabled()) {
  1368. containerLog.trace(" Found " + keys.size() + " direct roles");
  1369. for (String key: keys) {
  1370. containerLog.trace( " Found direct role " + key + " -> " + groupMap.get(key));
  1371. }
  1372. }
  1373. // if nested group search is enabled, perform searches for nested groups until no new group is found
  1374. if (getRoleNested()) {
  1375. // The following efficient algorithm is known as memberOf Algorithm, as described in "Practices in
  1376. // Directory Groups". It avoids group slurping and handles cyclic group memberships as well.
  1377. // See http://middleware.internet2.edu/dir/ for details
  1378. Map<String, String> newGroups = new HashMap<>(groupMap);
  1379. while (!newGroups.isEmpty()) {
  1380. Map<String, String> newThisRound = new HashMap<>(); // Stores the groups we find in this iteration
  1381. for (Entry<String, String> group : newGroups.entrySet()) {
  1382. filter = roleFormat.format(new String[] { group.getKey(), group.getValue(), group.getValue() });
  1383. if (containerLog.isTraceEnabled()) {
  1384. containerLog.trace("Perform a nested group search with base "+ roleBase + " and filter " + filter);
  1385. }
  1386. results = context.search(roleBase, filter, controls);
  1387. try {
  1388. while (results.hasMore()) {
  1389. SearchResult result = results.next();
  1390. Attributes attrs = result.getAttributes();
  1391. if (attrs == null)
  1392. continue;
  1393. String dname = getDistinguishedName(context, roleBase, result);
  1394. String name = getAttributeValue(roleName, attrs);
  1395. if (name != null && dname != null && !groupMap.keySet().contains(dname)) {
  1396. groupMap.put(dname, name);
  1397. newThisRound.put(dname, name);
  1398. if (containerLog.isTraceEnabled()) {
  1399. containerLog.trace(" Found nested role " + dname + " -> " + name);
  1400. }
  1401. }
  1402. }
  1403. } catch (PartialResultException ex) {
  1404. if (!adCompat)
  1405. throw ex;
  1406. }
  1407. }
  1408. newGroups = newThisRound;
  1409. }
  1410. }
  1411. list.addAll(groupMap.values());
  1412. return list;
  1413. }
  1414. /**
  1415. * Return a String representing the value of the specified attribute.
  1416. *
  1417. * @param attrId Attribute name
  1418. * @param attrs Attributes containing the required value
  1419. *
  1420. * @exception NamingException if a directory server error occurs
  1421. */
  1422. private String getAttributeValue(String attrId, Attributes attrs)
  1423. throws NamingException {
  1424. if (containerLog.isTraceEnabled())
  1425. containerLog.trace(" retrieving attribute " + attrId);
  1426. if (attrId == null || attrs == null)
  1427. return null;
  1428. Attribute attr = attrs.get(attrId);
  1429. if (attr == null)
  1430. return (null);
  1431. Object value = attr.get();
  1432. if (value == null)
  1433. return (null);
  1434. String valueString = null;
  1435. if (value instanceof byte[])
  1436. valueString = new String((byte[]) value);
  1437. else
  1438. valueString = value.toString();
  1439. return valueString;
  1440. }
  1441. /**
  1442. * Add values of a specified attribute to a list
  1443. *
  1444. * @param attrId Attribute name
  1445. * @param attrs Attributes containing the new values
  1446. * @param values ArrayList containing values found so far
  1447. *
  1448. * @exception NamingException if a directory server error occurs
  1449. */
  1450. private ArrayList<String> addAttributeValues(String attrId,
  1451. Attributes attrs,
  1452. ArrayList<String> values)
  1453. throws NamingException{
  1454. if (containerLog.isTraceEnabled())
  1455. containerLog.trace(" retrieving values for attribute " + attrId);
  1456. if (attrId == null || attrs == null)
  1457. return values;
  1458. if (values == null)
  1459. values = new ArrayList<>();
  1460. Attribute attr = attrs.get(attrId);
  1461. if (attr == null)
  1462. return (values);
  1463. NamingEnumeration<?> e = attr.getAll();
  1464. try {
  1465. while(e.hasMore()) {
  1466. String value = (String)e.next();
  1467. values.add(value);
  1468. }
  1469. } catch (PartialResultException ex) {
  1470. if (!adCompat)
  1471. throw ex;
  1472. }
  1473. return values;
  1474. }
  1475. /**
  1476. * Close any open connection to the directory server for this Realm.
  1477. *
  1478. * @param context The directory context to be closed
  1479. */
  1480. protected void close(DirContext context) {
  1481. // Do nothing if there is no opened connection
  1482. if (context == null)
  1483. return;
  1484. // Close our opened connection
  1485. try {
  1486. if (containerLog.isDebugEnabled())
  1487. containerLog.debug("Closing directory context");
  1488. context.close();
  1489. } catch (NamingException e) {
  1490. containerLog.error(sm.getString("jndiRealm.close"), e);
  1491. }
  1492. this.context = null;
  1493. }
  1494. /**
  1495. * Return a short name for this Realm implementation.
  1496. */
  1497. @Override
  1498. protected String getName() {
  1499. return (name);
  1500. }
  1501. /**
  1502. * Return the password associated with the given principal's user name.
  1503. */
  1504. @Override
  1505. protected String getPassword(String username) {
  1506. return (null);
  1507. }
  1508. /**
  1509. * Return the Principal associated with the given user name.
  1510. */
  1511. @Override
  1512. protected Principal getPrincipal(String username) {
  1513. return getPrincipal(username, null);
  1514. }
  1515. @Override
  1516. protected Principal getPrincipal(String username,
  1517. GSSCredential gssCredential) {
  1518. DirContext context = null;
  1519. Principal principal = null;
  1520. try {
  1521. // Ensure that we have a directory context available
  1522. context = open();
  1523. // Occasionally the directory context will timeout. Try one more
  1524. // time before giving up.
  1525. try {
  1526. // Authenticate the specified username if possible
  1527. principal = getPrincipal(context, username, gssCredential);
  1528. } catch (CommunicationException e) {
  1529. // log the exception so we know it's there.
  1530. containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
  1531. // close the connection so we know it will be reopened.
  1532. if (context != null)
  1533. close(context);
  1534. // open a new directory context.
  1535. context = open();
  1536. // Try the authentication again.
  1537. principal = getPrincipal(context, username, gssCredential);
  1538. } catch (ServiceUnavailableException e) {
  1539. // log the exception so we know it's there.
  1540. containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
  1541. // close the connection so we know it will be reopened.
  1542. if (context != null)
  1543. close(context);
  1544. // open a new directory context.
  1545. context = open();
  1546. // Try the authentication again.
  1547. principal = getPrincipal(context, username, gssCredential);
  1548. }
  1549. // Release this context
  1550. release(context);
  1551. // Return the authenticated Principal (if any)
  1552. return (principal);
  1553. } catch (NamingException e) {
  1554. // Log the problem for posterity
  1555. containerLog.error(sm.getString("jndiRealm.exception"), e);
  1556. // Close the connection so that it gets reopened next time
  1557. if (context != null)
  1558. close(context);
  1559. // Return "not authenticated" for this request
  1560. return (null);
  1561. }
  1562. }
  1563. /**
  1564. * Return the Principal associated with the given user name.
  1565. */
  1566. protected synchronized Principal getPrincipal(DirContext context,
  1567. String username, GSSCredential gssCredential)
  1568. throws NamingException {
  1569. User user = null;
  1570. List<String> roles = null;
  1571. Hashtable<?, ?> preservedEnvironment = null;
  1572. try {
  1573. if (gssCredential != null && isUseDelegatedCredential()) {
  1574. // Preserve the current context environment parameters
  1575. preservedEnvironment = context.getEnvironment();
  1576. // Set up context
  1577. context.addToEnvironment(
  1578. Context.SECURITY_AUTHENTICATION, "GSSAPI");
  1579. context.addToEnvironment(
  1580. "javax.security.sasl.server.authentication", "true");
  1581. context.addToEnvironment(
  1582. "javax.security.sasl.qop", spnegoDelegationQop);
  1583. // Note: Subject already set in SPNEGO authenticator so no need
  1584. // for Subject.doAs() here
  1585. }
  1586. user = getUser(context, username);
  1587. if (user != null) {
  1588. roles = getRoles(context, user);
  1589. }
  1590. } finally {
  1591. restoreEnvironmentParameter(context,
  1592. Context.SECURITY_AUTHENTICATION, preservedEnvironment);
  1593. restoreEnvironmentParameter(context,
  1594. "javax.security.sasl.server.authentication", preservedEnvironment);
  1595. restoreEnvironmentParameter(context, "javax.security.sasl.qop",
  1596. preservedEnvironment);
  1597. }
  1598. if (user != null) {
  1599. return new GenericPrincipal(user.getUserName(), user.getPassword(),
  1600. roles, null, null, gssCredential);
  1601. }
  1602. return null;
  1603. }
  1604. private void restoreEnvironmentParameter(DirContext context,
  1605. String parameterName, Hashtable<?, ?> preservedEnvironment) {
  1606. try {
  1607. context.removeFromEnvironment(parameterName);
  1608. if (preservedEnvironment != null && preservedEnvironment.containsKey(parameterName)) {
  1609. context.addToEnvironment(parameterName,
  1610. preservedEnvironment.get(parameterName));
  1611. }
  1612. } catch (NamingException e) {
  1613. // Ignore
  1614. }
  1615. }
  1616. /**
  1617. * Open (if necessary) and return a connection to the configured
  1618. * directory server for this Realm.
  1619. *
  1620. * @exception NamingException if a directory server error occurs
  1621. */
  1622. protected DirContext open() throws NamingException {
  1623. // Do nothing if there is a directory server connection already open
  1624. if (context != null)
  1625. return (context);
  1626. try {
  1627. // Ensure that we have a directory context available
  1628. context = new InitialDirContext(getDirectoryContextEnvironment());
  1629. } catch (Exception e) {
  1630. connectionAttempt = 1;
  1631. // log the first exception.
  1632. containerLog.info(sm.getString("jndiRealm.exception.retry"), e);
  1633. // Try connecting to the alternate url.
  1634. context = new InitialDirContext(getDirectoryContextEnvironment());
  1635. } finally {
  1636. // reset it in case the connection times out.
  1637. // the primary may come back.
  1638. connectionAttempt = 0;
  1639. }
  1640. return (context);
  1641. }
  1642. /**
  1643. * Create our directory context configuration.
  1644. *
  1645. * @return java.util.Hashtable the configuration for the directory context.
  1646. */
  1647. protected Hashtable<String,String> getDirectoryContextEnvironment() {
  1648. Hashtable<String,String> env = new Hashtable<>();
  1649. // Configure our directory context environment.
  1650. if (containerLog.isDebugEnabled() && connectionAttempt == 0)
  1651. containerLog.debug("Connecting to URL " + connectionURL);
  1652. else if (containerLog.isDebugEnabled() && connectionAttempt > 0)
  1653. containerLog.debug("Connecting to URL " + alternateURL);
  1654. env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory);
  1655. if (connectionName != null)
  1656. env.put(Context.SECURITY_PRINCIPAL, connectionName);
  1657. if (connectionPassword != null)
  1658. env.put(Context.SECURITY_CREDENTIALS, connectionPassword);
  1659. if (connectionURL != null && connectionAttempt == 0)
  1660. env.put(Context.PROVIDER_URL, connectionURL);
  1661. else if (alternateURL != null && connectionAttempt > 0)
  1662. env.put(Context.PROVIDER_URL, alternateURL);
  1663. if (authentication != null)
  1664. env.put(Context.SECURITY_AUTHENTICATION, authentication);
  1665. if (protocol != null)
  1666. env.put(Context.SECURITY_PROTOCOL, protocol);
  1667. if (referrals != null)
  1668. env.put(Context.REFERRAL, referrals);
  1669. if (derefAliases != null)
  1670. env.put(JNDIRealm.DEREF_ALIASES, derefAliases);
  1671. if (connectionTimeout != null)
  1672. env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout);
  1673. return env;
  1674. }
  1675. /**
  1676. * Release our use of this connection so that it can be recycled.
  1677. *
  1678. * @param context The directory context to release
  1679. */
  1680. protected void release(DirContext context) {
  1681. // NO-OP since we are not pooling anything
  1682. }
  1683. // ------------------------------------------------------ Lifecycle Methods
  1684. /**
  1685. * Prepare for the beginning of active use of the public methods of this
  1686. * component and implement the requirements of
  1687. * {@link org.apache.catalina.util.LifecycleBase#startInternal()}.
  1688. *
  1689. * @exception LifecycleException if this component detects a fatal error
  1690. * that prevents this component from being used
  1691. */
  1692. @Override
  1693. protected void startInternal() throws LifecycleException {
  1694. // Check to see if the connection to the directory can be opened
  1695. try {
  1696. open();
  1697. } catch (NamingException e) {
  1698. // A failure here is not fatal as the directory may be unavailable
  1699. // now but available later. Unavailability of the directory is not
  1700. // fatal once the Realm has started so there is no reason for it to
  1701. // be fatal when the Realm starts.
  1702. containerLog.error(sm.getString("jndiRealm.open"), e);
  1703. }
  1704. super.startInternal();
  1705. }
  1706. /**
  1707. * Gracefully terminate the active use of the public methods of this
  1708. * component and implement the requirements of
  1709. * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}.
  1710. *
  1711. * @exception LifecycleException if this component detects a fatal error
  1712. * that needs to be reported
  1713. */
  1714. @Override
  1715. protected void stopInternal() throws LifecycleException {
  1716. super.stopInternal();
  1717. // Close any open directory server connection
  1718. close(this.context);
  1719. }
  1720. /**
  1721. * Given a string containing LDAP patterns for user locations (separated by
  1722. * parentheses in a pseudo-LDAP search string format -
  1723. * "(location1)(location2)", returns an array of those paths. Real LDAP
  1724. * search strings are supported as well (though only the "|" "OR" type).
  1725. *
  1726. * @param userPatternString - a string LDAP search paths surrounded by
  1727. * parentheses
  1728. */
  1729. protected String[] parseUserPatternString(String userPatternString) {
  1730. if (userPatternString != null) {
  1731. ArrayList<String> pathList = new ArrayList<>();
  1732. int startParenLoc = userPatternString.indexOf('(');
  1733. if (startParenLoc == -1) {
  1734. // no parens here; return whole thing
  1735. return new String[] {userPatternString};
  1736. }
  1737. int startingPoint = 0;
  1738. while (startParenLoc > -1) {
  1739. int endParenLoc = 0;
  1740. // weed out escaped open parens and parens enclosing the
  1741. // whole statement (in the case of valid LDAP search
  1742. // strings: (|(something)(somethingelse))
  1743. while ( (userPatternString.charAt(startParenLoc + 1) == '|') ||
  1744. (startParenLoc != 0 && userPatternString.charAt(startParenLoc - 1) == '\\') ) {
  1745. startParenLoc = userPatternString.indexOf("(", startParenLoc+1);
  1746. }
  1747. endParenLoc = userPatternString.indexOf(")", startParenLoc+1);
  1748. // weed out escaped end-parens
  1749. while (userPatternString.charAt(endParenLoc - 1) == '\\') {
  1750. endParenLoc = userPatternString.indexOf(")", endParenLoc+1);
  1751. }
  1752. String nextPathPart = userPatternString.substring
  1753. (startParenLoc+1, endParenLoc);
  1754. pathList.add(nextPathPart);
  1755. startingPoint = endParenLoc+1;
  1756. startParenLoc = userPatternString.indexOf('(', startingPoint);
  1757. }
  1758. return pathList.toArray(new String[] {});
  1759. }
  1760. return null;
  1761. }
  1762. /**
  1763. * Given an LDAP search string, returns the string with certain characters
  1764. * escaped according to RFC 2254 guidelines.
  1765. * The character mapping is as follows:
  1766. * char -> Replacement
  1767. * ---------------------------
  1768. * * -> \2a
  1769. * ( -> \28
  1770. * ) -> \29
  1771. * \ -> \5c
  1772. * \0 -> \00
  1773. * @param inString string to escape according to RFC 2254 guidelines
  1774. * @return String the escaped/encoded result
  1775. */
  1776. protected String doRFC2254Encoding(String inString) {
  1777. StringBuilder buf = new StringBuilder(inString.length());
  1778. for (int i = 0; i < inString.length(); i++) {
  1779. char c = inString.charAt(i);
  1780. switch (c) {
  1781. case '\\':
  1782. buf.append("\\5c");
  1783. break;
  1784. case '*':
  1785. buf.append("\\2a");
  1786. break;
  1787. case '(':
  1788. buf.append("\\28");
  1789. break;
  1790. case ')':
  1791. buf.append("\\29");
  1792. break;
  1793. case '\0':
  1794. buf.append("\\00");
  1795. break;
  1796. default:
  1797. buf.append(c);
  1798. break;
  1799. }
  1800. }
  1801. return buf.toString();
  1802. }
  1803. /**
  1804. * Returns the distinguished name of a search result.
  1805. *
  1806. * @param context Our DirContext
  1807. * @param base The base DN
  1808. * @param result The search result
  1809. * @return String containing the distinguished name
  1810. */
  1811. protected String getDistinguishedName(DirContext context, String base,
  1812. SearchResult result) throws NamingException {
  1813. // Get the entry's distinguished name. For relative results, this means
  1814. // we need to composite a name with the base name, the context name, and
  1815. // the result name. For non-relative names, use the returned name.
  1816. if (result.isRelative()) {
  1817. if (containerLog.isTraceEnabled()) {
  1818. containerLog.trace(" search returned relative name: " +
  1819. result.getName());
  1820. }
  1821. NameParser parser = context.getNameParser("");
  1822. Name contextName = parser.parse(context.getNameInNamespace());
  1823. Name baseName = parser.parse(base);
  1824. // Bugzilla 32269
  1825. Name entryName =
  1826. parser.parse(new CompositeName(result.getName()).get(0));
  1827. Name name = contextName.addAll(baseName);
  1828. name = name.addAll(entryName);
  1829. return name.toString();
  1830. } else {
  1831. String absoluteName = result.getName();
  1832. if (containerLog.isTraceEnabled())
  1833. containerLog.trace(" search returned absolute name: " +
  1834. result.getName());
  1835. try {
  1836. // Normalize the name by running it through the name parser.
  1837. NameParser parser = context.getNameParser("");
  1838. URI userNameUri = new URI(absoluteName);
  1839. String pathComponent = userNameUri.getPath();
  1840. // Should not ever have an empty path component, since that is /{DN}
  1841. if (pathComponent.length() < 1 ) {
  1842. throw new InvalidNameException(
  1843. "Search returned unparseable absolute name: " +
  1844. absoluteName );
  1845. }
  1846. Name name = parser.parse(pathComponent.substring(1));
  1847. return name.toString();
  1848. } catch ( URISyntaxException e ) {
  1849. throw new InvalidNameException(
  1850. "Search returned unparseable absolute name: " +
  1851. absoluteName );
  1852. }
  1853. }
  1854. }
  1855. // ------------------------------------------------------ Private Classes
  1856. /**
  1857. * A protected class representing a User
  1858. */
  1859. protected static class User {
  1860. private final String username;
  1861. private final String dn;
  1862. private final String password;
  1863. private final List<String> roles;
  1864. private final String userRoleId;
  1865. public User(String username, String dn, String password,
  1866. List<String> roles, String userRoleId) {
  1867. this.username = username;
  1868. this.dn = dn;
  1869. this.password = password;
  1870. if (roles == null) {
  1871. this.roles = Collections.emptyList();
  1872. } else {
  1873. this.roles = Collections.unmodifiableList(roles);
  1874. }
  1875. this.userRoleId = userRoleId;
  1876. }
  1877. public String getUserName() {
  1878. return username;
  1879. }
  1880. public String getDN() {
  1881. return dn;
  1882. }
  1883. public String getPassword() {
  1884. return password;
  1885. }
  1886. public List<String> getRoles() {
  1887. return roles;
  1888. }
  1889. public String getUserRoleId() {
  1890. return userRoleId;
  1891. }
  1892. }
  1893. }