Java tutorial
/* * SonarQube LDAP Plugin * Copyright (C) 2009-2016 SonarSource SA * mailto:contact 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.plugins.ldap; import javax.annotation.Nullable; import java.io.IOException; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.directory.InitialDirContext; import javax.naming.ldap.InitialLdapContext; import javax.security.auth.Subject; import javax.security.auth.login.Configuration; import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Properties; import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsResponse; import org.apache.commons.lang.StringUtils; import org.sonar.api.config.Settings; import org.sonar.api.utils.log.Logger; import org.sonar.api.utils.log.Loggers; /** * @author Evgeny Mandrikov */ public class LdapContextFactory { private static final Logger LOG = Loggers.get(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 DEFAULT_AUTHENTICATION = AUTH_METHOD_SIMPLE; private static final String DEFAULT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; private static final String DEFAULT_REFERRAL = "follow"; /** * 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; public LdapContextFactory(Settings settings, String settingsPrefix, String ldapUrl) { this.authentication = StringUtils.defaultString(settings.getString(settingsPrefix + ".authentication"), DEFAULT_AUTHENTICATION); this.factory = StringUtils.defaultString(settings.getString(settingsPrefix + ".contextFactoryClass"), DEFAULT_FACTORY); this.realm = settings.getString(settingsPrefix + ".realm"); this.providerUrl = ldapUrl; this.startTLS = settings.getBoolean(settingsPrefix + ".StartTLS"); this.username = settings.getString(settingsPrefix + ".bindDn"); this.password = settings.getString(settingsPrefix + ".bindPassword"); } /** * 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, DEFAULT_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); ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, principal); 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<InitialDirContext>() { @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, DEFAULT_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, DEFAULT_REFERRAL); if (principal != null) { env.put(Context.SECURITY_PRINCIPAL, principal); } // 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; } @Override public String toString() { return getClass().getSimpleName() + "{" + "url=" + providerUrl + ", authentication=" + authentication + ", factory=" + factory + ", bindDn=" + username + ", realm=" + realm + "}"; } }