Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the choice of whether to perform certificate verification when connect to ldaps:// #86

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/hudson/security/LDAPSecurityRealm.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ public class LDAPConfiguration extends AbstractDescribableImpl<LDAPConfiguration
*/
private final String server;

/**
* whether to verify ldaps sever certificate? default is true
*/
private final boolean sslVerify;

/**
* The root DN to connect to. Normally something like "dc=sun,dc=com"
*/
Expand Down Expand Up @@ -145,9 +150,18 @@ public class LDAPConfiguration extends AbstractDescribableImpl<LDAPConfiguration
private transient LDAPExtendedTemplate ldapTemplate;
private transient String id;

@DataBoundConstructor
/**
* @deprecated retained for backwards binary compatibility.
*/
@Deprecated
public LDAPConfiguration(@NonNull String server, String rootDN, boolean inhibitInferRootDN, String managerDN, Secret managerPasswordSecret) {
this(server, true, rootDN, inhibitInferRootDN, managerDN, managerPasswordSecret);
}

@DataBoundConstructor
public LDAPConfiguration(@NonNull String server, boolean sslVerify, String rootDN, boolean inhibitInferRootDN, String managerDN, Secret managerPasswordSecret) {
this.server = server.trim();
this.sslVerify = sslVerify;
this.managerDN = fixEmpty(managerDN);
this.managerPasswordSecret = managerPasswordSecret;
this.inhibitInferRootDN = inhibitInferRootDN;
Expand All @@ -171,6 +185,13 @@ public String getServer() {
return server;
}

/**
* whether to verify ldaps sever certificate? default is true
*/
public boolean isSslVerify() {
return sslVerify;
}

public String getServerUrl() {
StringBuilder buf = new StringBuilder();
boolean first = true;
Expand Down Expand Up @@ -402,15 +423,16 @@ public String getDisplayName() {
return "ldap";
}

public FormValidation doCheckServer(@QueryParameter String value, @QueryParameter String managerDN, @QueryParameter Secret managerPasswordSecret,@QueryParameter String rootDN) {
public FormValidation doCheckServer(@QueryParameter String value, @QueryParameter boolean sslVerify, @QueryParameter String managerDN, @QueryParameter Secret managerPasswordSecret,@QueryParameter String rootDN) {
String server = value;
String managerPassword = Secret.toString(managerPasswordSecret);

if(!Jenkins.get().hasPermission(Jenkins.ADMINISTER))
return FormValidation.ok();
String url = LDAPSecurityRealm.toProviderUrl(server,rootDN);

Context ctx = null;
try {
try(SetContextClassLoader sccl = new SetContextClassLoader()) {
Hashtable<String,Object> props = new Hashtable<>();
if(StringUtils.isNotBlank(managerDN) && !"undefined".equals(managerDN)) {
props.put(Context.SECURITY_PRINCIPAL,managerDN);
Expand All @@ -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));
Expand Down Expand Up @@ -481,14 +506,18 @@ public DescriptorExtensionList<LDAPGroupMembershipStrategy, Descriptor<LDAPGroup
* @return null if not found.
*/
private String inferRootDN(String server) {
try {
Hashtable<String, String> props = new Hashtable<>();
try(SetContextClassLoader sccl = new SetContextClassLoader()) {
Hashtable<String, String> props = new Hashtable<String, String>();
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("");
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<f:textbox/>
</f:entry>
<f:advanced title="${%Advanced Server Configuration}">
<f:entry field="sslVerify">
<f:checkbox default="true" title="${%SSL Verify}"/>
</f:entry>
<f:entry field="rootDN" title="${%root DN}">
<f:textbox/>
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div>
<p>
If unchecked and ldap server is an ldaps:// style URL, Jenkins will not verify the server certificate when it connects to perform a query
</p>
<p>
If checked (the default), requiring the certificate to be verified.
</p>
</div>
4 changes: 4 additions & 0 deletions src/test/java/hudson/security/LDAPSecurityRealmTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ configurations:
- inhibitInferRootDN: false
rootDN: "dc=acme,dc=fr"
server: "ldap.acme.com"
sslVerify: false
disableMailAddressResolver: false
groupIdStrategy: "caseSensitive"
userIdStrategy: "caseInsensitive"