Git over HTTP BasicAuth against Gerrit basic auth.

Allows the configuration of native Gerrit
username/password authentication scheme used for
Git over HTTP BasicAuth, as alternative of default
DigestAuth scheme against the random generated
password on Gerrit DB.

Example gerrit.config:

[auth]
        type = LDAP
        gitBasicAuth = true

Git over HTTP protocol will be authenticated using
HTTP-BasicAuth and credentials checked on LDAP.

Change-Id: Ibe589ab2b0887d85fd5ae5a4c110dea10dac5d6d
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index df0d5da..cb70254 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -319,6 +319,18 @@
 +
 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 aa004e3..6ca9949 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 @@
     Class<? extends Filter> 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 0000000..5b39cb2
--- /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.
+ * <p>
+ * 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 <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
+ */
+@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<WebSession> session;
+  private final AccountCache accountCache;
+  private final AccountManager accountManager;
+  private final AuthConfig authConfig;
+
+  @Inject
+  ProjectBasicAuthFilter(Provider<WebSession> 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 a0f0d36..6f1096c 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 @@
   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<OpenIdProviderPattern> trustedOpenIDs;
   private final List<OpenIdProviderPattern> allowedOpenIDs;
@@ -56,6 +58,9 @@
     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 @@
     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<AccountExternalId> ids) {
     switch (getAuthType()) {
       case DEVELOPMENT_BECOME_ANY_ACCOUNT: