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.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 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.lang3.StringUtils;
  37. import org.slf4j.Logger;
  38. import org.slf4j.LoggerFactory;
  39. /**
  40. * @author Evgeny Mandrikov
  41. */
  42. public class LdapContextFactory {
  43. private static final Logger LOG = LoggerFactory.getLogger(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. private final String saslQop;
  68. private final String saslStrength;
  69. private final String saslMaxbuf;
  70. public LdapContextFactory(org.sonar.api.config.Configuration config, String settingsPrefix, String ldapUrl) {
  71. this.authentication = StringUtils.defaultString(config.get(settingsPrefix + ".authentication").orElse(null), DEFAULT_AUTHENTICATION);
  72. this.factory = StringUtils.defaultString(config.get(settingsPrefix + ".contextFactoryClass").orElse(null), DEFAULT_FACTORY);
  73. this.realm = config.get(settingsPrefix + ".realm").orElse(null);
  74. this.providerUrl = ldapUrl;
  75. this.startTLS = config.getBoolean(settingsPrefix + ".StartTLS").orElse(false);
  76. this.username = config.get(settingsPrefix + ".bindDn").orElse(null);
  77. this.password = config.get(settingsPrefix + ".bindPassword").orElse(null);
  78. this.referral = getReferralsMode(config, settingsPrefix + ".followReferrals");
  79. this.saslQop = config.get(settingsPrefix + ".saslQop").orElse(null);
  80. this.saslStrength = config.get(settingsPrefix + ".saslStrength").orElse(null);
  81. this.saslMaxbuf = config.get(settingsPrefix + ".saslMaxbuf").orElse(null);
  82. }
  83. /**
  84. * Returns {@code InitialDirContext} for Bind user.
  85. */
  86. public InitialDirContext createBindContext() throws NamingException {
  87. if (isGssapi()) {
  88. return createInitialDirContextUsingGssapi(username, password);
  89. } else {
  90. return createInitialDirContext(username, password, true);
  91. }
  92. }
  93. /**
  94. * Returns {@code InitialDirContext} for specified user.
  95. * Note that pooling intentionally disabled by this method.
  96. */
  97. public InitialDirContext createUserContext(String principal, String credentials) throws NamingException {
  98. return createInitialDirContext(principal, credentials, false);
  99. }
  100. private InitialDirContext createInitialDirContext(String principal, String credentials, boolean pooling) throws NamingException {
  101. final InitialLdapContext ctx;
  102. if (startTLS) {
  103. // Note that pooling is not enabled for such connections, because "Stop TLS" is not performed.
  104. Properties env = new Properties();
  105. env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
  106. env.put(Context.PROVIDER_URL, providerUrl);
  107. env.put(Context.REFERRAL, referral);
  108. // At this point env should not contain properties SECURITY_AUTHENTICATION, SECURITY_PRINCIPAL and SECURITY_CREDENTIALS to avoid
  109. // "bind" operation prior to StartTLS:
  110. ctx = new InitialLdapContext(env, null);
  111. // http://docs.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html
  112. StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
  113. try {
  114. tls.negotiate();
  115. } catch (IOException e) {
  116. NamingException ex = new NamingException("StartTLS failed");
  117. ex.initCause(e);
  118. throw ex;
  119. }
  120. // Explicitly initiate "bind" operation:
  121. ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, authentication);
  122. if (principal != null) {
  123. ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal);
  124. }
  125. if (credentials != null) {
  126. ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials);
  127. }
  128. ctx.reconnect(null);
  129. } else {
  130. ctx = new InitialLdapContext(getEnvironment(principal, credentials, pooling), null);
  131. }
  132. return ctx;
  133. }
  134. private InitialDirContext createInitialDirContextUsingGssapi(String principal, String credentials) throws NamingException {
  135. Configuration.setConfiguration(new Krb5LoginConfiguration());
  136. InitialDirContext initialDirContext;
  137. try {
  138. LoginContext lc = new LoginContext(getClass().getName(), new CallbackHandlerImpl(principal, credentials));
  139. lc.login();
  140. initialDirContext = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction<InitialDirContext>() {
  141. @Override
  142. public InitialDirContext run() throws NamingException {
  143. Properties env = new Properties();
  144. env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
  145. env.put(Context.PROVIDER_URL, providerUrl);
  146. env.put(Context.REFERRAL, referral);
  147. return new InitialLdapContext(env, null);
  148. }
  149. });
  150. } catch (LoginException | PrivilegedActionException e) {
  151. NamingException namingException = new NamingException(e.getMessage());
  152. namingException.initCause(e);
  153. throw namingException;
  154. }
  155. return initialDirContext;
  156. }
  157. private Properties getEnvironment(@Nullable String principal, @Nullable String credentials, boolean pooling) {
  158. Properties env = new Properties();
  159. env.put(Context.SECURITY_AUTHENTICATION, authentication);
  160. if (realm != null) {
  161. env.put(SASL_REALM_PROPERTY, realm);
  162. }
  163. if (pooling) {
  164. // Enable connection pooling
  165. env.put(SUN_CONNECTION_POOLING_PROPERTY, "true");
  166. }
  167. env.put(Context.INITIAL_CONTEXT_FACTORY, factory);
  168. env.put(Context.PROVIDER_URL, providerUrl);
  169. env.put(Context.REFERRAL, referral);
  170. if (principal != null) {
  171. env.put(Context.SECURITY_PRINCIPAL, principal);
  172. }
  173. if (saslQop != null) {
  174. env.put("javax.security.sasl.qop", saslQop);
  175. }
  176. if (saslStrength != null) {
  177. env.put("javax.security.sasl.strength", saslStrength);
  178. }
  179. if (saslMaxbuf != null) {
  180. env.put("javax.security.sasl.maxbuf", saslMaxbuf);
  181. }
  182. // Note: debug is intentionally was placed here - in order to not expose password in log
  183. LOG.debug("Initializing LDAP context {}", env);
  184. if (credentials != null) {
  185. env.put(Context.SECURITY_CREDENTIALS, credentials);
  186. }
  187. return env;
  188. }
  189. public boolean isSasl() {
  190. return AUTH_METHOD_DIGEST_MD5.equals(authentication) ||
  191. AUTH_METHOD_CRAM_MD5.equals(authentication) ||
  192. AUTH_METHOD_GSSAPI.equals(authentication);
  193. }
  194. public boolean isGssapi() {
  195. return AUTH_METHOD_GSSAPI.equals(authentication);
  196. }
  197. /**
  198. * Tests connection.
  199. *
  200. * @throws LdapException if unable to open connection
  201. */
  202. public void testConnection() {
  203. if (StringUtils.isBlank(username) && isSasl()) {
  204. throw new IllegalArgumentException("When using SASL - property ldap.bindDn is required");
  205. }
  206. try {
  207. createBindContext();
  208. LOG.info("Test LDAP connection on {}: OK", providerUrl);
  209. } catch (NamingException e) {
  210. LOG.info("Test LDAP connection: FAIL");
  211. throw new LdapException("Unable to open LDAP connection", e);
  212. }
  213. }
  214. public String getProviderUrl() {
  215. return providerUrl;
  216. }
  217. public String getReferral() {
  218. return referral;
  219. }
  220. private static String getReferralsMode(org.sonar.api.config.Configuration config, String followReferralsSettingKey) {
  221. // By default follow referrals
  222. return config.getBoolean(followReferralsSettingKey).orElse(true) ? REFERRALS_FOLLOW_MODE : REFERRALS_IGNORE_MODE;
  223. }
  224. @Override
  225. public String toString() {
  226. return getClass().getSimpleName() + "{" +
  227. "url=" + providerUrl +
  228. ", authentication=" + authentication +
  229. ", factory=" + factory +
  230. ", bindDn=" + username +
  231. ", realm=" + realm +
  232. ", referral=" + referral +
  233. "}";
  234. }
  235. }