Skip to content

Commit

Permalink
Add email and display name header injection support - revised from PR #…
Browse files Browse the repository at this point in the history
…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 <steve.boardwell@gmail.com>
  • Loading branch information
akrasinsky authored Jan 25, 2024
1 parent d5ad0c4 commit ad24059
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 2 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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"

Expand All @@ -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$" ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -262,6 +273,8 @@ public class ReverseProxySecurityRealm extends SecurityRealm {
@DataBoundConstructor
public ReverseProxySecurityRealm(
String forwardedUser,
String forwardedEmail,
String forwardedDisplayName,
String headerGroups,
String headerGroupsDelimiter,
String customLogInUrl,
Expand All @@ -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)) {
Expand Down Expand Up @@ -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) {

Check warning on line 550 in src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 550 is only partially covered, one branch is missing
forwardedData.update(user);
}
String groups = r.getHeader(headerGroups);

List<GrantedAuthority> localAuthorities = new ArrayList<GrantedAuthority>();
Expand Down Expand Up @@ -576,6 +597,17 @@ public void destroy() {}
return new ChainedServletFilter(defaultFilter, filter);
}

private ForwardedUserData retrieveForwardedData(HttpServletRequest r) {
ForwardedUserData toReturn = new ForwardedUserData();
if (forwardedEmail != null) {

Check warning on line 602 in src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 602 is only partially covered, one branch is missing
toReturn.setEmail(r.getHeader(forwardedEmail));
}
if (forwardedDisplayName != null) {

Check warning on line 605 in src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/ReverseProxySecurityRealm.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 605 is only partially covered, one branch is missing
toReturn.setDisplayName(r.getHeader(forwardedDisplayName));
}
return toReturn;
}

@Override
public boolean canLogOut() {
if (customLogOutUrl == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);

Check warning on line 44 in src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 43-44 are not covered by tests
}
}

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())) {

Check warning on line 64 in src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 64 is only partially covered, 2 branches are missing
emailProp = new Mailer.UserProperty(email);
try {
user.addProperty(emailProp);
} catch (IOException e) {
throw new IllegalStateException(e);

Check warning on line 69 in src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 68-69 are not covered by tests
}
toReturn = true;
}
}
return toReturn;
}

private static boolean isNotNullHeader(String value) {
return value != null && !value.equals(NULL_HEADER);

Check warning on line 78 in src/main/java/org/jenkinsci/plugins/reverse_proxy_auth/data/ForwardedUserData.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 78 is only partially covered, one branch is missing
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ THE SOFTWARE.
<f:entry title="${%Header User Name}">
<f:textbox name="forwardedUser" value="${instance.forwardedUser}" default="X-Forwarded-User" />
</f:entry>
<f:entry title="${%Header User Mail}">
<f:textbox name="forwardedEmail" value="${instance.forwardedEmail}" default="X-Forwarded-Mail" />
</f:entry>
<f:entry title="${%Header User Display Name}">
<f:textbox name="forwardedDisplayName" value="${instance.forwardedDisplayName}" default="X-Forwarded-DisplayName" />
</f:entry>
<f:entry title="${%Header Groups Name}">
<f:textbox field="headerGroups" default="X-Forwarded-Groups" />
</f:entry>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}

0 comments on commit ad24059

Please sign in to comment.