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 : }
|