2009-02-06 21:05:24 +01:00
|
|
|
/*
|
|
|
|
|
* The MIT License
|
|
|
|
|
*
|
2012-12-05 23:16:06 +01:00
|
|
|
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi, Seiji Sogabe,
|
|
|
|
|
* Olivier Lamy
|
2009-02-06 21:05:24 +01:00
|
|
|
*
|
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
|
|
|
* in the Software without restriction, including without limitation the rights
|
|
|
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
|
|
|
* furnished to do so, subject to the following conditions:
|
|
|
|
|
*
|
|
|
|
|
* The above copyright notice and this permission notice shall be included in
|
|
|
|
|
* all copies or substantial portions of the Software.
|
|
|
|
|
*
|
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
|
|
* THE SOFTWARE.
|
|
|
|
|
*/
|
2007-12-05 08:09:29 +01:00
|
|
|
package hudson.security;
|
|
|
|
|
|
2008-01-05 01:17:06 +01:00
|
|
|
import groovy.lang.Binding;
|
2014-05-22 16:43:20 +02:00
|
|
|
import hudson.DescriptorExtensionList;
|
2009-02-24 01:05:47 +01:00
|
|
|
import hudson.Extension;
|
2009-09-24 20:02:30 +02:00
|
|
|
import static hudson.Util.fixEmpty;
|
2014-05-07 19:55:05 +02:00
|
|
|
import static hudson.Util.fixEmptyAndTrim;
|
|
|
|
|
import static hudson.Util.fixNull;
|
2014-08-12 11:51:41 +02:00
|
|
|
|
|
|
|
|
import hudson.Util;
|
2013-12-09 13:22:29 +01:00
|
|
|
import hudson.model.AbstractDescribableImpl;
|
2007-12-05 08:09:29 +01:00
|
|
|
import hudson.model.Descriptor;
|
2008-03-28 00:46:20 +01:00
|
|
|
import hudson.model.User;
|
2009-03-23 04:26:55 +01:00
|
|
|
import hudson.tasks.MailAddressResolver;
|
2014-05-07 19:55:05 +02:00
|
|
|
import hudson.tasks.Mailer;
|
2014-09-25 20:27:42 +02:00
|
|
|
import hudson.tasks.Mailer.UserProperty;
|
2009-03-23 04:26:55 +01:00
|
|
|
import hudson.util.FormValidation;
|
2014-05-07 19:55:05 +02:00
|
|
|
import hudson.util.ListBoxModel;
|
2008-04-11 08:03:36 +02:00
|
|
|
import hudson.util.Scrambler;
|
2014-05-07 20:16:35 +02:00
|
|
|
import hudson.util.Secret;
|
2008-01-04 08:13:30 +01:00
|
|
|
import hudson.util.spring.BeanBuilder;
|
2014-05-07 19:55:05 +02:00
|
|
|
import java.io.File;
|
|
|
|
|
import java.io.FileInputStream;
|
|
|
|
|
import java.io.FileNotFoundException;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.Serializable;
|
|
|
|
|
import java.net.InetAddress;
|
|
|
|
|
import java.net.Socket;
|
|
|
|
|
import java.net.UnknownHostException;
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
import java.util.Collections;
|
|
|
|
|
import java.util.HashSet;
|
|
|
|
|
import java.util.Hashtable;
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Set;
|
|
|
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
|
import java.util.logging.Level;
|
|
|
|
|
import java.util.logging.Logger;
|
|
|
|
|
import java.util.regex.Matcher;
|
|
|
|
|
import java.util.regex.Pattern;
|
|
|
|
|
import javax.naming.Context;
|
|
|
|
|
import javax.naming.NamingException;
|
|
|
|
|
import javax.naming.directory.Attribute;
|
|
|
|
|
import javax.naming.directory.Attributes;
|
|
|
|
|
import javax.naming.directory.BasicAttributes;
|
|
|
|
|
import javax.naming.directory.DirContext;
|
|
|
|
|
import javax.naming.directory.InitialDirContext;
|
2014-08-12 11:51:41 +02:00
|
|
|
|
|
|
|
|
import jenkins.model.IdStrategy;
|
2014-05-07 19:55:05 +02:00
|
|
|
import jenkins.model.Jenkins;
|
2014-05-22 16:43:20 +02:00
|
|
|
import jenkins.security.plugins.ldap.FromGroupSearchLDAPGroupMembershipStrategy;
|
|
|
|
|
import jenkins.security.plugins.ldap.LDAPGroupMembershipStrategy;
|
2014-05-07 19:55:05 +02:00
|
|
|
import org.acegisecurity.AcegiSecurityException;
|
2014-01-17 17:58:01 +01:00
|
|
|
import org.acegisecurity.Authentication;
|
2014-05-07 19:55:05 +02:00
|
|
|
import org.acegisecurity.AuthenticationException;
|
2008-01-05 01:17:06 +01:00
|
|
|
import org.acegisecurity.AuthenticationManager;
|
2009-02-13 17:49:36 +01:00
|
|
|
import org.acegisecurity.GrantedAuthority;
|
2011-03-19 07:28:34 +01:00
|
|
|
import org.acegisecurity.GrantedAuthorityImpl;
|
2009-01-29 20:34:13 +01:00
|
|
|
import org.acegisecurity.ldap.InitialDirContextFactory;
|
2009-03-23 04:26:55 +01:00
|
|
|
import org.acegisecurity.ldap.LdapDataAccessException;
|
2009-01-29 20:34:13 +01:00
|
|
|
import org.acegisecurity.ldap.LdapTemplate;
|
2009-03-23 04:26:55 +01:00
|
|
|
import org.acegisecurity.ldap.LdapUserSearch;
|
|
|
|
|
import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch;
|
2010-08-13 06:14:22 +02:00
|
|
|
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
|
2009-02-13 17:49:36 +01:00
|
|
|
import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
|
2009-02-26 04:21:23 +01:00
|
|
|
import org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator;
|
2009-03-23 04:26:55 +01:00
|
|
|
import org.acegisecurity.userdetails.UserDetails;
|
|
|
|
|
import org.acegisecurity.userdetails.UserDetailsService;
|
|
|
|
|
import org.acegisecurity.userdetails.UsernameNotFoundException;
|
|
|
|
|
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
|
|
|
|
|
import org.acegisecurity.userdetails.ldap.LdapUserDetailsImpl;
|
2010-08-30 20:34:28 +02:00
|
|
|
import org.apache.commons.collections.map.LRUMap;
|
2011-06-10 00:40:20 +02:00
|
|
|
import org.apache.commons.io.input.AutoCloseInputStream;
|
2014-01-17 17:58:01 +01:00
|
|
|
import org.apache.commons.lang.StringUtils;
|
2008-01-05 01:17:06 +01:00
|
|
|
import org.kohsuke.stapler.DataBoundConstructor;
|
|
|
|
|
import org.kohsuke.stapler.QueryParameter;
|
2008-01-25 03:05:08 +01:00
|
|
|
import org.springframework.dao.DataAccessException;
|
2009-03-23 04:26:55 +01:00
|
|
|
import org.springframework.web.context.WebApplicationContext;
|
2008-01-05 01:17:06 +01:00
|
|
|
|
2007-12-05 08:09:29 +01:00
|
|
|
/**
|
|
|
|
|
* {@link SecurityRealm} implementation that uses LDAP for authentication.
|
2009-01-29 20:34:13 +01:00
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* <h2>Key Object Classes</h2>
|
|
|
|
|
*
|
|
|
|
|
* <h4>Group Membership</h4>
|
|
|
|
|
*
|
|
|
|
|
* <p>
|
|
|
|
|
* Two object classes seem to be relevant. These are in RFC 2256 and core.schema. These use DN for membership,
|
|
|
|
|
* so it can create a group of anything. I don't know what the difference between these two are.
|
|
|
|
|
* <pre>
|
|
|
|
|
attributetype ( 2.5.4.31 NAME 'member'
|
|
|
|
|
DESC 'RFC2256: member of a group'
|
|
|
|
|
SUP distinguishedName )
|
|
|
|
|
|
|
|
|
|
attributetype ( 2.5.4.50 NAME 'uniqueMember'
|
|
|
|
|
DESC 'RFC2256: unique member of a group'
|
|
|
|
|
EQUALITY uniqueMemberMatch
|
|
|
|
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 )
|
|
|
|
|
|
|
|
|
|
objectclass ( 2.5.6.9 NAME 'groupOfNames'
|
|
|
|
|
DESC 'RFC2256: a group of names (DNs)'
|
|
|
|
|
SUP top STRUCTURAL
|
|
|
|
|
MUST ( member $ cn )
|
|
|
|
|
MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )
|
|
|
|
|
|
|
|
|
|
objectclass ( 2.5.6.17 NAME 'groupOfUniqueNames'
|
|
|
|
|
DESC 'RFC2256: a group of unique names (DN and Unique Identifier)'
|
|
|
|
|
SUP top STRUCTURAL
|
|
|
|
|
MUST ( uniqueMember $ cn )
|
|
|
|
|
MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) )
|
|
|
|
|
* </pre>
|
|
|
|
|
*
|
|
|
|
|
* <p>
|
|
|
|
|
* This one is from nis.schema, and appears to model POSIX group/user thing more closely.
|
|
|
|
|
* <pre>
|
|
|
|
|
objectclass ( 1.3.6.1.1.1.2.2 NAME 'posixGroup'
|
|
|
|
|
DESC 'Abstraction of a group of accounts'
|
|
|
|
|
SUP top STRUCTURAL
|
|
|
|
|
MUST ( cn $ gidNumber )
|
|
|
|
|
MAY ( userPassword $ memberUid $ description ) )
|
|
|
|
|
|
|
|
|
|
attributetype ( 1.3.6.1.1.1.1.12 NAME 'memberUid'
|
|
|
|
|
EQUALITY caseExactIA5Match
|
|
|
|
|
SUBSTR caseExactIA5SubstringsMatch
|
|
|
|
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
|
|
|
|
|
|
|
|
|
|
objectclass ( 1.3.6.1.1.1.2.0 NAME 'posixAccount'
|
|
|
|
|
DESC 'Abstraction of an account with POSIX attributes'
|
|
|
|
|
SUP top AUXILIARY
|
|
|
|
|
MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory )
|
|
|
|
|
MAY ( userPassword $ loginShell $ gecos $ description ) )
|
|
|
|
|
|
|
|
|
|
attributetype ( 1.3.6.1.1.1.1.0 NAME 'uidNumber'
|
|
|
|
|
DESC 'An integer uniquely identifying a user in an administrative domain'
|
|
|
|
|
EQUALITY integerMatch
|
|
|
|
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
|
|
|
|
|
|
|
|
|
|
attributetype ( 1.3.6.1.1.1.1.1 NAME 'gidNumber'
|
|
|
|
|
DESC 'An integer uniquely identifying a group in an administrative domain'
|
|
|
|
|
EQUALITY integerMatch
|
|
|
|
|
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
|
|
|
|
|
* </pre>
|
|
|
|
|
*
|
|
|
|
|
* <p>
|
|
|
|
|
* Active Directory specific schemas (from <a href="http://www.grotan.com/ldap/microsoft.schema">here</a>).
|
|
|
|
|
* <pre>
|
|
|
|
|
objectclass ( 1.2.840.113556.1.5.8
|
|
|
|
|
NAME 'group'
|
|
|
|
|
SUP top
|
|
|
|
|
STRUCTURAL
|
|
|
|
|
MUST (groupType )
|
|
|
|
|
MAY (member $ nTGroupMembers $ operatorCount $ adminCount $
|
|
|
|
|
groupAttributes $ groupMembershipSAM $ controlAccessRights $
|
|
|
|
|
desktopProfile $ nonSecurityMember $ managedBy $
|
|
|
|
|
primaryGroupToken $ mail ) )
|
|
|
|
|
|
|
|
|
|
objectclass ( 1.2.840.113556.1.5.9
|
|
|
|
|
NAME 'user'
|
|
|
|
|
SUP organizationalPerson
|
|
|
|
|
STRUCTURAL
|
|
|
|
|
MAY (userCertificate $ networkAddress $ userAccountControl $
|
|
|
|
|
badPwdCount $ codePage $ homeDirectory $ homeDrive $
|
|
|
|
|
badPasswordTime $ lastLogoff $ lastLogon $ dBCSPwd $
|
|
|
|
|
localeID $ scriptPath $ logonHours $ logonWorkstation $
|
|
|
|
|
maxStorage $ userWorkstations $ unicodePwd $
|
|
|
|
|
otherLoginWorkstations $ ntPwdHistory $ pwdLastSet $
|
|
|
|
|
preferredOU $ primaryGroupID $ userParameters $
|
|
|
|
|
profilePath $ operatorCount $ adminCount $ accountExpires $
|
|
|
|
|
lmPwdHistory $ groupMembershipSAM $ logonCount $
|
|
|
|
|
controlAccessRights $ defaultClassStore $ groupsToIgnore $
|
|
|
|
|
groupPriority $ desktopProfile $ dynamicLDAPServer $
|
|
|
|
|
userPrincipalName $ lockoutTime $ userSharedFolder $
|
|
|
|
|
userSharedFolderOther $ servicePrincipalName $
|
|
|
|
|
aCSPolicyName $ terminalServer $ mSMQSignCertificates $
|
|
|
|
|
mSMQDigests $ mSMQDigestsMig $ mSMQSignCertificatesMig $
|
|
|
|
|
msNPAllowDialin $ msNPCallingStationID $
|
|
|
|
|
msNPSavedCallingStationID $ msRADIUSCallbackNumber $
|
|
|
|
|
msRADIUSFramedIPAddress $ msRADIUSFramedRoute $
|
|
|
|
|
msRADIUSServiceType $ msRASSavedCallbackNumber $
|
|
|
|
|
msRASSavedFramedIPAddress $ msRASSavedFramedRoute $
|
|
|
|
|
mS-DS-CreatorSID ) )
|
|
|
|
|
* </pre>
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* <h2>References</h2>
|
|
|
|
|
* <dl>
|
|
|
|
|
* <dt><a href="http://www.openldap.org/doc/admin22/schema.html">Standard Schemas</a>
|
|
|
|
|
* <dd>
|
|
|
|
|
* The downloadable distribution contains schemas that define the structure of LDAP entries.
|
|
|
|
|
* Because this is a standard, we expect most LDAP servers out there to use it, although
|
|
|
|
|
* there are different objectClasses that can be used for similar purposes, and apparently
|
|
|
|
|
* many deployments choose to use different objectClasses.
|
|
|
|
|
*
|
|
|
|
|
* <dt><a href="http://www.ietf.org/rfc/rfc2256.txt">RFC 2256</a>
|
|
|
|
|
* <dd>
|
2014-05-07 19:55:05 +02:00
|
|
|
* Defines the meaning of several key datatypes used in the schemas with some explanations.
|
2009-01-29 20:34:13 +01:00
|
|
|
*
|
|
|
|
|
* <dt><a href="http://msdn.microsoft.com/en-us/library/ms675085(VS.85).aspx">Active Directory schema</a>
|
|
|
|
|
* <dd>
|
|
|
|
|
* More navigable schema list, including core and MS extensions specific to Active Directory.
|
|
|
|
|
* </dl>
|
2014-05-07 19:55:05 +02:00
|
|
|
*
|
2007-12-05 08:09:29 +01:00
|
|
|
* @author Kohsuke Kawaguchi
|
2008-01-05 01:21:11 +01:00
|
|
|
* @since 1.166
|
2007-12-05 08:09:29 +01:00
|
|
|
*/
|
2010-08-13 06:14:22 +02:00
|
|
|
public class LDAPSecurityRealm extends AbstractPasswordBasedSecurityRealm {
|
2014-03-27 11:49:03 +01:00
|
|
|
private static final boolean FORCE_USERNAME_LOWERCASE =
|
|
|
|
|
Boolean.getBoolean(LDAPSecurityRealm.class.getName() + ".forceUsernameLowercase");
|
2014-05-09 11:20:27 +02:00
|
|
|
private static final boolean FORCE_GROUPNAME_LOWERCASE =
|
|
|
|
|
Boolean.getBoolean(LDAPSecurityRealm.class.getName() + ".forceGroupnameLowercase");
|
2008-01-04 08:13:30 +01:00
|
|
|
/**
|
2013-07-24 16:29:42 +02:00
|
|
|
* LDAP server name(s) separated by spaces, optionally with TCP port number, like "ldap.acme.org"
|
|
|
|
|
* or "ldap.acme.org:389" and/or with protcol, like "ldap://ldap.acme.org".
|
2008-01-05 01:17:06 +01:00
|
|
|
*/
|
|
|
|
|
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;
|
|
|
|
|
|
2011-04-21 23:12:29 +02:00
|
|
|
/**
|
|
|
|
|
* Allow the rootDN to be inferred? Default is false.
|
|
|
|
|
* If true, allow rootDN to be blank.
|
|
|
|
|
*/
|
|
|
|
|
public final boolean inhibitInferRootDN;
|
|
|
|
|
|
2008-01-05 01:17:06 +01:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*
|
2010-04-07 01:18:41 +02:00
|
|
|
* Normally "uid={0}"
|
2008-01-05 01:17:06 +01:00
|
|
|
*
|
2010-04-07 01:18:41 +02:00
|
|
|
* @see FilterBasedLdapUserSearch
|
2008-01-05 01:17:06 +01:00
|
|
|
*/
|
|
|
|
|
public final String userSearch;
|
2008-06-27 19:37:50 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This defines the organizational unit that contains groups.
|
|
|
|
|
*
|
2009-01-29 20:34:13 +01:00
|
|
|
* Normally "" to indicate the full LDAP search, but can be often narrowed down to
|
|
|
|
|
* something like "ou=groups"
|
2008-06-27 19:37:50 +02:00
|
|
|
*
|
|
|
|
|
* @see FilterBasedLdapUserSearch
|
|
|
|
|
*/
|
|
|
|
|
public final String groupSearchBase;
|
2008-01-05 01:17:06 +01:00
|
|
|
|
2013-06-14 17:10:20 +02:00
|
|
|
/**
|
|
|
|
|
* Query to locate an entry that identifies the group, given the group name string. If non-null it will override
|
|
|
|
|
* the default specified by {@link #GROUP_SEARCH}
|
|
|
|
|
*
|
|
|
|
|
* @since 1.5
|
|
|
|
|
*/
|
|
|
|
|
public final String groupSearchFilter;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Query to locate the group entries that a user belongs to, given the user object. <code>{0}</code>
|
|
|
|
|
* is the user's full DN while {1} is the username. If non-null it will override the default specified in
|
|
|
|
|
* {@code LDAPBindSecurityRealm.groovy}
|
|
|
|
|
*
|
|
|
|
|
* @since 1.5
|
2014-05-22 16:43:20 +02:00
|
|
|
* @deprecated use {@link #groupMembershipStrategy}
|
2013-06-14 17:10:20 +02:00
|
|
|
*/
|
2014-05-22 16:43:20 +02:00
|
|
|
@Deprecated
|
|
|
|
|
public transient String groupMembershipFilter;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @since 2.0
|
|
|
|
|
*/
|
|
|
|
|
public /*effectively final*/ LDAPGroupMembershipStrategy groupMembershipStrategy;
|
2013-06-14 17:10:20 +02:00
|
|
|
|
2008-01-05 01:17:06 +01:00
|
|
|
/*
|
|
|
|
|
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
|
2008-01-04 08:13:30 +01:00
|
|
|
*/
|
2007-12-06 07:39:29 +01:00
|
|
|
|
2008-04-11 08:03:36 +02:00
|
|
|
/**
|
2014-05-07 20:16:35 +02:00
|
|
|
* If non-null, we use this and {@link #managerPasswordSecret}
|
2008-04-11 08:03:36 +02:00
|
|
|
* when binding to LDAP.
|
|
|
|
|
*
|
|
|
|
|
* This is necessary when LDAP doesn't support anonymous access.
|
|
|
|
|
*/
|
|
|
|
|
public final String managerDN;
|
|
|
|
|
|
2014-05-07 20:16:35 +02:00
|
|
|
@Deprecated
|
|
|
|
|
private String managerPassword;
|
|
|
|
|
|
2008-04-11 08:03:36 +02:00
|
|
|
/**
|
2014-05-07 20:16:35 +02:00
|
|
|
* Password used to first bind to LDAP.
|
2008-04-11 08:03:36 +02:00
|
|
|
*/
|
2014-05-07 20:16:35 +02:00
|
|
|
private Secret managerPasswordSecret;
|
2008-04-11 08:03:36 +02:00
|
|
|
|
2009-01-29 20:34:13 +01:00
|
|
|
/**
|
|
|
|
|
* Created in {@link #createSecurityComponents()}. Can be used to connect to LDAP.
|
|
|
|
|
*/
|
2009-02-02 20:15:40 +01:00
|
|
|
private transient LdapTemplate ldapTemplate;
|
2009-01-29 20:34:13 +01:00
|
|
|
|
2012-12-05 23:16:06 +01:00
|
|
|
/**
|
|
|
|
|
* @since 1.2
|
|
|
|
|
*/
|
2012-12-05 23:26:09 +01:00
|
|
|
public final boolean disableMailAddressResolver;
|
2012-12-05 23:16:06 +01:00
|
|
|
|
2013-04-24 13:42:25 +02:00
|
|
|
/**
|
|
|
|
|
* The cache configuration
|
|
|
|
|
* @since 1.3
|
|
|
|
|
*/
|
|
|
|
|
private final CacheConfiguration cache;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The {@link UserDetails} cache.
|
|
|
|
|
*/
|
2013-04-24 13:57:39 +02:00
|
|
|
private transient Map<String,CacheEntry<LdapUserDetails>> userDetailsCache = null;
|
2013-04-24 13:42:25 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The group details cache.
|
|
|
|
|
*/
|
|
|
|
|
private transient Map<String,CacheEntry<Set<String>>> groupDetailsCache = null;
|
|
|
|
|
|
2013-12-09 13:22:29 +01:00
|
|
|
private final Map<String,String> extraEnvVars;
|
|
|
|
|
|
2014-01-17 17:58:01 +01:00
|
|
|
private final String displayNameAttributeName;
|
|
|
|
|
|
|
|
|
|
private final String mailAddressAttributeName;
|
|
|
|
|
|
2014-08-12 11:51:41 +02:00
|
|
|
private final IdStrategy userIdStrategy;
|
|
|
|
|
|
|
|
|
|
private final IdStrategy groupIdStrategy;
|
|
|
|
|
|
2013-06-14 17:10:20 +02:00
|
|
|
/**
|
|
|
|
|
* @deprecated retained for backwards binary compatibility.
|
|
|
|
|
*/
|
|
|
|
|
@Deprecated
|
2011-04-21 23:12:29 +02:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String managerDN, String managerPassword, boolean inhibitInferRootDN) {
|
2013-04-24 13:42:25 +02:00
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, managerDN, managerPassword, inhibitInferRootDN, false);
|
2012-12-05 23:16:06 +01:00
|
|
|
}
|
|
|
|
|
|
2013-06-14 17:10:20 +02:00
|
|
|
/**
|
|
|
|
|
* @deprecated retained for backwards binary compatibility.
|
|
|
|
|
*/
|
|
|
|
|
@Deprecated
|
2012-12-05 23:16:06 +01:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String managerDN, String managerPassword, boolean inhibitInferRootDN,
|
2012-12-05 23:26:09 +01:00
|
|
|
boolean disableMailAddressResolver) {
|
2013-04-24 13:42:25 +02:00
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, managerDN, managerPassword, inhibitInferRootDN,
|
|
|
|
|
disableMailAddressResolver, null);
|
|
|
|
|
}
|
|
|
|
|
|
2013-06-14 17:10:20 +02:00
|
|
|
/**
|
|
|
|
|
* @deprecated retained for backwards binary compatibility.
|
|
|
|
|
*/
|
|
|
|
|
@Deprecated
|
2013-04-24 13:42:25 +02:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String managerDN, String managerPassword, boolean inhibitInferRootDN,
|
|
|
|
|
boolean disableMailAddressResolver, CacheConfiguration cache) {
|
2013-06-14 17:10:20 +02:00
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, null, null, managerDN, managerPassword, inhibitInferRootDN, disableMailAddressResolver, cache);
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-09 13:22:29 +01:00
|
|
|
/**
|
|
|
|
|
* @deprecated retained for backwards binary compatibility.
|
|
|
|
|
*/
|
|
|
|
|
@Deprecated
|
2013-06-14 17:10:20 +02:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, String groupMembershipFilter, String managerDN, String managerPassword, boolean inhibitInferRootDN, boolean disableMailAddressResolver, CacheConfiguration cache) {
|
2013-12-09 13:22:29 +01:00
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, groupSearchFilter, groupMembershipFilter, managerDN, managerPassword, inhibitInferRootDN, disableMailAddressResolver, cache, null);
|
|
|
|
|
}
|
|
|
|
|
|
2014-01-17 17:58:01 +01:00
|
|
|
/**
|
|
|
|
|
* @deprecated retained for backwards binary compatibility.
|
|
|
|
|
*/
|
|
|
|
|
@Deprecated
|
2013-12-09 13:22:29 +01:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, String groupMembershipFilter, String managerDN, String managerPassword, boolean inhibitInferRootDN, boolean disableMailAddressResolver, CacheConfiguration cache, EnvironmentProperty[] environmentProperties) {
|
2014-01-17 17:58:01 +01:00
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, groupSearchFilter, groupMembershipFilter, managerDN, managerPassword, inhibitInferRootDN, disableMailAddressResolver, cache, environmentProperties, null, null);
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-07 20:16:35 +02:00
|
|
|
/**
|
|
|
|
|
* @deprecated retained for backwards binary compatibility.
|
|
|
|
|
*/
|
|
|
|
|
@Deprecated
|
2014-01-17 17:58:01 +01:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, String groupMembershipFilter, String managerDN, String managerPassword, boolean inhibitInferRootDN, boolean disableMailAddressResolver, CacheConfiguration cache, EnvironmentProperty[] environmentProperties, String displayNameAttributeName, String mailAddressAttributeName) {
|
2014-05-07 20:16:35 +02:00
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, groupSearchFilter, groupMembershipFilter, managerDN, Secret.fromString(managerPassword), inhibitInferRootDN, disableMailAddressResolver, cache, environmentProperties, null, null);
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-22 16:43:20 +02:00
|
|
|
/**
|
|
|
|
|
* @deprecated retained for backwards binary compatibility.
|
|
|
|
|
*/
|
|
|
|
|
@Deprecated
|
2014-05-07 20:16:35 +02:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, String groupMembershipFilter, String managerDN, Secret managerPasswordSecret, boolean inhibitInferRootDN, boolean disableMailAddressResolver, CacheConfiguration cache, EnvironmentProperty[] environmentProperties, String displayNameAttributeName, String mailAddressAttributeName) {
|
2014-05-22 16:43:20 +02:00
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, groupSearchFilter, new FromGroupSearchLDAPGroupMembershipStrategy(groupMembershipFilter), managerDN, managerPasswordSecret, inhibitInferRootDN, disableMailAddressResolver, cache, environmentProperties, displayNameAttributeName, mailAddressAttributeName);
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-12 11:51:41 +02:00
|
|
|
/**
|
|
|
|
|
* @deprecated retained for backwards binary compatibility.
|
|
|
|
|
*/
|
|
|
|
|
@Deprecated
|
2014-05-22 16:43:20 +02:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, LDAPGroupMembershipStrategy groupMembershipStrategy, String managerDN, Secret managerPasswordSecret, boolean inhibitInferRootDN, boolean disableMailAddressResolver, CacheConfiguration cache, EnvironmentProperty[] environmentProperties, String displayNameAttributeName, String mailAddressAttributeName) {
|
2014-08-12 11:51:41 +02:00
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, groupSearchFilter, groupMembershipStrategy, managerDN, managerPasswordSecret, inhibitInferRootDN, disableMailAddressResolver, cache, environmentProperties, displayNameAttributeName, mailAddressAttributeName, IdStrategy.CASE_INSENSITIVE, IdStrategy.CASE_INSENSITIVE);
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-12 16:19:45 +02:00
|
|
|
// BEING TODO Jenkins 1.577+
|
|
|
|
|
/**
|
|
|
|
|
* @deprecated will be removed once we depend on Jenkins 1.577+
|
|
|
|
|
*/
|
2014-08-12 11:51:41 +02:00
|
|
|
@DataBoundConstructor
|
2014-08-12 16:19:45 +02:00
|
|
|
@Deprecated
|
|
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, LDAPGroupMembershipStrategy groupMembershipStrategy, String managerDN, Secret managerPasswordSecret, boolean inhibitInferRootDN, boolean disableMailAddressResolver, CacheConfiguration cache, EnvironmentProperty[] environmentProperties, String displayNameAttributeName, String mailAddressAttributeName, String userIdStrategyClass, String groupIdStrategyClass) {
|
|
|
|
|
this(server, rootDN, userSearchBase, userSearch, groupSearchBase, groupSearchFilter, groupMembershipStrategy, managerDN, managerPasswordSecret, inhibitInferRootDN, disableMailAddressResolver, cache, environmentProperties, displayNameAttributeName, mailAddressAttributeName, DescriptorImpl.fromClassName(userIdStrategyClass), DescriptorImpl.fromClassName(groupIdStrategyClass));
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-12 11:51:41 +02:00
|
|
|
public LDAPSecurityRealm(String server, String rootDN, String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, LDAPGroupMembershipStrategy groupMembershipStrategy, String managerDN, Secret managerPasswordSecret, boolean inhibitInferRootDN, boolean disableMailAddressResolver, CacheConfiguration cache, EnvironmentProperty[] environmentProperties, String displayNameAttributeName, String mailAddressAttributeName, IdStrategy userIdStrategy, IdStrategy groupIdStrategy) {
|
2012-12-05 23:16:06 +01:00
|
|
|
this.server = server.trim();
|
|
|
|
|
this.managerDN = fixEmpty(managerDN);
|
2014-05-07 20:16:35 +02:00
|
|
|
this.managerPasswordSecret = managerPasswordSecret;
|
2012-12-05 23:16:06 +01:00
|
|
|
this.inhibitInferRootDN = inhibitInferRootDN;
|
|
|
|
|
if(!inhibitInferRootDN && fixEmptyAndTrim(rootDN)==null) rootDN= fixNull(inferRootDN(server));
|
|
|
|
|
this.rootDN = rootDN.trim();
|
|
|
|
|
this.userSearchBase = fixNull(userSearchBase).trim();
|
|
|
|
|
userSearch = fixEmptyAndTrim(userSearch);
|
2014-05-07 19:55:05 +02:00
|
|
|
this.userSearch = userSearch!=null ? userSearch : DescriptorImpl.DEFAULT_USER_SEARCH;
|
2012-12-05 23:16:06 +01:00
|
|
|
this.groupSearchBase = fixEmptyAndTrim(groupSearchBase);
|
2013-06-14 17:10:20 +02:00
|
|
|
this.groupSearchFilter = fixEmptyAndTrim(groupSearchFilter);
|
2014-05-22 16:43:20 +02:00
|
|
|
this.groupMembershipStrategy = groupMembershipStrategy == null ? new FromGroupSearchLDAPGroupMembershipStrategy("") : groupMembershipStrategy;
|
2012-12-05 23:26:09 +01:00
|
|
|
this.disableMailAddressResolver = disableMailAddressResolver;
|
2013-04-24 13:42:25 +02:00
|
|
|
this.cache = cache;
|
2013-12-09 13:22:29 +01:00
|
|
|
this.extraEnvVars = environmentProperties == null || environmentProperties.length == 0
|
|
|
|
|
? null
|
|
|
|
|
: EnvironmentProperty.toMap(Arrays.asList(environmentProperties));
|
2014-01-17 17:58:01 +01:00
|
|
|
this.displayNameAttributeName = StringUtils.defaultString(fixEmptyAndTrim(displayNameAttributeName),
|
2014-05-07 19:55:05 +02:00
|
|
|
DescriptorImpl.DEFAULT_DISPLAYNAME_ATTRIBUTE_NAME);
|
2014-01-17 17:58:01 +01:00
|
|
|
this.mailAddressAttributeName = StringUtils.defaultString(fixEmptyAndTrim(mailAddressAttributeName),
|
2014-05-07 19:55:05 +02:00
|
|
|
DescriptorImpl.DEFAULT_MAILADDRESS_ATTRIBUTE_NAME);
|
2014-08-12 11:51:41 +02:00
|
|
|
this.userIdStrategy = userIdStrategy == null ? IdStrategy.CASE_INSENSITIVE : userIdStrategy;
|
|
|
|
|
this.groupIdStrategy = groupIdStrategy == null ? IdStrategy.CASE_INSENSITIVE : groupIdStrategy;
|
2008-01-05 01:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
2014-08-12 16:19:45 +02:00
|
|
|
@Deprecated
|
|
|
|
|
public String getUserIdStrategyClass() {
|
|
|
|
|
return getUserIdStrategy().getClass().getName();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Deprecated
|
|
|
|
|
public String getGroupIdStrategyClass() {
|
|
|
|
|
return getGroupIdStrategy().getClass().getName();
|
|
|
|
|
}
|
|
|
|
|
// END TODO Jenkins 1.577+
|
|
|
|
|
|
|
|
|
|
|
2014-05-07 20:16:35 +02:00
|
|
|
private Object readResolve() {
|
|
|
|
|
if (managerPassword != null) {
|
|
|
|
|
managerPasswordSecret = Secret.fromString(Scrambler.descramble(managerPassword));
|
|
|
|
|
managerPassword = null;
|
|
|
|
|
}
|
2014-05-22 16:43:20 +02:00
|
|
|
if (groupMembershipStrategy == null) {
|
|
|
|
|
groupMembershipStrategy = new FromGroupSearchLDAPGroupMembershipStrategy(groupMembershipFilter);
|
|
|
|
|
groupMembershipFilter = null;
|
|
|
|
|
}
|
2014-05-07 20:16:35 +02:00
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2008-08-22 03:36:16 +02:00
|
|
|
public String getServerUrl() {
|
2013-07-24 16:29:42 +02:00
|
|
|
StringBuilder buf = new StringBuilder();
|
|
|
|
|
boolean first = true;
|
2014-08-12 11:51:41 +02:00
|
|
|
for (String s: Util.fixNull(server).split("\\s+")) {
|
2013-07-24 16:29:42 +02:00
|
|
|
if (s.trim().length() == 0) continue;
|
|
|
|
|
if (first) first = false; else buf.append(' ');
|
|
|
|
|
buf.append(addPrefix(s));
|
|
|
|
|
}
|
|
|
|
|
return buf.toString();
|
2008-08-22 03:36:16 +02:00
|
|
|
}
|
|
|
|
|
|
2014-08-12 11:51:41 +02:00
|
|
|
@Override
|
|
|
|
|
public IdStrategy getUserIdStrategy() {
|
|
|
|
|
return userIdStrategy == null ? IdStrategy.CASE_INSENSITIVE : userIdStrategy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public IdStrategy getGroupIdStrategy() {
|
|
|
|
|
return groupIdStrategy == null ? IdStrategy.CASE_INSENSITIVE : groupIdStrategy;
|
|
|
|
|
}
|
|
|
|
|
|
2013-04-24 13:42:25 +02:00
|
|
|
public CacheConfiguration getCache() {
|
|
|
|
|
return cache;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Integer getCacheSize() {
|
|
|
|
|
return cache == null ? null : cache.getSize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Integer getCacheTTL() {
|
|
|
|
|
return cache == null ? null : cache.getTtl();
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-22 16:43:20 +02:00
|
|
|
@Deprecated
|
2013-06-14 17:10:20 +02:00
|
|
|
public String getGroupMembershipFilter() {
|
|
|
|
|
return groupMembershipFilter;
|
|
|
|
|
}
|
|
|
|
|
|
2014-05-22 16:43:20 +02:00
|
|
|
public LDAPGroupMembershipStrategy getGroupMembershipStrategy() {
|
|
|
|
|
return groupMembershipStrategy;
|
|
|
|
|
}
|
|
|
|
|
|
2013-06-14 17:10:20 +02:00
|
|
|
public String getGroupSearchFilter() {
|
|
|
|
|
return groupSearchFilter;
|
|
|
|
|
}
|
|
|
|
|
|
2013-12-09 13:22:29 +01:00
|
|
|
public Map<String,String> getExtraEnvVars() {
|
|
|
|
|
return extraEnvVars == null || extraEnvVars.isEmpty()
|
|
|
|
|
? Collections.<String,String>emptyMap()
|
|
|
|
|
: Collections.unmodifiableMap(extraEnvVars);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public EnvironmentProperty[] getEnvironmentProperties() {
|
|
|
|
|
if (extraEnvVars == null || extraEnvVars.isEmpty()) {
|
|
|
|
|
return new EnvironmentProperty[0];
|
|
|
|
|
}
|
|
|
|
|
EnvironmentProperty[] result = new EnvironmentProperty[extraEnvVars.size()];
|
|
|
|
|
int i = 0;
|
|
|
|
|
for (Map.Entry<String,String> entry: extraEnvVars.entrySet()) {
|
|
|
|
|
result[i++] = new EnvironmentProperty(entry.getKey(), entry.getValue());
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2008-01-05 01:17:06 +01:00
|
|
|
/**
|
|
|
|
|
* Infer the root DN.
|
|
|
|
|
*
|
|
|
|
|
* @return null if not found.
|
|
|
|
|
*/
|
|
|
|
|
private String inferRootDN(String server) {
|
|
|
|
|
try {
|
2008-04-11 08:03:36 +02:00
|
|
|
Hashtable<String,String> props = new Hashtable<String,String>();
|
|
|
|
|
if(managerDN!=null) {
|
|
|
|
|
props.put(Context.SECURITY_PRINCIPAL,managerDN);
|
|
|
|
|
props.put(Context.SECURITY_CREDENTIALS,getManagerPassword());
|
|
|
|
|
}
|
2009-08-19 03:24:49 +02:00
|
|
|
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
|
2013-07-24 16:29:42 +02:00
|
|
|
props.put(Context.PROVIDER_URL, toProviderUrl(getServerUrl(), ""));
|
2009-08-19 03:24:49 +02:00
|
|
|
|
|
|
|
|
DirContext ctx = new InitialDirContext(props);
|
2008-01-05 01:17:06 +01:00
|
|
|
Attributes atts = ctx.getAttributes("");
|
2008-01-05 20:48:38 +01:00
|
|
|
Attribute a = atts.get("defaultNamingContext");
|
2012-01-24 23:08:10 +01:00
|
|
|
if(a!=null && a.get()!=null) // this entry is available on Active Directory. See http://msdn2.microsoft.com/en-us/library/ms684291(VS.85).aspx
|
|
|
|
|
return a.get().toString();
|
2008-01-05 20:48:38 +01:00
|
|
|
|
|
|
|
|
a = atts.get("namingcontexts");
|
2008-01-05 01:17:06 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-07-24 16:29:42 +02:00
|
|
|
private static String toProviderUrl(String serverUrl, String rootDN) {
|
|
|
|
|
StringBuilder buf = new StringBuilder();
|
|
|
|
|
boolean first = true;
|
|
|
|
|
for (String s: serverUrl.split("\\s+")) {
|
|
|
|
|
if (s.trim().length() == 0) continue;
|
|
|
|
|
if (first) first = false; else buf.append(' ');
|
|
|
|
|
s = addPrefix(s);
|
|
|
|
|
buf.append(s);
|
|
|
|
|
if (!s.endsWith("/")) buf.append('/');
|
|
|
|
|
buf.append(fixNull(rootDN));
|
|
|
|
|
}
|
|
|
|
|
return buf.toString();
|
|
|
|
|
}
|
|
|
|
|
|
2008-04-11 08:03:36 +02:00
|
|
|
public String getManagerPassword() {
|
2014-05-07 20:16:35 +02:00
|
|
|
return Secret.toString(managerPasswordSecret);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Secret getManagerPasswordSecret() {
|
|
|
|
|
return managerPasswordSecret;
|
2008-04-11 08:03:36 +02:00
|
|
|
}
|
|
|
|
|
|
2008-01-05 01:17:06 +01:00
|
|
|
public String getLDAPURL() {
|
2013-07-24 16:29:42 +02:00
|
|
|
return toProviderUrl(getServerUrl(), fixNull(rootDN));
|
2007-12-06 07:39:29 +01:00
|
|
|
}
|
|
|
|
|
|
2014-01-17 17:58:01 +01:00
|
|
|
public String getDisplayNameAttributeName() {
|
2014-05-07 19:55:05 +02:00
|
|
|
return StringUtils.defaultString(displayNameAttributeName, DescriptorImpl.DEFAULT_DISPLAYNAME_ATTRIBUTE_NAME);
|
2014-01-17 17:58:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getMailAddressAttributeName() {
|
2014-05-07 19:55:05 +02:00
|
|
|
return StringUtils.defaultString(mailAddressAttributeName, DescriptorImpl.DEFAULT_MAILADDRESS_ATTRIBUTE_NAME);
|
2014-01-17 17:58:01 +01:00
|
|
|
}
|
|
|
|
|
|
2008-01-19 18:42:15 +01:00
|
|
|
public SecurityComponents createSecurityComponents() {
|
2008-01-04 08:13:30 +01:00
|
|
|
Binding binding = new Binding();
|
2008-01-05 01:17:06 +01:00
|
|
|
binding.setVariable("instance", this);
|
2008-01-04 08:13:30 +01:00
|
|
|
|
2012-06-07 01:41:13 +02:00
|
|
|
BeanBuilder builder = new BeanBuilder(Jenkins.getInstance().pluginManager.uberClassLoader);
|
2011-06-10 00:40:20 +02:00
|
|
|
String fileName = "LDAPBindSecurityRealm.groovy";
|
|
|
|
|
try {
|
|
|
|
|
File override = new File(Jenkins.getInstance().getRootDir(), fileName);
|
|
|
|
|
builder.parse(
|
|
|
|
|
override.exists() ? new AutoCloseInputStream(new FileInputStream(override)) :
|
2012-05-30 18:53:59 +02:00
|
|
|
getClass().getResourceAsStream(fileName), binding);
|
2011-06-10 00:40:20 +02:00
|
|
|
} catch (FileNotFoundException e) {
|
|
|
|
|
throw new Error("Failed to load "+fileName,e);
|
|
|
|
|
}
|
2010-08-30 20:34:28 +02:00
|
|
|
WebApplicationContext appContext = builder.createApplicationContext();
|
2008-01-25 03:05:08 +01:00
|
|
|
|
2009-01-29 20:34:13 +01:00
|
|
|
ldapTemplate = new LdapTemplate(findBean(InitialDirContextFactory.class, appContext));
|
|
|
|
|
|
2014-05-22 16:43:20 +02:00
|
|
|
if (groupMembershipStrategy != null) {
|
|
|
|
|
groupMembershipStrategy.setAuthoritiesPopulator(findBean(LdapAuthoritiesPopulator.class, appContext));
|
2013-06-14 17:10:20 +02:00
|
|
|
}
|
|
|
|
|
|
2008-01-19 18:42:15 +01:00
|
|
|
return new SecurityComponents(
|
2014-01-17 17:58:01 +01:00
|
|
|
new LDAPAuthenticationManager(findBean(AuthenticationManager.class, appContext)),
|
2014-05-22 16:43:20 +02:00
|
|
|
new LDAPUserDetailsService(appContext, groupMembershipStrategy));
|
2007-12-05 08:09:29 +01:00
|
|
|
}
|
|
|
|
|
|
2010-08-13 06:14:22 +02:00
|
|
|
/**
|
|
|
|
|
* {@inheritDoc}
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
protected UserDetails authenticate(String username, String password) throws AuthenticationException {
|
2014-01-17 17:58:01 +01:00
|
|
|
return updateUserDetails((UserDetails) getSecurityComponents().manager.authenticate(
|
2014-05-09 11:20:27 +02:00
|
|
|
new UsernamePasswordAuthenticationToken(fixUsername(username), password)).getPrincipal());
|
2010-08-13 06:14:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* {@inheritDoc}
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
|
2014-05-09 11:20:27 +02:00
|
|
|
return updateUserDetails(getSecurityComponents().userDetails.loadUserByUsername(fixUsername(username)));
|
2014-01-17 17:58:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Authentication updateUserDetails(Authentication authentication) {
|
|
|
|
|
updateUserDetails((UserDetails) authentication.getPrincipal());
|
|
|
|
|
return authentication;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public UserDetails updateUserDetails(UserDetails userDetails) {
|
|
|
|
|
if (userDetails instanceof LdapUserDetails) {
|
|
|
|
|
updateUserDetails((LdapUserDetails)userDetails);
|
|
|
|
|
}
|
|
|
|
|
return userDetails;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LdapUserDetails updateUserDetails(LdapUserDetails d) {
|
2014-05-09 11:20:27 +02:00
|
|
|
hudson.model.User u = hudson.model.User.get(fixUsername(d.getUsername()));
|
2014-01-17 17:58:01 +01:00
|
|
|
try {
|
|
|
|
|
Attribute attribute = d.getAttributes().get(getDisplayNameAttributeName());
|
|
|
|
|
String displayName = attribute == null ? null : (String) attribute.get();
|
2014-09-25 20:27:42 +02:00
|
|
|
if (StringUtils.isNotBlank(displayName) && u.getId().equals(u.getFullName()) && !u.getFullName().equals(displayName)) {
|
2014-01-17 17:58:01 +01:00
|
|
|
u.setFullName(displayName);
|
|
|
|
|
}
|
|
|
|
|
} catch (NamingException e) {
|
|
|
|
|
LOGGER.log(Level.FINEST, "Could not retrieve display name attribute", e);
|
|
|
|
|
}
|
|
|
|
|
if (!disableMailAddressResolver) {
|
|
|
|
|
try {
|
|
|
|
|
Attribute attribute = d.getAttributes().get(getMailAddressAttributeName());
|
|
|
|
|
String mailAddress = attribute == null ? null : (String) attribute.get();
|
|
|
|
|
if (StringUtils.isNotBlank(mailAddress)) {
|
2014-09-25 20:27:42 +02:00
|
|
|
UserProperty existing = u.getProperty(UserProperty.class);
|
|
|
|
|
if (existing==null || !existing.hasExplicitlyConfiguredAddress())
|
|
|
|
|
u.addProperty(new Mailer.UserProperty(mailAddress));
|
2014-01-17 17:58:01 +01:00
|
|
|
}
|
|
|
|
|
} catch (NamingException e) {
|
|
|
|
|
LOGGER.log(Level.FINEST, "Could not retrieve email address attribute", e);
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
LOGGER.log(Level.WARNING, "Failed to associate the e-mail address", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return d;
|
2010-08-13 06:14:22 +02:00
|
|
|
}
|
|
|
|
|
|
2009-01-29 20:34:13 +01:00
|
|
|
@Override
|
|
|
|
|
public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException {
|
2014-05-09 11:20:27 +02:00
|
|
|
groupname = fixGroupname(groupname);
|
2013-04-24 13:42:25 +02:00
|
|
|
Set<String> cachedGroups;
|
|
|
|
|
if (cache != null) {
|
|
|
|
|
final CacheEntry<Set<String>> cached;
|
|
|
|
|
synchronized (this) {
|
|
|
|
|
cached = groupDetailsCache != null ? groupDetailsCache.get(groupname) : null;
|
|
|
|
|
}
|
|
|
|
|
if (cached != null && cached.isValid()) {
|
|
|
|
|
cachedGroups = cached.getValue();
|
|
|
|
|
} else {
|
|
|
|
|
cachedGroups = null;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
cachedGroups = null;
|
|
|
|
|
}
|
|
|
|
|
|
2009-01-29 20:34:13 +01:00
|
|
|
// TODO: obtain a DN instead so that we can obtain multiple attributes later
|
2009-02-02 20:36:23 +01:00
|
|
|
String searchBase = groupSearchBase != null ? groupSearchBase : "";
|
2013-06-14 17:10:20 +02:00
|
|
|
String searchFilter = groupSearchFilter != null ? groupSearchFilter : GROUP_SEARCH;
|
2013-04-24 13:42:25 +02:00
|
|
|
final Set<String> groups = cachedGroups != null
|
|
|
|
|
? cachedGroups
|
|
|
|
|
: (Set<String>) ldapTemplate
|
2013-06-14 17:10:20 +02:00
|
|
|
.searchForSingleAttributeValues(searchBase, searchFilter, new String[]{groupname}, "cn");
|
2013-04-24 13:42:25 +02:00
|
|
|
if (cache != null && cachedGroups == null && !groups.isEmpty()) {
|
|
|
|
|
synchronized (this) {
|
|
|
|
|
if (groupDetailsCache == null) {
|
|
|
|
|
groupDetailsCache = new CacheMap<String, Set<String>>(cache.getSize());
|
|
|
|
|
}
|
|
|
|
|
groupDetailsCache.put(groupname, new CacheEntry<Set<String>>(cache.getTtl(), groups));
|
|
|
|
|
}
|
|
|
|
|
}
|
2009-01-29 20:34:13 +01:00
|
|
|
|
|
|
|
|
if(groups.isEmpty())
|
|
|
|
|
throw new UsernameNotFoundException(groupname);
|
|
|
|
|
|
2014-05-09 11:20:27 +02:00
|
|
|
return new GroupDetailsImpl(fixGroupname(groups.iterator().next()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String fixGroupname(String groupname) {
|
|
|
|
|
return FORCE_GROUPNAME_LOWERCASE ? groupname.toLowerCase() : groupname;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String fixUsername(String username) {
|
|
|
|
|
return FORCE_USERNAME_LOWERCASE ? username.toLowerCase() : username;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class GroupDetailsImpl extends GroupDetails {
|
|
|
|
|
|
|
|
|
|
private String name;
|
|
|
|
|
|
|
|
|
|
public GroupDetailsImpl(String name) {
|
|
|
|
|
this.name = name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getName() {
|
|
|
|
|
return name;
|
|
|
|
|
}
|
2009-01-29 20:34:13 +01:00
|
|
|
}
|
|
|
|
|
|
2014-01-17 17:58:01 +01:00
|
|
|
private class LDAPAuthenticationManager implements AuthenticationManager {
|
|
|
|
|
|
|
|
|
|
private final AuthenticationManager delegate;
|
|
|
|
|
|
|
|
|
|
private LDAPAuthenticationManager(AuthenticationManager delegate) {
|
|
|
|
|
this.delegate = delegate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
|
|
|
|
return updateUserDetails(delegate.authenticate(authentication));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2009-08-15 04:38:23 +02:00
|
|
|
public static class LDAPUserDetailsService implements UserDetailsService {
|
|
|
|
|
public final LdapUserSearch ldapSearch;
|
|
|
|
|
public final LdapAuthoritiesPopulator authoritiesPopulator;
|
2014-05-22 16:43:20 +02:00
|
|
|
public final LDAPGroupMembershipStrategy groupMembershipStrategy;
|
2010-08-30 20:34:28 +02:00
|
|
|
/**
|
|
|
|
|
* {@link BasicAttributes} in LDAP tend to be bulky (about 20K at size), so interning them
|
|
|
|
|
* to keep the size under control. When a programmatic client is not smart enough to
|
|
|
|
|
* reuse a session, this helps keeping the memory consumption low.
|
|
|
|
|
*/
|
|
|
|
|
private final LRUMap attributesCache = new LRUMap(32);
|
|
|
|
|
|
2009-08-15 04:38:23 +02:00
|
|
|
LDAPUserDetailsService(WebApplicationContext appContext) {
|
2014-05-22 16:43:20 +02:00
|
|
|
this(appContext, null);
|
2009-08-15 04:38:23 +02:00
|
|
|
}
|
2010-08-30 20:34:28 +02:00
|
|
|
|
|
|
|
|
LDAPUserDetailsService(LdapUserSearch ldapSearch, LdapAuthoritiesPopulator authoritiesPopulator) {
|
2014-05-22 16:43:20 +02:00
|
|
|
this(ldapSearch, authoritiesPopulator, null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LDAPUserDetailsService(LdapUserSearch ldapSearch, LdapAuthoritiesPopulator authoritiesPopulator, LDAPGroupMembershipStrategy groupMembershipStrategy) {
|
2010-08-30 20:34:28 +02:00
|
|
|
this.ldapSearch = ldapSearch;
|
|
|
|
|
this.authoritiesPopulator = authoritiesPopulator;
|
2014-05-22 16:43:20 +02:00
|
|
|
this.groupMembershipStrategy = groupMembershipStrategy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LDAPUserDetailsService(WebApplicationContext appContext,
|
|
|
|
|
LDAPGroupMembershipStrategy groupMembershipStrategy) {
|
|
|
|
|
this(findBean(LdapUserSearch.class, appContext), findBean(LdapAuthoritiesPopulator.class, appContext), groupMembershipStrategy);
|
2010-08-30 20:34:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public LdapUserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
|
2014-05-09 11:20:27 +02:00
|
|
|
username = fixUsername(username);
|
2009-08-15 04:38:23 +02:00
|
|
|
try {
|
2013-04-24 13:57:39 +02:00
|
|
|
SecurityRealm securityRealm =
|
|
|
|
|
Jenkins.getInstance() == null ? null : Jenkins.getInstance().getSecurityRealm();
|
|
|
|
|
if (securityRealm instanceof LDAPSecurityRealm
|
|
|
|
|
&& securityRealm.getSecurityComponents().userDetails == this) {
|
|
|
|
|
LDAPSecurityRealm ldapSecurityRealm = (LDAPSecurityRealm) securityRealm;
|
|
|
|
|
if (ldapSecurityRealm.cache != null) {
|
|
|
|
|
final CacheEntry<LdapUserDetails> cached;
|
|
|
|
|
synchronized (ldapSecurityRealm) {
|
|
|
|
|
cached = (ldapSecurityRealm.userDetailsCache != null) ? ldapSecurityRealm.userDetailsCache
|
|
|
|
|
.get(username) : null;
|
|
|
|
|
}
|
|
|
|
|
if (cached != null && cached.isValid()) {
|
|
|
|
|
return cached.getValue();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2009-08-15 04:38:23 +02:00
|
|
|
LdapUserDetails ldapUser = ldapSearch.searchForUser(username);
|
|
|
|
|
// LdapUserSearch does not populate granted authorities (group search).
|
|
|
|
|
// Add those, as done in LdapAuthenticationProvider.createUserDetails().
|
|
|
|
|
if (ldapUser != null) {
|
|
|
|
|
LdapUserDetailsImpl.Essence user = new LdapUserDetailsImpl.Essence(ldapUser);
|
2010-08-30 20:34:28 +02:00
|
|
|
|
|
|
|
|
// intern attributes
|
|
|
|
|
Attributes v = ldapUser.getAttributes();
|
|
|
|
|
if (v instanceof BasicAttributes) {// BasicAttributes.equals is what makes the interning possible
|
2011-02-18 19:34:46 +01:00
|
|
|
synchronized (attributesCache) {
|
|
|
|
|
Attributes vv = (Attributes)attributesCache.get(v);
|
|
|
|
|
if (vv==null) attributesCache.put(v,vv=v);
|
|
|
|
|
user.setAttributes(vv);
|
|
|
|
|
}
|
2010-08-30 20:34:28 +02:00
|
|
|
}
|
|
|
|
|
|
2014-05-22 16:43:20 +02:00
|
|
|
GrantedAuthority[] extraAuthorities = groupMembershipStrategy == null
|
|
|
|
|
? authoritiesPopulator.getGrantedAuthorities(ldapUser)
|
|
|
|
|
: groupMembershipStrategy.getGrantedAuthorities(ldapUser);
|
2010-08-30 20:34:28 +02:00
|
|
|
for (GrantedAuthority extraAuthority : extraAuthorities) {
|
2014-05-22 16:43:20 +02:00
|
|
|
if (FORCE_GROUPNAME_LOWERCASE) {
|
|
|
|
|
user.addAuthority(new GrantedAuthorityImpl(extraAuthority.getAuthority().toLowerCase()));
|
|
|
|
|
} else {
|
|
|
|
|
user.addAuthority(extraAuthority);
|
|
|
|
|
}
|
2009-08-15 04:38:23 +02:00
|
|
|
}
|
|
|
|
|
ldapUser = user.createUserDetails();
|
|
|
|
|
}
|
2013-04-24 13:57:39 +02:00
|
|
|
if (securityRealm instanceof LDAPSecurityRealm
|
|
|
|
|
&& securityRealm.getSecurityComponents().userDetails == this) {
|
|
|
|
|
LDAPSecurityRealm ldapSecurityRealm = (LDAPSecurityRealm) securityRealm;
|
|
|
|
|
if (ldapSecurityRealm.cache != null) {
|
|
|
|
|
synchronized (ldapSecurityRealm) {
|
|
|
|
|
if (ldapSecurityRealm.userDetailsCache == null) {
|
|
|
|
|
ldapSecurityRealm.userDetailsCache =
|
|
|
|
|
new CacheMap<String, LdapUserDetails>(ldapSecurityRealm.cache.getSize());
|
|
|
|
|
}
|
|
|
|
|
ldapSecurityRealm.userDetailsCache.put(username,
|
2014-01-17 17:58:01 +01:00
|
|
|
new CacheEntry<LdapUserDetails>(ldapSecurityRealm.cache.getTtl(),
|
|
|
|
|
ldapSecurityRealm.updateUserDetails(ldapUser)));
|
2013-04-24 13:57:39 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2009-08-15 04:38:23 +02:00
|
|
|
return ldapUser;
|
|
|
|
|
} catch (LdapDataAccessException e) {
|
|
|
|
|
LOGGER.log(Level.WARNING, "Failed to search LDAP for username="+username,e);
|
|
|
|
|
throw new UserMayOrMayNotExistException(e.getMessage(),e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2008-03-28 00:46:20 +01:00
|
|
|
/**
|
|
|
|
|
* If the security realm is LDAP, try to pick up e-mail address from LDAP.
|
|
|
|
|
*/
|
2009-03-05 19:28:31 +01:00
|
|
|
@Extension
|
2008-03-28 00:46:20 +01:00
|
|
|
public static final class MailAdressResolverImpl extends MailAddressResolver {
|
|
|
|
|
public String findMailAddressFor(User u) {
|
|
|
|
|
// LDAP not active
|
2011-05-28 19:11:48 +02:00
|
|
|
SecurityRealm realm = Jenkins.getInstance().getSecurityRealm();
|
2009-03-05 19:28:31 +01:00
|
|
|
if(!(realm instanceof LDAPSecurityRealm))
|
2008-03-28 00:46:20 +01:00
|
|
|
return null;
|
2012-12-05 23:26:09 +01:00
|
|
|
if (((LDAPSecurityRealm)realm).disableMailAddressResolver) {
|
2012-12-05 23:16:06 +01:00
|
|
|
LOGGER.info( "LDAPSecurityRealm MailAddressResolver is disabled" );
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2008-03-28 00:46:20 +01:00
|
|
|
try {
|
2009-03-05 19:28:31 +01:00
|
|
|
LdapUserDetails details = (LdapUserDetails)realm.getSecurityComponents().userDetails.loadUserByUsername(u.getId());
|
2014-01-17 17:58:01 +01:00
|
|
|
Attribute mail = details.getAttributes().get(((LDAPSecurityRealm)realm).getMailAddressAttributeName());
|
2008-03-28 00:46:20 +01:00
|
|
|
if(mail==null) return null; // not found
|
|
|
|
|
return (String)mail.get();
|
|
|
|
|
} catch (UsernameNotFoundException e) {
|
|
|
|
|
LOGGER.log(Level.FINE, "Failed to look up LDAP for e-mail address",e);
|
|
|
|
|
return null;
|
|
|
|
|
} catch (DataAccessException e) {
|
|
|
|
|
LOGGER.log(Level.FINE, "Failed to look up LDAP for e-mail address",e);
|
|
|
|
|
return null;
|
|
|
|
|
} catch (NamingException e) {
|
|
|
|
|
LOGGER.log(Level.FINE, "Failed to look up LDAP for e-mail address",e);
|
|
|
|
|
return null;
|
2009-08-17 20:36:47 +02:00
|
|
|
} catch (AcegiSecurityException e) {
|
|
|
|
|
LOGGER.log(Level.FINE, "Failed to look up LDAP for e-mail address",e);
|
|
|
|
|
return null;
|
2008-03-28 00:46:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2009-02-26 04:21:23 +01:00
|
|
|
/**
|
|
|
|
|
* {@link LdapAuthoritiesPopulator} that adds the automatic 'authenticated' role.
|
|
|
|
|
*/
|
|
|
|
|
public static final class AuthoritiesPopulatorImpl extends DefaultLdapAuthoritiesPopulator {
|
2009-08-15 04:38:23 +02:00
|
|
|
// Make these available (private in parent class and no get methods!)
|
2011-03-19 07:28:34 +01:00
|
|
|
String rolePrefix = "ROLE_";
|
|
|
|
|
boolean convertToUpperCase = true;
|
|
|
|
|
|
2009-02-26 04:21:23 +01:00
|
|
|
public AuthoritiesPopulatorImpl(InitialDirContextFactory initialDirContextFactory, String groupSearchBase) {
|
2009-09-24 20:02:30 +02:00
|
|
|
super(initialDirContextFactory, fixNull(groupSearchBase));
|
2011-03-19 07:28:34 +01:00
|
|
|
|
|
|
|
|
super.setRolePrefix("");
|
|
|
|
|
super.setConvertToUpperCase(false);
|
2009-02-26 04:21:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected Set getAdditionalRoles(LdapUserDetails ldapUser) {
|
|
|
|
|
return Collections.singleton(AUTHENTICATED_AUTHORITY);
|
|
|
|
|
}
|
2009-08-15 04:38:23 +02:00
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setRolePrefix(String rolePrefix) {
|
2011-03-19 07:28:34 +01:00
|
|
|
// super.setRolePrefix(rolePrefix);
|
2009-08-15 04:38:23 +02:00
|
|
|
this.rolePrefix = rolePrefix;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void setConvertToUpperCase(boolean convertToUpperCase) {
|
2011-03-19 07:28:34 +01:00
|
|
|
// super.setConvertToUpperCase(convertToUpperCase);
|
2009-08-15 04:38:23 +02:00
|
|
|
this.convertToUpperCase = convertToUpperCase;
|
|
|
|
|
}
|
2011-03-19 07:28:34 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Retrieves the group membership in two ways.
|
|
|
|
|
*
|
|
|
|
|
* We'd like to retain the original name, but we historically used to do "ROLE_GROUPNAME".
|
|
|
|
|
* So to remain backward compatible, we make the super class pass the unmodified "groupName",
|
|
|
|
|
* then do the backward compatible translation here, so that the user gets both "ROLE_GROUPNAME" and "groupName".
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public Set getGroupMembershipRoles(String userDn, String username) {
|
|
|
|
|
Set<GrantedAuthority> names = super.getGroupMembershipRoles(userDn,username);
|
|
|
|
|
|
|
|
|
|
Set<GrantedAuthority> r = new HashSet<GrantedAuthority>(names.size()*2);
|
2014-05-22 16:43:20 +02:00
|
|
|
r.addAll(names);
|
2011-03-19 07:28:34 +01:00
|
|
|
|
|
|
|
|
for (GrantedAuthority ga : names) {
|
|
|
|
|
String role = ga.getAuthority();
|
|
|
|
|
|
|
|
|
|
// backward compatible name mangling
|
|
|
|
|
if (convertToUpperCase)
|
|
|
|
|
role = role.toUpperCase();
|
|
|
|
|
r.add(new GrantedAuthorityImpl(rolePrefix + role));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return r;
|
|
|
|
|
}
|
2009-02-26 04:21:23 +01:00
|
|
|
}
|
|
|
|
|
|
2009-02-24 01:05:47 +01:00
|
|
|
@Extension
|
2008-01-05 01:17:06 +01:00
|
|
|
public static final class DescriptorImpl extends Descriptor<SecurityRealm> {
|
2014-05-07 19:55:05 +02:00
|
|
|
|
|
|
|
|
public static final String DEFAULT_DISPLAYNAME_ATTRIBUTE_NAME = "displayname";
|
|
|
|
|
public static final String DEFAULT_MAILADDRESS_ATTRIBUTE_NAME = "mail";
|
|
|
|
|
public static final String DEFAULT_USER_SEARCH = "uid={0}";
|
|
|
|
|
|
2007-12-05 08:09:29 +01:00
|
|
|
public String getDisplayName() {
|
2008-06-20 15:56:47 +02:00
|
|
|
return Messages.LDAPSecurityRealm_DisplayName();
|
2007-12-05 08:09:29 +01:00
|
|
|
}
|
2008-01-05 01:17:06 +01:00
|
|
|
|
2014-08-12 11:51:41 +02:00
|
|
|
public IdStrategy getDefaultIdStrategy() {
|
|
|
|
|
return IdStrategy.CASE_INSENSITIVE;
|
|
|
|
|
}
|
|
|
|
|
|
2014-08-12 16:19:45 +02:00
|
|
|
// BEGIN TODO Jenkins 1.577+
|
|
|
|
|
@Deprecated
|
|
|
|
|
public static IdStrategy fromClassName(String className) {
|
|
|
|
|
for (Descriptor<IdStrategy> d: Jenkins.getInstance().getDescriptorList(IdStrategy.class)) {
|
|
|
|
|
if (d.clazz.getName().equals(className)) {
|
|
|
|
|
try {
|
|
|
|
|
return d.clazz.newInstance();
|
|
|
|
|
} catch (InstantiationException e) {
|
|
|
|
|
// ignore
|
|
|
|
|
} catch (IllegalAccessException e) {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return IdStrategy.CASE_INSENSITIVE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Deprecated
|
|
|
|
|
public ListBoxModel doFillUserIdStrategyClassItems() {
|
|
|
|
|
ListBoxModel result = new ListBoxModel();
|
|
|
|
|
for (Descriptor<IdStrategy> d: Jenkins.getInstance().getDescriptorList(IdStrategy.class)) {
|
|
|
|
|
try {
|
|
|
|
|
d.clazz.newInstance();
|
|
|
|
|
result.add(d.getDisplayName(), d.clazz.getName());
|
|
|
|
|
} catch (InstantiationException e) {
|
|
|
|
|
// ignore
|
|
|
|
|
} catch (IllegalAccessException e) {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Deprecated
|
|
|
|
|
public ListBoxModel doFillGroupIdStrategyClassItems() {
|
|
|
|
|
return doFillUserIdStrategyClassItems();
|
|
|
|
|
}
|
|
|
|
|
// END TODO Jenkins 1.577+
|
|
|
|
|
|
2014-05-07 19:55:05 +02:00
|
|
|
// note that this works better in 1.528+ (JENKINS-19124)
|
2014-05-07 20:16:35 +02:00
|
|
|
public FormValidation doCheckServer(@QueryParameter String value, @QueryParameter String managerDN, @QueryParameter Secret managerPasswordSecret) {
|
2014-05-07 19:55:05 +02:00
|
|
|
String server = value;
|
2014-05-07 20:16:35 +02:00
|
|
|
String managerPassword = Secret.toString(managerPasswordSecret);
|
2008-01-05 01:17:06 +01:00
|
|
|
|
2011-05-28 19:11:48 +02:00
|
|
|
if(!Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER))
|
2009-03-23 04:26:55 +01:00
|
|
|
return FormValidation.ok();
|
2008-01-05 01:17:06 +01:00
|
|
|
|
2009-03-23 04:26:55 +01:00
|
|
|
try {
|
|
|
|
|
Hashtable<String,String> props = new Hashtable<String,String>();
|
|
|
|
|
if(managerDN!=null && managerDN.trim().length() > 0 && !"undefined".equals(managerDN)) {
|
|
|
|
|
props.put(Context.SECURITY_PRINCIPAL,managerDN);
|
|
|
|
|
}
|
|
|
|
|
if(managerPassword!=null && managerPassword.trim().length() > 0 && !"undefined".equals(managerPassword)) {
|
|
|
|
|
props.put(Context.SECURITY_CREDENTIALS,managerPassword);
|
2008-01-05 01:17:06 +01:00
|
|
|
}
|
2009-08-19 03:24:49 +02:00
|
|
|
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
|
2013-07-24 16:29:42 +02:00
|
|
|
props.put(Context.PROVIDER_URL, toProviderUrl(server, ""));
|
2009-08-19 03:24:49 +02:00
|
|
|
|
|
|
|
|
DirContext ctx = new InitialDirContext(props);
|
2009-03-23 04:26:55 +01:00
|
|
|
ctx.getAttributes("");
|
|
|
|
|
return FormValidation.ok(); // connected
|
|
|
|
|
} catch (NamingException e) {
|
|
|
|
|
// trouble-shoot
|
2013-07-24 16:29:42 +02:00
|
|
|
Matcher m = Pattern.compile("(ldaps?://)?([^:]+)(?:\\:(\\d+))?(\\s+(ldaps?://)?([^:]+)(?:\\:(\\d+))?)*").matcher(server.trim());
|
2009-03-23 04:26:55 +01:00
|
|
|
if(!m.matches())
|
2010-02-28 15:15:28 +01:00
|
|
|
return FormValidation.error(Messages.LDAPSecurityRealm_SyntaxOfServerField());
|
2009-03-23 04:26:55 +01:00
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
InetAddress adrs = InetAddress.getByName(m.group(2));
|
|
|
|
|
int port = m.group(1)!=null ? 636 : 389;
|
|
|
|
|
if(m.group(3)!=null)
|
|
|
|
|
port = Integer.parseInt(m.group(3));
|
|
|
|
|
Socket s = new Socket(adrs,port);
|
|
|
|
|
s.close();
|
|
|
|
|
} catch (UnknownHostException x) {
|
2010-02-28 15:15:28 +01:00
|
|
|
return FormValidation.error(Messages.LDAPSecurityRealm_UnknownHost(x.getMessage()));
|
2009-03-23 04:26:55 +01:00
|
|
|
} catch (IOException x) {
|
2010-03-31 00:41:24 +02:00
|
|
|
return FormValidation.error(x,Messages.LDAPSecurityRealm_UnableToConnect(server, x.getMessage()));
|
2009-03-23 04:26:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// otherwise we don't know what caused it, so fall back to the general error report
|
|
|
|
|
// getMessage() alone doesn't offer enough
|
2010-03-31 00:41:24 +02:00
|
|
|
return FormValidation.error(e,Messages.LDAPSecurityRealm_UnableToConnect(server, e));
|
2009-03-23 04:26:55 +01:00
|
|
|
} catch (NumberFormatException x) {
|
|
|
|
|
// The getLdapCtxInstance method throws this if it fails to parse the port number
|
2010-02-28 15:15:28 +01:00
|
|
|
return FormValidation.error(Messages.LDAPSecurityRealm_InvalidPortNumber());
|
2009-03-23 04:26:55 +01:00
|
|
|
}
|
2008-01-05 01:17:06 +01:00
|
|
|
}
|
2014-05-22 16:43:20 +02:00
|
|
|
|
|
|
|
|
public DescriptorExtensionList<LDAPGroupMembershipStrategy, Descriptor<LDAPGroupMembershipStrategy>> getGroupMembershipStrategies() {
|
|
|
|
|
return Jenkins.getInstance().getDescriptorList(LDAPGroupMembershipStrategy.class);
|
|
|
|
|
}
|
2007-12-05 08:09:29 +01:00
|
|
|
}
|
2007-12-20 06:14:46 +01:00
|
|
|
|
2008-08-22 03:36:16 +02:00
|
|
|
/**
|
|
|
|
|
* If the given "server name" is just a host name (plus optional host name), add ldap:// prefix.
|
|
|
|
|
* Otherwise assume it already contains the scheme, and leave it intact.
|
|
|
|
|
*/
|
|
|
|
|
private static String addPrefix(String server) {
|
|
|
|
|
if(server.contains("://")) return server;
|
|
|
|
|
else return "ldap://"+server;
|
|
|
|
|
}
|
|
|
|
|
|
2008-01-05 01:17:06 +01:00
|
|
|
private static final Logger LOGGER = Logger.getLogger(LDAPSecurityRealm.class.getName());
|
2009-01-29 20:34:13 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* LDAP filter to look for groups by their names.
|
|
|
|
|
*
|
|
|
|
|
* "{0}" is the group name as given by the user.
|
|
|
|
|
* See http://msdn.microsoft.com/en-us/library/aa746475(VS.85).aspx for the syntax by example.
|
|
|
|
|
* WANTED: The specification of the syntax.
|
|
|
|
|
*/
|
|
|
|
|
public static String GROUP_SEARCH = System.getProperty(LDAPSecurityRealm.class.getName()+".groupSearch",
|
|
|
|
|
"(& (cn={0}) (| (objectclass=groupOfNames) (objectclass=groupOfUniqueNames) (objectclass=posixGroup)))");
|
2013-04-24 13:42:25 +02:00
|
|
|
|
2014-05-07 19:55:05 +02:00
|
|
|
public static class CacheConfiguration extends AbstractDescribableImpl<CacheConfiguration> {
|
2013-04-24 13:42:25 +02:00
|
|
|
private final int size;
|
|
|
|
|
private final int ttl;
|
|
|
|
|
|
|
|
|
|
@DataBoundConstructor
|
|
|
|
|
public CacheConfiguration(int size, int ttl) {
|
|
|
|
|
this.size = Math.max(10, Math.min(size, 1000));
|
|
|
|
|
this.ttl = Math.max(30, Math.min(ttl, 3600));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getSize() {
|
|
|
|
|
return size;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public int getTtl() {
|
|
|
|
|
return ttl;
|
|
|
|
|
}
|
2014-05-07 19:55:05 +02:00
|
|
|
|
|
|
|
|
@Extension public static class DescriptorImpl extends Descriptor<CacheConfiguration> {
|
|
|
|
|
|
|
|
|
|
@Override public String getDisplayName() {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ListBoxModel doFillSizeItems() {
|
|
|
|
|
ListBoxModel m = new ListBoxModel();
|
|
|
|
|
m.add("10");
|
|
|
|
|
m.add("20");
|
|
|
|
|
m.add("50");
|
|
|
|
|
m.add("100");
|
|
|
|
|
m.add("200");
|
|
|
|
|
m.add("500");
|
|
|
|
|
m.add("1000");
|
|
|
|
|
return m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public ListBoxModel doFillTtlItems() {
|
|
|
|
|
ListBoxModel m = new ListBoxModel();
|
|
|
|
|
// TODO use Messages (not that there were any translations before)
|
|
|
|
|
m.add("30 sec", "30");
|
|
|
|
|
m.add("1 min", "60");
|
|
|
|
|
m.add("2 min", "120");
|
|
|
|
|
m.add("5 min", "300");
|
|
|
|
|
m.add("10 min", "600");
|
|
|
|
|
m.add("15 min", "900");
|
|
|
|
|
m.add("30 min", "1800");
|
|
|
|
|
m.add("1 hour", "3600");
|
|
|
|
|
return m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2013-04-24 13:42:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static class CacheEntry<T> {
|
|
|
|
|
private final long expires;
|
|
|
|
|
private final T value;
|
|
|
|
|
|
|
|
|
|
public CacheEntry(int ttlSeconds, T value) {
|
|
|
|
|
this.expires = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(ttlSeconds);
|
|
|
|
|
this.value = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public T getValue() {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean isValid() {
|
|
|
|
|
return System.currentTimeMillis() < expires;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* While we could use Guava's CacheBuilder the method signature changes make using it problematic.
|
|
|
|
|
* Safer to roll our own and ensure compatibility across as wide a range of Jenkins versions as possible.
|
|
|
|
|
*
|
|
|
|
|
* @param <K> Key type
|
|
|
|
|
* @param <V> Cache entry type
|
|
|
|
|
*/
|
|
|
|
|
private static class CacheMap<K, V> extends LinkedHashMap<K, CacheEntry<V>> {
|
|
|
|
|
|
|
|
|
|
private final int cacheSize;
|
|
|
|
|
|
|
|
|
|
public CacheMap(int cacheSize) {
|
|
|
|
|
super(cacheSize + 1); // prevent realloc when hitting cache size limit
|
|
|
|
|
this.cacheSize = cacheSize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected boolean removeEldestEntry(Map.Entry<K, CacheEntry<V>> eldest) {
|
|
|
|
|
return size() > cacheSize || eldest.getValue() == null || !eldest.getValue().isValid();
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-12-09 13:22:29 +01:00
|
|
|
|
|
|
|
|
public static class EnvironmentProperty extends AbstractDescribableImpl<EnvironmentProperty> implements Serializable {
|
|
|
|
|
private final String name;
|
|
|
|
|
private final String value;
|
|
|
|
|
|
|
|
|
|
@DataBoundConstructor
|
|
|
|
|
public EnvironmentProperty(String name, String value) {
|
|
|
|
|
this.name = name;
|
|
|
|
|
this.value = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getName() {
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getValue() {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static Map<String,String> toMap(List<EnvironmentProperty> properties) {
|
|
|
|
|
if (properties != null) {
|
|
|
|
|
final Map<String, String> result = new LinkedHashMap<String, String>();
|
|
|
|
|
for (EnvironmentProperty property:properties) {
|
|
|
|
|
result.put(property.getName(), property.getValue());
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Extension
|
|
|
|
|
public static class DescriptorImpl extends Descriptor<EnvironmentProperty> {
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String getDisplayName() {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2007-12-05 08:09:29 +01:00
|
|
|
}
|