mirror of
https://github.com/nicolabs/ldap-plugin.git
synced 2025-09-07 05:14:24 +02:00
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:
parent
95a129f3fa
commit
030745498a
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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";
|
||||
|
|
Loading…
Reference in a new issue