blob: 0a4d8ed72b698c5426165c20951959c545879d3c [file] [log] [blame]
/*
* 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 java.util.concurrent.TimeUnit;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
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.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.HttpHeaderAuthProvider;
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.transport.ssh.SshKey;
import com.gitblit.utils.Base64;
import com.gitblit.utils.HttpUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.X509Utils.X509Metadata;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
* The authentication manager handles user login & logout.
*
* @author James Moger
*
*/
@Singleton
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;
@Inject
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("httpheader", HttpHeaderAuthProvider.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(Constants.BORDER2);
logger.warn(" IUserService '{}' is obsolete!", realm);
logger.warn(" Please set '{}={}'", "realm.authenticationProviders", legacyRedirects.get(realm));
logger.warn(Constants.BORDER2);
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() {
for (AuthenticationProvider provider : authenticationProviders) {
try {
provider.stop();
} catch (Exception e) {
logger.error("Failed to stop " + provider.getClass().getSimpleName(), e);
}
}
return this;
}
public void addAuthenticationProvider(AuthenticationProvider prov) {
authenticationProviders.add(prov);
}
/**
* 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.
*
* @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 custom HTTP header, 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) {
// Check if this request has already been authenticated, and trust that instead of re-processing
String reqAuthUser = (String) httpRequest.getAttribute(Constants.ATTRIB_AUTHUSER);
if (!StringUtils.isEmpty(reqAuthUser)) {
logger.debug("Called servlet authenticate when request is already authenticated.");
return userManager.getUserModel(reqAuthUser);
}
// 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 = userManager.isInternalAccount(username);
UserModel user = userManager.getUserModel(username);
if (user != null) {
// existing user
flagRequest(httpRequest, AuthenticationType.CONTAINER, user.username);
logger.debug(MessageFormat.format("{0} authenticated by servlet container principal from {1}",
user.username, httpRequest.getRemoteAddr()));
return validateAuthentication(user, AuthenticationType.CONTAINER);
} 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;
// Try to extract user's informations for the session
// it uses "realm.container.autoAccounts.*" as the attribute name to look for
HttpSession session = httpRequest.getSession();
String emailAddress = resolveAttribute(session, Keys.realm.container.autoAccounts.emailAddress);
if(emailAddress != null) {
user.emailAddress = emailAddress;
}
String displayName = resolveAttribute(session, Keys.realm.container.autoAccounts.displayName);
if(displayName != null) {
user.displayName = displayName;
}
String userLocale = resolveAttribute(session, Keys.realm.container.autoAccounts.locale);
if(userLocale != null) {
user.getPreferences().setLocale(userLocale);
}
String adminRole = settings.getString(Keys.realm.container.autoAccounts.adminRole, null);
if(adminRole != null && ! adminRole.isEmpty()) {
if(httpRequest.isUserInRole(adminRole)) {
user.canAdmin = true;
}
}
userManager.updateUserModel(user);
flagRequest(httpRequest, AuthenticationType.CONTAINER, user.username);
logger.debug(MessageFormat.format("{0} authenticated and created by servlet container principal from {1}",
user.username, httpRequest.getRemoteAddr()));
return validateAuthentication(user, AuthenticationType.CONTAINER);
} 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) {
flagRequest(httpRequest, AuthenticationType.CERTIFICATE, user.username);
logger.debug(MessageFormat.format("{0} authenticated by client certificate {1} from {2}",
user.username, metadata.serialNumber, httpRequest.getRemoteAddr()));
return validateAuthentication(user, AuthenticationType.CERTIFICATE);
} 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;
}
UserModel user = null;
// try to authenticate by cookie
String cookie = getCookie(httpRequest);
if (!StringUtils.isEmpty(cookie)) {
user = userManager.getUserModel(cookie.toCharArray());
if (user != null) {
flagRequest(httpRequest, AuthenticationType.COOKIE, user.username);
logger.debug(MessageFormat.format("{0} authenticated by cookie from {1}",
user.username, httpRequest.getRemoteAddr()));
return validateAuthentication(user, AuthenticationType.COOKIE);
}
}
// 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, httpRequest.getRemoteAddr());
if (user != null) {
flagRequest(httpRequest, AuthenticationType.CREDENTIALS, user.username);
logger.debug(MessageFormat.format("{0} authenticated by BASIC request header from {1}",
user.username, httpRequest.getRemoteAddr()));
return validateAuthentication(user, AuthenticationType.CREDENTIALS);
}
}
}
// 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;
}
/**
* Extract given attribute from the session and return it's content
* it return null if attributeMapping is empty, or if the value is
* empty
*
* @param session The user session
* @param attributeMapping
* @return
*/
private String resolveAttribute(HttpSession session, String attributeMapping) {
String attributeName = settings.getString(attributeMapping, null);
if(StringUtils.isEmpty(attributeName)) {
return null;
}
Object attributeValue = session.getAttribute(attributeName);
if(attributeValue == null) {
return null;
}
String value = attributeValue.toString();
if(value.isEmpty()) {
return null;
}
return value;
}
/**
* Authenticate a user based on a public key.
*
* This implementation assumes that the authentication has already take place
* (e.g. SSHDaemon) and that this is a validation/verification of the user.
*
* @param username
* @param key
* @return a user object or null
*/
@Override
public UserModel authenticate(String username, SshKey key) {
if (username != null) {
if (!StringUtils.isEmpty(username)) {
UserModel user = userManager.getUserModel(username);
if (user != null) {
// existing user
logger.debug(MessageFormat.format("{0} authenticated by {1} public key",
user.username, key.getAlgorithm()));
return validateAuthentication(user, AuthenticationType.PUBLIC_KEY);
}
logger.warn(MessageFormat.format("Failed to find UserModel for {0} during public key authentication",
username));
}
} else {
logger.warn("Empty user passed to AuthenticationManager.authenticate!");
}
return null;
}
/**
* Return the UserModel for already authenticated user.
*
* This implementation assumes that the authentication has already take place
* (e.g. SSHDaemon) and that this is a validation/verification of the user.
*
* @param username
* @return a user object or null
*/
@Override
public UserModel authenticate(String username) {
if (username != null) {
if (!StringUtils.isEmpty(username)) {
UserModel user = userManager.getUserModel(username);
if (user != null) {
// existing user
logger.debug(MessageFormat.format("{0} authenticated externally", user.username));
return validateAuthentication(user, AuthenticationType.CONTAINER);
}
logger.warn(MessageFormat.format("Failed to find UserModel for {0} during external authentication",
username));
}
} else {
logger.warn("Empty user passed to AuthenticationManager.authenticate!");
}
return null;
}
/**
* This method allows the authentication manager to reject authentication
* attempts. It is called after the username/secret have been verified to
* ensure that the authentication technique has been logged.
*
* @param user
* @return
*/
protected UserModel validateAuthentication(UserModel user, AuthenticationType type) {
if (user == null) {
return null;
}
if (user.disabled) {
// user has been disabled
logger.warn("Rejected {} authentication attempt by disabled account \"{}\"",
type, user.username);
return null;
}
return user;
}
protected void flagRequest(HttpServletRequest httpRequest, AuthenticationType authenticationType, String authedUsername) {
httpRequest.setAttribute(Constants.ATTRIB_AUTHUSER, authedUsername);
httpRequest.setAttribute(Constants.ATTRIB_AUTHTYPE, 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, String remoteIP) {
if (StringUtils.isEmpty(username)) {
// can not authenticate empty username
return null;
}
if (username.equalsIgnoreCase(Constants.FEDERATION_USER)) {
// can not authenticate internal FEDERATION_USER at this point
// it must be routed to FederationManager
return null;
}
String usernameDecoded = StringUtils.decodeUsername(username);
String pw = new String(password);
if (StringUtils.isEmpty(pw)) {
// can not authenticate empty password
return null;
}
UserModel user = userManager.getUserModel(usernameDecoded);
// try local authentication
if (user != null && user.isLocalAccount()) {
UserModel returnedUser = authenticateLocal(user, password);
if (returnedUser != null) {
// user authenticated
return returnedUser;
}
} else {
// try registered external authentication providers
for (AuthenticationProvider provider : authenticationProviders) {
if (provider instanceof UsernamePasswordAuthenticationProvider) {
UserModel returnedUser = provider.authenticate(usernameDecoded, password);
if (returnedUser != null) {
// user authenticated
returnedUser.accountType = provider.getAccountType();
return validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
}
}
}
}
// could not authenticate locally or with a provider
logger.warn(MessageFormat.format("Failed login attempt for {0}, invalid credentials from {1}", username,
remoteIP != null ? remoteIP : "unknown"));
return null;
}
/**
* Returns a UserModel if local authentication succeeds.
*
* @param user
* @param password
* @return a UserModel if local authentication succeeds, null otherwise
*/
protected UserModel authenticateLocal(UserModel user, char [] password) {
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(user.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 validateAuthentication(returnedUser, AuthenticationType.CREDENTIALS);
}
/**
* Returns the Gitlbit cookie in the request.
*
* @param request
* @return the Gitblit cookie for the request or null if not found
*/
@Override
public String getCookie(HttpServletRequest request) {
if (settings.getBoolean(Keys.web.allowCookieAuthentication, true)) {
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(Constants.NAME)) {
String value = cookie.getValue();
return value;
}
}
}
}
return null;
}
/**
* Sets a cookie for the specified user.
*
* @param response
* @param user
*/
@Override
@Deprecated
public void setCookie(HttpServletResponse response, UserModel user) {
setCookie(null, response, user);
}
/**
* Sets a cookie for the specified user.
*
* @param request
* @param response
* @param user
*/
@Override
public void setCookie(HttpServletRequest request, HttpServletResponse response, UserModel user) {
if (settings.getBoolean(Keys.web.allowCookieAuthentication, true)) {
boolean standardLogin = true;
if (null != request) {
// Pull the auth type from the request, it is set there if container managed
AuthenticationType authenticationType = (AuthenticationType) request.getAttribute(Constants.ATTRIB_AUTHTYPE);
if (null != authenticationType)
standardLogin = 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);
// expire the cookie in 7 days
userCookie.setMaxAge((int) TimeUnit.DAYS.toSeconds(7));
// Set cookies HttpOnly so they are not accessible to JavaScript engines
userCookie.setHttpOnly(true);
// Set secure cookie if only HTTPS is used
userCookie.setSecure(httpsOnly());
}
}
String path = "/";
if (request != null) {
if (!StringUtils.isEmpty(request.getContextPath())) {
path = request.getContextPath();
}
}
userCookie.setPath(path);
response.addCookie(userCookie);
}
}
}
private boolean httpsOnly() {
int port = settings.getInteger(Keys.server.httpPort, 0);
int tlsPort = settings.getInteger(Keys.server.httpsPort, 0);
return (port <= 0 && tlsPort > 0) ||
(port > 0 && tlsPort > 0 && settings.getBoolean(Keys.server.redirectToHttpsPort, true) );
}
/**
* Logout a user.
*
* @param response
* @param user
*/
@Override
@Deprecated
public void logout(HttpServletResponse response, UserModel user) {
setCookie(null, response, null);
}
/**
* Logout a user.
*
* @param request
* @param response
* @param user
*/
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, UserModel user) {
setCookie(request, 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();
}
/**
* Returns true if the user's role can be changed.
*
* @param user
* @return true if the user's role can be changed
*/
@Override
public boolean supportsRoleChanges(UserModel user, Role role) {
return (user != null && user.isLocalAccount()) || findProvider(user).supportsRoleChanges(user, role);
}
/**
* Returns true if the team's role can be changed.
*
* @param user
* @return true if the team's role can be changed
*/
@Override
public boolean supportsRoleChanges(TeamModel team, Role role) {
return (team != null && team.isLocalTeam()) || findProvider(team).supportsRoleChanges(team, role);
}
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;
}
}