diff --git a/core/src/main/java/hudson/security/LDAPSecurityRealm.java b/core/src/main/java/hudson/security/LDAPSecurityRealm.java index 0a97e97..521c67e 100644 --- a/core/src/main/java/hudson/security/LDAPSecurityRealm.java +++ b/core/src/main/java/hudson/security/LDAPSecurityRealm.java @@ -1,14 +1,34 @@ package hudson.security; -import org.acegisecurity.AuthenticationManager; -import org.acegisecurity.MockAuthenticationManager; -import org.kohsuke.stapler.StaplerRequest; -import org.kohsuke.stapler.DataBoundConstructor; +import com.sun.jndi.ldap.LdapCtxFactory; +import groovy.lang.Binding; +import hudson.Util; import hudson.model.Descriptor; import hudson.model.Hudson; +import hudson.util.FormFieldValidator; import hudson.util.spring.BeanBuilder; import net.sf.json.JSONObject; -import groovy.lang.Binding; +import org.acegisecurity.AuthenticationManager; +import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.StaplerRequest; +import org.kohsuke.stapler.StaplerResponse; + +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.servlet.ServletException; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Hashtable; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * {@link SecurityRealm} implementation that uses LDAP for authentication. @@ -17,19 +37,86 @@ import groovy.lang.Binding; */ public class LDAPSecurityRealm extends SecurityRealm { /** - * LDAP to connect to, and root DN. - * String like "ldap://monkeymachine:389/dc=acegisecurity,dc=org" + * LDAP server name, optionally with TCP port number, like "ldap.acme.org" + * or "ldap.acme.org:389". + */ + public final String server; + + /** + * The root DN to connect to. Normally something like "dc=sun,dc=com" + * + * How do I infer this? + */ + public final String rootDN; + + /** + * Specifies the relative DN from {@link #rootDN the root DN}. + * This is used to narrow down the search space when doing user search. + * + * Something like "ou=people" but can be empty. + */ + public final String userSearchBase; + + /** + * Query to locate an entry that identifies the user, given the user name string. + * + * Normally "uid={0}" + * + * @see FilterBasedLdapUserSearch + */ + public final String userSearch; + + /* + Other configurations that are needed: + + group search base DN (relative to root DN) + group search filter (uniquemember={1} seems like a reasonable default) + group target (CN is a reasonable default) + + manager dn/password if anonyomus search is not allowed. + + See GF configuration at http://weblogs.java.net/blog/tchangu/archive/2007/01/ldap_security_r.html + Geronimo configuration at http://cwiki.apache.org/GMOxDOC11/ldap-realm.html */ - public final String providerUrl; @DataBoundConstructor - public LDAPSecurityRealm(String providerUrl) { - this.providerUrl = providerUrl; + public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch) { + this.server = server.trim(); + if(Util.fixEmptyAndTrim(rootDN)==null) rootDN=Util.fixNull(inferRootDN(server)); + this.rootDN = rootDN.trim(); + this.userSearchBase = userSearchBase.trim(); + if(Util.fixEmptyAndTrim(userSearch)==null) userSearch="uid={0}"; + this.userSearch = userSearch.trim(); + } + + /** + * Infer the root DN. + * + * @return null if not found. + */ + private String inferRootDN(String server) { + try { + DirContext ctx = LdapCtxFactory.getLdapCtxInstance("ldap://"+server+'/', new Hashtable()); + Attributes atts = ctx.getAttributes(""); + Attribute a = atts.get("namingcontexts"); + if(a==null) { + LOGGER.warning("namingcontexts attribute not found in root DSE of "+server); + return null; + } + return a.get().toString(); + } catch (NamingException e) { + LOGGER.log(Level.WARNING,"Failed to connect to LDAP to infer Root DN for "+server,e); + return null; + } + } + + public String getLDAPURL() { + return "ldap://"+server+'/'+rootDN; } public AuthenticationManager createAuthenticationManager() { Binding binding = new Binding(); - binding.setVariable("it", this); + binding.setVariable("instance", this); BeanBuilder builder = new BeanBuilder(); builder.parse(Hudson.getInstance().servletContext.getResourceAsStream("/WEB-INF/security/LDAPBindSecurityRealm.groovy"),binding); @@ -42,7 +129,7 @@ public class LDAPSecurityRealm extends SecurityRealm { public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); - private static final class DescriptorImpl extends Descriptor { + public static final class DescriptorImpl extends Descriptor { private DescriptorImpl() { super(LDAPSecurityRealm.class); } @@ -54,10 +141,54 @@ public class LDAPSecurityRealm extends SecurityRealm { public String getDisplayName() { return "LDAP"; } + + public void doServerCheck(StaplerRequest req, StaplerResponse rsp, @QueryParameter("server") final String server) throws IOException, ServletException { + new FormFieldValidator(req,rsp,true) { + protected void check() throws IOException, ServletException { + try { + DirContext ctx = LdapCtxFactory.getLdapCtxInstance("ldap://"+server+'/', new Hashtable()); + ctx.getAttributes(""); + ok(); // connected + } catch (NamingException e) { + // trouble-shoot + Matcher m = Pattern.compile("([^:]+)(?:\\:(\\d+))?").matcher(server.trim()); + if(!m.matches()) { + error("Syntax of this field is SERVER or SERVER:PORT"); + return; + } + + try { + InetAddress adrs = InetAddress.getByName(m.group(1)); + int port=389; + if(m.group(2)!=null) + port = Integer.parseInt(m.group(2)); + Socket s = new Socket(adrs,port); + s.close(); + } catch (NumberFormatException x) { + // impossible, because of the regexp + } catch (UnknownHostException x) { + error("Unknown host: "+x.getMessage()); + return; + } catch (IOException x) { + error("Unable to connect to "+server+" : "+x.getMessage()); + return; + } + + // otherwise we don't know what caused it, so fall back to the general error report + // getMessage() alone doesn't offer enough + error("Unable to connect to "+server+": "+e); + } catch (NumberFormatException x) { + // The getLdapCtxInstance method throws this if it fails to parse the port number + error("Invalid port number"); + } + } + }.check(); + } } static { - if(Boolean.getBoolean("LDAP")) - LIST.add(DESCRIPTOR); + LIST.add(DESCRIPTOR); } + + private static final Logger LOGGER = Logger.getLogger(LDAPSecurityRealm.class.getName()); } diff --git a/core/src/main/resources/hudson/security/LDAPSecurityRealm/config.jelly b/core/src/main/resources/hudson/security/LDAPSecurityRealm/config.jelly index 0741e26..2a120d8 100644 --- a/core/src/main/resources/hudson/security/LDAPSecurityRealm/config.jelly +++ b/core/src/main/resources/hudson/security/LDAPSecurityRealm/config.jelly @@ -1,5 +1,17 @@ - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/war/resources/WEB-INF/security/LDAPBindSecurityRealm.groovy b/war/resources/WEB-INF/security/LDAPBindSecurityRealm.groovy index 22cc699..23ed6a6 100644 --- a/war/resources/WEB-INF/security/LDAPBindSecurityRealm.groovy +++ b/war/resources/WEB-INF/security/LDAPBindSecurityRealm.groovy @@ -4,14 +4,16 @@ import org.acegisecurity.providers.ldap.LdapAuthenticationProvider import org.acegisecurity.providers.ldap.authenticator.BindAuthenticator import org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator import org.acegisecurity.ldap.DefaultInitialDirContextFactory +import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch /* Configure LDAP as the authentication realm. Authentication is performed by doing LDAP bind. + The 'instance' object refers to the instance of LDAPSecurityRealm */ -initialDirContextFactory(DefaultInitialDirContextFactory,it.providerUrl) { +initialDirContextFactory(DefaultInitialDirContextFactory, instance.getLDAPURL() ) { // if anonymous bind is not allowed --- but what is the use of anonymous bind? // managerDn = "..." @@ -19,9 +21,14 @@ initialDirContextFactory(DefaultInitialDirContextFactory,it.providerUrl) { } bindAuthenticator(BindAuthenticator,initialDirContextFactory) { - userDnPatterns = [ - "uid={0},ou=people" - ] + // this is when you the user name can be translated into DN. +// userDnPatterns = [ +// "uid={0},ou=people" +// ] + // this is when we need to find it. + userSearch = bean(FilterBasedLdapUserSearch, instance.userSearchBase, instance.userSearch, initialDirContextFactory) { + searchSubtree=true + } } authoritiesPopulator(DefaultLdapAuthoritiesPopulator,initialDirContextFactory,"ou=groups") { // groupRoleAttribute = "ou";