Dissolve gerrit-httpd top-level directory
Change-Id: I287b29b893c6f561dbbf6fa04dc714b4b7f48d54
This commit is contained in:

committed by
Dave Borowitz

parent
3675f601fa
commit
a84dbc81bf
339
java/com/google/gerrit/httpd/ProjectOAuthFilter.java
Normal file
339
java/com/google/gerrit/httpd/ProjectOAuthFilter.java
Normal file
@@ -0,0 +1,339 @@
|
||||
// Copyright (C) 2015 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 com.google.gerrit.httpd.ProjectBasicAuthFilter.authenticationFailedMsg;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.gerrit.extensions.auth.oauth.OAuthLoginProvider;
|
||||
import com.google.gerrit.extensions.registration.DynamicItem;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap;
|
||||
import com.google.gerrit.extensions.registration.DynamicMap.Entry;
|
||||
import com.google.gerrit.server.AccessPath;
|
||||
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.AuthRequest;
|
||||
import com.google.gerrit.server.account.AuthResult;
|
||||
import com.google.gerrit.server.config.GerritServerConfig;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.Locale;
|
||||
import java.util.NoSuchElementException;
|
||||
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.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Authenticates the current user with an OAuth2 server.
|
||||
*
|
||||
* @see <a href="https://tools.ietf.org/rfc/rfc6750.txt">RFC 6750</a>
|
||||
*/
|
||||
@Singleton
|
||||
class ProjectOAuthFilter implements Filter {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ProjectOAuthFilter.class);
|
||||
|
||||
private static final String REALM_NAME = "Gerrit Code Review";
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
private static final String BASIC = "Basic ";
|
||||
private static final String GIT_COOKIE_PREFIX = "git-";
|
||||
|
||||
private final DynamicItem<WebSession> session;
|
||||
private final DynamicMap<OAuthLoginProvider> loginProviders;
|
||||
private final AccountCache accountCache;
|
||||
private final AccountManager accountManager;
|
||||
private final String gitOAuthProvider;
|
||||
private final boolean userNameToLowerCase;
|
||||
|
||||
private String defaultAuthPlugin;
|
||||
private String defaultAuthProvider;
|
||||
|
||||
@Inject
|
||||
ProjectOAuthFilter(
|
||||
DynamicItem<WebSession> session,
|
||||
DynamicMap<OAuthLoginProvider> pluginsProvider,
|
||||
AccountCache accountCache,
|
||||
AccountManager accountManager,
|
||||
@GerritServerConfig Config gerritConfig) {
|
||||
this.session = session;
|
||||
this.loginProviders = pluginsProvider;
|
||||
this.accountCache = accountCache;
|
||||
this.accountManager = accountManager;
|
||||
this.gitOAuthProvider = gerritConfig.getString("auth", null, "gitOAuthProvider");
|
||||
this.userNameToLowerCase = gerritConfig.getBoolean("auth", null, "userNameToLowerCase", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig config) throws ServletException {
|
||||
if (Strings.isNullOrEmpty(gitOAuthProvider)) {
|
||||
pickOnlyProvider();
|
||||
} else {
|
||||
pickConfiguredProvider();
|
||||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
AuthInfo authInfo = null;
|
||||
|
||||
// first check if there is a BASIC authentication header
|
||||
String hdr = req.getHeader(AUTHORIZATION);
|
||||
if (hdr != null && hdr.startsWith(BASIC)) {
|
||||
authInfo = extractAuthInfo(hdr, encoding(req));
|
||||
if (authInfo == null) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// if there is no BASIC authentication header, check if there is
|
||||
// a cookie starting with the prefix "git-"
|
||||
Cookie cookie = findGitCookie(req);
|
||||
if (cookie != null) {
|
||||
authInfo = extractAuthInfo(cookie);
|
||||
if (authInfo == null) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// if there is no authentication information at all, it might be
|
||||
// an anonymous connection, or there might be a session cookie
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// if there is authentication information but no secret => 401
|
||||
if (Strings.isNullOrEmpty(authInfo.tokenOrSecret)) {
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
AccountState who = accountCache.getByUsername(authInfo.username);
|
||||
if (who == null || !who.getAccount().isActive()) {
|
||||
log.warn(
|
||||
authenticationFailedMsg(authInfo.username, req)
|
||||
+ ": account inactive or not provisioned in Gerrit");
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthRequest authRequest = AuthRequest.forExternalUser(authInfo.username);
|
||||
authRequest.setEmailAddress(who.getAccount().getPreferredEmail());
|
||||
authRequest.setDisplayName(who.getAccount().getFullName());
|
||||
authRequest.setPassword(authInfo.tokenOrSecret);
|
||||
authRequest.setAuthPlugin(authInfo.pluginName);
|
||||
authRequest.setAuthProvider(authInfo.exportName);
|
||||
|
||||
try {
|
||||
AuthResult authResult = accountManager.authenticate(authRequest);
|
||||
WebSession ws = session.get();
|
||||
ws.setUserAccountId(authResult.getAccountId());
|
||||
ws.setAccessPathOk(AccessPath.GIT, true);
|
||||
ws.setAccessPathOk(AccessPath.REST_API, true);
|
||||
return true;
|
||||
} catch (AccountException e) {
|
||||
log.warn(authenticationFailedMsg(authInfo.username, req), e);
|
||||
rsp.sendError(SC_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the only installed OAuth provider. If there is a multiude of providers available, the
|
||||
* actual provider must be determined from the authentication request.
|
||||
*
|
||||
* @throws ServletException if there is no {@code OAuthLoginProvider} installed at all.
|
||||
*/
|
||||
private void pickOnlyProvider() throws ServletException {
|
||||
try {
|
||||
Entry<OAuthLoginProvider> loginProvider = Iterables.getOnlyElement(loginProviders);
|
||||
defaultAuthPlugin = loginProvider.getPluginName();
|
||||
defaultAuthProvider = loginProvider.getExportName();
|
||||
} catch (NoSuchElementException e) {
|
||||
throw new ServletException("No OAuth login provider installed");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// multiple providers found => do not pick any
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks the {@code OAuthLoginProvider} configured with <tt>auth.gitOAuthProvider</tt>.
|
||||
*
|
||||
* @throws ServletException if the configured provider was not found.
|
||||
*/
|
||||
private void pickConfiguredProvider() throws ServletException {
|
||||
int splitPos = gitOAuthProvider.lastIndexOf(':');
|
||||
if (splitPos < 1 || splitPos == gitOAuthProvider.length() - 1) {
|
||||
// no colon at all or leading/trailing colon: malformed providerId
|
||||
throw new ServletException(
|
||||
"OAuth login provider configuration is"
|
||||
+ " invalid: Must be of the form pluginName:providerName");
|
||||
}
|
||||
defaultAuthPlugin = gitOAuthProvider.substring(0, splitPos);
|
||||
defaultAuthProvider = gitOAuthProvider.substring(splitPos + 1);
|
||||
OAuthLoginProvider provider = loginProviders.get(defaultAuthPlugin, defaultAuthProvider);
|
||||
if (provider == null) {
|
||||
throw new ServletException(
|
||||
"Configured OAuth login provider " + gitOAuthProvider + " wasn't installed");
|
||||
}
|
||||
}
|
||||
|
||||
private AuthInfo extractAuthInfo(String hdr, String encoding)
|
||||
throws UnsupportedEncodingException {
|
||||
byte[] decoded = Base64.decodeBase64(hdr.substring(BASIC.length()));
|
||||
String usernamePassword = new String(decoded, encoding);
|
||||
int splitPos = usernamePassword.indexOf(':');
|
||||
if (splitPos < 1 || splitPos == usernamePassword.length() - 1) {
|
||||
return null;
|
||||
}
|
||||
return new AuthInfo(
|
||||
usernamePassword.substring(0, splitPos),
|
||||
usernamePassword.substring(splitPos + 1),
|
||||
defaultAuthPlugin,
|
||||
defaultAuthProvider);
|
||||
}
|
||||
|
||||
private AuthInfo extractAuthInfo(Cookie cookie) throws UnsupportedEncodingException {
|
||||
String username =
|
||||
URLDecoder.decode(cookie.getName().substring(GIT_COOKIE_PREFIX.length()), UTF_8.name());
|
||||
String value = cookie.getValue();
|
||||
int splitPos = value.lastIndexOf('@');
|
||||
if (splitPos < 1 || splitPos == value.length() - 1) {
|
||||
// no providerId in the cookie value => assume default provider
|
||||
// note: a leading/trailing at sign is considered to belong to
|
||||
// the access token rather than being a separator
|
||||
return new AuthInfo(username, cookie.getValue(), defaultAuthPlugin, defaultAuthProvider);
|
||||
}
|
||||
String token = value.substring(0, splitPos);
|
||||
String providerId = value.substring(splitPos + 1);
|
||||
splitPos = providerId.lastIndexOf(':');
|
||||
if (splitPos < 1 || splitPos == providerId.length() - 1) {
|
||||
// no colon at all or leading/trailing colon: malformed providerId
|
||||
return null;
|
||||
}
|
||||
String pluginName = providerId.substring(0, splitPos);
|
||||
String exportName = providerId.substring(splitPos + 1);
|
||||
OAuthLoginProvider provider = loginProviders.get(pluginName, exportName);
|
||||
if (provider == null) {
|
||||
return null;
|
||||
}
|
||||
return new AuthInfo(username, token, pluginName, exportName);
|
||||
}
|
||||
|
||||
private static String encoding(HttpServletRequest req) {
|
||||
return MoreObjects.firstNonNull(req.getCharacterEncoding(), UTF_8.name());
|
||||
}
|
||||
|
||||
private static Cookie findGitCookie(HttpServletRequest req) {
|
||||
Cookie[] cookies = req.getCookies();
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if (cookie.getName().startsWith(GIT_COOKIE_PREFIX)) {
|
||||
return cookie;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private class AuthInfo {
|
||||
private final String username;
|
||||
private final String tokenOrSecret;
|
||||
private final String pluginName;
|
||||
private final String exportName;
|
||||
|
||||
private AuthInfo(String username, String tokenOrSecret, String pluginName, String exportName) {
|
||||
this.username = userNameToLowerCase ? username.toLowerCase(Locale.US) : username;
|
||||
this.tokenOrSecret = tokenOrSecret;
|
||||
this.pluginName = pluginName;
|
||||
this.exportName = exportName;
|
||||
}
|
||||
}
|
||||
|
||||
private static 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(BASIC);
|
||||
v.append("realm=\"").append(REALM_NAME).append("\"");
|
||||
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
|
||||
@Deprecated
|
||||
public void setStatus(int sc, String sm) {
|
||||
status(sc);
|
||||
super.setStatus(sc, sm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatus(int sc) {
|
||||
status(sc);
|
||||
super.setStatus(sc);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user