From 46f61d3990813d488454ce48596620e9c1cded1a Mon Sep 17 00:00:00 2001
From: Joel Johnson <mrjoel@lixil.net>
Date: Wed, 09 Dec 2015 09:38:36 -0500
Subject: [PATCH] implement an HTTP header AuthenticationProvider

---
 src/main/java/com/gitblit/auth/HttpHeaderAuthProvider.java   |  161 ++++++++++++++++++++++++++++++++
 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/auth/AuthenticationProvider.java   |   53 +++++++++-
 5 files changed, 268 insertions(+), 9 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/Constants.java b/src/main/java/com/gitblit/Constants.java
index 4aa8c0c..e925ee4 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, EXTERNAL, 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;
 	}
 	

--
Gitblit v1.9.1