You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LdapContextFactory.java 9.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2020 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.auth.ldap;
  21. import java.io.IOException;
  22. import java.security.PrivilegedActionException;
  23. import java.security.PrivilegedExceptionAction;
  24. import java.util.Properties;
  25. import javax.annotation.Nullable;
  26. import javax.naming.Context;
  27. import javax.naming.NamingException;
  28. import javax.naming.directory.InitialDirContext;
  29. import javax.naming.ldap.InitialLdapContext;
  30. import javax.naming.ldap.StartTlsRequest;
  31. import javax.naming.ldap.StartTlsResponse;
  32. import javax.security.auth.Subject;
  33. import javax.security.auth.login.Configuration;
  34. import javax.security.auth.login.LoginContext;
  35. import javax.security.auth.login.LoginException;
  36. import org.apache.commons.lang.StringUtils;
  37. import org.sonar.api.utils.log.Logger;
  38. import org.sonar.api.utils.log.Loggers;
  39. /**
  40. * @author Evgeny Mandrikov
  41. */
  42. public class LdapContextFactory {
  43. private static final Logger LOG = Loggers.get(LdapContextFactory.class);
  44. // visible for testing
  45. static final String AUTH_METHOD_SIMPLE = "simple";
  46. static final String AUTH_METHOD_GSSAPI = "GSSAPI";
  47. static final String AUTH_METHOD_DIGEST_MD5 = "DIGEST-MD5";
  48. static final String AUTH_METHOD_CRAM_MD5 = "CRAM-MD5";
  49. private static final String REFERRALS_FOLLOW_MODE = "follow";
  50. private static final String REFERRALS_IGNORE_MODE = "ignore";
  51. private static final String DEFAULT_AUTHENTICATION = AUTH_METHOD_SIMPLE;
  52. private static final String DEFAULT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
  53. /**
  54. * The Sun LDAP property used to enable connection pooling. This is used in the default implementation to enable
  55. * LDAP connection pooling.
  56. */
  57. private static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool";
  58. private static final String SASL_REALM_PROPERTY = "java.naming.security.sasl.realm";
  59. private final String providerUrl;
  60. private final boolean startTLS;
  61. private final String authentication;
  62. private final String factory;
  63. private final String username;
  64. private final String password;
  65. private final String realm;
  66. private final String referral;
  67. public LdapContextFactory(org.sonar.api.config.Configuration config, String settingsPrefix, String ldapUrl) {
  68. this.authentication = StringUtils.defaultString(config.get(settingsPrefix + ".authentication").orElse(null), DEFAULT_AUTHENTICATION);
  69. this.factory = StringUtils.defaultString(config.get(settingsPrefix + ".contextFactoryClass").orElse(null), DEFAULT_FACTORY);
  70. this.realm = config.get(settingsPrefix + ".realm").orElse(null);
  71. this.providerUrl = ldapUrl;
  72. this.startTLS = config.getBoolean(settingsPrefix + ".StartTLS").orElse(false);
  73. this.username = config.get(settingsPrefix + ".bindDn").orElse(null);
  74. this.password = config.get(settingsPrefix + ".bindPassword").orElse(null);
  75. this.referral = getReferralsMode(config, settingsPrefix + ".followReferrals");
  76. }
  77. /**
  78. * Returns {@code InitialDirContext} for Bind user.
  79. */
  80. public InitialDirContext createBindContext() throws NamingException {
  81. if (isGssapi()) {
  82. return createInitialDirContextUsingGssapi(username, password);
  83. } else {
  84. return createInitialDirContext(username, password, true);
  85. }
  86. }
  87. /**
  88. * Returns {@code InitialDirContext} for specified user.
  89. * Note that pooling intentionally disabled by this method.
  90. */
  91. public InitialDirContext createUserContext(String principal, String credentials) throws NamingException {
  92. return createInitialDirContext(principal, credentials, false);
  93. }
  94. private InitialDirContext createInitialDirContext(String principal, String credentials, boolean pooling) throws NamingException {
  95. final InitialLdapContext ctx;
  96. if (startTLS) {
  97. // Note that pooling is not enabled for such connections, because "Stop TLS" is not performed.
  98. Properties env = new Properties();
  99. env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
  100. env.put(Context.PROVIDER_URL, providerUrl);
  101. env.put(Context.REFERRAL, referral);
  102. // At this point env should not contain properties SECURITY_AUTHENTICATION, SECURITY_PRINCIPAL and SECURITY_CREDENTIALS to avoid
  103. // "bind" operation prior to StartTLS:
  104. ctx = new InitialLdapContext(env, null);
  105. // http://docs.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html
  106. StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
  107. try {
  108. tls.negotiate();
  109. } catch (IOException e) {
  110. NamingException ex = new NamingException("StartTLS failed");
  111. ex.initCause(e);
  112. throw ex;
  113. }
  114. // Explicitly initiate "bind" operation:
  115. ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, authentication);
  116. if (principal != null) {
  117. ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal);
  118. }
  119. if (credentials != null) {
  120. ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
  121. }
  122. ctx.reconnect(null);
  123. } else {
  124. ctx = new InitialLdapContext(getEnvironment(principal, credentials, pooling), null);
  125. }
  126. return ctx;
  127. }
  128. private InitialDirContext createInitialDirContextUsingGssapi(String principal, String credentials) throws NamingException {
  129. Configuration.setConfiguration(new Krb5LoginConfiguration());
  130. InitialDirContext initialDirContext;
  131. try {
  132. LoginContext lc = new LoginContext(getClass().getName(), new CallbackHandlerImpl(principal, credentials));
  133. lc.login();
  134. initialDirContext = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<InitialDirContext>() {
  135. @Override
  136. public InitialDirContext run() throws NamingException {
  137. Properties env = new Properties();
  138. env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
  139. env.put(Context.PROVIDER_URL, providerUrl);
  140. env.put(Context.REFERRAL, referral);
  141. return new InitialLdapContext(env, null);
  142. }
  143. });
  144. } catch (LoginException | PrivilegedActionException e) {
  145. NamingException namingException = new NamingException(e.getMessage());
  146. namingException.initCause(e);
  147. throw namingException;
  148. }
  149. return initialDirContext;
  150. }
  151. private Properties getEnvironment(@Nullable String principal, @Nullable String credentials, boolean pooling) {
  152. Properties env = new Properties();
  153. env.put(Context.SECURITY_AUTHENTICATION, authentication);
  154. if (realm != null) {
  155. env.put(SASL_REALM_PROPERTY, realm);
  156. }
  157. if (pooling) {
  158. // Enable connection pooling
  159. env.put(SUN_CONNECTION_POOLING_PROPERTY, "true");
  160. }
  161. env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
  162. env.put(Context.PROVIDER_URL, providerUrl);
  163. env.put(Context.REFERRAL, referral);
  164. if (principal != null) {
  165. env.put(Context.SECURITY_PRINCIPAL, principal);
  166. }
  167. // Note: debug is intentionally was placed here - in order to not expose password in log
  168. LOG.debug("Initializing LDAP context {}", env);
  169. if (credentials != null) {
  170. env.put(Context.SECURITY_CREDENTIALS, credentials);
  171. }
  172. return env;
  173. }
  174. public boolean isSasl() {
  175. return AUTH_METHOD_DIGEST_MD5.equals(authentication) ||
  176. AUTH_METHOD_CRAM_MD5.equals(authentication) ||
  177. AUTH_METHOD_GSSAPI.equals(authentication);
  178. }
  179. public boolean isGssapi() {
  180. return AUTH_METHOD_GSSAPI.equals(authentication);
  181. }
  182. /**
  183. * Tests connection.
  184. *
  185. * @throws LdapException if unable to open connection
  186. */
  187. public void testConnection() {
  188. if (StringUtils.isBlank(username) && isSasl()) {
  189. throw new IllegalArgumentException("When using SASL - property ldap.bindDn is required");
  190. }
  191. try {
  192. createBindContext();
  193. LOG.info("Test LDAP connection on {}: OK", providerUrl);
  194. } catch (NamingException e) {
  195. LOG.info("Test LDAP connection: FAIL");
  196. throw new LdapException("Unable to open LDAP connection", e);
  197. }
  198. }
  199. public String getProviderUrl() {
  200. return providerUrl;
  201. }
  202. public String getReferral() {
  203. return referral;
  204. }
  205. private static String getReferralsMode(org.sonar.api.config.Configuration config, String followReferralsSettingKey) {
  206. // By default follow referrals
  207. return config.getBoolean(followReferralsSettingKey).orElse(true) ? REFERRALS_FOLLOW_MODE : REFERRALS_IGNORE_MODE;
  208. }
  209. @Override
  210. public String toString() {
  211. return getClass().getSimpleName() + "{" +
  212. "url=" + providerUrl +
  213. ", authentication=" + authentication +
  214. ", factory=" + factory +
  215. ", bindDn=" + username +
  216. ", realm=" + realm +
  217. ", referral=" + referral +
  218. "}";
  219. }
  220. }