From 46f61d3990813d488454ce48596620e9c1cded1a Mon Sep 17 00:00:00 2001 From: Joel Johnson Date: Fri, 12 Jun 2015 14:31:07 -0600 Subject: [PATCH] implement an HTTP header AuthenticationProvider --- src/main/distrib/data/defaults.properties | 37 ++++ src/main/java/com/gitblit/Constants.java | 4 +- .../gitblit/auth/AuthenticationProvider.java | 53 +++++- .../gitblit/auth/HttpHeaderAuthProvider.java | 161 ++++++++++++++++++ .../manager/AuthenticationManager.java | 22 ++- 5 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/gitblit/auth/HttpHeaderAuthProvider.java diff --git a/src/main/distrib/data/defaults.properties b/src/main/distrib/data/defaults.properties index ce6267a5..403b7417 100644 --- a/src/main/distrib/data/defaults.properties +++ b/src/main/distrib/data/defaults.properties @@ -817,6 +817,7 @@ realm.userService = ${baseFolder}/users.conf # Valid providers are: # # htpasswd +# httpheader # ldap # pam # redmine @@ -1739,6 +1740,42 @@ realm.pam.serviceName = system-auth # 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 4aa8c0ca..e925ee47 100644 --- a/src/main/java/com/gitblit/Constants.java +++ b/src/main/java/com/gitblit/Constants.java @@ -574,7 +574,7 @@ public class Constants { } 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 class Constants { } 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 c56c1843..0bfe2351 100644 --- a/src/main/java/com/gitblit/auth/AuthenticationProvider.java +++ b/src/main/java/com/gitblit/auth/AuthenticationProvider.java @@ -18,11 +18,14 @@ package com.gitblit.auth; 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 @@ public abstract class AuthenticationProvider { 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 class AuthenticationProvider { 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 @@ public abstract class AuthenticationProvider { * 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 @@ public abstract class AuthenticationProvider { * 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 @@ public abstract class AuthenticationProvider { * 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 @@ public abstract class AuthenticationProvider { super(serviceName); } + @Override + public UserModel authenticate(HttpServletRequest httpRequest) { + return null; + } + + @Override + public AuthenticationType getAuthenticationType() { + return AuthenticationType.CREDENTIALS; + } + @Override public void stop() { @@ -202,6 +235,11 @@ public abstract class AuthenticationProvider { } + @Override + public UserModel authenticate(HttpServletRequest httpRequest) { + return null; + } + @Override public UserModel authenticate(String username, char[] password) { return null; @@ -212,6 +250,11 @@ public abstract class AuthenticationProvider { return AccountType.LOCAL; } + @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 00000000..3a9c5399 --- /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 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 7e0b07be..f092bfed 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.Keys; 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 @@ public class AuthenticationManager implements IAuthenticationManager { // map of shortcut provider names providerNames = new HashMap>(); 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 @@ public class AuthenticationManager implements IAuthenticationManager { } /** - * 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 @@ public class AuthenticationManager implements IAuthenticationManager { /** * 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 @@ public class AuthenticationManager implements IAuthenticationManager { } } } + + // 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; } -- 2.39.5