diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index df0d5da549..cb70254db5 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -319,6 +319,18 @@ then Gerrit will do the authentication (using DIGEST authentication). + By default this is set to false. +[[auth.gitBasicAuth]]auth.gitBasicAuth:: ++ +If true then Git over HTTP and HTTP/S traffic is authenticated using +standard BasicAuth and credentials validated using the same auth +method configured for Gerrit Web UI. ++ +This parameter only affects git over http traffic. If set to false +then Gerrit will authenticate through DIGEST authentication and +the randomly generated HTTP password in Gerrit DB. ++ +By default this is set to false. + [[auth.userNameToLowerCase]]auth.userNameToLowerCase:: + If set the username that is received to authenticate a git operation diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java index aa004e3385..6ca9949295 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java @@ -34,6 +34,8 @@ public class GitOverHttpModule extends ServletModule { Class authFilter; if (authConfig.isTrustContainerAuth()) { authFilter = ContainerAuthFilter.class; + } else if (authConfig.isGitBasichAuth()) { + authFilter = ProjectBasicAuthFilter.class; } else { authFilter = ProjectDigestFilter.class; } diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java new file mode 100644 index 0000000000..5b39cb2f9e --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java @@ -0,0 +1,202 @@ +// Copyright (C) 2012 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.google.gerrit.httpd; + +import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; + +import com.google.common.base.Objects; +import com.google.common.base.Strings; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountException; +import com.google.gerrit.server.account.AccountManager; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.AuthMethod; +import com.google.gerrit.server.account.AuthRequest; +import com.google.gerrit.server.account.AuthResult; +import com.google.gerrit.server.config.AuthConfig; +import com.google.inject.Inject; +import com.google.inject.Provider; +import com.google.inject.Singleton; + +import org.apache.commons.codec.binary.Base64; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Locale; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; + +/** + * Authenticates the current user by HTTP basic authentication. + *

+ * The current HTTP request is authenticated by looking up the username and + * password from the Base64 encoded Authorization header and validating them + * against any username/password configured authentication system in Gerrit. + * This filter is intended only to protect the {@link ProjectServlet} and its + * handled URLs, which provide remote repository access over HTTP. + * + * @see RFC 2617 + */ +@Singleton +class ProjectBasicAuthFilter implements Filter { + private static final Logger log = LoggerFactory + .getLogger(ProjectBasicAuthFilter.class); + + public static final String REALM_NAME = "Gerrit Code Review"; + private static final String AUTHORIZATION = "Authorization"; + private static final String LIT_BASIC = "Basic "; + + private final Provider session; + private final AccountCache accountCache; + private final AccountManager accountManager; + private final AuthConfig authConfig; + + @Inject + ProjectBasicAuthFilter(Provider session, + AccountCache accountCache, AccountManager accountManager, + AuthConfig authConfig) { + this.session = session; + this.accountCache = accountCache; + this.accountManager = accountManager; + this.authConfig = authConfig; + } + + @Override + public void init(FilterConfig config) { + } + + @Override + public void destroy() { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + Response rsp = new Response((HttpServletResponse) response); + + if (verify(req, rsp)) { + chain.doFilter(req, rsp); + } + } + + private boolean verify(HttpServletRequest req, Response rsp) + throws IOException { + final String hdr = req.getHeader(AUTHORIZATION); + if (hdr == null) { + // Allow an anonymous connection through, or it might be using a + // session cookie instead of basic authentication. + // + return true; + } + + final byte[] decoded = + Base64.decodeBase64(hdr.substring(LIT_BASIC.length())); + String usernamePassword = new String(decoded, encoding(req)); + int splitPos = usernamePassword.indexOf(':'); + if (splitPos < 1) { + rsp.sendError(SC_UNAUTHORIZED); + return false; + } + + String username = usernamePassword.substring(0, splitPos); + String password = usernamePassword.substring(splitPos + 1); + if (Strings.isNullOrEmpty(password)) { + rsp.sendError(SC_UNAUTHORIZED); + return false; + } + if (authConfig.isUserNameToLowerCase()) { + username = username.toLowerCase(Locale.US); + } + + final AccountState who = accountCache.getByUsername(username); + if (who == null || !who.getAccount().isActive()) { + log.warn("Authentication failed for " + username + + ": account inactive or not provisioned in Gerrit"); + rsp.sendError(SC_UNAUTHORIZED); + return false; + } + + AuthRequest whoAuth = AuthRequest.forUser(username); + whoAuth.setPassword(password); + + try { + AuthResult whoAuthResult = accountManager.authenticate(whoAuth); + session.get().setUserAccountId(whoAuthResult.getAccountId(), + AuthMethod.PASSWORD); + return true; + } catch (AccountException e) { + log.warn("Authentication failed for " + username, e); + rsp.sendError(SC_UNAUTHORIZED); + return false; + } + } + + private String encoding(HttpServletRequest req) { + return Objects.firstNonNull(req.getCharacterEncoding(), "UTF-8"); + } + + class Response extends HttpServletResponseWrapper { + private static final String WWW_AUTHENTICATE = "WWW-Authenticate"; + + Response(HttpServletResponse rsp) { + super(rsp); + } + + private void status(int sc) { + if (sc == SC_UNAUTHORIZED) { + StringBuilder v = new StringBuilder(); + v.append(LIT_BASIC); + v.append("realm=\"" + REALM_NAME + "\""); + setHeader(WWW_AUTHENTICATE, v.toString()); + } else if (containsHeader(WWW_AUTHENTICATE)) { + setHeader(WWW_AUTHENTICATE, null); + } + } + + @Override + public void sendError(int sc, String msg) throws IOException { + status(sc); + super.sendError(sc, msg); + } + + @Override + public void sendError(int sc) throws IOException { + status(sc); + super.sendError(sc); + } + + @Override + public void setStatus(int sc, String sm) { + status(sc); + super.setStatus(sc, sm); + } + + @Override + public void setStatus(int sc) { + status(sc); + super.setStatus(sc); + } + } +} diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java index a0f0d365c1..6f1096c41f 100644 --- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java +++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java @@ -36,6 +36,8 @@ public class AuthConfig { private final AuthType authType; private final String httpHeader; private final boolean trustContainerAuth; + private final boolean userNameToLowerCase; + private final boolean gitBasicAuth; private final String logoutUrl; private final List trustedOpenIDs; private final List allowedOpenIDs; @@ -56,6 +58,9 @@ public class AuthConfig { cookiePath = cfg.getString("auth", null, "cookiepath"); cookieSecure = cfg.getBoolean("auth", "cookiesecure", false); trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false); + gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false); + userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false); + String key = cfg.getString("auth", null, "registerEmailPrivateKey"); if (key != null && !key.isEmpty()) { @@ -132,6 +137,16 @@ public class AuthConfig { return trustContainerAuth; } + /** Whether user name should be converted to lower-case before validation */ + public boolean isUserNameToLowerCase() { + return userNameToLowerCase; + } + + /** Whether git-over-http should use Gerrit basic authentication scheme. */ + public boolean isGitBasichAuth() { + return gitBasicAuth; + } + public boolean isIdentityTrustable(final Collection ids) { switch (getAuthType()) { case DEVELOPMENT_BECOME_ANY_ACCOUNT: