diff options
author | James Moger <james.moger@gitblit.com> | 2013-11-24 23:18:50 -0500 |
---|---|---|
committer | James Moger <james.moger@gitblit.com> | 2013-11-29 11:05:51 -0500 |
commit | 04a98505a4ab8f48aee22800fcac193d9367d0ae (patch) | |
tree | eb05bc77eeafda1c5b7af9d7b5b27012065f7a98 /src/main/java/com/gitblit/manager/AuthenticationManager.java | |
parent | f8f6aa4d07cdfaaf23e24bf9eaf0a5fb9b437dda (diff) | |
download | gitblit-04a98505a4ab8f48aee22800fcac193d9367d0ae.tar.gz gitblit-04a98505a4ab8f48aee22800fcac193d9367d0ae.zip |
Refactor user services and separate authentication (issue-281)
Change-Id: I336e005e02623fc5e11a4f8b4408bea5465a43fd
Diffstat (limited to 'src/main/java/com/gitblit/manager/AuthenticationManager.java')
-rw-r--r-- | src/main/java/com/gitblit/manager/AuthenticationManager.java | 511 |
1 files changed, 511 insertions, 0 deletions
diff --git a/src/main/java/com/gitblit/manager/AuthenticationManager.java b/src/main/java/com/gitblit/manager/AuthenticationManager.java new file mode 100644 index 00000000..6e541c45 --- /dev/null +++ b/src/main/java/com/gitblit/manager/AuthenticationManager.java @@ -0,0 +1,511 @@ +/* + * Copyright 2013 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.manager; + +import java.nio.charset.Charset; +import java.security.Principal; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.wicket.RequestCycle; +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.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.auth.AuthenticationProvider; +import com.gitblit.auth.AuthenticationProvider.UsernamePasswordAuthenticationProvider; +import com.gitblit.auth.HtpasswdAuthProvider; +import com.gitblit.auth.LdapAuthProvider; +import com.gitblit.auth.PAMAuthProvider; +import com.gitblit.auth.RedmineAuthProvider; +import com.gitblit.auth.SalesforceAuthProvider; +import com.gitblit.auth.WindowsAuthProvider; +import com.gitblit.models.TeamModel; +import com.gitblit.models.UserModel; +import com.gitblit.utils.Base64; +import com.gitblit.utils.HttpUtils; +import com.gitblit.utils.StringUtils; +import com.gitblit.utils.X509Utils.X509Metadata; +import com.gitblit.wicket.GitBlitWebSession; + +/** + * The authentication manager handles user login & logout. + * + * @author James Moger + * + */ +public class AuthenticationManager implements IAuthenticationManager { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final IStoredSettings settings; + + private final IRuntimeManager runtimeManager; + + private final IUserManager userManager; + + private final List<AuthenticationProvider> authenticationProviders; + + private final Map<String, Class<? extends AuthenticationProvider>> providerNames; + + private final Map<String, String> legacyRedirects; + + public AuthenticationManager( + IRuntimeManager runtimeManager, + IUserManager userManager) { + + this.settings = runtimeManager.getSettings(); + this.runtimeManager = runtimeManager; + this.userManager = userManager; + this.authenticationProviders = new ArrayList<AuthenticationProvider>(); + + // map of shortcut provider names + providerNames = new HashMap<String, Class<? extends AuthenticationProvider>>(); + providerNames.put("htpasswd", HtpasswdAuthProvider.class); + providerNames.put("ldap", LdapAuthProvider.class); + providerNames.put("pam", PAMAuthProvider.class); + providerNames.put("redmine", RedmineAuthProvider.class); + providerNames.put("salesforce", SalesforceAuthProvider.class); + providerNames.put("windows", WindowsAuthProvider.class); + + // map of legacy external user services + legacyRedirects = new HashMap<String, String>(); + legacyRedirects.put("com.gitblit.HtpasswdUserService", "htpasswd"); + legacyRedirects.put("com.gitblit.LdapUserService", "ldap"); + legacyRedirects.put("com.gitblit.PAMUserService", "pam"); + legacyRedirects.put("com.gitblit.RedmineUserService", "redmine"); + legacyRedirects.put("com.gitblit.SalesforceUserService", "salesforce"); + legacyRedirects.put("com.gitblit.WindowsUserService", "windows"); + } + + @Override + public AuthenticationManager start() { + // automatically adjust legacy configurations + String realm = settings.getString(Keys.realm.userService, "${baseFolder}/users.conf"); + if (legacyRedirects.containsKey(realm)) { + logger.warn(""); + logger.warn("#################################################################"); + logger.warn(" IUserService '{}' is obsolete!", realm); + logger.warn(" Please set '{}={}'", "realm.authenticationProviders", legacyRedirects.get(realm)); + logger.warn("#################################################################"); + logger.warn(""); + + // conditionally override specified authentication providers + if (StringUtils.isEmpty(settings.getString(Keys.realm.authenticationProviders, null))) { + settings.overrideSetting(Keys.realm.authenticationProviders, legacyRedirects.get(realm)); + } + } + + // instantiate and setup specified authentication providers + List<String> providers = settings.getStrings(Keys.realm.authenticationProviders); + if (providers.isEmpty()) { + logger.info("External authentication disabled."); + } else { + for (String provider : providers) { + try { + Class<?> authClass; + if (providerNames.containsKey(provider)) { + // map the name -> class + authClass = providerNames.get(provider); + } else { + // reflective lookup + authClass = Class.forName(provider); + } + logger.info("setting up {}", authClass.getName()); + AuthenticationProvider authImpl = (AuthenticationProvider) authClass.newInstance(); + authImpl.setup(runtimeManager, userManager); + authenticationProviders.add(authImpl); + } catch (Exception e) { + logger.error("", e); + } + } + } + return this; + } + + @Override + public AuthenticationManager stop() { + return this; + } + + /** + * Authenticate a user based on HTTP request parameters. + * + * Authentication by X509Certificate is tried first and then by cookie. + * + * @param httpRequest + * @return a user object or null + */ + @Override + public UserModel authenticate(HttpServletRequest httpRequest) { + return authenticate(httpRequest, false); + } + + /** + * Authenticate a user based on HTTP request parameters. + * + * Authentication by servlet container principal, X509Certificate, cookie, + * and finally BASIC header. + * + * @param httpRequest + * @param requiresCertificate + * @return a user object or null + */ + @Override + public UserModel authenticate(HttpServletRequest httpRequest, boolean requiresCertificate) { + // try to authenticate by servlet container principal + if (!requiresCertificate) { + Principal principal = httpRequest.getUserPrincipal(); + if (principal != null) { + String username = principal.getName(); + if (!StringUtils.isEmpty(username)) { + boolean internalAccount = isInternalAccount(username); + UserModel user = userManager.getUserModel(username); + if (user != null) { + // existing user + flagWicketSession(AuthenticationType.CONTAINER); + logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}", + user.username, httpRequest.getRemoteAddr())); + return user; + } else if (settings.getBoolean(Keys.realm.container.autoCreateAccounts, false) + && !internalAccount) { + // auto-create user from an authenticated container principal + user = new UserModel(username.toLowerCase()); + user.displayName = username; + user.password = Constants.EXTERNAL_ACCOUNT; + user.accountType = AccountType.CONTAINER; + userManager.updateUserModel(user); + flagWicketSession(AuthenticationType.CONTAINER); + logger.debug(MessageFormat.format("{0} authenticated and created by servlet container principal from {1}", + user.username, httpRequest.getRemoteAddr())); + return user; + } else if (!internalAccount) { + logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted servlet container authentication from {1}", + principal.getName(), httpRequest.getRemoteAddr())); + } + } + } + } + + // try to authenticate by certificate + boolean checkValidity = settings.getBoolean(Keys.git.enforceCertificateValidity, true); + String [] oids = settings.getStrings(Keys.git.certificateUsernameOIDs).toArray(new String[0]); + UserModel model = HttpUtils.getUserModelFromCertificate(httpRequest, checkValidity, oids); + if (model != null) { + // grab real user model and preserve certificate serial number + UserModel user = userManager.getUserModel(model.username); + X509Metadata metadata = HttpUtils.getCertificateMetadata(httpRequest); + if (user != null) { + flagWicketSession(AuthenticationType.CERTIFICATE); + logger.debug(MessageFormat.format("{0} authenticated by client certificate {1} from {2}", + user.username, metadata.serialNumber, httpRequest.getRemoteAddr())); + return user; + } else { + logger.warn(MessageFormat.format("Failed to find UserModel for {0}, attempted client certificate ({1}) authentication from {2}", + model.username, metadata.serialNumber, httpRequest.getRemoteAddr())); + } + } + + if (requiresCertificate) { + // caller requires client certificate authentication (e.g. git servlet) + return null; + } + + // try to authenticate by cookie + UserModel user = authenticate(httpRequest.getCookies()); + if (user != null) { + flagWicketSession(AuthenticationType.COOKIE); + logger.debug(MessageFormat.format("{0} authenticated by cookie from {1}", + user.username, httpRequest.getRemoteAddr())); + return user; + } + + // try to authenticate by BASIC + final String authorization = httpRequest.getHeader("Authorization"); + if (authorization != null && authorization.startsWith("Basic")) { + // Authorization: Basic base64credentials + String base64Credentials = authorization.substring("Basic".length()).trim(); + String credentials = new String(Base64.decode(base64Credentials), + Charset.forName("UTF-8")); + // credentials = username:password + final String[] values = credentials.split(":", 2); + + if (values.length == 2) { + String username = values[0]; + char[] password = values[1].toCharArray(); + user = authenticate(username, password); + if (user != null) { + flagWicketSession(AuthenticationType.CREDENTIALS); + logger.debug(MessageFormat.format("{0} authenticated by BASIC request header from {1}", + user.username, httpRequest.getRemoteAddr())); + return user; + } else { + logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials from {1}", + username, httpRequest.getRemoteAddr())); + } + } + } + return null; + } + + /** + * Authenticate a user based on their cookie. + * + * @param cookies + * @return a user object or null + */ + protected UserModel authenticate(Cookie[] cookies) { + if (settings.getBoolean(Keys.web.allowCookieAuthentication, true)) { + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(Constants.NAME)) { + String value = cookie.getValue(); + return userManager.getUserModel(value.toCharArray()); + } + } + } + } + return null; + } + + protected void flagWicketSession(AuthenticationType authenticationType) { + RequestCycle requestCycle = RequestCycle.get(); + if (requestCycle != null) { + // flag the Wicket session, if this is a Wicket request + GitBlitWebSession session = GitBlitWebSession.get(); + session.authenticationType = authenticationType; + } + } + + /** + * Authenticate a user based on a username and password. + * + * @see IUserService.authenticate(String, char[]) + * @param username + * @param password + * @return a user object or null + */ + @Override + public UserModel authenticate(String username, char[] password) { + if (StringUtils.isEmpty(username)) { + // can not authenticate empty username + return null; + } + + String usernameDecoded = StringUtils.decodeUsername(username); + String pw = new String(password); + if (StringUtils.isEmpty(pw)) { + // can not authenticate empty password + return null; + } + // check to see if this is the federation user +// if (canFederate()) { +// if (usernameDecoded.equalsIgnoreCase(Constants.FEDERATION_USER)) { +// List<String> tokens = getFederationTokens(); +// if (tokens.contains(pw)) { +// return getFederationUser(); +// } +// } +// } + + // try local authentication + UserModel user = userManager.getUserModel(usernameDecoded); + if (user != null) { + UserModel returnedUser = null; + if (user.password.startsWith(StringUtils.MD5_TYPE)) { + // password digest + String md5 = StringUtils.MD5_TYPE + StringUtils.getMD5(new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.startsWith(StringUtils.COMBINED_MD5_TYPE)) { + // username+password digest + String md5 = StringUtils.COMBINED_MD5_TYPE + + StringUtils.getMD5(username.toLowerCase() + new String(password)); + if (user.password.equalsIgnoreCase(md5)) { + returnedUser = user; + } + } else if (user.password.equals(new String(password))) { + // plain-text password + returnedUser = user; + } + return returnedUser; + } + + // try registered external authentication providers + if (user == null) { + for (AuthenticationProvider provider : authenticationProviders) { + if (provider instanceof UsernamePasswordAuthenticationProvider) { + user = provider.authenticate(usernameDecoded, password); + if (user != null) { + // user authenticated + user.accountType = provider.getAccountType(); + return user; + } + } + } + } + return user; + } + + /** + * Sets a cookie for the specified user. + * + * @param response + * @param user + */ + @Override + public void setCookie(HttpServletResponse response, UserModel user) { + if (settings.getBoolean(Keys.web.allowCookieAuthentication, true)) { + GitBlitWebSession session = GitBlitWebSession.get(); + boolean standardLogin = session.authenticationType.isStandard(); + + if (standardLogin) { + Cookie userCookie; + if (user == null) { + // clear cookie for logout + userCookie = new Cookie(Constants.NAME, ""); + } else { + // set cookie for login + String cookie = userManager.getCookie(user); + if (StringUtils.isEmpty(cookie)) { + // create empty cookie + userCookie = new Cookie(Constants.NAME, ""); + } else { + // create real cookie + userCookie = new Cookie(Constants.NAME, cookie); + userCookie.setMaxAge(Integer.MAX_VALUE); + } + } + userCookie.setPath("/"); + response.addCookie(userCookie); + } + } + } + + /** + * Logout a user. + * + * @param user + */ + @Override + public void logout(HttpServletResponse response, UserModel user) { + setCookie(response, null); + } + + /** + * Returns true if the user's credentials can be changed. + * + * @param user + * @return true if the user service supports credential changes + */ + @Override + public boolean supportsCredentialChanges(UserModel user) { + return (user != null && user.isLocalAccount()) || findProvider(user).supportsCredentialChanges(); + } + + /** + * Returns true if the user's display name can be changed. + * + * @param user + * @return true if the user service supports display name changes + */ + @Override + public boolean supportsDisplayNameChanges(UserModel user) { + return (user != null && user.isLocalAccount()) || findProvider(user).supportsDisplayNameChanges(); + } + + /** + * Returns true if the user's email address can be changed. + * + * @param user + * @return true if the user service supports email address changes + */ + @Override + public boolean supportsEmailAddressChanges(UserModel user) { + return (user != null && user.isLocalAccount()) || findProvider(user).supportsEmailAddressChanges(); + } + + /** + * Returns true if the user's team memberships can be changed. + * + * @param user + * @return true if the user service supports team membership changes + */ + @Override + public boolean supportsTeamMembershipChanges(UserModel user) { + return (user != null && user.isLocalAccount()) || findProvider(user).supportsTeamMembershipChanges(); + } + + /** + * Returns true if the team memberships can be changed. + * + * @param user + * @return true if the team membership can be changed + */ + @Override + public boolean supportsTeamMembershipChanges(TeamModel team) { + return (team != null && team.isLocalTeam()) || findProvider(team).supportsTeamMembershipChanges(); + } + + protected AuthenticationProvider findProvider(UserModel user) { + for (AuthenticationProvider provider : authenticationProviders) { + if (provider.getAccountType().equals(user.accountType)) { + return provider; + } + } + return AuthenticationProvider.NULL_PROVIDER; + } + + protected AuthenticationProvider findProvider(TeamModel team) { + for (AuthenticationProvider provider : authenticationProviders) { + if (provider.getAccountType().equals(team.accountType)) { + return provider; + } + } + return AuthenticationProvider.NULL_PROVIDER; + } + + /** + * Returns true if the username represents an internal account + * + * @param username + * @return true if the specified username represents an internal account + */ + protected boolean isInternalAccount(String username) { + return !StringUtils.isEmpty(username) + && (username.equalsIgnoreCase(Constants.FEDERATION_USER) + || username.equalsIgnoreCase(UserModel.ANONYMOUS.username)); + } + +// protected UserModel getFederationUser() { +// // the federation user is an administrator +// UserModel federationUser = new UserModel(Constants.FEDERATION_USER); +// federationUser.canAdmin = true; +// return federationUser; +// } +} |