// Copyright (C) 2014 The Android Open Source Project // // 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.googlesource.gerrit.plugins.gitblit.auth; import java.io.IOException; 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.AuthenticationType; import com.gitblit.Constants.Role; import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.manager.IAuthenticationManager; import com.gitblit.models.TeamModel; import com.gitblit.models.UserModel; import com.gitblit.transport.ssh.SshKey; import com.gitblit.utils.StringUtils; import com.google.common.base.Strings; import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl; import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.httpd.WebSession; import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthResult; import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.inject.Inject; import com.google.inject.Singleton; @Singleton public class GerritGitBlitAuthenticationManager implements IAuthenticationManager { private static final Logger log = LoggerFactory.getLogger(GerritGitBlitAuthenticationManager.class); private final AccountManager gerritAccountManager; private final DynamicItem<WebSession> gerritSession; private final GerritGitBlitUserManager userManager; private final IStoredSettings settings; private final String gerritUrl; private final String externalLogoutUrl; /** * Path part of the canonical plugin URL. */ private final String hostRelativePluginPath; @Inject public GerritGitBlitAuthenticationManager(final AccountManager gerritAccountManager, final DynamicItem<WebSession> gerritSession, final GerritGitBlitUserManager userManager, final IStoredSettings settings, @PluginName String pluginName, @PluginCanonicalWebUrl String pluginUrl, @CanonicalWebUrl String canonicalGerritUrl, AuthConfig authConfig) { this.gerritAccountManager = gerritAccountManager; this.gerritSession = gerritSession; this.userManager = userManager; this.settings = settings; this.gerritUrl = canonicalGerritUrl; this.externalLogoutUrl = authConfig.getLogoutURL(); this.hostRelativePluginPath = extractPluginPath(pluginUrl, pluginName); } /** * Determines the path part of the plugin Url. Should work OK even if Gerrit isn't at the root path, for instance at //somehost:someport/gerrit/. * * @param pluginUrl * as determined by Gerrit's plugin infrastructure * @param pluginName * name of this plugin * @return the absolute path to the plugin on this server. */ private String extractPluginPath(String pluginUrl, String pluginName) { // Let's be defensive int idx = -1; if (pluginUrl != null) { idx = pluginUrl.indexOf("//"); idx = pluginUrl.indexOf('/', idx < 0 ? 0 : idx + 2); } if (idx < 0) { // Huh? return "/plugins/" + pluginName; // A reasonable default; works only correctly if Gerrit is on the root path } // We do know that in any case it ends with /plugins/ + pluginName... return pluginUrl.substring(idx); } @Override public IAuthenticationManager start() { return this; } @Override public IAuthenticationManager stop() { return this; } @Override public UserModel authenticate(final HttpServletRequest httpRequest) { // Added by the GerritAuthenticationFilter. String username = (String) httpRequest.getAttribute("gerrit-username"); String token = (String) httpRequest.getAttribute("gerrit-token"); String password = (String) httpRequest.getAttribute("gerrit-password"); if (!Strings.isNullOrEmpty(token)) { return authenticateFromSession(httpRequest, username, token); } else if (!Strings.isNullOrEmpty(password)) { return authenticateViaGerrit(httpRequest, username, password); } else if (GerritGitBlitUserModel.ANONYMOUS_USER.equals(username)) { // XXX Do we really still need this branch? We "inherited" that from the old official plugin... return userManager.getUserModel(username); } return null; } @Override public UserModel authenticate(final String username, final SshKey key) { return null; } @Override public UserModel authenticate(final HttpServletRequest httpRequest, final boolean requiresCertificate) { return authenticate(httpRequest); } private UserModel authenticateFromSession(final HttpServletRequest httpRequest, String username, String token) { WebSession session = gerritSession.get(); if (session.getSessionId() == null || !session.getSessionId().equals(token)) { log.warn("Invalid Gerrit session token for user '{}'", username); return null; } if (!session.isSignedIn()) { log.warn("Gerrit session {} is not signed-in", session.getSessionId()); // XXX: maybe better return the anonymous user? // return userManager.getUserModel((String) null); return null; } String userName = session.getUser().getUserName(); // Gerrit users not necessarily have a username. Google OAuth returns users without user names. UserModel user; if (Strings.isNullOrEmpty(userName)) { user = userManager.getUnnamedGerritUser(); } else { if (!userName.equals(username)) { log.warn("Gerrit session {} is not assigned to user {}", session.getSessionId(), username); return null; } user = userManager.getUserModel(userName); } return loggedIn(httpRequest, user, token, null); } @Override public UserModel authenticate(String username, char[] password, String remoteIP) { return authenticateViaGerrit(null, username, new String(password)); } @Override public UserModel authenticate(String username) { // Only called via Gitblit's SSHD,which we have disabled. log.warn("External authentication in Gitblit not supported"); return null; } private UserModel authenticateViaGerrit(HttpServletRequest httpRequest, String username, String password) { if (Strings.isNullOrEmpty(username)) { log.warn("Authentication failed: no username"); return null; } if (Strings.isNullOrEmpty(password)) { log.warn("Authentication failed: no password for user '{}'", username); return null; } AuthRequest who = AuthRequest.forUser(username); who.setPassword(password); try { AuthResult authResp = gerritAccountManager.authenticate(who); gerritSession.get().login(authResp, false); log.info("Logged in {}", username); return loggedIn(httpRequest, userManager.getUserModel(username), password, authResp); } catch (AccountException | IOException e) { log.warn("Authentication failed for user '" + username + '\'', e); return null; } } @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; } private boolean isStandardLogin(HttpServletRequest request) { if (request == null) { log.warn("(Internal) Deprecated cookie method called."); return false; } HttpSession session = request.getSession(); AuthenticationType authenticationType = (AuthenticationType) session.getAttribute(Constants.ATTRIB_AUTHTYPE); return authenticationType != null && authenticationType.isStandard(); } @Override @Deprecated public void setCookie(HttpServletResponse response, UserModel user) { setCookie(null, response, user); } @Override public void setCookie(HttpServletRequest request, HttpServletResponse response, UserModel user) { if (settings.getBoolean(Keys.web.allowCookieAuthentication, true) && isStandardLogin(request)) { 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 (Strings.isNullOrEmpty(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)); } } userCookie.setPath(hostRelativePluginPath); response.addCookie(userCookie); } } @Override @Deprecated public void logout(HttpServletResponse response, UserModel user) { logout(null, response, user); } @Override public void logout(HttpServletRequest request, HttpServletResponse response, UserModel user) { gerritSession.get().logout(); setCookie(request, response, null); } /** * Logs out the user in GitBlit. Returns a URL to redirect to for Gerrit logout. * * @return a URL to redirect to, or {@code null} if none. */ public String logoutAndRedirect(HttpServletRequest request, HttpServletResponse response, UserModel user) { if (!Strings.isNullOrEmpty(gerritUrl)) { setCookie(request, response, null); // This redirect invokes the normal Gerrit logout process, regardless of what authentication mechanism is configured, // so logout should work properly also for OAuth, OpenID, and also respect auth.logoutUrl. return gerritUrl + (gerritUrl.endsWith("/") ? "" : "/") + "logout"; } log.warn("gerrit.config should define gerrit.canonicalWebUrl"); // Try to log out ourselves. We have no access to the OAuth/OpenId sessions, so we can't do anything for those. // See {@link com.google.gerrit.httpd.HttpLogoutServlet}. logout(request, response, user); return externalLogoutUrl; } @Override public boolean supportsCredentialChanges(UserModel user) { return false; } @Override public boolean supportsDisplayNameChanges(UserModel user) { return false; } @Override public boolean supportsEmailAddressChanges(UserModel user) { return false; } @Override public boolean supportsTeamMembershipChanges(UserModel user) { return false; } @Override public boolean supportsTeamMembershipChanges(TeamModel team) { return false; } private UserModel loggedIn(HttpServletRequest request, UserModel user, String credentials, AuthResult authentication) { if (authentication != null && !gerritSession.get().getUser().isIdentifiedUser()) { log.warn("Setting account after log-in for " + user.getName()); // We just logged in via Gerrit. However, if somehow somewhere some code called getCurrentUser() on that WebSession object before, // the "current user object" remains stuck on the previous value. Happens for instance in WrappedSyndicationFilter after the 401 // challenge. Frankly said, I don't know if that is a bug in Gerrit, or what's up. Methinks CacheBasedWebSession.login() should // (re-)set its private field "user" to null, so that the next call to getCurrentUser() re-computes the user object. We can get // around this by forcing the account id to the account we just authenticated. In this request, this user won't be able to do // Gerrit administration, but we're inside GitBlit anyway, so there's no need for this anyway. gerritSession.get().setUserAccountId(authentication.getAccountId()); } if (request != null) { request.getSession().setAttribute(Constants.ATTRIB_AUTHTYPE, AuthenticationType.CREDENTIALS); } user.cookie = StringUtils.getSHA1(user.getName() + credentials); // Code from GitBlit return user; } @Override public boolean supportsRoleChanges(UserModel user, Role role) { return false; } @Override public boolean supportsRoleChanges(TeamModel team, Role role) { return false; } }