LCOV - code coverage report
Current view: top level - httpd - ProjectBasicAuthFilter.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 92 115 80.0 %
Date: 2022-11-19 15:00:39 Functions: 18 19 94.7 %

          Line data    Source code
       1             : // Copyright (C) 2012 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.httpd;
      16             : 
      17             : import static java.nio.charset.StandardCharsets.UTF_8;
      18             : import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE;
      19             : import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
      20             : 
      21             : import com.google.common.base.MoreObjects;
      22             : import com.google.common.base.Strings;
      23             : import com.google.common.flogger.FluentLogger;
      24             : import com.google.common.io.BaseEncoding;
      25             : import com.google.gerrit.common.Nullable;
      26             : import com.google.gerrit.entities.Account;
      27             : import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
      28             : import com.google.gerrit.extensions.registration.DynamicItem;
      29             : import com.google.gerrit.server.AccessPath;
      30             : import com.google.gerrit.server.account.AccountCache;
      31             : import com.google.gerrit.server.account.AccountException;
      32             : import com.google.gerrit.server.account.AccountManager;
      33             : import com.google.gerrit.server.account.AccountState;
      34             : import com.google.gerrit.server.account.AuthRequest;
      35             : import com.google.gerrit.server.account.AuthResult;
      36             : import com.google.gerrit.server.account.AuthenticationFailedException;
      37             : import com.google.gerrit.server.account.externalids.PasswordVerifier;
      38             : import com.google.gerrit.server.auth.AuthenticationUnavailableException;
      39             : import com.google.gerrit.server.auth.NoSuchUserException;
      40             : import com.google.gerrit.server.config.AuthConfig;
      41             : import com.google.inject.Inject;
      42             : import com.google.inject.Singleton;
      43             : import java.io.IOException;
      44             : import java.util.Locale;
      45             : import java.util.Optional;
      46             : import javax.servlet.Filter;
      47             : import javax.servlet.FilterChain;
      48             : import javax.servlet.FilterConfig;
      49             : import javax.servlet.ServletException;
      50             : import javax.servlet.ServletRequest;
      51             : import javax.servlet.ServletResponse;
      52             : import javax.servlet.http.HttpServletRequest;
      53             : import javax.servlet.http.HttpServletResponse;
      54             : import javax.servlet.http.HttpServletResponseWrapper;
      55             : import org.eclipse.jgit.http.server.GitSmartHttpTools;
      56             : 
      57             : /**
      58             :  * Authenticates the current user by HTTP basic authentication.
      59             :  *
      60             :  * <p>The current HTTP request is authenticated by looking up the username and password from the
      61             :  * Base64 encoded Authorization header and validating them against any username/password configured
      62             :  * authentication system in Gerrit. This filter is intended only to protect the {@link
      63             :  * GitOverHttpServlet} and its handled URLs, which provide remote repository access over HTTP.
      64             :  *
      65             :  * @see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
      66             :  */
      67             : @Singleton
      68             : class ProjectBasicAuthFilter implements Filter {
      69         100 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      70             : 
      71             :   public static final String REALM_NAME = "Gerrit Code Review";
      72             :   private static final String AUTHORIZATION = "Authorization";
      73             :   private static final String LIT_BASIC = "Basic ";
      74             : 
      75             :   private final DynamicItem<WebSession> session;
      76             :   private final AccountCache accountCache;
      77             :   private final AccountManager accountManager;
      78             :   private final AuthConfig authConfig;
      79             :   private final AuthRequest.Factory authRequestFactory;
      80             :   private final PasswordVerifier passwordVerifier;
      81             : 
      82             :   @Inject
      83             :   ProjectBasicAuthFilter(
      84             :       DynamicItem<WebSession> session,
      85             :       AccountCache accountCache,
      86             :       AccountManager accountManager,
      87             :       AuthConfig authConfig,
      88             :       AuthRequest.Factory authRequestFactory,
      89         100 :       PasswordVerifier passwordVerifier) {
      90         100 :     this.session = session;
      91         100 :     this.accountCache = accountCache;
      92         100 :     this.accountManager = accountManager;
      93         100 :     this.authConfig = authConfig;
      94         100 :     this.authRequestFactory = authRequestFactory;
      95         100 :     this.passwordVerifier = passwordVerifier;
      96         100 :   }
      97             : 
      98             :   @Override
      99          99 :   public void init(FilterConfig config) {}
     100             : 
     101             :   @Override
     102          99 :   public void destroy() {}
     103             : 
     104             :   @Override
     105             :   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
     106             :       throws IOException, ServletException {
     107          38 :     HttpServletRequest req = (HttpServletRequest) request;
     108          38 :     Response rsp = new Response((HttpServletResponse) response);
     109             : 
     110          38 :     if (isSignedInGitRequest(req) || verify(req, rsp)) {
     111          38 :       chain.doFilter(req, rsp);
     112             :     }
     113          38 :   }
     114             : 
     115             :   private boolean isSignedInGitRequest(HttpServletRequest req) {
     116          38 :     boolean isGitRequest = req.getRequestURI() != null && GitSmartHttpTools.isGitClient(req);
     117          38 :     boolean isAlreadySignedIn = session.get().isSignedIn();
     118          38 :     boolean res = isAlreadySignedIn && isGitRequest;
     119          38 :     logger.atFine().log(
     120             :         "HTTP:%s %s signedIn=%s (isAlreadySignedIn=%s, isGitRequest=%s)",
     121          38 :         req.getMethod(), req.getRequestURI(), res, isAlreadySignedIn, isGitRequest);
     122          38 :     return res;
     123             :   }
     124             : 
     125             :   private boolean verify(HttpServletRequest req, Response rsp) throws IOException {
     126          38 :     final String hdr = req.getHeader(AUTHORIZATION);
     127          38 :     if (hdr == null || !hdr.startsWith(LIT_BASIC)) {
     128             :       // Allow an anonymous connection through, or it might be using a
     129             :       // session cookie instead of basic authentication.
     130          34 :       return true;
     131             :     }
     132             : 
     133          38 :     final byte[] decoded = BaseEncoding.base64().decode(hdr.substring(LIT_BASIC.length()));
     134          38 :     String usernamePassword = new String(decoded, encoding(req));
     135          38 :     int splitPos = usernamePassword.indexOf(':');
     136          38 :     if (splitPos < 1) {
     137           1 :       rsp.sendError(SC_UNAUTHORIZED);
     138           1 :       return false;
     139             :     }
     140             : 
     141          38 :     String username = usernamePassword.substring(0, splitPos);
     142          38 :     String password = usernamePassword.substring(splitPos + 1);
     143          38 :     if (Strings.isNullOrEmpty(password)) {
     144           0 :       rsp.sendError(SC_UNAUTHORIZED);
     145           0 :       return false;
     146             :     }
     147          38 :     if (authConfig.isUserNameToLowerCase()) {
     148           1 :       username = username.toLowerCase(Locale.US);
     149             :     }
     150             : 
     151          38 :     Optional<AccountState> accountState =
     152          38 :         accountCache.getByUsername(username).filter(a -> a.account().isActive());
     153          38 :     if (!accountState.isPresent()) {
     154           0 :       logger.atWarning().log(
     155             :           "Authentication failed for %s: account inactive or not provisioned in Gerrit", username);
     156           0 :       rsp.sendError(SC_UNAUTHORIZED);
     157           0 :       return false;
     158             :     }
     159             : 
     160          38 :     AccountState who = accountState.get();
     161          38 :     GitBasicAuthPolicy gitBasicAuthPolicy = authConfig.getGitBasicAuthPolicy();
     162          38 :     if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
     163             :         || gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
     164          38 :       if (passwordVerifier.checkPassword(who.externalIds(), username, password)) {
     165          38 :         logger.atFine().log(
     166             :             "HTTP:%s %s username/password authentication succeeded",
     167          38 :             req.getMethod(), req.getRequestURI());
     168          38 :         return succeedAuthentication(who, null);
     169             :       }
     170             :     }
     171             : 
     172           2 :     if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP) {
     173           1 :       return failAuthentication(rsp, username, req);
     174             :     }
     175             : 
     176           1 :     AuthRequest whoAuth = authRequestFactory.createForUser(username);
     177           1 :     whoAuth.setPassword(password);
     178             : 
     179             :     try {
     180           1 :       AuthResult whoAuthResult = accountManager.authenticate(whoAuth);
     181           1 :       setUserIdentified(whoAuthResult.getAccountId(), whoAuthResult);
     182           1 :       logger.atFine().log(
     183           1 :           "HTTP:%s %s Realm authentication succeeded", req.getMethod(), req.getRequestURI());
     184           1 :       return true;
     185           0 :     } catch (NoSuchUserException e) {
     186           0 :       if (passwordVerifier.checkPassword(who.externalIds(), username, password)) {
     187           0 :         return succeedAuthentication(who, null);
     188             :       }
     189           0 :       logger.atWarning().withCause(e).log("%s", authenticationFailedMsg(username, req));
     190           0 :       rsp.sendError(SC_UNAUTHORIZED);
     191           0 :       return false;
     192           0 :     } catch (AuthenticationFailedException e) {
     193             :       // This exception is thrown if the user provided wrong credentials, we don't need to log a
     194             :       // stacktrace for it.
     195           0 :       logger.atWarning().log(authenticationFailedMsg(username, req) + ": %s", e.getMessage());
     196           0 :       rsp.sendError(SC_UNAUTHORIZED);
     197           0 :       return false;
     198           0 :     } catch (AuthenticationUnavailableException e) {
     199           0 :       logger.atSevere().withCause(e).log("could not reach authentication backend");
     200           0 :       rsp.sendError(SC_SERVICE_UNAVAILABLE);
     201           0 :       return false;
     202           1 :     } catch (AccountException e) {
     203           1 :       logger.atWarning().withCause(e).log("%s", authenticationFailedMsg(username, req));
     204           1 :       rsp.sendError(SC_UNAUTHORIZED);
     205           1 :       return false;
     206             :     }
     207             :   }
     208             : 
     209             :   private boolean succeedAuthentication(AccountState who, @Nullable AuthResult whoAuthResult) {
     210          38 :     setUserIdentified(who.account().id(), whoAuthResult);
     211          38 :     return true;
     212             :   }
     213             : 
     214             :   private boolean failAuthentication(Response rsp, String username, HttpServletRequest req)
     215             :       throws IOException {
     216           1 :     logger.atWarning().log(
     217             :         "%s: password does not match the one stored in Gerrit",
     218           1 :         authenticationFailedMsg(username, req));
     219           1 :     rsp.sendError(SC_UNAUTHORIZED);
     220           1 :     return false;
     221             :   }
     222             : 
     223             :   static String authenticationFailedMsg(String username, HttpServletRequest req) {
     224           2 :     return String.format("Authentication from %s failed for %s", req.getRemoteAddr(), username);
     225             :   }
     226             : 
     227             :   private void setUserIdentified(Account.Id id, @Nullable AuthResult whoAuthResult) {
     228          38 :     WebSession ws = session.get();
     229          38 :     ws.setUserAccountId(id);
     230          38 :     ws.setAccessPathOk(AccessPath.GIT, true);
     231          38 :     ws.setAccessPathOk(AccessPath.REST_API, true);
     232             : 
     233          38 :     if (whoAuthResult != null) {
     234           1 :       ws.login(whoAuthResult, false);
     235             :     }
     236          38 :   }
     237             : 
     238             :   private String encoding(HttpServletRequest req) {
     239          38 :     return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
     240             :   }
     241             : 
     242             :   static class Response extends HttpServletResponseWrapper {
     243             :     private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
     244             : 
     245             :     Response(HttpServletResponse rsp) {
     246          38 :       super(rsp);
     247          38 :     }
     248             : 
     249             :     private void status(int sc) {
     250          34 :       if (sc == SC_UNAUTHORIZED) {
     251          32 :         StringBuilder v = new StringBuilder();
     252          32 :         v.append(LIT_BASIC);
     253          32 :         v.append("realm=\"").append(REALM_NAME).append("\"");
     254          32 :         setHeader(WWW_AUTHENTICATE, v.toString());
     255          34 :       } else if (containsHeader(WWW_AUTHENTICATE)) {
     256           0 :         setHeader(WWW_AUTHENTICATE, null);
     257             :       }
     258          34 :     }
     259             : 
     260             :     @Override
     261             :     public void sendError(int sc, String msg) throws IOException {
     262           1 :       status(sc);
     263           1 :       super.sendError(sc, msg);
     264           1 :     }
     265             : 
     266             :     @Override
     267             :     public void sendError(int sc) throws IOException {
     268          32 :       status(sc);
     269          32 :       super.sendError(sc);
     270          32 :     }
     271             : 
     272             :     @Override
     273             :     @Deprecated
     274             :     public void setStatus(int sc, String sm) {
     275           0 :       status(sc);
     276           0 :       super.setStatus(sc, sm);
     277           0 :     }
     278             : 
     279             :     @Override
     280             :     public void setStatus(int sc) {
     281          33 :       status(sc);
     282          33 :       super.setStatus(sc);
     283          33 :     }
     284             :   }
     285             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750