diff --git a/pom.xml b/pom.xml
index 5e7c9a3..1c123ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,6 +57,16 @@
mailer
1.5
+
+ com.atlassian.crowd
+ crowd-integration-client-rest
+ 2.9.0-OD-073
+
+
+ org.apache.commons
+ commons-lang3
+ 3.4
+
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.java
index 836355a..03982c3 100644
--- a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.java
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.java
@@ -35,7 +35,6 @@
import hudson.security.*;
import hudson.tasks.Mailer;
import hudson.tasks.Mailer.UserProperty;
-import hudson.util.FormValidation;
import hudson.util.Scrambler;
import hudson.util.spring.BeanBuilder;
@@ -44,20 +43,13 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
+import java.io.ObjectStreamException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Hashtable;
-import java.util.List;
import java.util.Set;
-import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
@@ -79,11 +71,8 @@
import jenkins.security.ApiTokenProperty;
import org.acegisecurity.Authentication;
-import org.acegisecurity.AuthenticationManager;
import org.acegisecurity.GrantedAuthority;
-import org.acegisecurity.GrantedAuthorityImpl;
import org.acegisecurity.context.SecurityContextHolder;
-import org.acegisecurity.ldap.InitialDirContextFactory;
import org.acegisecurity.ldap.LdapDataAccessException;
import org.acegisecurity.ldap.LdapTemplate;
import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch;
@@ -92,17 +81,15 @@
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
-import org.apache.commons.io.input.AutoCloseInputStream;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.reverse_proxy_auth.auth.ReverseProxyAuthoritiesPopulator;
-import org.jenkinsci.plugins.reverse_proxy_auth.data.GroupSearchTemplate;
-import org.jenkinsci.plugins.reverse_proxy_auth.data.SearchTemplate;
-import org.jenkinsci.plugins.reverse_proxy_auth.data.UserSearchTemplate;
+import org.jenkinsci.plugins.reverse_proxy_auth.types.AuthorizationTypeMappingFactory;
import org.jenkinsci.plugins.reverse_proxy_auth.model.ReverseProxyUserDetails;
-import org.jenkinsci.plugins.reverse_proxy_auth.service.ProxyLDAPAuthoritiesPopulator;
-import org.jenkinsci.plugins.reverse_proxy_auth.service.ProxyLDAPUserDetailsService;
+import org.jenkinsci.plugins.reverse_proxy_auth.types.GroupsAuthorizationType;
+import org.jenkinsci.plugins.reverse_proxy_auth.types.LdapAuthorizationType;
+import org.kohsuke.accmod.Restricted;
+import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
-import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
@@ -122,6 +109,7 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
* See http://msdn.microsoft.com/en-us/library/aa746475(VS.85).aspx for the syntax by example.
* WANTED: The specification of the syntax.
*/
+ @Deprecated
@SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "May be used in system groovy scripts")
public static String GROUP_SEARCH = System.getProperty(LDAPSecurityRealm.class.getName()+".groupSearch",
"(& (cn={0}) (| (objectclass=groupOfNames) (objectclass=groupOfUniqueNames) (objectclass=posixGroup)))");
@@ -134,21 +122,25 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
/**
* Scrambled password, used to first bind to LDAP.
*/
- private final String managerPassword;
+ @Deprecated
+ private transient final String managerPassword;
/**
* Search Template used when the groups are in the header.
*/
+ @Deprecated
private ReverseProxySearchTemplate proxyTemplate;
/**
* Created in {@link #createSecurityComponents()}. Can be used to connect to LDAP.
*/
+ @Deprecated
private transient LdapTemplate ldapTemplate;
/**
* Keeps the state of connected users and their granted authorities.
*/
+ @Deprecated
private transient Hashtable authContext;
/**
@@ -161,20 +153,24 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
* 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".
*/
- public final String server;
+ @Deprecated
+ public transient 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;
+
+ @Deprecated
+ public transient final String rootDN;
/**
* Allow the rootDN to be inferred? Default is false.
* If true, allow rootDN to be blank.
*/
- public final boolean inhibitInferRootDN;
+ @Deprecated
+ public transient final boolean inhibitInferRootDN;
/**
* Specifies the relative DN from {@link #rootDN the root DN}.
@@ -182,7 +178,8 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
*
* Something like "ou=people" but can be empty.
*/
- public final String userSearchBase;
+ @Deprecated
+ public transient final String userSearchBase;
/**
* Query to locate an entry that identifies the user, given the user name string.
@@ -191,7 +188,8 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
*
* @see FilterBasedLdapUserSearch
*/
- public final String userSearch;
+ @Deprecated
+ public transient final String userSearch;
/**
* This defines the organizational unit that contains groups.
@@ -201,7 +199,8 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
*
* @see FilterBasedLdapUserSearch
*/
- public final String groupSearchBase;
+ @Deprecated
+ public transient final String groupSearchBase;
/**
* Query to locate an entry that identifies the group, given the group name string. If non-null it will override
@@ -209,20 +208,23 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
*
* @since 1.5
*/
- public final String groupSearchFilter;
+ @Deprecated
+ public transient final String groupSearchFilter;
/**
* Query to locate the group entries that a user belongs to, given the user object. {0}
* is the user's full DN while {1} is the username.
*/
- public final String groupMembershipFilter;
+ @Deprecated
+ public transient final String groupMembershipFilter;
/**
* Attribute that should be used instead of CN as name to match a users group name to the groupSearchFilter name.
* When {@link #groupSearchFilter} is set to search for a field other than CN e.g. GroupDisplayName={0}
* here you can configure that this (GroupDisplayName
) or another field should be used when looking for a users groups.
*/
- public String groupNameAttribute;
+ @Deprecated
+ public transient String groupNameAttribute;
/**
* If non-null, we use this and {@link #managerPassword}
@@ -230,12 +232,14 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
*
* This is necessary when LDAP doesn't support anonymous access.
*/
- public final String managerDN;
+ @Deprecated
+ public transient final String managerDN;
/**
* Sets an interval for updating the LDAP authorities. The interval is specified in minutes.
*/
- public final int updateInterval;
+ @Deprecated
+ public transient final int updateInterval;
/**
* The authorities that are granted to the authenticated user.
@@ -257,18 +261,23 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
/**
* Header name of the groups field.
*/
- public final String headerGroups;
+ @Deprecated
+ public transient final String headerGroups;
/**
* Header name of the groups delimiter field.
*/
- public final String headerGroupsDelimiter;
+ @Deprecated
+ public transient final String headerGroupsDelimiter;
- public final boolean disableLdapEmailResolver;
+ @Deprecated
+ public transient final boolean disableLdapEmailResolver;
- private final String displayNameLdapAttribute;
+ @Deprecated
+ private transient final String displayNameLdapAttribute;
- private final String emailAddressLdapAttribute;
+ @Deprecated
+ private transient final String emailAddressLdapAttribute;
/**
* Custom post logout url
@@ -276,7 +285,12 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
public final String customLogInUrl;
public final String customLogOutUrl;
- @DataBoundConstructor
+ /**
+ * Represents the Authorization Types Mapping Factory
+ */
+ private AuthorizationTypeMappingFactory authorizationTypeMappingFactory;
+
+ @Deprecated
public ReverseProxySecurityRealm(String forwardedUser, String headerGroups, String headerGroupsDelimiter, String customLogInUrl, String customLogOutUrl, String server, String rootDN, boolean inhibitInferRootDN,
String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, String groupMembershipFilter, String groupNameAttribute, String managerDN, String managerPassword,
Integer updateInterval, boolean disableLdapEmailResolver, String displayNameLdapAttribute, String emailAddressLdapAttribute) {
@@ -331,6 +345,96 @@ public ReverseProxySecurityRealm(String forwardedUser, String headerGroups, Stri
this.emailAddressLdapAttribute = emailAddressLdapAttribute;
}
+ @DataBoundConstructor
+ public ReverseProxySecurityRealm(String forwardedUser, String headerGroups, String headerGroupsDelimiter, String customLogInUrl, String customLogOutUrl, String server, String rootDN, boolean inhibitInferRootDN,
+ String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, String groupMembershipFilter, String groupNameAttribute, String managerDN, String managerPassword,
+ Integer updateInterval, boolean disableLdapEmailResolver, String displayNameLdapAttribute, String emailAddressLdapAttribute, AuthorizationTypeMappingFactory authorizationTypeMappingFactory) {
+
+ this.forwardedUser = fixEmptyAndTrim(forwardedUser);
+
+ this.headerGroups = headerGroups;
+ if (!StringUtils.isBlank(headerGroupsDelimiter)) {
+ this.headerGroupsDelimiter = headerGroupsDelimiter.trim();
+ } else {
+ this.headerGroupsDelimiter = "|";
+ }
+
+ if(!StringUtils.isBlank(customLogInUrl)) {
+ this.customLogInUrl = customLogInUrl;
+ } else {
+ this.customLogInUrl = null;
+ }
+
+ if(!StringUtils.isBlank(customLogOutUrl)) {
+ this.customLogOutUrl = customLogOutUrl;
+ } else {
+ this.customLogOutUrl = null;
+ }
+
+ this.server = fixEmptyAndTrim(server);
+ this.managerDN = fixEmpty(managerDN);
+ this.managerPassword = Scrambler.scramble(fixEmpty(managerPassword));
+ this.inhibitInferRootDN = inhibitInferRootDN;
+
+ if (this.server != null) {
+ if(!inhibitInferRootDN && fixEmptyAndTrim(rootDN) == null) rootDN = fixNull(inferRootDN(server));
+ this.rootDN = rootDN.trim();
+ } else {
+ this.rootDN = null;
+ }
+
+ this.userSearchBase = fixNull(userSearchBase).trim();
+ userSearch = fixEmptyAndTrim(userSearch);
+ this.userSearch = userSearch != null ? userSearch : "uid={0}";
+ this.groupSearchBase = fixEmptyAndTrim(groupSearchBase);
+ this.groupSearchFilter = fixEmptyAndTrim(groupSearchFilter);
+ this.groupMembershipFilter = fixEmptyAndTrim(groupMembershipFilter);
+ this.groupNameAttribute = fixEmptyAndTrim(groupNameAttribute);
+
+ this.updateInterval = (updateInterval == null || updateInterval <= 0) ? CHECK_INTERVAL : updateInterval;
+
+ authorities = new GrantedAuthority[0];
+
+ this.disableLdapEmailResolver = disableLdapEmailResolver;
+ this.displayNameLdapAttribute = displayNameLdapAttribute;
+ this.emailAddressLdapAttribute = emailAddressLdapAttribute;
+
+ this.authorizationTypeMappingFactory = authorizationTypeMappingFactory;
+ }
+
+ public Object readResolve() throws ObjectStreamException {
+ // We are using LDAP authorization model
+ if (getServerUrl() != null) {
+ LdapAuthorizationType ldapAuthorizationType = new LdapAuthorizationType(
+ server,
+ rootDN,
+ inhibitInferRootDN,
+ userSearchBase,
+ userSearch,
+ groupSearchBase,
+ groupSearchFilter,
+ groupMembershipFilter,
+ groupNameAttribute,
+ managerDN,
+ managerPassword,
+ displayNameLdapAttribute,
+ emailAddressLdapAttribute,
+ disableLdapEmailResolver,
+ updateInterval
+ );
+
+ this.authorizationTypeMappingFactory = ldapAuthorizationType;
+ } else {
+ GroupsAuthorizationType groupsAuthorizationType = new GroupsAuthorizationType(
+ headerGroups,
+ headerGroupsDelimiter
+ );
+ this.authorizationTypeMappingFactory = groupsAuthorizationType;
+
+ }
+ return this;
+ }
+
/**
* Name of the HTTP header to look at.
*/
@@ -338,14 +442,17 @@ public String getForwardedUser() {
return forwardedUser;
}
+ @Deprecated
public String getHeaderGroups() {
return headerGroups;
}
+ @Deprecated
public String getHeaderGroupsDelimiter() {
return headerGroupsDelimiter;
}
+ @Deprecated
@CheckForNull
public String getServerUrl() {
if (server == null) {
@@ -362,35 +469,47 @@ public String getServerUrl() {
return buf.toString();
}
+ @Deprecated
public String getGroupSearchFilter() {
return groupSearchFilter;
}
+ @Deprecated
public String getGroupMembershipFilter() {
return groupMembershipFilter;
}
-
+
+ @Deprecated
public String getGroupNameAttribute() {
return groupNameAttribute;
}
-
+
+ @Deprecated
public void setGroupNameAttribute(String groupNameAttribute) {
this.groupNameAttribute = groupNameAttribute;
}
-
+
+ @Deprecated
public String getDisplayNameLdapAttribute() {
return displayNameLdapAttribute;
}
-
+
+ @Deprecated
public String getEmailAddressLdapAttribute() {
return emailAddressLdapAttribute;
}
-
+
+ @Restricted(NoExternalUse.class)
+ public AuthorizationTypeMappingFactory getAuthorizationTypeMappingFactory() {
+ return authorizationTypeMappingFactory;
+ }
+
/**
* Infer the root DN.
*
* @return null if not found.
*/
+ @Deprecated
private String inferRootDN(String server) {
try {
Hashtable props = new Hashtable();
@@ -421,6 +540,7 @@ private String inferRootDN(String server) {
}
}
+ @Deprecated
@Nullable
public static String toProviderUrl(@CheckForNull String serverUrl, @CheckForNull String rootDN) {
if (serverUrl == null) {
@@ -439,14 +559,17 @@ public static String toProviderUrl(@CheckForNull String serverUrl, @CheckForNull
return buf.toString();
}
+ @Deprecated
public String getManagerPassword() {
return Scrambler.descramble(managerPassword);
}
+ @Deprecated
public int getUpdateInterval() {
return updateInterval;
}
-
+
+ @Deprecated
public String getLDAPURL() {
return toProviderUrl(getServerUrl(), fixNull(rootDN));
}
@@ -491,65 +614,7 @@ public void doFilter(ServletRequest request,
userFromHeader = userFromApiToken;
}
- if (authContext == null) {
- authContext = new Hashtable<>();
- }
-
- if (getLDAPURL() != null) {
-
- GrantedAuthority [] storedGrants = authContext.get(userFromHeader);
- if (storedGrants != null && storedGrants.length > 1) {
- authorities = retrieveAuthoritiesIfNecessary(userFromHeader, storedGrants);
- } else {
- try {
- LdapUserDetails userDetails = (LdapUserDetails) loadUserByUsername(userFromHeader);
- authorities = userDetails.getAuthorities();
-
- Set tempLocalAuthorities = new HashSet(Arrays.asList(authorities));
- tempLocalAuthorities.add(AUTHENTICATED_AUTHORITY);
- authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
-
- } catch (UsernameNotFoundException e) {
- LOGGER.log(Level.WARNING, "User not found in the LDAP directory: " + e.getMessage());
-
- Set tempLocalAuthorities = new HashSet();
- tempLocalAuthorities.add(AUTHENTICATED_AUTHORITY);
- authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
- }
- }
-
- } else {
- String groups = r.getHeader(headerGroups);
-
- List localAuthorities = new ArrayList();
- localAuthorities.add(AUTHENTICATED_AUTHORITY);
-
- if (groups != null) {
- StringTokenizer tokenizer = new StringTokenizer(groups, headerGroupsDelimiter);
- while (tokenizer.hasMoreTokens()) {
- final String token = tokenizer.nextToken().trim();
- localAuthorities.add(new GrantedAuthorityImpl(token));
- }
- }
-
- authorities = localAuthorities.toArray(new GrantedAuthority[0]);
-
- SearchTemplate searchTemplate = new UserSearchTemplate(userFromHeader);
-
- Set foundAuthorities = proxyTemplate.searchForSingleAttributeValues(searchTemplate, authorities);
- Set tempLocalAuthorities = new HashSet();
-
- String[] authString = foundAuthorities.toArray(new String[0]);
- for (int i = 0; i < authString.length; i++) {
- tempLocalAuthorities.add(new GrantedAuthorityImpl(authString[i]));
- }
-
- authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
- authContext.put(userFromHeader, authorities);
-
- auth = new UsernamePasswordAuthenticationToken(userFromHeader, "", authorities);
- }
- authContext.put(userFromHeader, authorities);
+ authorities = authorizationTypeMappingFactory.retrieveAuthorities(userFromHeader, r);
auth = new UsernamePasswordAuthenticationToken(userFromHeader, "", authorities);
}
@@ -588,15 +653,11 @@ public String getPostLogOutUrl(StaplerRequest req, Authentication auth) {
public SecurityComponents createSecurityComponents() throws DataAccessException {
Binding binding = new Binding();
binding.setVariable("instance", this);
+ binding.setVariable("instanceAuthorizationType", this.authorizationTypeMappingFactory);
BeanBuilder builder = new BeanBuilder(Jenkins.getActiveInstance().pluginManager.uberClassLoader);
- String fileName;
- if (getLDAPURL() != null) {
- fileName = "ReverseProxyLDAPSecurityRealm.groovy";
- } else {
- fileName = "ReverseProxySecurityRealm.groovy";
- }
+ String fileName = authorizationTypeMappingFactory.getFilename();
File override = new File(Jenkins.getActiveInstance().getRootDir(), fileName);
try(InputStream istream = override.exists()
@@ -612,25 +673,7 @@ public SecurityComponents createSecurityComponents() throws DataAccessException
}
WebApplicationContext appContext = builder.createApplicationContext();
- if (getLDAPURL() == null) {
- proxyTemplate = new ReverseProxySearchTemplate();
-
- return new SecurityComponents(findBean(AuthenticationManager.class, appContext), new ReverseProxyUserDetailsService(appContext));
- } else {
- ldapTemplate = new LdapTemplate(findBean(InitialDirContextFactory.class, appContext));
-
- if (groupMembershipFilter != null || groupNameAttribute != null) {
- ProxyLDAPAuthoritiesPopulator authoritiesPopulator = findBean(ProxyLDAPAuthoritiesPopulator.class, appContext);
- if (groupMembershipFilter != null) {
- authoritiesPopulator.setGroupSearchFilter(groupMembershipFilter);
- }
- if (groupNameAttribute != null) {
- authoritiesPopulator.setGroupRoleAttribute(groupNameAttribute);
- }
- }
-
- return new SecurityComponents(findBean(AuthenticationManager.class, appContext), new ProxyLDAPUserDetailsService(this, appContext));
- }
+ return authorizationTypeMappingFactory.createUserDetailService(appContext);
}
/**
@@ -639,12 +682,10 @@ public SecurityComponents createSecurityComponents() throws DataAccessException
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
UserDetails userDetails = getSecurityComponents().userDetails.loadUserByUsername(username);
- if (userDetails instanceof LdapUserDetails) {
- updateLdapUserDetails((LdapUserDetails) userDetails);
- }
return userDetails;
}
+ @Deprecated
public LdapUserDetails updateLdapUserDetails(LdapUserDetails d) {
LOGGER.log(Level.FINEST, "displayNameLdapAttribute" + displayNameLdapAttribute);
LOGGER.log(Level.FINEST, "disableLdapEmailResolver" + disableLdapEmailResolver);
@@ -691,22 +732,7 @@ public LdapUserDetails updateLdapUserDetails(LdapUserDetails d) {
@Override
@SuppressWarnings("unchecked")
public GroupDetails loadGroupByGroupname(String groupname) throws UsernameNotFoundException, DataAccessException {
-
- final Set groups;
-
- if (getLDAPURL() != null) {
- // TODO: obtain a DN instead so that we can obtain multiple attributes later
- String searchBase = groupSearchBase != null ? groupSearchBase : "";
- String searchFilter = groupSearchFilter != null ? groupSearchFilter : GROUP_SEARCH;
- groups = ldapTemplate.searchForSingleAttributeValues(searchBase, searchFilter, new String[]{groupname}, "cn");
- } else {
- Authentication auth = SecurityContextHolder.getContext().getAuthentication();
- GrantedAuthority[] authorities = authContext != null ? authContext.get(auth.getName()) : null;
-
- SearchTemplate searchTemplate = new GroupSearchTemplate(groupname);
-
- groups = proxyTemplate.searchForSingleAttributeValues(searchTemplate, authorities);
- }
+ final Set groups = authorizationTypeMappingFactory.loadGroupByGroupname(groupname);
if(groups.isEmpty())
throw new UsernameNotFoundException(groupname);
@@ -732,63 +758,6 @@ public String getDisplayName() {
return Messages.ReverseProxySecurityRealm_DisplayName();
}
- public FormValidation doServerCheck(
- @QueryParameter final String server,
- @QueryParameter final String managerDN,
- @QueryParameter final String managerPassword) {
-
- final String trimmedServer = fixEmptyAndTrim(server);
- if (trimmedServer == null) {
- return FormValidation.error("Server is null or empty");
- }
-
- if (!Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) {
- return FormValidation.ok();
- }
-
- try {
- Hashtable props = new Hashtable();
- 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);
- }
-
- props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
- props.put(Context.PROVIDER_URL, toProviderUrl(trimmedServer, ""));
-
-
- DirContext ctx = new InitialDirContext(props);
- ctx.getAttributes("");
- return FormValidation.ok(); // connected
- } catch (NamingException e) {
- // trouble-shoot
- Matcher m = Pattern.compile("(ldaps?://)?([^:]+)(?:\\:(\\d+))?(\\s+(ldaps?://)?([^:]+)(?:\\:(\\d+))?)*").matcher(trimmedServer.trim());
- if(!m.matches())
- return FormValidation.error(hudson.security.Messages.LDAPSecurityRealm_SyntaxOfServerField());
-
- 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) {
- return FormValidation.error(hudson.security.Messages.LDAPSecurityRealm_UnknownHost(x.getMessage()));
- } catch (IOException x) {
- return FormValidation.error(x,hudson.security.Messages.LDAPSecurityRealm_UnableToConnect(trimmedServer, x.getMessage()));
- }
-
- // otherwise we don't know what caused it, so fall back to the general error report
- // getMessage() alone doesn't offer enough
- return FormValidation.error(e,hudson.security.Messages.LDAPSecurityRealm_UnableToConnect(trimmedServer, e));
- } catch (NumberFormatException x) {
- // The getLdapCtxInstance method throws this if it fails to parse the port number
- return FormValidation.error(hudson.security.Messages.LDAPSecurityRealm_InvalidPortNumber());
- }
- }
}
public static class ReverseProxyUserDetailsService implements UserDetailsService {
@@ -818,6 +787,7 @@ public ReverseProxyUserDetails loadUserByUsername(String username)
}
}
+ @Deprecated
private GrantedAuthority[] retrieveAuthoritiesIfNecessary(final String userFromHeader, final GrantedAuthority[] storedGrants) {
GrantedAuthority[] authorities = storedGrants;
@@ -861,6 +831,7 @@ private GrantedAuthority[] retrieveAuthoritiesIfNecessary(final String userFromH
* 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.
*/
+ @Deprecated
private static String addPrefix(String server) {
if(server.contains("://")) return server;
else return "ldap://"+server;
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyCrowdAuthoritiesPopulator.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyCrowdAuthoritiesPopulator.java
new file mode 100644
index 0000000..e5e4e2f
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyCrowdAuthoritiesPopulator.java
@@ -0,0 +1,59 @@
+package org.jenkinsci.plugins.reverse_proxy_auth.service;
+
+import com.atlassian.crowd.service.client.CrowdClient;
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+import org.jenkinsci.plugins.reverse_proxy_auth.auth.ReverseProxyAuthoritiesPopulator;
+import org.jenkinsci.plugins.reverse_proxy_auth.model.ReverseProxyUserDetails;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * {@link ProxyCrowdAuthoritiesPopulator} that adds the automatic 'authenticated' role.
+ */
+public class ProxyCrowdAuthoritiesPopulator implements ReverseProxyAuthoritiesPopulator {
+
+ private static final String ROLE_PREFIX = "ROLE_";
+
+ private static final Integer MAX_GROUPS = 1000;
+
+ private CrowdClient crowdClient;
+
+ public ProxyCrowdAuthoritiesPopulator(CrowdClient crowdClient){
+ this.crowdClient = crowdClient;
+ }
+
+ /**
+ * Retrieves the group membership in two ways.
+ *
+ * We'd like to retain the original name, but we historically used to do "ROLE_GROUPNAME".
+ * So this method return both, "ROLE_GROUPNAME" and "groupName".
+ */
+ @Override
+ public GrantedAuthority[] getGrantedAuthorities(ReverseProxyUserDetails userDetails) {
+
+ Set grantedAuthoritySet = new HashSet<>();
+ List namesOfGroupsForUser = new ArrayList<>();
+
+ try {
+ namesOfGroupsForUser = crowdClient.getNamesOfGroupsForUser(userDetails.getUsername(), 0, MAX_GROUPS);
+ } catch (Exception e) {
+ LOGGER.log(Level.SEVERE, String.format("Failed to search Crowd groups for username %s", userDetails.getUsername()), e);
+ }
+
+ for (final String groupName : namesOfGroupsForUser) {
+ grantedAuthoritySet.add(new GrantedAuthorityImpl(groupName));
+ grantedAuthoritySet.add(new GrantedAuthorityImpl(ROLE_PREFIX + groupName.toUpperCase()));
+ }
+
+ return grantedAuthoritySet.toArray(new GrantedAuthority[0]);
+ }
+
+ private static final Logger LOGGER = Logger.getLogger(ProxyCrowdAuthoritiesPopulator.class.getName());
+
+}
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyCrowdUserDetailsService.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyCrowdUserDetailsService.java
new file mode 100644
index 0000000..8c8e9ab
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyCrowdUserDetailsService.java
@@ -0,0 +1,29 @@
+package org.jenkinsci.plugins.reverse_proxy_auth.service;
+
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.UserDetailsService;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.jenkinsci.plugins.reverse_proxy_auth.auth.ReverseProxyAuthoritiesPopulator;
+import org.jenkinsci.plugins.reverse_proxy_auth.model.ReverseProxyUserDetails;
+import org.springframework.dao.DataAccessException;
+import org.springframework.web.context.WebApplicationContext;
+import org.jenkinsci.plugins.reverse_proxy_auth.ReverseProxySecurityRealm;
+
+public class ProxyCrowdUserDetailsService implements UserDetailsService {
+
+ private ReverseProxyAuthoritiesPopulator authoritiesPopulator;
+
+ public ProxyCrowdUserDetailsService(ReverseProxySecurityRealm securityRealm, WebApplicationContext appContext) {
+ this.authoritiesPopulator = securityRealm.extractBean(ProxyCrowdAuthoritiesPopulator.class, appContext);
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException, DataAccessException {
+ ReverseProxyUserDetails proxyUserDetails = new ReverseProxyUserDetails();
+ proxyUserDetails.setUsername(username);
+
+ proxyUserDetails.setAuthorities(this.authoritiesPopulator.getGrantedAuthorities(proxyUserDetails));
+
+ return proxyUserDetails;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyLDAPUserDetailsService.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyLDAPUserDetailsService.java
index 372750f..6250a0b 100644
--- a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyLDAPUserDetailsService.java
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/service/ProxyLDAPUserDetailsService.java
@@ -2,12 +2,16 @@
import hudson.security.UserMayOrMayNotExistException;
+import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttributes;
+import hudson.tasks.Mailer;
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.ldap.LdapDataAccessException;
import org.acegisecurity.ldap.LdapUserSearch;
@@ -17,7 +21,9 @@
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
import org.acegisecurity.userdetails.ldap.LdapUserDetailsImpl;
import org.apache.commons.collections.map.LRUMap;
+import org.apache.commons.lang.StringUtils;
import org.jenkinsci.plugins.reverse_proxy_auth.ReverseProxySecurityRealm;
+import org.jenkinsci.plugins.reverse_proxy_auth.types.LdapAuthorizationType;
import org.springframework.dao.DataAccessException;
import org.springframework.web.context.WebApplicationContext;
@@ -30,6 +36,9 @@ public class ProxyLDAPUserDetailsService implements UserDetailsService {
public final LdapUserSearch ldapSearch;
public final LdapAuthoritiesPopulator authoritiesPopulator;
+ public LdapAuthorizationType ldapAuthorizationType;
+
+
/**
* {@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
@@ -37,14 +46,10 @@ public class ProxyLDAPUserDetailsService implements UserDetailsService {
*/
private final LRUMap attributesCache = new LRUMap(32);
- public ProxyLDAPUserDetailsService(ReverseProxySecurityRealm securityRealm, WebApplicationContext appContext) {
+ public ProxyLDAPUserDetailsService(LdapAuthorizationType ldapAuthorizationType, ReverseProxySecurityRealm securityRealm, WebApplicationContext appContext) {
ldapSearch = securityRealm.extractBean(LdapUserSearch.class, appContext);
authoritiesPopulator = securityRealm.extractBean(LdapAuthoritiesPopulator.class, appContext);
- }
-
- public ProxyLDAPUserDetailsService(LdapUserSearch ldapSearch, LdapAuthoritiesPopulator authoritiesPopulator) {
- this.ldapSearch = ldapSearch;
- this.authoritiesPopulator = authoritiesPopulator;
+ this.ldapAuthorizationType = ldapAuthorizationType;
}
public LdapUserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
@@ -72,6 +77,7 @@ public LdapUserDetails loadUserByUsername(String username) throws UsernameNotFou
user.addAuthority(extraAuthority);
}
ldapUser = user.createUserDetails();
+ updateLdapUserDetails(ldapUser);
}
return ldapUser;
@@ -92,6 +98,50 @@ public LdapUserDetails loadUserByUsername(String username) throws UsernameNotFou
throw new UserMayOrMayNotExistException("Failed to search LDAP for user after all the retries.");
}
+
+ public LdapUserDetails updateLdapUserDetails(LdapUserDetails d) {
+ LOGGER.log(Level.FINEST, "displayNameLdapAttribute" + ldapAuthorizationType.displayNameLdapAttribute);
+ LOGGER.log(Level.FINEST, "disableLdapEmailResolver" + ldapAuthorizationType.disableLdapEmailResolver);
+ LOGGER.log(Level.FINEST, "emailAddressLdapAttribute" + ldapAuthorizationType.emailAddressLdapAttribute);
+ if (d.getAttributes() == null){
+ LOGGER.log(Level.FINEST, "getAttributes is null");
+ } else {
+ hudson.model.User u = hudson.model.User.get(d.getUsername());
+ if (!StringUtils.isBlank(ldapAuthorizationType.displayNameLdapAttribute)) {
+ LOGGER.log(Level.FINEST, "Getting user details from LDAP attributes");
+ try {
+ Attribute attribute = d.getAttributes().get(ldapAuthorizationType.displayNameLdapAttribute);
+ String displayName = attribute == null ? null : (String) attribute.get();
+ LOGGER.log(Level.FINEST, "displayName is " + displayName);
+ if (StringUtils.isNotBlank(displayName)) {
+ u.setFullName(displayName);
+ }
+ } catch (NamingException e) {
+ LOGGER.log(Level.FINEST, "Could not retrieve display name attribute", e);
+ }
+ }
+ if (!ldapAuthorizationType.disableLdapEmailResolver && !StringUtils.isBlank(ldapAuthorizationType.emailAddressLdapAttribute)) {
+ try {
+ Attribute attribute = d.getAttributes().get(ldapAuthorizationType.emailAddressLdapAttribute);
+ String mailAddress = attribute == null ? null : (String) attribute.get();
+ if (StringUtils.isNotBlank(mailAddress)) {
+ LOGGER.log(Level.FINEST, "mailAddress is " + mailAddress);
+ Mailer.UserProperty existing = u.getProperty(Mailer.UserProperty.class);
+ if (existing == null || !existing.hasExplicitlyConfiguredAddress()){
+ LOGGER.log(Level.FINEST, "user mail address has been changed");
+ u.addProperty(new Mailer.UserProperty(mailAddress));
+ }
+ }
+ } 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;
+ }
+
/*
* Returns the next wait interval, in milliseconds, using an exponential
* backoff algorithm.
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/AuthorizationTypeMappingFactory.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/AuthorizationTypeMappingFactory.java
new file mode 100644
index 0000000..ac4a225
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/AuthorizationTypeMappingFactory.java
@@ -0,0 +1,124 @@
+package org.jenkinsci.plugins.reverse_proxy_auth.types;
+
+import hudson.ExtensionPoint;
+import hudson.model.AbstractDescribableImpl;
+import hudson.model.Descriptor;
+import hudson.security.SecurityRealm;
+import jenkins.model.Jenkins;
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.userdetails.ldap.LdapUserDetails;
+import org.springframework.web.context.WebApplicationContext;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static hudson.security.SecurityRealm.AUTHENTICATED_AUTHORITY;
+
+/**
+ * Different types of authorization strategies
+ */
+public abstract class AuthorizationTypeMappingFactory extends AbstractDescribableImpl implements ExtensionPoint {
+
+ /**
+ * Retrieves the authorities from a given user
+ * @param userFromHeader - the user extracted from the HTTP header
+ * @param request - the http request
+ * @return GrantedAuthority[]
+ */
+ public abstract GrantedAuthority[] retrieveAuthorities(String userFromHeader, HttpServletRequest request);
+
+ /**
+ * Creates the security component for the corresponded authorization strategy
+ * @param appContext - application context
+ * @return - the security component
+ */
+ public abstract SecurityRealm.SecurityComponents createUserDetailService(WebApplicationContext appContext);
+
+ /**
+ * Path to the groovy file used in the Spring injection of the Security Realm
+ * @return - path to the groovy file of the corresponded security realm used
+ */
+ public abstract String getFilename();
+
+ /**
+ * Abstraction of loadGroupByGroupname so each authorization strategy could make its own implmentation
+ * @param groupname - group name to lookup
+ * @return the groups in the LDAP tree which contains the groupname
+ */
+ public abstract Set loadGroupByGroupname(String groupname);
+
+
+ /**
+ * Get the Security Realm
+ * @return the security realm
+ */
+ public static SecurityRealm getSecurityRealm() {
+ return Jenkins.getActiveInstance().getSecurityRealm();
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public AuthorizationTypeMappingFactoryDescriptor getDescriptor() {
+ return (AuthorizationTypeMappingFactoryDescriptor) super.getDescriptor();
+ }
+
+ /**
+ * Descriptor for the {@link AuthorizationTypeMappingFactory}
+ */
+ public static class AuthorizationTypeMappingFactoryDescriptor extends Descriptor {
+ @Override
+ public String getDisplayName() {
+ return "AuthorizationTypeMappingFactoryDescriptor";
+ }
+ }
+
+ /**
+ * Retrieves again the authorities in case the time they were in the cache was expired
+ * @param authorityUpdateCache - the frequency which the authorities cache is updated per connected user
+ * @param updateInterval - The interval specified in minutes for updating the LDAP authorities
+ * @param userFromHeader - The user extracted from the HTTP header
+ * @param storedGrants - The granted authorities stored in the cache
+ * @return the GrantedAuthorities for a given user
+ */
+ protected GrantedAuthority[] retrieveAuthoritiesIfNecessary(Hashtable authorityUpdateCache, int updateInterval, final String userFromHeader, final GrantedAuthority[] storedGrants) {
+ GrantedAuthority[] authorities = storedGrants;
+ long current = System.currentTimeMillis();
+
+ if (authorityUpdateCache != null && authorityUpdateCache.containsKey(userFromHeader)) {
+ long lastTime = authorityUpdateCache.get(userFromHeader);
+
+ //Time in minutes since last occurrence
+ long check = (current - lastTime) / 1000 / 60;
+ if (check >= updateInterval) {
+
+ LOGGER.log(Level.INFO, "The check interval reached the threshold of " + check + "min, will now update the authorities");
+
+ LdapUserDetails userDetails = (LdapUserDetails) getSecurityRealm().loadUserByUsername(userFromHeader);
+ authorities = userDetails.getAuthorities();
+
+ Set tempLocalAuthorities = new HashSet(Arrays.asList(authorities));
+ tempLocalAuthorities.add(AUTHENTICATED_AUTHORITY);
+ authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
+
+ authorityUpdateCache.put(userFromHeader, current);
+
+ LOGGER.log(Level.INFO, "Authorities for user " + userFromHeader + " have been updated.");
+ }
+ } else {
+ if (authorityUpdateCache == null) {
+ authorityUpdateCache = new Hashtable();
+ }
+ authorityUpdateCache.put(userFromHeader, current);
+ }
+
+ return authorities;
+ }
+
+ private static final Logger LOGGER = Logger.getLogger(AuthorizationTypeMappingFactory.class.getName());
+}
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType.java
new file mode 100644
index 0000000..e7595ab
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType.java
@@ -0,0 +1,188 @@
+package org.jenkinsci.plugins.reverse_proxy_auth.types;
+
+import com.atlassian.crowd.exception.GroupNotFoundException;
+import com.atlassian.crowd.integration.rest.service.factory.RestCrowdClientFactory;
+import com.atlassian.crowd.service.client.CrowdClient;
+import hudson.Extension;
+import hudson.security.SecurityRealm;
+import hudson.util.FormValidation;
+import org.acegisecurity.AuthenticationManager;
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.jenkinsci.plugins.reverse_proxy_auth.ReverseProxySecurityRealm;
+import org.jenkinsci.plugins.reverse_proxy_auth.service.ProxyCrowdUserDetailsService;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.StaplerRequest;
+import org.kohsuke.stapler.StaplerResponse;
+import org.springframework.web.context.WebApplicationContext;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static hudson.security.SecurityRealm.AUTHENTICATED_AUTHORITY;
+import static hudson.security.SecurityRealm.findBean;
+
+/**
+ * Represents the Crowd authorization strategy
+ */
+public class CrowdAuthorizationType extends AuthorizationTypeMappingFactory {
+ /**
+ * The Crowd server URl
+ */
+ private String crowdUrl;
+
+ /**
+ * The Crowd application username
+ */
+ private String crowdApplicationName;
+
+ /**
+ * The Crowd application password
+ */
+ private String crowdApplicationPassword;
+
+ /**
+ * The {@link CrowdClient}
+ */
+ private transient CrowdClient crowdClient;
+
+ /**
+ * The authorities that are granted to the authenticated user.
+ * It is not necessary, that the authorities will be stored in the config.xml, they blow up the config.xml
+ */
+ private transient GrantedAuthority[] authorities = new GrantedAuthority[0];
+
+ /**
+ * The username retrieved from the header field, which is represented by the forwardedUser attribute.
+ */
+ public String retrievedUser;
+
+ /**
+ * Keeps the frequency which the authorities cache is updated per connected user.
+ * The types String and Long are used for username and last time checked (in minutes) respectively.
+ */
+ private transient Hashtable authorityUpdateCache;
+
+ /**
+ * Sets an interval for updating the LDAP authorities. The interval is specified in minutes.
+ */
+ public final int updateInterval;
+
+ /**
+ * The authorization context
+ */
+ private Hashtable authContext;
+
+ /**
+ * Path to the groovy files which contains the injection of this authorization strategy
+ */
+ private static final String FILE_NAME = "types/CrowdAuthorizationType/ReverseProxyCrowdSecurityRealm.groovy";
+
+ @DataBoundConstructor
+ public CrowdAuthorizationType(String crowdUrl, String crowdApplicationName, String crowdApplicationPassword, int updateInterval) {
+ this.crowdUrl = crowdUrl;
+ this.crowdApplicationName = crowdApplicationName;
+ this.crowdApplicationPassword = crowdApplicationPassword;
+ this.updateInterval = updateInterval;
+
+ this.crowdClient = new RestCrowdClientFactory()
+ .newInstance(this.crowdUrl, this.crowdApplicationName, this.crowdApplicationPassword);
+
+ this.authContext = new Hashtable<>();
+ }
+
+ @Override
+ public GrantedAuthority[] retrieveAuthorities(String userFromHeader, HttpServletRequest r) {
+ if (authContext == null) {
+ authContext = new Hashtable<>();
+ }
+
+ GrantedAuthority [] storedGrants = authContext.get(userFromHeader);
+
+ if (storedGrants != null && storedGrants.length > 1) {
+ authorities = retrieveAuthoritiesIfNecessary(authorityUpdateCache, updateInterval, userFromHeader, storedGrants);
+ } else {
+ try {
+ UserDetails userDetails = getSecurityRealm().loadUserByUsername(userFromHeader);
+ authorities = userDetails.getAuthorities();
+
+ Set tempLocalAuthorities = new HashSet(Arrays.asList(authorities));
+ tempLocalAuthorities.add(AUTHENTICATED_AUTHORITY);
+ authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
+
+ } catch (UsernameNotFoundException e) {
+ LOGGER.log(Level.WARNING, "User not found in the Crowd directory: " + e.getMessage());
+
+ Set tempLocalAuthorities = new HashSet();
+ tempLocalAuthorities.add(AUTHENTICATED_AUTHORITY);
+ authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
+ }
+ }
+ authContext.put(userFromHeader, authorities);
+
+ return Collections.unmodifiableList(Arrays.asList(authorities)).toArray(new GrantedAuthority[authorities.length]);
+ }
+
+ @Override
+ public SecurityRealm.SecurityComponents createUserDetailService(WebApplicationContext appContext) {
+ return new SecurityRealm.SecurityComponents(findBean(AuthenticationManager.class, appContext), new ProxyCrowdUserDetailsService((ReverseProxySecurityRealm) getSecurityRealm(), appContext));
+ }
+
+ @Override
+ public String getFilename() {
+ return FILE_NAME;
+ }
+
+ @Override
+ public Set loadGroupByGroupname(String groupname) {
+ Set groups = new HashSet<>();
+ try {
+ groups.add(crowdClient.getGroup(groupname).getName());
+ } catch (GroupNotFoundException g){
+ String msg = String.format("Ignoring %s, isn't a group", groupname);
+ LOGGER.log(Level.INFO, msg);
+ } catch (Exception e) {
+ String msg = String.format("Failed to search group name %s in Crowd", groupname);
+ LOGGER.log(Level.SEVERE, msg, e);
+ }
+ return groups;
+ }
+
+ @Extension
+ public static class DescriptorImpl extends AuthorizationTypeMappingFactoryDescriptor {
+ public String getDisplayName() {
+ return "Crowd";
+ }
+
+ public FormValidation doTestCrowdConnection(StaplerRequest req, StaplerResponse rsp,
+ @QueryParameter("crowdUrl") final String crowdUrl,
+ @QueryParameter("crowdApplicationName") final String crowdApplicationName,
+ @QueryParameter("crowdApplicationPassword") final String crowdApplicationPassword)
+ throws IOException, ServletException {
+
+ try {
+ CrowdClient crowdClient = new RestCrowdClientFactory().newInstance(
+ crowdUrl, crowdApplicationName, crowdApplicationPassword);
+ crowdClient.testConnection();
+ return FormValidation.ok("Success");
+ } catch (Exception e) {
+ String errorMsg = "Error connecting to Crowd: " + e.getMessage();
+ return FormValidation.error(errorMsg);
+ }
+ }
+
+ }
+
+ private static final Logger LOGGER = Logger.getLogger(CrowdAuthorizationType.class.getName());
+
+}
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType.java
new file mode 100644
index 0000000..44825b4
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType.java
@@ -0,0 +1,152 @@
+package org.jenkinsci.plugins.reverse_proxy_auth.types;
+
+import hudson.Extension;
+import hudson.security.SecurityRealm;
+import org.acegisecurity.Authentication;
+import org.acegisecurity.AuthenticationManager;
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+import org.acegisecurity.context.SecurityContextHolder;
+import org.jenkinsci.plugins.reverse_proxy_auth.ReverseProxySearchTemplate;
+import org.jenkinsci.plugins.reverse_proxy_auth.ReverseProxySecurityRealm;
+import org.jenkinsci.plugins.reverse_proxy_auth.data.GroupSearchTemplate;
+import org.jenkinsci.plugins.reverse_proxy_auth.data.SearchTemplate;
+import org.jenkinsci.plugins.reverse_proxy_auth.data.UserSearchTemplate;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.springframework.context.ApplicationContext;
+import org.springframework.web.context.WebApplicationContext;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import static hudson.security.SecurityRealm.AUTHENTICATED_AUTHORITY;
+
+/**
+ * Represents the Group authorization strategy
+ */
+public class GroupsAuthorizationType extends AuthorizationTypeMappingFactory {
+
+ /**
+ * Header name of the groups field.
+ */
+ public final String headerGroups;
+
+ /**
+ * Header name of the groups delimiter field.
+ */
+ public final String headerGroupsDelimiter;
+
+ /**
+ * Search Template used when the groups are in the header.
+ */
+ private ReverseProxySearchTemplate proxyTemplate;
+
+ /**
+ * Keeps the state of connected users and their granted authorities.
+ */
+ private Hashtable authContext = new Hashtable<>();
+
+
+ /**
+ * Path to the groovy files which contains the injection of this authorization strategy
+ */
+ private static final String FILE_NAME = "types/GroupsAuthorizationType/ReverseProxySecurityRealm.groovy";
+
+ @DataBoundConstructor
+ public GroupsAuthorizationType(String headerGroups, String headerGroupsDelimiter) {
+ this.headerGroups = headerGroups;
+ this.headerGroupsDelimiter = headerGroupsDelimiter;
+ this.authContext = new Hashtable<>();
+ }
+
+ @Override
+ public GrantedAuthority[] retrieveAuthorities(String userFromHeader, HttpServletRequest r) {
+ GrantedAuthority[] authorities = null;
+
+ List localAuthorities = new ArrayList();
+ localAuthorities.add(AUTHENTICATED_AUTHORITY);
+
+ String groupsFromHeader = r.getHeader(headerGroups);
+
+ if (groupsFromHeader != null) {
+ StringTokenizer tokenizer = new StringTokenizer(groupsFromHeader, headerGroupsDelimiter);
+ while (tokenizer.hasMoreTokens()) {
+ final String token = tokenizer.nextToken().trim();
+ localAuthorities.add(new GrantedAuthorityImpl(token));
+ }
+ }
+
+ authorities = localAuthorities.toArray(new GrantedAuthority[0]);
+
+ SearchTemplate searchTemplate = new UserSearchTemplate(userFromHeader);
+
+ Set foundAuthorities = proxyTemplate.searchForSingleAttributeValues(searchTemplate, authorities);
+ Set tempLocalAuthorities = new HashSet();
+
+ String[] authString = foundAuthorities.toArray(new String[0]);
+ for (int i = 0; i < authString.length; i++) {
+ tempLocalAuthorities.add(new GrantedAuthorityImpl(authString[i]));
+ }
+
+ authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
+ authContext.put(userFromHeader, authorities);
+ return authorities;
+ }
+
+ @Override
+ public SecurityRealm.SecurityComponents createUserDetailService(WebApplicationContext appContext) {
+ proxyTemplate = new ReverseProxySearchTemplate();
+
+ return new SecurityRealm.SecurityComponents(findBean(AuthenticationManager.class, appContext), new ReverseProxySecurityRealm.ReverseProxyUserDetailsService(appContext));
+ }
+
+ @Override
+ public String getFilename() {
+ return FILE_NAME;
+ }
+
+ @Override
+ public Set loadGroupByGroupname(String groupname) {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ GrantedAuthority[] authorities = authContext != null ? authContext.get(auth.getName()) : null;
+ SearchTemplate searchTemplate = new GroupSearchTemplate(groupname);
+
+ return proxyTemplate.searchForSingleAttributeValues(searchTemplate, authorities);
+ }
+
+ /**
+ * Picks up the instance of the given type from the spring context.
+ * If there are multiple beans of the same type or if there are none,
+ * this method treats that as an {@link IllegalArgumentException}.
+ *
+ * This method is intended to be used to pick up a Acegi object from
+ * spring once the bean definition file is parsed.
+ *
+ * @param context - the {@link ApplicationContext}
+ */
+ public static T findBean(Class type, ApplicationContext context) {
+ Map m = context.getBeansOfType(type);
+ switch(m.size()) {
+ case 0:
+ throw new IllegalArgumentException("No beans of "+type+" are defined");
+ case 1:
+ return type.cast(m.values().iterator().next());
+ default:
+ throw new IllegalArgumentException("Multiple beans of "+type+" are defined: "+m);
+ }
+ }
+
+ @Extension
+ public static class DescriptorImpl extends AuthorizationTypeMappingFactoryDescriptor {
+ public String getDisplayName() {
+ return "Groups";
+ }
+ }
+
+}
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType.java
new file mode 100644
index 0000000..e87436c
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType.java
@@ -0,0 +1,447 @@
+package org.jenkinsci.plugins.reverse_proxy_auth.types;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import hudson.Extension;
+import hudson.security.LDAPSecurityRealm;
+import hudson.security.SecurityRealm;
+import hudson.util.FormValidation;
+import hudson.util.Scrambler;
+import jenkins.model.Jenkins;
+import org.acegisecurity.AuthenticationManager;
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.ldap.InitialDirContextFactory;
+import org.acegisecurity.ldap.LdapTemplate;
+import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch;
+import org.acegisecurity.userdetails.UsernameNotFoundException;
+import org.acegisecurity.userdetails.ldap.LdapUserDetails;
+import org.jenkinsci.plugins.reverse_proxy_auth.ReverseProxySecurityRealm;
+import org.jenkinsci.plugins.reverse_proxy_auth.service.ProxyLDAPAuthoritiesPopulator;
+import org.jenkinsci.plugins.reverse_proxy_auth.service.ProxyLDAPUserDetailsService;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.QueryParameter;
+import org.springframework.web.context.WebApplicationContext;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.naming.Context;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+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.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static hudson.Util.fixEmpty;
+import static hudson.Util.fixEmptyAndTrim;
+import static hudson.Util.fixNull;
+import static hudson.security.SecurityRealm.AUTHENTICATED_AUTHORITY;
+import static hudson.security.SecurityRealm.findBean;
+
+/**
+ * Represents the LDAP authorization strategy
+ */
+public class LdapAuthorizationType extends AuthorizationTypeMappingFactory {
+
+ /**
+ * Interval to check user authorities via LDAP.
+ */
+ private static final int CHECK_INTERVAL = 15;
+
+ /**
+ * 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.
+ */
+ @SuppressFBWarnings(value = "MS_SHOULD_BE_FINAL", justification = "May be used in system groovy scripts")
+ public static String GROUP_SEARCH = System.getProperty(LDAPSecurityRealm.class.getName()+".groupSearch",
+ "(& (cn={0}) (| (objectclass=groupOfNames) (objectclass=groupOfUniqueNames) (objectclass=posixGroup)))");
+
+ /**
+ * 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".
+ */
+ public String server;
+
+ /**
+ * The root DN to connect to. Normally something like "dc=sun,dc=com"
+ *
+ * How do I infer this?
+ */
+ public String rootDN;
+
+ /**
+ * Allow the rootDN to be inferred? Default is false.
+ * If true, allow rootDN to be blank.
+ */
+ public boolean inhibitInferRootDN;
+
+ /**
+ * 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 String userSearchBase;
+
+ /**
+ * Query to locate an entry that identifies the user, given the user name string.
+ *
+ * Normally "uid={0}"
+ *
+ * @see FilterBasedLdapUserSearch
+ */
+ public String userSearch;
+
+ /**
+ * This defines the organizational unit that contains groups.
+ *
+ * Normally "" to indicate the full LDAP search, but can be often narrowed down to
+ * something like "ou=groups"
+ *
+ * @see FilterBasedLdapUserSearch
+ */
+ public String groupSearchBase;
+
+ /**
+ * 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 String groupSearchFilter;
+
+ /**
+ * Query to locate the group entries that a user belongs to, given the user object. {0}
+ * is the user's full DN while {1} is the username.
+ */
+ public String groupMembershipFilter;
+
+ /**
+ * Attribute that should be used instead of CN as name to match a users group name to the groupSearchFilter name.
+ * When {@link #groupSearchFilter} is set to search for a field other than CN e.g. GroupDisplayName={0}
+ * here you can configure that this (GroupDisplayName
) or another field should be used when looking for a users groups.
+ */
+ public String groupNameAttribute;
+
+ /**
+ * If non-null, we use this and {@link #managerPassword}
+ * when binding to LDAP.
+ *
+ * This is necessary when LDAP doesn't support anonymous access.
+ */
+ public String managerDN;
+
+ /**
+ * Scrambled password, used to first bind to LDAP.
+ */
+ public String managerPassword;
+
+ /**
+ * The LDAP display name attribute
+ */
+ public String displayNameLdapAttribute;
+
+ /**
+ * The LDAP email address attribute
+ */
+ public String emailAddressLdapAttribute;
+
+ /**
+ * Disable the LDAP email resolver
+ */
+ public boolean disableLdapEmailResolver;
+
+ /**
+ * Sets an interval for updating the LDAP authorities. The interval is specified in minutes.
+ */
+ public int updateInterval;
+
+ /**
+ * Path to the groovy files which contains the injection of this authorization strategy
+ */
+ private static final String FILE_NAME = "types/LdapAuthorizationType/ReverseProxyLDAPSecurityRealm.groovy";
+
+ /**
+ * Keeps the frequency which the authorities cache is updated per connected user.
+ * The types String and Long are used for username and last time checked (in minutes) respectively.
+ */
+ private transient Hashtable authorityUpdateCache = new Hashtable<>();
+
+ /**
+ * Ldap template to connect with LDAP
+ */
+ private transient LdapTemplate ldapTemplate;
+
+ /**
+ * Gets the LDAP URL
+ * @return
+ */
+ public String getLDAPURL() {
+ return toProviderUrl(getServerUrl(), fixNull(rootDN));
+ }
+
+ /**
+ * Keeps the state of connected users and their granted authorities.
+ */
+ private Hashtable authContext = new Hashtable<>();
+
+ /**
+ * The authorities that are granted to the authenticated user.
+ * It is not necessary, that the authorities will be stored in the config.xml, they blow up the config.xml
+ */
+ private transient GrantedAuthority[] authorities = new GrantedAuthority[0];
+
+ @DataBoundConstructor
+ public LdapAuthorizationType(String server, String rootDN, boolean inhibitInferRootDN,
+ String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, String groupMembershipFilter, String groupNameAttribute, String managerDN, String managerPassword,
+ String displayNameLdapAttribute, String emailAddressLdapAttribute, boolean disableLdapEmailResolver, Integer updateInterval) {
+
+ this.server = fixEmptyAndTrim(server);
+ this.managerDN = fixEmpty(managerDN);
+ this.managerPassword = Scrambler.scramble(fixEmpty(managerPassword));
+ this.inhibitInferRootDN = inhibitInferRootDN;
+
+ if (this.server != null) {
+ if(!inhibitInferRootDN && fixEmptyAndTrim(rootDN) == null) rootDN = fixNull(inferRootDN(server));
+ this.rootDN = rootDN.trim();
+ } else {
+ this.rootDN = null;
+ }
+
+ this.userSearchBase = fixNull(userSearchBase).trim();
+ userSearch = fixEmptyAndTrim(userSearch);
+ this.userSearch = userSearch != null ? userSearch : "uid={0}";
+ this.groupSearchBase = fixEmptyAndTrim(groupSearchBase);
+ this.groupSearchFilter = fixEmptyAndTrim(groupSearchFilter);
+ this.groupMembershipFilter = fixEmptyAndTrim(groupMembershipFilter);
+ this.groupNameAttribute = fixEmptyAndTrim(groupNameAttribute);
+ this.disableLdapEmailResolver = disableLdapEmailResolver;
+ this.displayNameLdapAttribute = displayNameLdapAttribute;
+ this.emailAddressLdapAttribute = emailAddressLdapAttribute;
+ this.updateInterval = (updateInterval == null || updateInterval <= 0) ? CHECK_INTERVAL : updateInterval;
+ }
+
+ /**
+ * Infer the root DN.
+ *
+ * @return null if not found.
+ */
+ private String inferRootDN(String server) {
+ try {
+ Hashtable props = new Hashtable();
+ if(managerDN != null) {
+ props.put(Context.SECURITY_PRINCIPAL, managerDN);
+ props.put(Context.SECURITY_CREDENTIALS, getManagerPassword());
+ }
+ props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+ //TODO: should it pass null instead and check the result?
+ props.put(Context.PROVIDER_URL, toProviderUrl(fixNull(getServerUrl()), ""));
+
+ DirContext ctx = new InitialDirContext(props);
+ Attributes atts = ctx.getAttributes("");
+ Attribute a = atts.get("defaultNamingContext");
+ 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();
+ }
+
+ 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 getManagerPassword() {
+ return Scrambler.descramble(managerPassword);
+ }
+
+ @CheckForNull
+ public String getServerUrl() {
+ if (server == null) {
+ return null;
+ }
+ StringBuilder buf = new StringBuilder();
+ boolean first = true;
+
+ for (String s: server.split("\\s+")) {
+ if (s.trim().length() == 0) continue;
+ if (first) first = false; else buf.append(' ');
+ buf.append(addPrefix(s));
+ }
+ return buf.toString();
+ }
+
+ @Nullable
+ public static String toProviderUrl(@CheckForNull String serverUrl, @CheckForNull String rootDN) {
+ if (serverUrl == null) {
+ return null;
+ }
+ 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();
+ }
+
+ /**
+ * 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;
+ }
+
+ public GrantedAuthority[] retrieveAuthorities(String userFromHeader, HttpServletRequest r) {
+ if (authContext == null) {
+ authContext = new Hashtable<>();
+ }
+
+ GrantedAuthority [] storedGrants = authContext.get(userFromHeader);
+
+ if (storedGrants != null && storedGrants.length > 1) {
+ authorities = retrieveAuthoritiesIfNecessary(authorityUpdateCache, updateInterval, userFromHeader, storedGrants);
+ } else {
+ try {
+ LdapUserDetails userDetails = (LdapUserDetails) getSecurityRealm().loadUserByUsername(userFromHeader);
+ authorities = userDetails.getAuthorities();
+
+ Set tempLocalAuthorities = new HashSet(Arrays.asList(authorities));
+ tempLocalAuthorities.add(AUTHENTICATED_AUTHORITY);
+ authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
+
+ } catch (UsernameNotFoundException e) {
+ LOGGER.log(Level.WARNING, "User not found in the LDAP directory: " + e.getMessage());
+
+ Set tempLocalAuthorities = new HashSet();
+ tempLocalAuthorities.add(AUTHENTICATED_AUTHORITY);
+ authorities = tempLocalAuthorities.toArray(new GrantedAuthority[0]);
+ }
+ }
+ authContext.put(userFromHeader, authorities);
+ return Collections.unmodifiableList(Arrays.asList(authorities)).toArray(new GrantedAuthority[authorities.length]);
+ }
+
+
+ public SecurityRealm.SecurityComponents createUserDetailService(WebApplicationContext appContext) {
+ ldapTemplate = new LdapTemplate(findBean(InitialDirContextFactory.class, appContext));
+
+ if (groupMembershipFilter != null || groupNameAttribute != null) {
+ ProxyLDAPAuthoritiesPopulator authoritiesPopulator = findBean(ProxyLDAPAuthoritiesPopulator.class, appContext);
+ if (groupMembershipFilter != null) {
+ authoritiesPopulator.setGroupSearchFilter(groupMembershipFilter);
+ }
+ if (groupNameAttribute != null) {
+ authoritiesPopulator.setGroupRoleAttribute(groupNameAttribute);
+ }
+ }
+
+ return new SecurityRealm.SecurityComponents(findBean(AuthenticationManager.class, appContext), new ProxyLDAPUserDetailsService(this, (ReverseProxySecurityRealm) getSecurityRealm(), appContext));
+ }
+
+ @Override
+ public String getFilename() {
+ return FILE_NAME;
+ }
+
+ @Override
+ public Set loadGroupByGroupname(String groupname) {
+ // TODO: obtain a DN instead so that we can obtain multiple attributes later
+ String searchBase = groupSearchBase != null ? groupSearchBase : "";
+ String searchFilter = groupSearchFilter != null ? groupSearchFilter : GROUP_SEARCH;
+ return ldapTemplate.searchForSingleAttributeValues(searchBase, searchFilter, new String[]{groupname}, "cn");
+ }
+
+ @Extension
+ public static class DescriptorImpl extends AuthorizationTypeMappingFactoryDescriptor {
+ public String getDisplayName() {
+ return "LDAP";
+ }
+
+ public FormValidation doServerCheck(
+ @QueryParameter final String server,
+ @QueryParameter final String managerDN,
+ @QueryParameter final String managerPassword) {
+
+ final String trimmedServer = fixEmptyAndTrim(server);
+ if (trimmedServer == null) {
+ return FormValidation.error("Server is null or empty");
+ }
+
+ if (!Jenkins.getActiveInstance().hasPermission(Jenkins.ADMINISTER)) {
+ return FormValidation.ok();
+ }
+
+ try {
+ Hashtable props = new Hashtable();
+ 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);
+ }
+
+ props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+ props.put(Context.PROVIDER_URL, toProviderUrl(trimmedServer, ""));
+
+ DirContext ctx = new InitialDirContext(props);
+ ctx.getAttributes("");
+ return FormValidation.ok(); // connected
+ } catch (NamingException e) {
+ // trouble-shoot
+ Matcher m = Pattern.compile("(ldaps?://)?([^:]+)(?:\\:(\\d+))?(\\s+(ldaps?://)?([^:]+)(?:\\:(\\d+))?)*").matcher(trimmedServer.trim());
+ if(!m.matches())
+ return FormValidation.error(hudson.security.Messages.LDAPSecurityRealm_SyntaxOfServerField());
+
+ 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) {
+ return FormValidation.error(hudson.security.Messages.LDAPSecurityRealm_UnknownHost(x.getMessage()));
+ } catch (IOException x) {
+ return FormValidation.error(x,hudson.security.Messages.LDAPSecurityRealm_UnableToConnect(trimmedServer, x.getMessage()));
+ }
+
+ // otherwise we don't know what caused it, so fall back to the general error report
+ // getMessage() alone doesn't offer enough
+ return FormValidation.error(e,hudson.security.Messages.LDAPSecurityRealm_UnableToConnect(trimmedServer, e));
+ } catch (NumberFormatException x) {
+ // The getLdapCtxInstance method throws this if it fails to parse the port number
+ return FormValidation.error(hudson.security.Messages.LDAPSecurityRealm_InvalidPortNumber());
+ }
+ }
+ }
+
+ private static final Logger LOGGER = Logger.getLogger(LdapAuthorizationType.class.getName());
+
+}
diff --git a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm/config.jelly b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm/config.jelly
index 5912ca2..da6adce 100644
--- a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm/config.jelly
+++ b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm/config.jelly
@@ -26,12 +26,6 @@ THE SOFTWARE.
-
-
-
-
-
-
@@ -39,54 +33,9 @@ THE SOFTWARE.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
diff --git a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType/ReverseProxyCrowdSecurityRealm.groovy b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType/ReverseProxyCrowdSecurityRealm.groovy
new file mode 100644
index 0000000..51f22d7
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType/ReverseProxyCrowdSecurityRealm.groovy
@@ -0,0 +1,36 @@
+import jenkins.model.Jenkins
+import org.acegisecurity.providers.ProviderManager
+import org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider
+import org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider
+import org.jenkinsci.plugins.reverse_proxy_auth.auth.DefaultReverseProxyAuthenticator
+import org.jenkinsci.plugins.reverse_proxy_auth.auth.ReverseProxyAuthenticationProvider
+import org.jenkinsci.plugins.reverse_proxy_auth.service.ProxyCrowdAuthoritiesPopulator
+
+/*
+ Configure Reverse Proxy as the authentication realm.
+ The 'instance' object refers to the instance of ReverseProxySecurityRealm
+*/
+
+authoritiesPopulator(ProxyCrowdAuthoritiesPopulator, instanceAuthorizationType.crowdClient) {
+}
+
+authenticator(DefaultReverseProxyAuthenticator, instance.retrievedUser, instanceAuthorizationType.authorities) {
+}
+
+authenticationManager(ProviderManager) {
+ providers = [
+ // talk to Reverse Proxy Authentication
+ bean(ReverseProxyAuthenticationProvider,authenticator,authoritiesPopulator),
+
+ // these providers apply everywhere
+ bean(RememberMeAuthenticationProvider) {
+ key = Jenkins.getInstance().getSecretKey();
+ },
+ // this doesn't mean we allow anonymous access.
+ // we just authenticate anonymous users as such,
+ // so that later authorization can reject them if so configured
+ bean(AnonymousAuthenticationProvider) {
+ key = "anonymous"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType/config.jelly b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType/config.jelly
new file mode 100644
index 0000000..b49bbe2
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/CrowdAuthorizationType/config.jelly
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.groovy b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType/ReverseProxySecurityRealm.groovy
similarity index 98%
rename from src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.groovy
rename to src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType/ReverseProxySecurityRealm.groovy
index 6b93888..000ad0a 100644
--- a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType/ReverseProxySecurityRealm.groovy
@@ -30,8 +30,6 @@ import org.jenkinsci.plugins.reverse_proxy_auth.auth.DefaultReverseProxyAuthenti
import org.jenkinsci.plugins.reverse_proxy_auth.auth.ReverseProxyAuthoritiesPopulatorImpl
import jenkins.model.Jenkins
-import hudson.Util
-import javax.naming.Context
/*
Configure Reverse Proxy as the authentication realm.
diff --git a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType/config.jelly b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType/config.jelly
new file mode 100644
index 0000000..b1f4fa0
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/GroupsAuthorizationType/config.jelly
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxyLDAPSecurityRealm.groovy b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType/ReverseProxyLDAPSecurityRealm.groovy
similarity index 88%
rename from src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxyLDAPSecurityRealm.groovy
rename to src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType/ReverseProxyLDAPSecurityRealm.groovy
index 1cd0ee7..5dfc0db 100644
--- a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxyLDAPSecurityRealm.groovy
+++ b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType/ReverseProxyLDAPSecurityRealm.groovy
@@ -33,7 +33,6 @@ import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch
import org.jenkinsci.plugins.reverse_proxy_auth.service.ProxyLDAPAuthoritiesPopulator
import jenkins.model.Jenkins
-import hudson.Util
import javax.naming.Context
/*
@@ -43,15 +42,15 @@ import javax.naming.Context
The 'instance' object refers to the instance of ReverseProxySecurityRealm
*/
-initialDirContextFactory(DefaultInitialDirContextFactory, instance.getLDAPURL() ) {
- if(instance.managerDN != null) {
- managerDn = instance.managerDN;
- managerPassword = instance.getManagerPassword();
+initialDirContextFactory(DefaultInitialDirContextFactory, instanceAuthorizationType.getLDAPURL() ) {
+ if(instanceAuthorizationType.managerDN != null) {
+ managerDn = instanceAuthorizationType.managerDN;
+ managerPassword = instanceAuthorizationType.getManagerPassword();
}
extraEnvVars = [(Context.REFERRAL):"follow"];
}
-ldapUserSearch(FilterBasedLdapUserSearch, instance.userSearchBase, instance.userSearch, initialDirContextFactory) {
+ldapUserSearch(FilterBasedLdapUserSearch, instanceAuthorizationType.userSearchBase, instanceAuthorizationType.userSearch, initialDirContextFactory) {
searchSubtree = true
}
@@ -60,7 +59,7 @@ bindAuthenticator(BindAuthenticator2, initialDirContextFactory) {
userSearch = ldapUserSearch;
}
-authoritiesPopulator(ProxyLDAPAuthoritiesPopulator, initialDirContextFactory, instance.groupSearchBase) {
+authoritiesPopulator(ProxyLDAPAuthoritiesPopulator, initialDirContextFactory, instanceAuthorizationType.groupSearchBase) {
// see DefaultLdapAuthoritiesPopulator for other possible configurations
searchSubtree = true;
groupSearchFilter = "(| (member={0}) (uniqueMember={0}) (memberUid={1}))";
diff --git a/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType/config.jelly b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType/config.jelly
new file mode 100644
index 0000000..79d5fe7
--- /dev/null
+++ b/src/main/resources/org/jenkinsci/plugins/reverse_proxy_auth/types/LdapAuthorizationType/config.jelly
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest.java b/src/test/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest.java
index cb73aa6..27d3384 100644
--- a/src/test/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest.java
+++ b/src/test/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest.java
@@ -6,12 +6,19 @@
import org.acegisecurity.GrantedAuthority;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.userdetails.UserDetails;
+import org.jenkinsci.plugins.reverse_proxy_auth.types.GroupsAuthorizationType;
+import org.jenkinsci.plugins.reverse_proxy_auth.types.LdapAuthorizationType;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;
+import org.jvnet.hudson.test.recipes.LocalData;
+
+import static org.hamcrest.Matchers.isEmptyOrNullString;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.Matchers.is;
import java.util.concurrent.Callable;
@@ -56,7 +63,8 @@ public Authentication call() {
}
private ReverseProxySecurityRealm createBasicRealm() {
- return new ReverseProxySecurityRealm(
+ GroupsAuthorizationType groupsAuthorizationType = new GroupsAuthorizationType("X-Forwarded-Groups", "|");
+ ReverseProxySecurityRealm reverseProxySecurityRealm = new ReverseProxySecurityRealm(
"X-Forwarded-User", // forwardedUser
"X-Forwarded-Groups", // headerGroups
"|", // headerGroupsDelimiter
@@ -76,7 +84,37 @@ private ReverseProxySecurityRealm createBasicRealm() {
15, // updateInterval
false, // disableLdapEmailResolver
"", // displayNameLdapAttribute
- "" // emailAddressLdapAttribute
+ "", // emailAddressLdapAttribute
+ groupsAuthorizationType
);
+ return reverseProxySecurityRealm;
+ }
+
+ @LocalData
+ @Test
+ public void readResolveLdap() {
+ ReverseProxySecurityRealm reverseProxySecurityRealm = (ReverseProxySecurityRealm) jenkins.getSecurityRealm();
+ LdapAuthorizationType ldapAuthorizationType = (LdapAuthorizationType) reverseProxySecurityRealm.getAuthorizationTypeMappingFactory();
+ assertThat(ldapAuthorizationType.server, is("ldap://127.0.0.1:3890"));
+ assertThat(ldapAuthorizationType.rootDN, is("dc=corporation,dc=net"));
+ assertThat(ldapAuthorizationType.inhibitInferRootDN, is(false));
+ assertThat(ldapAuthorizationType.userSearchBase, is("ou=employees,ou=people"));
+ assertThat(ldapAuthorizationType.userSearch, is("uid={0}"));
+ assertThat(ldapAuthorizationType.groupSearchBase, is("ou=groups"));
+ assertThat(ldapAuthorizationType.groupSearchFilter, is("(uniqueMember={0})"));
+ assertThat(ldapAuthorizationType.managerDN, is("cn=admin,dc=corporation,dc=net"));
+ assertThat(ldapAuthorizationType.disableLdapEmailResolver, is(false));
+ assertThat(ldapAuthorizationType.displayNameLdapAttribute, isEmptyOrNullString());
+ assertThat(ldapAuthorizationType.emailAddressLdapAttribute, isEmptyOrNullString());
+ }
+
+ @LocalData
+ @Test
+ public void readResolveGroups() {
+ ReverseProxySecurityRealm reverseProxySecurityRealm = (ReverseProxySecurityRealm) jenkins.getSecurityRealm();
+ GroupsAuthorizationType groupsAuthorizationType = (GroupsAuthorizationType) reverseProxySecurityRealm.getAuthorizationTypeMappingFactory();
+ assertThat(groupsAuthorizationType.headerGroups, is("X-Forwarded-Groups"));
+ assertThat(groupsAuthorizationType.headerGroupsDelimiter, is("|"));
+
}
}
diff --git a/src/test/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest/readResolveGroups.zip b/src/test/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest/readResolveGroups.zip
new file mode 100644
index 0000000..f4db6f4
Binary files /dev/null and b/src/test/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest/readResolveGroups.zip differ
diff --git a/src/test/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest/readResolveLdap.zip b/src/test/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest/readResolveLdap.zip
new file mode 100644
index 0000000..6628651
Binary files /dev/null and b/src/test/resources/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealmTest/readResolveLdap.zip differ