From ad2405990eab158ca1bbef3a65206dc4c0fb903b Mon Sep 17 00:00:00 2001 From: akrasinsky <143082310+akrasinsky@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:44:42 +0100 Subject: [PATCH] Add email and display name header injection support - revised from PR #30 (#124) * Initial commit * Apply suggestions from code review * Added tests * Fixed tests * Pre commit changes * fix: reformatting using mvn spotless:apply --------- Co-authored-by: Steve Boardwell --- README.md | 11 ++- .../ReverseProxySecurityRealm.java | 32 ++++++++ .../data/ForwardedUserData.java | 80 +++++++++++++++++++ .../ReverseProxySecurityRealm/config.jelly | 6 ++ .../ReverseProxySecurityRealmTest.java | 2 + .../data/ForwardedUserDataTest.java | 50 ++++++++++++ 6 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java create mode 100644 src/test/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserDataTest.java diff --git a/README.md b/README.md index d0fcd42..ac4f8dc 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,10 @@ When it comes to authorisation, the offers two options to developers: HTTP heade ## 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. @@ -73,6 +75,8 @@ The default values for the HTTP header fields are: # Remove these header before to set the right value after, it 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" # Remove the basic authorization header to avoid to use it in Jenkins RequestHeader unset "Authorization" @@ -84,6 +88,9 @@ The default values for the HTTP header fields are: RequestHeader set "X-Forwarded-User" "%{RU}e" # Groups are separated by | RequestHeader set "X-Forwarded-Groups" "%{RU}e|users" + # Inject mail & display name + RequestHeader set "X-Forwarded-Mail" %{AUTHENTICATE_MAIL}e + RequestHeader set "X-Forwarded-DisplayName" %{AUTHENTICATE_DISPLAYNAME}e # strip the REALM of Kerberos Login # RequestHeader edit X-Forwarded-User "@REALM$" "" 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 c251087..363d957 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 @@ -94,6 +94,7 @@ import org.jenkinsci.plugins.reverse_proxy_auth.auth.ReverseProxyAuthenticationProvider; import org.jenkinsci.plugins.reverse_proxy_auth.auth.ReverseProxyAuthoritiesPopulator; import org.jenkinsci.plugins.reverse_proxy_auth.auth.ReverseProxyAuthoritiesPopulatorImpl; +import org.jenkinsci.plugins.reverse_proxy_auth.data.ForwardedUserData; 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; @@ -139,6 +140,16 @@ public class ReverseProxySecurityRealm extends SecurityRealm { /** Search Template used when the groups are in the header. */ private ReverseProxySearchTemplate proxyTemplate; + /** + * 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; + /** Created in {@link #createSecurityComponents()}. Can be used to connect to LDAP. */ private transient LdapTemplate ldapTemplate; @@ -262,6 +273,8 @@ public class ReverseProxySecurityRealm extends SecurityRealm { @DataBoundConstructor public ReverseProxySecurityRealm( String forwardedUser, + String forwardedEmail, + String forwardedDisplayName, String headerGroups, String headerGroupsDelimiter, String customLogInUrl, @@ -283,6 +296,8 @@ public ReverseProxySecurityRealm( String emailAddressLdapAttribute) { this.forwardedUser = fixEmptyAndTrim(forwardedUser); + this.forwardedEmail = fixEmptyAndTrim(forwardedEmail); + this.forwardedDisplayName = fixEmptyAndTrim(forwardedDisplayName); this.headerGroups = headerGroups; if (!StringUtils.isBlank(headerGroupsDelimiter)) { @@ -529,6 +544,12 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } } 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(); @@ -576,6 +597,17 @@ public void destroy() {} return new ChainedServletFilter(defaultFilter, filter); } + 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() { if (customLogOutUrl == null) { 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..24e7629 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java @@ -0,0 +1,80 @@ +package org.jenkinsci.plugins.reverse_proxy_auth.data; + +import hudson.model.User; +import hudson.tasks.Mailer; +import java.io.IOException; + +/** + * User data forwarded by the reverse proxy + * **/ +public class ForwardedUserData { + /** Empty header may be a null string **/ + private static final 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 21015e7..1734392 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. + + + + + + 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 c24dc7a..73cc58f 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 @@ -69,6 +69,8 @@ private ReverseProxySecurityRealm createBasicRealm() { return new ReverseProxySecurityRealm( "X-Forwarded-User", // forwardedUser "X-Forwarded-Groups", // headerGroups + "X-Forwarded-Email", // forwardedEmail + "X-Forwarded-DisplayName", // forwardedDisplayName "|", // headerGroupsDelimiter "", // customLogInUrl "", // customLogOutUrl diff --git a/src/test/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserDataTest.java b/src/test/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserDataTest.java new file mode 100644 index 0000000..d45255e --- /dev/null +++ b/src/test/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserDataTest.java @@ -0,0 +1,50 @@ +package org.jenkinsci.plugins.reverse_proxy_auth.data; + +import hudson.model.User; +import hudson.tasks.Mailer; +import jenkins.model.Jenkins; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.MockAuthorizationStrategy; + +public class ForwardedUserDataTest { + private ForwardedUserData forwardedUserData; + private User user; + + @Rule + public JenkinsRule j = new JenkinsRule(); + + @Before + public void setup() { + j.jenkins.setAuthorizationStrategy( + new MockAuthorizationStrategy().grant(Jenkins.READ).everywhere().to("Max Mustermann")); + + forwardedUserData = new ForwardedUserData(); + user = User.getOrCreateByIdOrFullName("Max Mustermann"); + } + + @Test + public void basicForwardedUserData() { + forwardedUserData.setEmail("max.mustermann@example.com"); + Assert.assertEquals("max.mustermann@example.com", forwardedUserData.getEmail()); + + forwardedUserData.setDisplayName("Max Mustermann"); + Assert.assertEquals("Max Mustermann", forwardedUserData.getDisplayName()); + } + + @Test + public void testUpdate() { + user.setFullName("John Doe"); + forwardedUserData.setDisplayName("Max Mustermann"); + forwardedUserData.update(user); + Assert.assertEquals("Max Mustermann", user.getFullName()); + + forwardedUserData.setEmail("max.mustermann@example.com"); + forwardedUserData.update(user); + Mailer.UserProperty emailProp = user.getProperty(Mailer.UserProperty.class); + Assert.assertEquals("max.mustermann@example.com", emailProp.getAddress()); + } +}