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;
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<SecurityRealm> {
public static final class DescriptorImpl extends Descriptor<SecurityRealm> {
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);
}
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">
<f:entry title="Server URL" >
<f:textbox name="ldap.providerUrl" value="${instance.providerUrl}" />
<f:entry title="${%Server}" help="/help/security/ldap/server.html">
<f:textbox name="ldap.server" value="${instance.server}"
checkUrl="'${rootURL}/securityRealms/LDAPSecurityRealm/serverCheck?server='+escape(this.value)"/>
</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>

View file

@ -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";