summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/gitblit/manager/AuthenticationManager.java
diff options
context:
space:
mode:
authorJames Moger <james.moger@gitblit.com>2013-11-24 23:18:50 -0500
committerJames Moger <james.moger@gitblit.com>2013-11-29 11:05:51 -0500
commit04a98505a4ab8f48aee22800fcac193d9367d0ae (patch)
treeeb05bc77eeafda1c5b7af9d7b5b27012065f7a98 /src/main/java/com/gitblit/manager/AuthenticationManager.java
parentf8f6aa4d07cdfaaf23e24bf9eaf0a5fb9b437dda (diff)
downloadgitblit-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.java511
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;
+// }
+}