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

[NEEDS REVIEW] Fixes JENKINS-14520 - LDAP StartTLS support #97

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -54,13 +54,20 @@
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
import org.springframework.security.ldap.search.LdapUserSearch;
import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.StartTlsRequest;
import javax.naming.ldap.StartTlsResponse;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;

import java.io.IOException;
import java.net.InetAddress;
Expand Down Expand Up @@ -405,18 +412,37 @@ public FormValidation doCheckServer(@QueryParameter String value, @QueryParamete

try {
Hashtable<String,String> props = new Hashtable<String,String>();
if(managerDN!=null && managerDN.trim().length() > 0 && !"undefined".equals(managerDN)) {
props.put(Context.SECURITY_PRINCIPAL,managerDN);
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
props.put(Context.PROVIDER_URL, LDAPSecurityRealm.toProviderUrl(server,rootDN));

LdapContext ctx = new InitialLdapContext(props, null);
StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
SSLSession session = tls.negotiate();

if (!session.isValid()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this not break for anyone using non SSL LDAP? (in other words this should be available for users to opt into, or opt out of).
For the AD plugin we also need to close the StartTlsResponse https://github.com/jenkinsci/active-directory-plugin/blob/19e9fbcf0518537c6c4bd24fb1fd09901e7fd60e/src/main/java/hudson/plugins/active_directory/ActiveDirectorySecurityRealm.java#L627-L644

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree definitely using startTLS must be optional and not per default.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed, this definitely breaks non-STARTTLS implementations. I commented below with some additional context for this PR.

throw new IOException("Couldn't negotiate StartTls session, session is invalid");
}
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed, it would appear to be an unrelated change and would preclude the use of any stronger sasl algorithms?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

                ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");

is copy pasted from https://docs.oracle.com/javase/jndi/tutorial/ldap/ext/starttls.html#TLS%20with%20Simple%20Authentication

I did see the External SASL Authentication section in that link, however I wasn't familiar enough with SASL or the Jenkins UI to add additional form controls.

if (managerDN != null && managerDN.trim().length() > 0 && !"undefined".equals(managerDN)) {
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, managerDN);
props.put(Context.SECURITY_PRINCIPAL, managerDN);
}
if(managerPassword!=null && managerPassword.trim().length() > 0 && !"undefined".equals(managerPassword)) {
props.put(Context.SECURITY_CREDENTIALS,managerPassword);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, managerPassword);
props.put(Context.SECURITY_CREDENTIALS, managerPassword);
}
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
props.put(Context.PROVIDER_URL, LDAPSecurityRealm.toProviderUrl(server,rootDN));

DirContext ctx = new InitialDirContext(props);

ctx.getAttributes("");

// Stop TLS
tls.close();
// Close the context when we're done
ctx.close();

return FormValidation.ok(); // connected
} catch (IOException ioe) {
return FormValidation.error(Messages.LDAPSecurityRealm_UnableToConnect(server, ioe.getMessage()));
} catch (NamingException e) {
// trouble-shoot
Matcher m = Pattern.compile("(ldaps?://)?([^:]+)(?:\\:(\\d+))?(\\s+(ldaps?://)?([^:]+)(?:\\:(\\d+))?)*").matcher(server.trim());
Expand Down Expand Up @@ -458,26 +484,38 @@ public DescriptorExtensionList<LDAPGroupMembershipStrategy, Descriptor<LDAPGroup
private String inferRootDN(String server) {
try {
Hashtable<String, String> props = new Hashtable<String, String>();
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
props.put(Context.PROVIDER_URL, LDAPSecurityRealm.toProviderUrl(getServerUrl(), ""));

LdapContext ctx = new InitialLdapContext(props, null);
StartTlsResponse tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
SSLSession session = tls.negotiate();
if (session.isValid()) {
LOGGER.log(Level.INFO, "session is valid!s");
}
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");

if (managerDN != null) {
props.put(Context.SECURITY_PRINCIPAL, managerDN);
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, managerDN);
props.put(Context.SECURITY_CREDENTIALS, getManagerPassword());
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, getManagerPassword());
}
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
props.put(Context.PROVIDER_URL, LDAPSecurityRealm.toProviderUrl(getServerUrl(), ""));

DirContext ctx = new InitialDirContext(props);
Attributes atts = ctx.getAttributes("");
Attribute a = atts.get("defaultNamingContext");
if (a != null && a.get() != null) // this entry is available on Active Directory. See http://msdn2.microsoft.com/en-us/library/ms684291(VS.85).aspx
return a.get().toString();

a = atts.get("namingcontexts");
tls.close();
ctx.close();
if (a == null) {
LOGGER.warning("namingcontexts attribute not found in root DSE of " + server);
return null;
}
return a.get().toString();
} catch (NamingException e) {
} catch (IOException|NamingException e) {
LOGGER.log(Level.WARNING, "Failed to connect to LDAP to infer Root DN for " + server, e);
return null;
}
Expand Down Expand Up @@ -577,6 +615,10 @@ public ApplicationContext createApplicationContext(LDAPSecurityRealm realm) {
vars.put("com.sun.jndi.ldap.read.timeout", "60000"); // timeout if no response after 60 seconds
vars.putAll(getExtraEnvVars());
contextSource.setBaseEnvironmentProperties(vars);

// pooled connections do not work with StartTLS
contextSource.setPooled(false);
Comment on lines +619 to +620
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting. so you mean com.sun.jndi.ldap.connect.pool doesn't work anymore? it creates connectivity issues or just ignored? If so there are few usage this property in the code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, this one took me a while to figure out, but every example i was able to find on the internet related to StartTLS and Spring had setPooled(false)

Explicitly disable connection pooling, or Spring may attempt to StartTLS twice on the same connection.

Also there seems to be some known issues as documented in the spring-ldap docs: https://docs.spring.io/spring-ldap/docs/1.3.2.RELEASE/reference/html/pooling.html

contextSource.setAuthenticationStrategy(new DefaultTlsDirContextAuthenticationStrategy());
contextSource.afterPropertiesSet();

FilterBasedLdapUserSearch ldapUserSearch = new FilterBasedLdapUserSearch(getUserSearchBase(), getUserSearch(), contextSource);
Expand Down