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;
|
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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in a new issue