diff --git a/README.md b/README.md
index 2429a5e..2c77024 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-Jenkins Reverse Proxy Authentication and Authorisation Plugin
+# Jenkins Reverse Proxy Authentication and Authorisation Plugin
The Reverse Proxy Plugin providers developers the ability to have easy and simple Authentication and Authorisation using SSO techniques. The plugin expects that the user to have Jenkins authenticated agains will be informed via a HHTP header field.
@@ -7,8 +7,10 @@ When it comes to Authorisation, the plugin has been extended in order to offer t
The default values for the HTTP header fields are:
1. Header User Name: X-Forwarded-User
-2. Header Groups Name: X-Forwarded-Groups
-3. Header Groups Delimiter: |
+2. Header User Mail: X-Forwarded-Mail
+3. Header User Display Name: X-Forwarded-DisplayName
+4. Header Groups Name: X-Forwarded-Groups
+5. Header Groups Delimiter: |
The LDAP options can be displayed via the Advanced... button, located on the right side of the security settings.
@@ -18,3 +20,83 @@ If the username is not forwaded to Jenkins, the user will be authenticated as AN
However, once the LDAP is properly configured instead of groups on the HTTP header, there is guarantee that only the groups of a given user will be returned. There is no possibility to get groups injected via the header.
+
+## Apache httpd configuration example
+
+Here is a simple httpd configuration (apache.conf) made to proxypass 1 to 100 jenkins called ci00 to ci99.
+Basic authentication uses an AuthUserFile and many AuthLDAP.
+User, display name, mail and groups are injected as headers.
+Injected groups are the ldap ones and the local httpd ones (in dbm format).
+
+```
+
+ AuthBasicProvider auth-file ldap-1 ldap-2
+ AuthType Basic
+ AuthName "Jenkins"
+
+ Require valid-user
+ Order deny,allow
+ Allow from all
+
+
+
+ AuthBasicProvider auth-file ldap-1 ldap-2
+ AuthType Basic
+ AuthName "Jenkins"
+
+ Require valid-user
+ Order deny,allow
+ Allow from all
+
+
+
+ #Redirect jenkins (for headers)
+ RewriteRule ^/ci01$ /ci01/ [R]
+ RewriteRule ^/ci02$ /ci02/ [R]
+
+ ProxyPass /ci01 http://jenkins-1-real-address/ci01 nocanon
+ ProxyPassReverse /ci01 http://jenkins-1-real-address/ci01
+
+ ProxyPass /ci02 http://jenkins-2-real-address/ci02 nocanon
+ ProxyPassReverse /ci02 http://jenkins-2-real-address/ci02
+
+ RewriteMap jenkins-groups dbm:/path-to-jenkins-groups.dbm
+
+ #WARNING jenkins is not protected on direct access !
+ #Allow any jenkins from ci00 to ci99
+
+
+ #Keep the location match regex as simple as possible
+ #Otherwise we may send some internal js call without authentication.
+
+
+ # jenkins reverse proxy auth configuration
+ # prevent the client from setting this header
+ RequestHeader unset X-Forwarded-User
+ RequestHeader unset X-Forwarded-Groups
+ RequestHeader unset X-Forwarded-Mail
+ RequestHeader unset X-Forwarded-DisplayName
+
+ RequestHeader set X-Forwarded-Proto "https"
+ RequestHeader set X-Forwarded-Port "443"
+
+ # Adds the X-Forwarded-User header that indicates the current user name.
+ # this portion came from http://old.nabble.com/Forcing-a-proxied-host-to-generate-REMOTE_USER-td2911573.html#a2914465
+ RewriteEngine On
+ # see the Apache documentation on why this has to be lookahead
+ RewriteCond %{LA-U:REMOTE_USER} (.+)
+ # this actually doesn't rewrite anything. what we do here is to set RU to the match above
+ # "NS" prevents flooding the error log
+ RewriteRule .* - [E=RU:%1,NS]
+ RequestHeader set X-Forwarded-User %{RU}e
+
+ #inject mail & display name
+ RequestHeader set X-Forwarded-Mail %{AUTHENTICATE_MAIL}e
+ RequestHeader set X-Forwarded-DisplayName %{AUTHENTICATE_DISPLAYNAME}e
+
+ #inject groups
+ RewriteRule .* - [E=RG:${jenkins-groups:%{REMOTE_USER}}]
+ RequestHeader set X-Forwarded-Groups %{RG}e
+
+
+```
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index a8a80ef..9a275bf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
reverse-proxy-auth-plugin
- 1.4.1-SNAPSHOT
+ 1.5
hpi
Jenkins Reverse Proxy Auth Plugin
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 10f7e16..28f9347 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
@@ -30,6 +30,7 @@
import hudson.Extension;
import hudson.model.Descriptor;
import hudson.model.Hudson;
+import hudson.model.UserPropertyDescriptor;
import hudson.model.User;
import hudson.security.GroupDetails;
import hudson.security.UserMayOrMayNotExistException;
@@ -93,6 +94,7 @@
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.data.ForwardedUserData;
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;
@@ -110,147 +112,157 @@
*/
public class ReverseProxySecurityRealm extends SecurityRealm {
- private static final Logger LOGGER = Logger.getLogger(ReverseProxySecurityRealm.class.getName());
-
- /**
- * 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)))");
-
- /**
- * Interval to check user authorities via LDAP.
- */
- private static final int CHECK_INTERVAL = 15;
-
- /**
- * Scrambled password, used to first bind to LDAP.
- */
- private final String managerPassword;
-
- /**
- * Search Template used when the groups are in the header.
- */
- private ReverseProxySearchTemplate proxyTemplate;
-
- /**
- * Created in {@link #createSecurityComponents()}. Can be used to connect to LDAP.
- */
- private transient LdapTemplate ldapTemplate;
-
- /**
- * Keeps the state of connected users and their granted authorities.
- */
- private final Hashtable authContext;
-
- /**
- * 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 Hashtable authorityUpdateCache;
-
- /**
- * 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;
-
- /**
- * The root DN to connect to. Normally something like "dc=sun,dc=com"
- *
- * How do I infer this?
- */
- public final String rootDN;
-
- /**
- * Allow the rootDN to be inferred? Default is false.
- * If true, allow rootDN to be blank.
- */
- public final 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 final String userSearchBase;
-
- /**
- * Query to locate an entry that identifies the user, given the user name string.
- *
- * Normally "uid={0}"
- *
- * @see FilterBasedLdapUserSearch
- */
- public final String userSearch;
-
- /**
- * 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 final 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 final String groupSearchFilter;
+ private static final Logger LOGGER = Logger.getLogger(ReverseProxySecurityRealm.class.getName());
/**
- * 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;
-
- /**
- * If non-null, we use this and {@link #managerPassword}
- * when binding to LDAP.
- *
- * This is necessary when LDAP doesn't support anonymous access.
- */
- public final String managerDN;
-
- /**
- * Sets an interval for updating the LDAP authorities. The interval is specified in minutes.
- */
- public final int updateInterval;
-
- /**
- * 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
- */
- public transient GrantedAuthority[] authorities = new GrantedAuthority[0];
-
- /**
- * The name of the header which the username has to be extracted from.
- */
- public final String forwardedUser;
-
- /**
- * The username retrieved from the header field, which is represented by the forwardedUser attribute.
- */
- public String retrievedUser;
-
- /**
- * Header name of the groups field.
- */
- public final String headerGroups;
-
- /**
- * Header name of the groups delimiter field.
- */
- public final String headerGroupsDelimiter;
+ * 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)))");
+
+ /**
+ * Interval to check user authorities via LDAP.
+ */
+ private static final int CHECK_INTERVAL = 15;
+
+ /**
+ * Scrambled password, used to first bind to LDAP.
+ */
+ private final String managerPassword;
+
+ /**
+ * Search Template used when the groups are in the header.
+ */
+ private ReverseProxySearchTemplate proxyTemplate;
+
+ /**
+ * Created in {@link #createSecurityComponents()}. Can be used to connect to LDAP.
+ */
+ private transient LdapTemplate ldapTemplate;
+
+ /**
+ * Keeps the state of connected users and their granted authorities.
+ */
+ private final Hashtable authContext;
+
+ /**
+ * 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 Hashtable authorityUpdateCache;
+
+ /**
+ * 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;
+
+ /**
+ * The root DN to connect to. Normally something like "dc=sun,dc=com"
+ *
+ * How do I infer this?
+ */
+ public final String rootDN;
+
+ /**
+ * Allow the rootDN to be inferred? Default is false.
+ * If true, allow rootDN to be blank.
+ */
+ public final 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 final String userSearchBase;
+
+ /**
+ * Query to locate an entry that identifies the user, given the user name string.
+ *
+ * Normally "uid={0}"
+ *
+ * @see FilterBasedLdapUserSearch
+ */
+ public final String userSearch;
+
+ /**
+ * 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 final 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 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;
+
+ /**
+ * If non-null, we use this and {@link #managerPassword}
+ * when binding to LDAP.
+ *
+ * This is necessary when LDAP doesn't support anonymous access.
+ */
+ public final String managerDN;
+
+ /**
+ * Sets an interval for updating the LDAP authorities. The interval is specified in minutes.
+ */
+ public final int updateInterval;
+
+ /**
+ * 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
+ */
+ public transient GrantedAuthority[] authorities = new GrantedAuthority[0];
+
+ /**
+ * The name of the header which the username has to be extracted from.
+ */
+ public final String forwardedUser;
+
+ /**
+ * The username retrieved from the header field, which is represented by the forwardedUser attribute.
+ */
+ public String retrievedUser;
+
+ /**
+ * The name of the header which the email has to be extracted from.
+ */
+ public final String forwardedEmail;
+
+ /**
+ * The name of the header which the display name has to be extracted from.
+ */
+ public final String forwardedDisplayName;
+
+ /**
+ * Header name of the groups field.
+ */
+ public final String headerGroups;
+
+ /**
+ * Header name of the groups delimiter field.
+ */
+ public final String headerGroupsDelimiter;
public final boolean disableLdapEmailResolver;
@@ -258,322 +270,357 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
private final String emailAddressLdapAttribute;
- @DataBoundConstructor
- public ReverseProxySecurityRealm(String forwardedUser, String headerGroups, String headerGroupsDelimiter, String server, String rootDN, boolean inhibitInferRootDN,
- String userSearchBase, String userSearch, String groupSearchBase, String groupSearchFilter, String groupMembershipFilter, String managerDN, String managerPassword, Integer updateInterval, boolean disableLdapEmailResolver, String displayNameLdapAttribute, String emailAddressLdapAttribute) {
-
- this.forwardedUser = fixEmptyAndTrim(forwardedUser);
-
- this.headerGroups = headerGroups;
- if (!StringUtils.isBlank(headerGroupsDelimiter)) {
- this.headerGroupsDelimiter = headerGroupsDelimiter.trim();
- } else {
- this.headerGroupsDelimiter = "|";
- }
- //
- 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.updateInterval = (updateInterval == null || updateInterval <= 0) ? CHECK_INTERVAL : updateInterval;
-
- authorities = new GrantedAuthority[0];
- authContext = new Hashtable();
- authorityUpdateCache = new Hashtable();
-
- this.disableLdapEmailResolver = disableLdapEmailResolver;
- this.displayNameLdapAttribute = displayNameLdapAttribute;
- this.emailAddressLdapAttribute = emailAddressLdapAttribute;
- }
-
- /**
- * Name of the HTTP header to look at.
- */
- public String getForwardedUser() {
- return forwardedUser;
- }
-
- public String getHeaderGroups() {
- return headerGroups;
- }
-
- public String getHeaderGroupsDelimiter() {
- return headerGroupsDelimiter;
- }
-
- 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();
- }
-
- public String getGroupSearchFilter() {
- return groupSearchFilter;
- }
-
- public String getGroupMembershipFilter() {
- return groupMembershipFilter;
- }
-
- public String getDisplayNameLdapAttribute() {
- return displayNameLdapAttribute;
- }
-
- public String getEmailAddressLdapAttribute() {
- return emailAddressLdapAttribute;
- }
-
- /**
- * 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");
- 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 static String toProviderUrl(String serverUrl, 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();
- }
-
- public String getManagerPassword() {
- return Scrambler.descramble(managerPassword);
- }
-
- public int getUpdateInterval() {
- return updateInterval;
- }
-
- public String getLDAPURL() {
- return toProviderUrl(getServerUrl(), fixNull(rootDN));
- }
-
- @Override
- public Filter createFilter(FilterConfig filterConfig) {
- return new Filter() {
- public void init(FilterConfig filterConfig) throws ServletException {
- }
-
- public void doFilter(ServletRequest request,
- ServletResponse response, FilterChain chain)
- throws IOException, ServletException {
- HttpServletRequest r = (HttpServletRequest) request;
-
- String authorization = null;
- String userFromApiToken = null;
- if ((authorization = r.getHeader("Authorization")) != null && authorization.toLowerCase().startsWith("basic ")) {
- String uidpassword = Scrambler.descramble(authorization.substring(6));
- int idx = uidpassword.indexOf(':');
- if (idx >= 0) {
- String username = uidpassword.substring(0, idx);
- String password = uidpassword.substring(idx+1);
-
- // attempt to authenticate as API token
- User u = User.get(username, false);
- ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
- if (t != null && t.matchesPassword(password)) {
- userFromApiToken = username;
- }
- }
- }
-
- String userFromHeader = null;
-
- Authentication auth = Hudson.ANONYMOUS;
- if ((forwardedUser != null
- && (userFromHeader = r.getHeader(forwardedUser)) != null)
- || userFromApiToken != null) {
- //LOGGER.log(Level.INFO, "USER LOGGED IN: {0}", userFromHeader);
- if (userFromHeader == null && userFromApiToken != null) {
- userFromHeader = userFromApiToken;
- }
-
- 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);
- auth = new UsernamePasswordAuthenticationToken(userFromHeader, "", authorities);
- }
-
- retrievedUser = userFromHeader;
-
- SecurityContextHolder.getContext().setAuthentication(auth);
- chain.doFilter(r, response);
- }
-
- public void destroy() {
- }
- };
- }
-
- @Override
- public boolean canLogOut() {
- return false;
- }
-
- @Override
- public SecurityComponents createSecurityComponents() {
- Binding binding = new Binding();
- binding.setVariable("instance", this);
-
- BeanBuilder builder = new BeanBuilder(Jenkins.getInstance().pluginManager.uberClassLoader);
-
- String fileName;
- if (getLDAPURL() != null) {
- fileName = "ReverseProxyLDAPSecurityRealm.groovy";
- } else {
- fileName = "ReverseProxySecurityRealm.groovy";
- }
-
- try {
- File override = new File(Jenkins.getInstance().getRootDir(), fileName);
- builder.parse(override.exists() ? new AutoCloseInputStream(new FileInputStream(override)) :
- getClass().getResourceAsStream(fileName), binding);
- } catch (FileNotFoundException e) {
- throw new Error("Failed to load "+fileName,e);
- }
- 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) {
- ProxyLDAPAuthoritiesPopulator authoritiesPopulator = findBean(ProxyLDAPAuthoritiesPopulator.class, appContext);
- authoritiesPopulator.setGroupSearchFilter(groupMembershipFilter);
- }
-
- return new SecurityComponents(findBean(AuthenticationManager.class, appContext), new ProxyLDAPUserDetailsService(this, appContext));
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
- UserDetails userDetails = getSecurityComponents().userDetails.loadUserByUsername(username);
+ @DataBoundConstructor
+ public ReverseProxySecurityRealm(
+ String forwardedUser,
+ String forwardedEmail,
+ String forwardedDisplayName,
+ String headerGroups,
+ String headerGroupsDelimiter,
+ String server,
+ String rootDN,
+ boolean inhibitInferRootDN,
+ String userSearchBase,
+ String userSearch,
+ String groupSearchBase,
+ String groupSearchFilter,
+ String groupMembershipFilter,
+ String managerDN,
+ String managerPassword,
+ Integer updateInterval,
+ boolean disableLdapEmailResolver,
+ String displayNameLdapAttribute,
+ String emailAddressLdapAttribute) {
+
+ this.forwardedUser = fixEmptyAndTrim(forwardedUser);
+ this.forwardedEmail = forwardedEmail;
+ this.forwardedDisplayName = forwardedDisplayName;
+
+ this.headerGroups = headerGroups;
+ if (!StringUtils.isBlank(headerGroupsDelimiter)) {
+ this.headerGroupsDelimiter = headerGroupsDelimiter.trim();
+ } else {
+ this.headerGroupsDelimiter = "|";
+ }
+ //
+ 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.updateInterval = (updateInterval == null || updateInterval <= 0) ? CHECK_INTERVAL : updateInterval;
+
+ authorities = new GrantedAuthority[0];
+ authContext = new Hashtable();
+ authorityUpdateCache = new Hashtable();
+
+ this.disableLdapEmailResolver = disableLdapEmailResolver;
+ this.displayNameLdapAttribute = displayNameLdapAttribute;
+ this.emailAddressLdapAttribute = emailAddressLdapAttribute;
+ }
+
+ /**
+ * Name of the HTTP header to look at.
+ */
+ public String getForwardedUser() {
+ return forwardedUser;
+ }
+
+ public String getHeaderGroups() {
+ return headerGroups;
+ }
+
+ public String getHeaderGroupsDelimiter() {
+ return headerGroupsDelimiter;
+ }
+
+ 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();
+ }
+
+ public String getGroupSearchFilter() {
+ return groupSearchFilter;
+ }
+
+ public String getGroupMembershipFilter() {
+ return groupMembershipFilter;
+ }
+
+ public String getDisplayNameLdapAttribute() {
+ return displayNameLdapAttribute;
+ }
+
+ public String getEmailAddressLdapAttribute() {
+ return emailAddressLdapAttribute;
+ }
+
+ /**
+ * 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");
+ 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 static String toProviderUrl(String serverUrl, 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();
+ }
+
+ public String getManagerPassword() {
+ return Scrambler.descramble(managerPassword);
+ }
+
+ public int getUpdateInterval() {
+ return updateInterval;
+ }
+
+ public String getLDAPURL() {
+ return toProviderUrl(getServerUrl(), fixNull(rootDN));
+ }
+
+ @Override
+ public Filter createFilter(FilterConfig filterConfig) {
+ return new Filter() {
+ public void init(FilterConfig filterConfig) throws ServletException {
+ }
+
+ public void doFilter(ServletRequest request,
+ ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
+ HttpServletRequest r = (HttpServletRequest) request;
+
+ String authorization = null;
+ String userFromApiToken = null;
+ if ((authorization = r.getHeader("Authorization")) != null && authorization.toLowerCase().startsWith("basic ")) {
+ String uidpassword = Scrambler.descramble(authorization.substring(6));
+ int idx = uidpassword.indexOf(':');
+ if (idx >= 0) {
+ String username = uidpassword.substring(0, idx);
+ String password = uidpassword.substring(idx+1);
+
+ // attempt to authenticate as API token
+ User u = User.get(username, false);
+ ApiTokenProperty t = u.getProperty(ApiTokenProperty.class);
+ if (t != null && t.matchesPassword(password)) {
+ userFromApiToken = username;
+ }
+ }
+ }
+
+ String userFromHeader = null;
+
+ Authentication auth = Hudson.ANONYMOUS;
+ if ((forwardedUser != null
+ && (userFromHeader = r.getHeader(forwardedUser)) != null)
+ || userFromApiToken != null) {
+ //LOGGER.log(Level.INFO, "USER LOGGED IN: {0}", userFromHeader);
+ if (userFromHeader == null && userFromApiToken != null) {
+ userFromHeader = userFromApiToken;
+ }
+
+ 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 {
+ //Without LDAP, retrieve user data from the headers
+
+ ForwardedUserData forwardedData=retrieveForwardedData(r);
+ User user=User.get(userFromHeader);
+ if(user!=null){
+ forwardedData.update(user);
+ }
+ 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);
+ }
+
+ retrievedUser = userFromHeader;
+
+ SecurityContextHolder.getContext().setAuthentication(auth);
+ chain.doFilter(r, response);
+ }
+
+ public void destroy() {
+ }
+ };
+ }
+
+ private ForwardedUserData retrieveForwardedData(HttpServletRequest r) {
+ ForwardedUserData toReturn=new ForwardedUserData();
+ if(forwardedEmail!=null){
+ toReturn.setEmail(r.getHeader(forwardedEmail));
+ }
+ if(forwardedDisplayName!=null){
+ toReturn.setDisplayName(r.getHeader(forwardedDisplayName));
+ }
+ return toReturn;
+}
+
+ @Override
+ public boolean canLogOut() {
+ return false;
+ }
+
+ @Override
+ public SecurityComponents createSecurityComponents() {
+ Binding binding = new Binding();
+ binding.setVariable("instance", this);
+
+ BeanBuilder builder = new BeanBuilder(Jenkins.getInstance().pluginManager.uberClassLoader);
+
+ String fileName;
+ if (getLDAPURL() != null) {
+ fileName = "ReverseProxyLDAPSecurityRealm.groovy";
+ } else {
+ fileName = "ReverseProxySecurityRealm.groovy";
+ }
+
+ try {
+ File override = new File(Jenkins.getInstance().getRootDir(), fileName);
+ builder.parse(override.exists() ? new AutoCloseInputStream(new FileInputStream(override)) :
+ getClass().getResourceAsStream(fileName), binding);
+ } catch (FileNotFoundException e) {
+ throw new Error("Failed to load "+fileName,e);
+ }
+ 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) {
+ ProxyLDAPAuthoritiesPopulator authoritiesPopulator = findBean(ProxyLDAPAuthoritiesPopulator.class, appContext);
+ authoritiesPopulator.setGroupSearchFilter(groupMembershipFilter);
+ }
+
+ return new SecurityComponents(findBean(AuthenticationManager.class, appContext), new ProxyLDAPUserDetailsService(this, appContext));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
+ UserDetails userDetails = getSecurityComponents().userDetails.loadUserByUsername(username);
if (userDetails instanceof LdapUserDetails) {
updateLdapUserDetails((LdapUserDetails) userDetails);
}
return userDetails;
- }
+ }
public LdapUserDetails updateLdapUserDetails(LdapUserDetails d) {
LOGGER.log(Level.FINEST, "displayNameLdapAttribute" + displayNameLdapAttribute);
@@ -618,175 +665,175 @@ public LdapUserDetails updateLdapUserDetails(LdapUserDetails d) {
return 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.get(auth.getName());
-
- SearchTemplate searchTemplate = new GroupSearchTemplate(groupname);
-
- groups = proxyTemplate.searchForSingleAttributeValues(searchTemplate, authorities);
- }
-
- if(groups.isEmpty())
- throw new UsernameNotFoundException(groupname);
-
- return new GroupDetails() {
- @Override
- public String getName() {
- return groups.iterator().next();
- }
- };
- }
-
- public T extractBean(Class type, WebApplicationContext appContext) {
- T returnedObj = findBean(type, appContext);
- return returnedObj;
- }
-
- @Extension
- public static class ProxyLDAPDescriptor extends Descriptor {
-
- @Override
- public String getDisplayName() {
- return Messages.ReverseProxySecurityRealm_DisplayName();
- }
-
- public FormValidation doServerCheck(
- @QueryParameter final String server,
- @QueryParameter final String managerDN,
- @QueryParameter final String managerPassword) {
-
- if(!Jenkins.getInstance().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(server, ""));
-
-
- 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(server.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(server, 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(server, 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 {
-
- private final ReverseProxyAuthoritiesPopulator authoritiesPopulator;
-
- public ReverseProxyUserDetailsService(WebApplicationContext appContext) {
- authoritiesPopulator = findBean(
- ReverseProxyAuthoritiesPopulator.class, appContext);
- }
-
- public ReverseProxyUserDetails loadUserByUsername(String username)
- throws UsernameNotFoundException, DataAccessException {
- try {
- ReverseProxyUserDetails proxyUser = new ReverseProxyUserDetails();
- proxyUser.setUsername(username);
-
- GrantedAuthority[] localAuthorities = authoritiesPopulator.getGrantedAuthorities(proxyUser);
-
- proxyUser.setAuthorities(localAuthorities);
-
- return proxyUser;
- } catch (LdapDataAccessException e) {
- LOGGER.log(Level.WARNING, "Failed to search LDAP for username=" + username, e);
- throw new UserMayOrMayNotExistException(e.getMessage(), e);
- }
- }
- }
-
- private GrantedAuthority[] retrieveAuthoritiesIfNecessary(final String userFromHeader, final GrantedAuthority[] storedGrants) {
-
- GrantedAuthority[] authorities = storedGrants;
-
- if (getLDAPURL() != null) {
-
- 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) 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;
- }
-
- /**
- * 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;
- }
+ @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.get(auth.getName());
+
+ SearchTemplate searchTemplate = new GroupSearchTemplate(groupname);
+
+ groups = proxyTemplate.searchForSingleAttributeValues(searchTemplate, authorities);
+ }
+
+ if(groups.isEmpty())
+ throw new UsernameNotFoundException(groupname);
+
+ return new GroupDetails() {
+ @Override
+ public String getName() {
+ return groups.iterator().next();
+ }
+ };
+ }
+
+ public T extractBean(Class type, WebApplicationContext appContext) {
+ T returnedObj = findBean(type, appContext);
+ return returnedObj;
+ }
+
+ @Extension
+ public static class ProxyLDAPDescriptor extends Descriptor {
+
+ @Override
+ public String getDisplayName() {
+ return Messages.ReverseProxySecurityRealm_DisplayName();
+ }
+
+ public FormValidation doServerCheck(
+ @QueryParameter final String server,
+ @QueryParameter final String managerDN,
+ @QueryParameter final String managerPassword) {
+
+ if(!Jenkins.getInstance().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(server, ""));
+
+
+ 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(server.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(server, 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(server, 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 {
+
+ private final ReverseProxyAuthoritiesPopulator authoritiesPopulator;
+
+ public ReverseProxyUserDetailsService(WebApplicationContext appContext) {
+ authoritiesPopulator = findBean(
+ ReverseProxyAuthoritiesPopulator.class, appContext);
+ }
+
+ public ReverseProxyUserDetails loadUserByUsername(String username)
+ throws UsernameNotFoundException, DataAccessException {
+ try {
+ ReverseProxyUserDetails proxyUser = new ReverseProxyUserDetails();
+ proxyUser.setUsername(username);
+
+ GrantedAuthority[] localAuthorities = authoritiesPopulator.getGrantedAuthorities(proxyUser);
+
+ proxyUser.setAuthorities(localAuthorities);
+
+ return proxyUser;
+ } catch (LdapDataAccessException e) {
+ LOGGER.log(Level.WARNING, "Failed to search LDAP for username=" + username, e);
+ throw new UserMayOrMayNotExistException(e.getMessage(), e);
+ }
+ }
+ }
+
+ private GrantedAuthority[] retrieveAuthoritiesIfNecessary(final String userFromHeader, final GrantedAuthority[] storedGrants) {
+
+ GrantedAuthority[] authorities = storedGrants;
+
+ if (getLDAPURL() != null) {
+
+ 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) 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;
+ }
+
+ /**
+ * 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;
+ }
}
diff --git a/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java
new file mode 100644
index 0000000..8e8f08c
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java
@@ -0,0 +1,81 @@
+package org.jenkinsci.plugins.reverse_proxy_auth.data;
+
+import java.io.IOException;
+
+import hudson.model.User;
+import hudson.tasks.Mailer;
+
+/**
+ * User data forwarded by the reverse proxy
+ * **/
+public class ForwardedUserData {
+ /** Empty header may be a null string **/
+ final private static String NULL_HEADER="(null)";
+
+ private String email;
+ private String displayName;
+ public String getEmail() {
+ return email;
+ }
+ public void setEmail(String email) {
+ this.email = email;
+ }
+ public String getDisplayName() {
+ return displayName;
+ }
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ /**
+ * Update the forwarded data to the jenkins user.
+ * @return true if updated and saved
+ * **/
+ public boolean update(User user) {
+ boolean toReturn=false;
+ if(updateDisplayName(user) || updateEmail(user)){
+ toReturn=true;
+ try {
+ user.save();
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ return toReturn;
+ }
+
+ private boolean updateDisplayName(User user) {
+ boolean toReturn = false;
+ if(isNotNullHeader(displayName) &&
+ !displayName.equals(user.getFullName())){
+ user.setFullName(displayName);
+ toReturn=true;
+ }
+ return toReturn;
+ }
+
+ private boolean updateEmail(User user){
+ boolean toReturn = false;
+ if(isNotNullHeader(email)){
+ Mailer.UserProperty emailProp = user.getProperty(
+ Mailer.UserProperty.class);
+ if (emailProp == null ||
+ !email.equals(emailProp.getConfiguredAddress())) {
+ emailProp=new Mailer.UserProperty(email);
+ try {
+ user.addProperty(emailProp);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ toReturn=true;
+ }
+ }
+ return toReturn;
+ }
+
+ private static boolean isNotNullHeader(String value){
+ return value!=null && !value.equals(NULL_HEADER);
+ }
+
+}
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 b64c4a8..d272340 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,6 +26,12 @@ THE SOFTWARE.
+
+
+
+
+
+