From 7b7b0d54b606e5a7d63ea39ec8918968f612d61d Mon Sep 17 00:00:00 2001
From: James Moger <james.moger@gmail.com>
Date: Thu, 10 Dec 2015 09:55:23 -0500
Subject: [PATCH] Merge pull request #980 from mrjoel/mrjoel-httpheaders

---
 src/main/java/com/gitblit/auth/HttpHeaderAuthProvider.java   |  161 ++++++++++++++++++++++++++++++++
 src/site/setup_authentication.mkd                            |   12 ++
 src/main/java/com/gitblit/Constants.java                     |    4 
 src/main/java/com/gitblit/manager/AuthenticationManager.java |   22 ++++
 src/main/distrib/data/defaults.properties                    |   37 +++++++
 src/main/java/com/gitblit/ConfigUserService.java             |    3 
 src/main/java/com/gitblit/auth/AuthenticationProvider.java   |   53 +++++++++-
 7 files changed, 280 insertions(+), 12 deletions(-)

diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties
index ce6267a..403b741 100644
--- a/src/main/distrib/data/defaults.properties
+++ b/src/main/distrib/data/defaults.properties
@@ -817,6 +817,7 @@
 # Valid providers are:
 #
 #    htpasswd
+#    httpheader
 #    ldap
 #    pam
 #    redmine
@@ -1739,6 +1740,42 @@
 # SINCE 1.3.2
 realm.htpasswd.userfile = ${baseFolder}/htpasswd
 
+# The name of the HTTP header containing the user name to trust as authenticated
+# default: none
+#
+# WARNING: only use this mechanism if your requests are coming from a trusted
+#          and secure source such as a self managed reverse proxy!
+#
+# RESTART REQUIRED
+# SINCE 1.7.2
+realm.httpheader.userheader =
+
+# The name of the HTTP header containing the team names of which the user is a member.
+# If this is defined, then only groups from the headers will be available, whereas
+# if this remains undefined, then local groups will be used.
+#
+# This setting requires that you have configured realm.httpheader.userheader.
+#
+# default: none
+#
+# RESTART REQUIRED
+# SINCE 1.7.2
+realm.httpheader.teamheader =
+
+# The regular expression pattern used to separate team names in the team header value
+# default: ,
+#
+# This setting requires that you have configured realm.httpheader.teamheader
+#
+# RESTART REQUIRED
+# SINCE 1.7.2
+realm.httpheader.teamseparator = ,
+
+# Auto-creates user accounts when successfully authenticated based on HTTP headers.
+#
+# SINCE 1.7.2
+realm.httpheader.autoCreateAccounts = false
+
 # Restrict the Salesforce user to members of this org.
 # default: 0 (i.e. do not check the Org ID)
 #
diff --git a/src/main/java/com/gitblit/ConfigUserService.java b/src/main/java/com/gitblit/ConfigUserService.java
index 200ec8a..6d7230f 100644
--- a/src/main/java/com/gitblit/ConfigUserService.java
+++ b/src/main/java/com/gitblit/ConfigUserService.java
@@ -890,9 +890,6 @@
 					user.displayName = config.getString(USER, username, DISPLAYNAME);
 					user.emailAddress = config.getString(USER, username, EMAILADDRESS);
 					user.accountType = AccountType.fromString(config.getString(USER, username, ACCOUNTTYPE));
-					if (Constants.EXTERNAL_ACCOUNT.equals(user.password) && user.accountType.isLocal()) {
-						user.accountType = AccountType.EXTERNAL;
-					}
 					user.disabled = config.getBoolean(USER, username, DISABLED, false);
 					user.organizationalUnit = config.getString(USER, username, ORGANIZATIONALUNIT);
 					user.organization = config.getString(USER, username, ORGANIZATION);
diff --git a/src/main/java/com/gitblit/Constants.java b/src/main/java/com/gitblit/Constants.java
index 4aa8c0c..0a99953 100644
--- a/src/main/java/com/gitblit/Constants.java
+++ b/src/main/java/com/gitblit/Constants.java
@@ -574,7 +574,7 @@
 	}
 
 	public static enum AuthenticationType {
-		PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER;
+		PUBLIC_KEY, CREDENTIALS, COOKIE, CERTIFICATE, CONTAINER, HTTPHEADER;
 
 		public boolean isStandard() {
 			return ordinal() <= COOKIE.ordinal();
@@ -582,7 +582,7 @@
 	}
 
 	public static enum AccountType {
-		LOCAL, EXTERNAL, CONTAINER, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM, HTPASSWD;
+		LOCAL, CONTAINER, LDAP, REDMINE, SALESFORCE, WINDOWS, PAM, HTPASSWD, HTTPHEADER;
 
 		public static AccountType fromString(String value) {
 			for (AccountType type : AccountType.values()) {
diff --git a/src/main/java/com/gitblit/auth/AuthenticationProvider.java b/src/main/java/com/gitblit/auth/AuthenticationProvider.java
index c56c184..0bfe235 100644
--- a/src/main/java/com/gitblit/auth/AuthenticationProvider.java
+++ b/src/main/java/com/gitblit/auth/AuthenticationProvider.java
@@ -18,11 +18,14 @@
 import java.io.File;
 import java.math.BigInteger;
 
+import javax.servlet.http.HttpServletRequest;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.gitblit.Constants.AccountType;
 import com.gitblit.Constants.Role;
+import com.gitblit.Constants.AuthenticationType;
 import com.gitblit.IStoredSettings;
 import com.gitblit.manager.IRuntimeManager;
 import com.gitblit.manager.IUserManager;
@@ -73,6 +76,8 @@
 		return serviceName;
 	}
 
+	public abstract AuthenticationType getAuthenticationType();
+
 	protected void setCookie(UserModel user, char [] password) {
 		// create a user cookie
 		if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {
@@ -116,14 +121,32 @@
 
 	public abstract void stop();
 
+	/**
+	 * Used to handle requests for requests for pages requiring authentication.
+	 * This allows authentication to occur based on the contents of the request
+	 * itself.
+	 *
+	 * @param httpRequest
+	 * @return
+	 */
+	public abstract UserModel authenticate(HttpServletRequest httpRequest);
+
+	/**
+	 * Used to authentication user/password credentials, both for login form
+	 * and HTTP Basic authentication processing.
+	 *
+	 * @param username
+	 * @param password
+	 * @return
+	 */
 	public abstract UserModel authenticate(String username, char[] password);
 
 	public abstract AccountType getAccountType();
 
 	/**
-	 * Does the user service support changes to credentials?
+	 * Returns true if the users's credentials can be changed.
 	 *
-	 * @return true or false
+	 * @return true if the authentication provider supports credential changes
 	 * @since 1.0.0
 	 */
 	public abstract boolean supportsCredentialChanges();
@@ -132,7 +155,7 @@
 	 * Returns true if the user's display name can be changed.
 	 *
 	 * @param user
-	 * @return true if the user service supports display name changes
+	 * @return true if the authentication provider supports display name changes
 	 */
 	public abstract boolean supportsDisplayNameChanges();
 
@@ -140,7 +163,7 @@
 	 * Returns true if the user's email address can be changed.
 	 *
 	 * @param user
-	 * @return true if the user service supports email address changes
+	 * @return true if the authentication provider supports email address changes
 	 */
 	public abstract boolean supportsEmailAddressChanges();
 
@@ -148,7 +171,7 @@
 	 * Returns true if the user's team memberships can be changed.
 	 *
 	 * @param user
-	 * @return true if the user service supports team membership changes
+	 * @return true if the authentication provider supports team membership changes
 	 */
 	public abstract boolean supportsTeamMembershipChanges();
 
@@ -180,6 +203,16 @@
     		super(serviceName);
     	}
 
+		@Override
+		public UserModel authenticate(HttpServletRequest httpRequest) {
+			return null;
+		}
+
+		@Override
+		public AuthenticationType getAuthenticationType() {
+			return AuthenticationType.CREDENTIALS;
+		}
+
     	@Override
 		public void stop() {
 
@@ -203,6 +236,11 @@
 		}
 
 		@Override
+		public UserModel authenticate(HttpServletRequest httpRequest) {
+			return null;
+		}
+
+		@Override
 		public UserModel authenticate(String username, char[] password) {
 			return null;
 		}
@@ -213,6 +251,11 @@
 		}
 
 		@Override
+		public AuthenticationType getAuthenticationType() {
+			return null;
+		}
+
+		@Override
 		public boolean supportsCredentialChanges() {
 			return true;
 		}
diff --git a/src/main/java/com/gitblit/auth/HttpHeaderAuthProvider.java b/src/main/java/com/gitblit/auth/HttpHeaderAuthProvider.java
new file mode 100644
index 0000000..3a9c539
--- /dev/null
+++ b/src/main/java/com/gitblit/auth/HttpHeaderAuthProvider.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2015 gitblit.com.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.gitblit.auth;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.Constants;
+import com.gitblit.Constants.AccountType;
+import com.gitblit.Constants.AuthenticationType;
+import com.gitblit.Constants.Role;
+import com.gitblit.Keys;
+import com.gitblit.models.TeamModel;
+import com.gitblit.models.UserModel;
+import com.gitblit.utils.StringUtils;
+
+public class HttpHeaderAuthProvider extends AuthenticationProvider {
+
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+    protected String userHeaderName;
+    protected String teamHeaderName;
+    protected String teamHeaderSeparator;
+
+	public HttpHeaderAuthProvider() {
+		super("httpheader");
+	}
+
+	@Override
+	public void setup() {
+		// Load HTTP header configuration
+		userHeaderName = settings.getString(Keys.realm.httpheader.userheader, null);
+		teamHeaderName = settings.getString(Keys.realm.httpheader.teamheader, null);
+		teamHeaderSeparator = settings.getString(Keys.realm.httpheader.teamseparator, ",");
+
+		if (StringUtils.isEmpty(userHeaderName)) {
+			logger.warn("HTTP Header authentication is enabled, but no header is not defined in " + Keys.realm.httpheader.userheader);
+		}
+	}
+
+	@Override
+	public void stop() {}
+
+
+	@Override
+	public UserModel authenticate(HttpServletRequest httpRequest) {
+		// Try to authenticate using custom HTTP header if user header is defined
+		if (!StringUtils.isEmpty(userHeaderName)) {
+			String headerUserName = httpRequest.getHeader(userHeaderName);
+			if (!StringUtils.isEmpty(headerUserName) && !userManager.isInternalAccount(headerUserName)) {
+				// We have a user, try to load team names as well
+				Set<TeamModel> userTeams = new HashSet<>();
+				if (!StringUtils.isEmpty(teamHeaderName)) {
+					String headerTeamValue = httpRequest.getHeader(teamHeaderName);
+					if (!StringUtils.isEmpty(headerTeamValue)) {
+						String[] headerTeamNames = headerTeamValue.split(teamHeaderSeparator);
+						for (String teamName : headerTeamNames) {
+							teamName = teamName.trim();
+							if (!StringUtils.isEmpty(teamName)) {
+								TeamModel team = userManager.getTeamModel(teamName);
+								if (null == team) {
+									// Create teams here so they can marked with the correct AccountType
+									team = new TeamModel(teamName);
+									team.accountType = AccountType.HTTPHEADER;
+									updateTeam(team);
+								}
+								userTeams.add(team);
+							}
+						}
+					}
+				}
+
+				UserModel user = userManager.getUserModel(headerUserName);
+				if (user != null) {
+					// If team header is provided in request, reset all team memberships, even if resetting to empty set
+					if (!StringUtils.isEmpty(teamHeaderName)) {
+						user.teams.clear();
+						user.teams.addAll(userTeams);
+					}
+					updateUser(user);
+					return user;
+				} else if (settings.getBoolean(Keys.realm.httpheader.autoCreateAccounts, false)) {
+					// auto-create user from HTTP header
+					user = new UserModel(headerUserName.toLowerCase());
+					user.displayName = headerUserName;
+					user.password = Constants.EXTERNAL_ACCOUNT;
+					user.accountType = AccountType.HTTPHEADER;
+					user.teams.addAll(userTeams);
+					updateUser(user);
+					return user;
+				}
+			}
+		}
+
+		return null;
+	}
+
+	@Override
+	public UserModel authenticate(String username, char[] password){
+		// Username/password is not supported for HTTP header authentication
+		return null;
+	}
+
+	@Override
+	public AccountType getAccountType() {
+		return AccountType.HTTPHEADER;
+	}
+
+	@Override
+	public AuthenticationType getAuthenticationType() {
+		return AuthenticationType.HTTPHEADER;
+	}
+
+	@Override
+	public boolean supportsCredentialChanges() {
+		return false;
+	}
+
+	@Override
+	public boolean supportsDisplayNameChanges() {
+		return false;
+	}
+
+	@Override
+	public boolean supportsEmailAddressChanges() {
+		return false;
+	}
+
+	@Override
+	public boolean supportsTeamMembershipChanges() {
+		return StringUtils.isEmpty(teamHeaderName);
+	}
+
+	@Override
+	public boolean supportsRoleChanges(UserModel user, Role role) {
+		return true;
+	}
+
+	@Override
+	public boolean supportsRoleChanges(TeamModel team, Role role) {
+		return true;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/gitblit/manager/AuthenticationManager.java b/src/main/java/com/gitblit/manager/AuthenticationManager.java
index 7e0b07b..f092bfe 100644
--- a/src/main/java/com/gitblit/manager/AuthenticationManager.java
+++ b/src/main/java/com/gitblit/manager/AuthenticationManager.java
@@ -41,6 +41,7 @@
 import com.gitblit.auth.AuthenticationProvider;
 import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider;
 import com.gitblit.auth.HtpasswdAuthProvider;
+import com.gitblit.auth.HttpHeaderAuthProvider;
 import com.gitblit.auth.LdapAuthProvider;
 import com.gitblit.auth.PAMAuthProvider;
 import com.gitblit.auth.RedmineAuthProvider;
@@ -92,6 +93,7 @@
 		// map of shortcut provider names
 		providerNames = new HashMap<String, Class<? extends AuthenticationProvider>>();
 		providerNames.put("htpasswd", HtpasswdAuthProvider.class);
+		providerNames.put("httpheader", HttpHeaderAuthProvider.class);
 		providerNames.put("ldap", LdapAuthProvider.class);
 		providerNames.put("pam", PAMAuthProvider.class);
 		providerNames.put("redmine", RedmineAuthProvider.class);
@@ -170,7 +172,11 @@
 	}
 
 	/**
-	 * Authenticate a user based on HTTP request parameters.
+	 * Used to handle authentication for page requests.
+	 *
+	 * This allows authentication to occur based on the contents of the request
+	 * itself. If no configured @{AuthenticationProvider}s authenticate succesffully,
+	 * a request for login will be shown.
 	 *
 	 * Authentication by X509Certificate is tried first and then by cookie.
 	 *
@@ -185,7 +191,7 @@
 	/**
 	 * Authenticate a user based on HTTP request parameters.
 	 *
-	 * Authentication by servlet container principal, X509Certificate, cookie,
+	 * Authentication by custom HTTP header, servlet container principal, X509Certificate, cookie,
 	 * and finally BASIC header.
 	 *
 	 * @param httpRequest
@@ -319,6 +325,18 @@
 				}
 			}
 		}
+
+		// Check each configured AuthenticationProvider
+		for (AuthenticationProvider ap : authenticationProviders) {
+			UserModel authedUser = ap.authenticate(httpRequest);
+			if (null != authedUser) {
+				flagRequest(httpRequest, ap.getAuthenticationType(), authedUser.username);
+				logger.debug(MessageFormat.format("{0} authenticated by {1} from {2} for {3}",
+						authedUser.username, ap.getServiceName(), httpRequest.getRemoteAddr(),
+						httpRequest.getPathInfo()));
+				return validateAuthentication(authedUser, ap.getAuthenticationType());
+			}
+		}
 		return null;
 	}
 	
diff --git a/src/site/setup_authentication.mkd b/src/site/setup_authentication.mkd
index a3bf445..7113667 100644
--- a/src/site/setup_authentication.mkd
+++ b/src/site/setup_authentication.mkd
@@ -8,6 +8,7 @@
 * Windows authentication
 * PAM authentication
 * Htpasswd authentication
+* HTTP header authentication
 * Redmine auhentication
 * Salesforce.com authentication
 * Servlet container authentication
@@ -101,6 +102,17 @@
     realm.authenticationProviders = htpasswd
     realm.htpasswd.userFile = /path/to/htpasswd
 
+### HTTP Header Authentication
+
+HTTP header authentication allows you to use existing authentication performed by a trusted frontend, such as a reverse proxy. Ensure that when used, gitblit is ONLY availabe via the trusted frontend, otherwise it is vulnerable to a user adding the header explicitly.
+
+By default, no user or team header is defined, which results in all authentication failing this mechanism. The user header can also be defined while leaving the team header undefined, which causes users to be authenticated from the headers, but team memberships to be maintained locally.
+
+    realm.httpheader.userheader = REMOTE_USER
+    realm.httpheader.teamheader = X-GitblitExample-GroupNames
+    realm.httpheader.teamseparator = ,
+    realm.httpheader.autoCreateAccounts = false
+
 ### Redmine Authentication
 
 You may authenticate your users against a Redmine installation as long as your Redmine install has properly enabled [API authentication](http://www.redmine.org/projects/redmine/wiki/Rest_Api#Authentication).  This user service only supports user authentication; it does not support team creation based on Redmine groups.  Redmine administrators will also be Gitblit administrators.

--
Gitblit v1.9.1