implemented LDAP authentication support.

git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@6479 71c3de6d-444a-0410-be80-ed276b4c234a

Originally-Committed-As: 89238bb50289e16194c6492dd6153ede4f0282e3
This commit is contained in:
kohsuke 2008-01-05 00:17:06 +00:00
parent 95a129f3fa
commit 030745498a
3 changed files with 170 additions and 20 deletions

View file

@ -1,14 +1,34 @@
package hudson.security; package hudson.security;
import org.acegisecurity.AuthenticationManager; import com.sun.jndi.ldap.LdapCtxFactory;
import org.acegisecurity.MockAuthenticationManager; import groovy.lang.Binding;
import org.kohsuke.stapler.StaplerRequest; import hudson.Util;
import org.kohsuke.stapler.DataBoundConstructor;
import hudson.model.Descriptor; import hudson.model.Descriptor;
import hudson.model.Hudson; import hudson.model.Hudson;
import hudson.util.FormFieldValidator;
import hudson.util.spring.BeanBuilder; import hudson.util.spring.BeanBuilder;
import net.sf.json.JSONObject; 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. * {@link SecurityRealm} implementation that uses LDAP for authentication.
@ -17,19 +37,86 @@ import groovy.lang.Binding;
*/ */
public class LDAPSecurityRealm extends SecurityRealm { public class LDAPSecurityRealm extends SecurityRealm {
/** /**
* LDAP to connect to, and root DN. * LDAP server name, optionally with TCP port number, like "ldap.acme.org"
* String like "ldap://monkeymachine:389/dc=acegisecurity,dc=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 @DataBoundConstructor
public LDAPSecurityRealm(String providerUrl) { public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch) {
this.providerUrl = providerUrl; 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() { public AuthenticationManager createAuthenticationManager() {
Binding binding = new Binding(); Binding binding = new Binding();
binding.setVariable("it", this); binding.setVariable("instance", this);
BeanBuilder builder = new BeanBuilder(); BeanBuilder builder = new BeanBuilder();
builder.parse(Hudson.getInstance().servletContext.getResourceAsStream("/WEB-INF/security/LDAPBindSecurityRealm.groovy"),binding); 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(); public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();
private static final class DescriptorImpl extends Descriptor<SecurityRealm> { public static final class DescriptorImpl extends Descriptor<SecurityRealm> {
private DescriptorImpl() { private DescriptorImpl() {
super(LDAPSecurityRealm.class); super(LDAPSecurityRealm.class);
} }
@ -54,10 +141,54 @@ public class LDAPSecurityRealm extends SecurityRealm {
public String getDisplayName() { public String getDisplayName() {
return "LDAP"; 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 { static {
if(Boolean.getBoolean("LDAP")) LIST.add(DESCRIPTOR);
LIST.add(DESCRIPTOR);
} }
private static final Logger LOGGER = Logger.getLogger(LDAPSecurityRealm.class.getName());
} }

View file

@ -1,5 +1,17 @@
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<f:entry title="Server URL" > <f:entry title="${%Server}" help="/help/security/ldap/server.html">
<f:textbox name="ldap.providerUrl" value="${instance.providerUrl}" /> <f:textbox name="ldap.server" value="${instance.server}"
checkUrl="'${rootURL}/securityRealms/LDAPSecurityRealm/serverCheck?server='+escape(this.value)"/>
</f:entry> </f:entry>
<f:advanced>
<f:entry title="${%root DN}" help="/help/security/ldap/rootDN.html">
<f:textbox name="ldap.rootDN" value="${instance.rootDN}" />
</f:entry>
<f:entry title="${%User search base}" help="/help/security/ldap/userSearchBase.html">
<f:textbox name="ldap.userSearchBase" value="${instance.userSearchBase}" />
</f:entry>
<f:entry title="${%User search filter}" help="/help/security/ldap/userSearchFilter.html">
<f:textbox name="ldap.userSearch" value="${instance.userSearch}" />
</f:entry>
</f:advanced>
</j:jelly> </j:jelly>

View file

@ -4,14 +4,16 @@ import org.acegisecurity.providers.ldap.LdapAuthenticationProvider
import org.acegisecurity.providers.ldap.authenticator.BindAuthenticator import org.acegisecurity.providers.ldap.authenticator.BindAuthenticator
import org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator import org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator
import org.acegisecurity.ldap.DefaultInitialDirContextFactory import org.acegisecurity.ldap.DefaultInitialDirContextFactory
import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch
/* /*
Configure LDAP as the authentication realm. Configure LDAP as the authentication realm.
Authentication is performed by doing LDAP bind. 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? // if anonymous bind is not allowed --- but what is the use of anonymous bind?
// managerDn = "..." // managerDn = "..."
@ -19,9 +21,14 @@ initialDirContextFactory(DefaultInitialDirContextFactory,it.providerUrl) {
} }
bindAuthenticator(BindAuthenticator,initialDirContextFactory) { bindAuthenticator(BindAuthenticator,initialDirContextFactory) {
userDnPatterns = [ // this is when you the user name can be translated into DN.
"uid={0},ou=people" // 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") { authoritiesPopulator(DefaultLdapAuthoritiesPopulator,initialDirContextFactory,"ou=groups") {
// groupRoleAttribute = "ou"; // groupRoleAttribute = "ou";