diff --git a/src/main/java/hudson/security/LDAPSecurityRealm.java b/src/main/java/hudson/security/LDAPSecurityRealm.java index c64031b4..b4fd348a 100644 --- a/src/main/java/hudson/security/LDAPSecurityRealm.java +++ b/src/main/java/hudson/security/LDAPSecurityRealm.java @@ -1592,7 +1592,7 @@ public FormValidation validate(LDAPSecurityRealm realm, String user, String pass // we can only do deep validation if the connection is correct LDAPConfiguration.LDAPConfigurationDescriptor confDescriptor = Jenkins.get().getDescriptorByType(LDAPConfiguration.LDAPConfigurationDescriptor.class); for (LDAPConfiguration configuration : realm.getConfigurations()) { - FormValidation connectionCheck = confDescriptor.doCheckServer(configuration.getServerUrl(), configuration.getManagerDN(), configuration.getManagerPasswordSecret(),configuration.getRootDN()); + FormValidation connectionCheck = confDescriptor.doCheckServer(configuration.getServerUrl(), configuration.isSslVerify(),configuration.getManagerDN(), configuration.getManagerPasswordSecret(),configuration.getRootDN()); if (connectionCheck.kind != FormValidation.Kind.OK) { return connectionCheck; } diff --git a/src/main/java/jenkins/security/plugins/ldap/BlindSSLSocketFactory.java b/src/main/java/jenkins/security/plugins/ldap/BlindSSLSocketFactory.java new file mode 100644 index 00000000..d5ea37c4 --- /dev/null +++ b/src/main/java/jenkins/security/plugins/ldap/BlindSSLSocketFactory.java @@ -0,0 +1,96 @@ +package jenkins.security.plugins.ldap; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +public class BlindSSLSocketFactory extends SSLSocketFactory { + private static final BlindSSLSocketFactory INSTANCE; + + static { + final X509TrustManager dummyTrustManager = + new X509TrustManager() { + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) {} + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) {} + }; + try { + final SSLContext context = SSLContext.getInstance("SSL"); + final TrustManager[] trustManagers = {dummyTrustManager}; + final SecureRandom rng = new SecureRandom(); + context.init(null, trustManagers, rng); + INSTANCE = new BlindSSLSocketFactory(context.getSocketFactory()); + } catch (GeneralSecurityException e) { + throw new RuntimeException("Cannot create BlindSslSocketFactory", e); + } + } + + public static SocketFactory getDefault() { + return INSTANCE; + } + + private final SSLSocketFactory sslFactory; + + private BlindSSLSocketFactory(SSLSocketFactory sslFactory) { + this.sslFactory = sslFactory; + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return sslFactory.createSocket(s, host, port, autoClose); + } + + @Override + public String[] getDefaultCipherSuites() { + return sslFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return sslFactory.createSocket(); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return sslFactory.createSocket(host, port); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return sslFactory.createSocket(host, port); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + return sslFactory.createSocket(host, port, localHost, localPort); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + return sslFactory.createSocket(address, port, localAddress, localPort); + } +} diff --git a/src/main/java/jenkins/security/plugins/ldap/LDAPConfiguration.java b/src/main/java/jenkins/security/plugins/ldap/LDAPConfiguration.java index 9a7e6ab4..e4e0fcf9 100644 --- a/src/main/java/jenkins/security/plugins/ldap/LDAPConfiguration.java +++ b/src/main/java/jenkins/security/plugins/ldap/LDAPConfiguration.java @@ -105,6 +105,11 @@ public class LDAPConfiguration extends AbstractDescribableImpl props = new Hashtable<>(); if(StringUtils.isNotBlank(managerDN) && !"undefined".equals(managerDN)) { props.put(Context.SECURITY_PRINCIPAL,managerDN); @@ -420,7 +442,10 @@ public FormValidation doCheckServer(@QueryParameter String value, @QueryParamete } // normal props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - props.put(Context.PROVIDER_URL, LDAPSecurityRealm.toProviderUrl(server,rootDN)); + props.put(Context.PROVIDER_URL, url); + if(url.startsWith("ldaps:") && !sslVerify) { + props.put("java.naming.ldap.factory.socket", BlindSSLSocketFactory.class.getName()); + } props.put("java.naming.referral", "follow"); props.put("com.sun.jndi.ldap.connect.timeout", Integer.toString(CONNECT_TIMEOUT)); @@ -481,14 +506,18 @@ public DescriptorExtensionList props = new Hashtable<>(); + try(SetContextClassLoader sccl = new SetContextClassLoader()) { + Hashtable props = new Hashtable(); + String url = LDAPSecurityRealm.toProviderUrl(getServerUrl(), ""); if (managerDN != null) { props.put(Context.SECURITY_PRINCIPAL, managerDN); props.put(Context.SECURITY_CREDENTIALS, getManagerPassword()); } props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - props.put(Context.PROVIDER_URL, LDAPSecurityRealm.toProviderUrl(getServerUrl(), "")); + props.put(Context.PROVIDER_URL, url); + if(url.startsWith("ldaps:") && !sslVerify) { + props.put("java.naming.ldap.factory.socket", BlindSSLSocketFactory.class.getName()); + } DirContext ctx = new InitialDirContext(props); Attributes atts = ctx.getAttributes(""); @@ -606,6 +635,9 @@ public ApplicationContext createApplicationContext(LDAPSecurityRealm realm) { vars.put("com.sun.jndi.ldap.connect.pool", "true"); vars.put("com.sun.jndi.ldap.connect.timeout", Integer.toString(CONNECT_TIMEOUT)); // timeout if no connection after 30 seconds vars.put("com.sun.jndi.ldap.read.timeout", Integer.toString(READ_TIMEOUT)); // timeout if no response after 60 seconds + if(getLDAPURL().startsWith("ldaps:") && !sslVerify) { + vars.put("java.naming.ldap.factory.socket", BlindSSLSocketFactory.class.getName()); + } vars.putAll(getExtraEnvVars()); contextSource.setBaseEnvironmentProperties(vars); contextSource.afterPropertiesSet(); diff --git a/src/main/resources/jenkins/security/plugins/ldap/LDAPConfiguration/config.jelly b/src/main/resources/jenkins/security/plugins/ldap/LDAPConfiguration/config.jelly index 3ee96c6a..74628094 100644 --- a/src/main/resources/jenkins/security/plugins/ldap/LDAPConfiguration/config.jelly +++ b/src/main/resources/jenkins/security/plugins/ldap/LDAPConfiguration/config.jelly @@ -4,6 +4,9 @@ + + + diff --git a/src/main/resources/jenkins/security/plugins/ldap/LDAPConfiguration/help-sslVerify.html b/src/main/resources/jenkins/security/plugins/ldap/LDAPConfiguration/help-sslVerify.html new file mode 100644 index 00000000..d5f6a841 --- /dev/null +++ b/src/main/resources/jenkins/security/plugins/ldap/LDAPConfiguration/help-sslVerify.html @@ -0,0 +1,8 @@ +
+

+ If unchecked and ldap server is an ldaps:// style URL, Jenkins will not verify the server certificate when it connects to perform a query +

+

+ If checked (the default), requiring the certificate to be verified. +

+
\ No newline at end of file diff --git a/src/test/java/hudson/security/LDAPSecurityRealmTest.java b/src/test/java/hudson/security/LDAPSecurityRealmTest.java index 846e0648..5ed5ecef 100644 --- a/src/test/java/hudson/security/LDAPSecurityRealmTest.java +++ b/src/test/java/hudson/security/LDAPSecurityRealmTest.java @@ -100,6 +100,7 @@ private void check() { LDAPSecurityRealm sr = (LDAPSecurityRealm) r.jenkins.getSecurityRealm(); LDAPConfiguration cnf = sr.getConfigurations().get(0); assertEquals("s", cnf.getServer()); + assertTrue(cnf.isSslVerify()); assertEquals("rDN=x", cnf.getRootDN()); assertEquals("uSB", cnf.getUserSearchBase()); assertEquals("uS", cnf.getUserSearch()); @@ -217,6 +218,7 @@ public void configRoundTrip() throws Exception { assertNotSame(realm, newRealm); LDAPConfiguration config = newRealm.getConfigurations().get(0); assertEquals(server, config.getServer()); + assertTrue(config.isSslVerify()); assertEquals(rootDN, config.getRootDN()); assertEquals(userSearchBase, config.getUserSearchBase()); assertEquals(managerDN, config.getManagerDN()); @@ -273,6 +275,7 @@ public void configRoundTripTwo() throws Exception { LDAPConfiguration config = configurations.get(i); TestConf conf = confs[i]; assertEquals(conf.server, config.getServer()); + assertTrue(config.isSslVerify()); assertEquals(conf.rootDN, config.getRootDN()); assertEquals(conf.userSearchBase, config.getUserSearchBase()); assertEquals(conf.managerDN, config.getManagerDN()); @@ -404,6 +407,7 @@ public void configRoundTripEnvironmentProperties() throws Exception { assertNotSame(realm, newRealm); LDAPConfiguration newConfig = newRealm.getConfigurations().get(0); assertEquals(server, newConfig.getServer()); + assertTrue(newConfig.isSslVerify()); assertEquals(rootDN, newConfig.getRootDN()); assertEquals(userSearchBase, newConfig.getUserSearchBase()); assertEquals(managerDN, newConfig.getManagerDN()); diff --git a/src/test/resources/jenkins/security/plugins/ldap/LDAPSecurityRealmTestNoSecretExpected.yml b/src/test/resources/jenkins/security/plugins/ldap/LDAPSecurityRealmTestNoSecretExpected.yml index 888cf513..e89cf389 100644 --- a/src/test/resources/jenkins/security/plugins/ldap/LDAPSecurityRealmTestNoSecretExpected.yml +++ b/src/test/resources/jenkins/security/plugins/ldap/LDAPSecurityRealmTestNoSecretExpected.yml @@ -5,6 +5,7 @@ configurations: - inhibitInferRootDN: false rootDN: "dc=acme,dc=fr" server: "ldap.acme.com" + sslVerify: false disableMailAddressResolver: false groupIdStrategy: "caseSensitive" userIdStrategy: "caseInsensitive"