LDAPSecurityRealm.java 8.1 KB
Newer Older
1 2
package hudson.security;

3 4 5
import com.sun.jndi.ldap.LdapCtxFactory;
import groovy.lang.Binding;
import hudson.Util;
6
import hudson.model.Descriptor;
K
kohsuke 已提交
7
import hudson.model.Hudson;
8
import hudson.util.FormFieldValidator;
K
kohsuke 已提交
9
import hudson.util.spring.BeanBuilder;
K
kohsuke 已提交
10
import net.sf.json.JSONObject;
11
import org.acegisecurity.AuthenticationManager;
K
kohsuke 已提交
12
import org.acegisecurity.userdetails.UserDetailsService;
13 14
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UsernameNotFoundException;
15
import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch;
16
import org.acegisecurity.ldap.LdapUserSearch;
17 18 19 20
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
K
kohsuke 已提交
21
import org.springframework.web.context.WebApplicationContext;
22
import org.springframework.dao.DataAccessException;
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

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;
38 39 40 41 42

/**
 * {@link SecurityRealm} implementation that uses LDAP for authentication.
 * 
 * @author Kohsuke Kawaguchi
K
kohsuke 已提交
43
 * @since 1.166
44 45
 */
public class LDAPSecurityRealm extends SecurityRealm {
K
kohsuke 已提交
46
    /**
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
     * 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
K
kohsuke 已提交
87
     */
K
kohsuke 已提交
88 89

    @DataBoundConstructor
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
    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("");
108 109 110 111 112
            Attribute a = atts.get("defaultNamingContext");
            if(a!=null) // this entry is available on Active Directory. See http://msdn2.microsoft.com/en-us/library/ms684291(VS.85).aspx
                return a.toString();
            
            a = atts.get("namingcontexts");
113 114 115 116 117 118 119 120 121 122 123 124 125
            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;
K
kohsuke 已提交
126 127
    }

K
kohsuke 已提交
128
    public SecurityComponents createSecurityComponents() {
K
kohsuke 已提交
129
        Binding binding = new Binding();
130
        binding.setVariable("instance", this);
K
kohsuke 已提交
131 132 133

        BeanBuilder builder = new BeanBuilder();
        builder.parse(Hudson.getInstance().servletContext.getResourceAsStream("/WEB-INF/security/LDAPBindSecurityRealm.groovy"),binding);
134 135
        final WebApplicationContext appContext = builder.createApplicationContext();

K
kohsuke 已提交
136 137
        return new SecurityComponents(
            findBean(AuthenticationManager.class, appContext),
138 139 140 141 142 143
            new UserDetailsService() {
                final LdapUserSearch ldapSerach = findBean(LdapUserSearch.class, appContext);
                public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
                    return ldapSerach.searchForUser(username);
                }
            });
144 145 146 147 148 149 150 151
    }

    public DescriptorImpl getDescriptor() {
        return DESCRIPTOR;
    }

    public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl();

152
    public static final class DescriptorImpl extends Descriptor<SecurityRealm> {
153 154 155 156
        private DescriptorImpl() {
            super(LDAPSecurityRealm.class);
        }

K
kohsuke 已提交
157 158 159 160
        public LDAPSecurityRealm newInstance(StaplerRequest req, JSONObject formData) throws FormException {
            return req.bindJSON(LDAPSecurityRealm.class,formData);
        }

161 162 163
        public String getDisplayName() {
            return "LDAP";
        }
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206

        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();
        }
207
    }
208 209

    static {
210
        LIST.add(DESCRIPTOR);
211
    }
212 213

    private static final Logger LOGGER = Logger.getLogger(LDAPSecurityRealm.class.getName());
214
}