123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- /*
- * 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<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, 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 +
- "}";
- }
-
- }
|