/* * SonarQube * Copyright (C) 2009-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.auth.ldap; import java.io.IOException; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Properties; import javax.annotation.Nullable; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.InitialDirContext; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsResponse; import javax.security.auth.Subject; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Evgeny Mandrikov */ public class LdapContextFactory { private static final Logger LOG = LoggerFactory.getLogger(LdapContextFactory.class); // visible for testing static final String AUTH_METHOD_SIMPLE = "simple"; static final String AUTH_METHOD_GSSAPI = "GSSAPI"; static final String AUTH_METHOD_DIGEST_MD5 = "DIGEST-MD5"; static final String AUTH_METHOD_CRAM_MD5 = "CRAM-MD5"; private static final String REFERRALS_FOLLOW_MODE = "follow"; private static final String REFERRALS_IGNORE_MODE = "ignore"; private static final String DEFAULT_AUTHENTICATION = AUTH_METHOD_SIMPLE; private static final String DEFAULT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; /** * The Sun LDAP property used to enable connection pooling. This is used in the default implementation to enable * LDAP connection pooling. */ private static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool"; private static final String SASL_REALM_PROPERTY = "java.naming.security.sasl.realm"; private final String providerUrl; private final boolean startTLS; private final String authentication; private final String factory; private final String username; private final String password; private final String realm; private final String referral; private final String saslQop; private final String saslStrength; private final String saslMaxbuf; public LdapContextFactory(org.sonar.api.config.Configuration config, String settingsPrefix, String ldapUrl) { this.authentication = StringUtils.defaultString(config.get(settingsPrefix + ".authentication").orElse(null), DEFAULT_AUTHENTICATION); this.factory = StringUtils.defaultString(config.get(settingsPrefix + ".contextFactoryClass").orElse(null), DEFAULT_FACTORY); this.realm = config.get(settingsPrefix + ".realm").orElse(null); this.providerUrl = ldapUrl; this.startTLS = config.getBoolean(settingsPrefix + ".StartTLS").orElse(false); this.username = config.get(settingsPrefix + ".bindDn").orElse(null); this.password = config.get(settingsPrefix + ".bindPassword").orElse(null); this.referral = getReferralsMode(config, settingsPrefix + ".followReferrals"); this.saslQop = config.get(settingsPrefix + ".saslQop").orElse(null); this.saslStrength = config.get(settingsPrefix + ".saslStrength").orElse(null); this.saslMaxbuf = config.get(settingsPrefix + ".saslMaxbuf").orElse(null); } /** * Returns {@code InitialDirContext} for Bind user. */ public InitialDirContext createBindContext() throws NamingException { if (isGssapi()) { return createInitialDirContextUsingGssapi(username, password); } else { return createInitialDirContext(username, password, true); } } /** * Returns {@code InitialDirContext} for specified user. * Note that pooling intentionally disabled by this method. */ public InitialDirContext createUserContext(String principal, String credentials) throws NamingException { return createInitialDirContext(principal, credentials, false); } private InitialDirContext createInitialDirContext(String principal, String credentials, boolean pooling) throws NamingException { final InitialLdapContext ctx; if (startTLS) { // Note that pooling is not enabled for such connections, because "Stop TLS" is not performed. Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY, factory); env.put(Context.PROVIDER_URL, providerUrl); env.put(Context.REFERRAL, referral); // At this point env should not contain properties SECURITY_AUTHENTICATION, SECURITY_PRINCIPAL and SECURITY_CREDENTIALS to avoid // "bind" operation prior to StartTLS: ctx = new InitialLdapContext(env, null); // http://docs.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest()); try { tls.negotiate(); } catch (IOException e) { NamingException ex = new NamingException("StartTLS failed"); ex.initCause(e); throw ex; } // Explicitly initiate "bind" operation: ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, authentication); if (principal != null) { ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal); } if (credentials != null) { ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); } ctx.reconnect(null); } else { ctx = new InitialLdapContext(getEnvironment(principal, credentials, pooling), null); } return ctx; } private InitialDirContext createInitialDirContextUsingGssapi(String principal, String credentials) throws NamingException { Configuration.setConfiguration(new Krb5LoginConfiguration()); InitialDirContext initialDirContext; try { LoginContext lc = new LoginContext(getClass().getName(), new CallbackHandlerImpl(principal, credentials)); lc.login(); initialDirContext = Subject.doAs(lc.getSubject(), new PrivilegedExceptionAction() { @Override public InitialDirContext run() throws NamingException { Properties env = new Properties(); env.put(Context.INITIAL_CONTEXT_FACTORY, factory); env.put(Context.PROVIDER_URL, providerUrl); env.put(Context.REFERRAL, referral); return new InitialLdapContext(env, null); } }); } catch (LoginException | PrivilegedActionException e) { NamingException namingException = new NamingException(e.getMessage()); namingException.initCause(e); throw namingException; } return initialDirContext; } private Properties getEnvironment(@Nullable String principal, @Nullable String credentials, boolean pooling) { Properties env = new Properties(); env.put(Context.SECURITY_AUTHENTICATION, authentication); if (realm != null) { env.put(SASL_REALM_PROPERTY, realm); } if (pooling) { // Enable connection pooling env.put(SUN_CONNECTION_POOLING_PROPERTY, "true"); } env.put(Context.INITIAL_CONTEXT_FACTORY, factory); env.put(Context.PROVIDER_URL, providerUrl); env.put(Context.REFERRAL, referral); if (principal != null) { env.put(Context.SECURITY_PRINCIPAL, principal); } if (saslQop != null) { env.put("javax.security.sasl.qop", saslQop); } if (saslStrength != null) { env.put("javax.security.sasl.strength", saslStrength); } if (saslMaxbuf != null) { env.put("javax.security.sasl.maxbuf", saslMaxbuf); } // Note: debug is intentionally was placed here - in order to not expose password in log LOG.debug("Initializing LDAP context {}", env); if (credentials != null) { env.put(Context.SECURITY_CREDENTIALS, credentials); } return env; } public boolean isSasl() { return AUTH_METHOD_DIGEST_MD5.equals(authentication) || AUTH_METHOD_CRAM_MD5.equals(authentication) || AUTH_METHOD_GSSAPI.equals(authentication); } public boolean isGssapi() { return AUTH_METHOD_GSSAPI.equals(authentication); } /** * Tests connection. * * @throws LdapException if unable to open connection */ public void testConnection() { if (StringUtils.isBlank(username) && isSasl()) { throw new IllegalArgumentException("When using SASL - property ldap.bindDn is required"); } try { createBindContext(); LOG.info("Test LDAP connection on {}: OK", providerUrl); } catch (NamingException e) { LOG.info("Test LDAP connection: FAIL"); throw new LdapException("Unable to open LDAP connection", e); } } public String getProviderUrl() { return providerUrl; } public String getReferral() { return referral; } private static String getReferralsMode(org.sonar.api.config.Configuration config, String followReferralsSettingKey) { // By default follow referrals return config.getBoolean(followReferralsSettingKey).orElse(true) ? REFERRALS_FOLLOW_MODE : REFERRALS_IGNORE_MODE; } @Override public String toString() { return getClass().getSimpleName() + "{" + "url=" + providerUrl + ", authentication=" + authentication + ", factory=" + factory + ", bindDn=" + username + ", realm=" + realm + ", referral=" + referral + "}"; } }