Merge branch 'stable-3.1' into stable-3.2

* stable-3.1:
  Allow GerritAccount Cookie authentication for Git/HTTP

Also, add //lib/bouncycastle:bcprov to the HTTP unit tests
to allow testing of the Git/HTTP basic authentication
with user/pass validation against the one stored in
the account external id.

Change-Id: I282db1ca5720b4e32c6ee5e0547740dfc68d6d91
This commit is contained in:
Luca Milanesio
2021-05-10 20:07:09 +01:00
8 changed files with 161 additions and 55 deletions

View File

@@ -604,7 +604,14 @@ it does not match, it is then validated against the `LDAP` password.
By default this is set to `LDAP` when link:#auth.type[`auth.type`] is `LDAP`
and `OAUTH` when link:#auth.type[`auth.type`] is `OAUTH`.
Otherwise, the default value is `HTTP`.
+
When gitBasicAuthPolicy is set to `LDAP` or `HTTP_LDAP` and the user
is authenticating with the LDAP username/password, the Git client config
needs to have `http.cookieFile` set to a local file, otherwise every
single call would trigger a full LDAP authentication and groups resolution
which could introduce a noticeable latency on the overall execution
and produce unwanted load to the LDAP server.
+
[[auth.gitOAuthProvider]]auth.gitOAuthProvider::
+
Selects the OAuth 2 provider to authenticate git over HTTP traffic with.

View File

@@ -29,6 +29,13 @@ credentials are validated using:
* Both, the HTTP and the LDAP passwords (in this order) if `gitBasicAuthPolicy`
is `HTTP_LDAP`.
When gitBasicAuthPolicy is set to `LDAP` or `HTTP_LDAP` and the user
is authenticating with the LDAP username/password, the Git client config
needs to have `http.cookieFile` set to a local file, otherwise every
single call would trigger a full LDAP authentication and groups resolution
which could introduce a noticeable latency on the overall execution
and produce unwanted load to the LDAP server.
When gitBasicAuthPolicy is not `LDAP`, the user's HTTP credentials can
be regenerated by going to `Settings`, and then accessing the `HTTP
Password` tab. Revocation can effectively be done by regenerating the

View File

@@ -75,30 +75,28 @@ public abstract class CacheBasedWebSession implements WebSession {
this.identified = identified;
this.byIdCache = byIdCache;
if (request.getRequestURI() == null || !GitSmartHttpTools.isGitClient(request)) {
String cookie = readCookie(request);
if (cookie != null) {
authFromCookie(cookie);
} else {
String token;
try {
token = ParameterParser.getQueryParams(request).accessToken();
} catch (BadRequestException e) {
token = null;
}
if (token != null) {
authFromQueryParameter(token);
}
String cookie = readCookie(request);
if (cookie != null) {
authFromCookie(cookie);
} else if (request.getRequestURI() == null || !GitSmartHttpTools.isGitClient(request)) {
String token;
try {
token = ParameterParser.getQueryParams(request).accessToken();
} catch (BadRequestException e) {
token = null;
}
if (val != null && !checkAccountStatus(val.getAccountId())) {
val = null;
okPaths.clear();
}
if (val != null && val.needsCookieRefresh()) {
// Session is more than half old; update cache entry with new expiration date.
val = manager.createVal(key, val);
if (token != null) {
authFromQueryParameter(token);
}
}
if (val != null && !checkAccountStatus(val.getAccountId())) {
val = null;
okPaths.clear();
}
if (val != null && val.needsCookieRefresh()) {
// Session is more than half old; update cache entry with new expiration date.
val = manager.createVal(key, val);
}
}
private void authFromCookie(String cookie) {

View File

@@ -22,6 +22,7 @@ import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
import com.google.gerrit.extensions.registration.DynamicItem;
@@ -144,7 +145,7 @@ class ProjectBasicAuthFilter implements Filter {
if (gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP
|| gitBasicAuthPolicy == GitBasicAuthPolicy.HTTP_LDAP) {
if (PasswordVerifier.checkPassword(who.externalIds(), username, password)) {
return succeedAuthentication(who);
return succeedAuthentication(who, null);
}
}
@@ -157,11 +158,11 @@ class ProjectBasicAuthFilter implements Filter {
try {
AuthResult whoAuthResult = accountManager.authenticate(whoAuth);
setUserIdentified(whoAuthResult.getAccountId());
setUserIdentified(whoAuthResult.getAccountId(), whoAuthResult);
return true;
} catch (NoSuchUserException e) {
if (PasswordVerifier.checkPassword(who.externalIds(), username, password)) {
return succeedAuthentication(who);
return succeedAuthentication(who, null);
}
logger.atWarning().withCause(e).log(authenticationFailedMsg(username, req));
rsp.sendError(SC_UNAUTHORIZED);
@@ -183,8 +184,8 @@ class ProjectBasicAuthFilter implements Filter {
}
}
private boolean succeedAuthentication(AccountState who) {
setUserIdentified(who.account().id());
private boolean succeedAuthentication(AccountState who, @Nullable AuthResult whoAuthResult) {
setUserIdentified(who.account().id(), whoAuthResult);
return true;
}
@@ -201,11 +202,15 @@ class ProjectBasicAuthFilter implements Filter {
return String.format("Authentication from %s failed for %s", req.getRemoteAddr(), username);
}
private void setUserIdentified(Account.Id id) {
private void setUserIdentified(Account.Id id, @Nullable AuthResult whoAuthResult) {
WebSession ws = session.get();
ws.setUserAccountId(id);
ws.setAccessPathOk(AccessPath.GIT, true);
ws.setAccessPathOk(AccessPath.REST_API, true);
if (whoAuthResult != null) {
ws.login(whoAuthResult, false);
}
}
private String encoding(HttpServletRequest req) {

View File

@@ -18,6 +18,7 @@ junit_tests(
"//lib:junit",
"//lib:servlet-api-without-neverlink",
"//lib:soy",
"//lib/bouncycastle:bcprov",
"//lib/guice",
"//lib/guice:guice-servlet",
"//lib/mockito",

View File

@@ -19,12 +19,15 @@ import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager;
@@ -55,11 +58,17 @@ public class ProjectBasicAuthFilterTest {
private static final String AUTH_USER_B64 =
B64_ENC.encodeToString(AUTH_USER.getBytes(StandardCharsets.UTF_8));
private static final String AUTH_PASSWORD = "jd123";
private static final String GERRIT_COOKIE_KEY = "GerritAccount";
private static final String AUTH_COOKIE_VALUE = "gerritcookie";
private static final ExternalId AUTH_USER_PASSWORD_EXTERNAL_ID =
ExternalId.createWithPassword(
ExternalId.Key.create(ExternalId.SCHEME_USERNAME, AUTH_USER),
AUTH_ACCOUNT_ID,
null,
AUTH_PASSWORD);
@Mock private DynamicItem<WebSession> webSessionItem;
@Mock private WebSession webSession;
@Mock private AccountCache accountCache;
@Mock private AccountState accountState;
@@ -74,21 +83,57 @@ public class ProjectBasicAuthFilterTest {
@Captor private ArgumentCaptor<HttpServletResponse> filterResponseCaptor;
@Mock private IdentifiedUser.RequestFactory userRequestFactory;
@Mock private WebSessionManager webSessionManager;
private WebSession webSession;
private FakeHttpServletRequest req;
private HttpServletResponse res;
private AuthResult authSuccessful;
@Before
public void setUp() throws Exception {
doReturn(webSession).when(webSessionItem).get();
doReturn(Optional.of(accountState)).when(accountCache).getByUsername(AUTH_USER);
doReturn(account).when(accountState).account();
req = new FakeHttpServletRequest();
res = new FakeHttpServletResponse();
authSuccessful =
new AuthResult(AUTH_ACCOUNT_ID, ExternalId.Key.create("username", AUTH_USER), false);
doReturn(Optional.of(accountState)).when(accountCache).getByUsername(AUTH_USER);
doReturn(Optional.of(accountState)).when(accountCache).get(AUTH_ACCOUNT_ID);
doReturn(account).when(accountState).account();
doReturn(ImmutableSet.builder().add(AUTH_USER_PASSWORD_EXTERNAL_ID).build())
.when(accountState)
.externalIds();
doReturn(new WebSessionManager.Key(AUTH_COOKIE_VALUE)).when(webSessionManager).createKey(any());
WebSessionManager.Val webSessionValue =
new WebSessionManager.Val(AUTH_ACCOUNT_ID, 0L, false, null, 0L, "", "");
doReturn(webSessionValue)
.when(webSessionManager)
.createVal(any(), any(), eq(false), any(), any(), any());
}
private void initWebSessionWithCookie(String cookie) {
req.addHeader("Cookie", cookie);
initWebSessionWithoutCookie();
}
private void initWebSessionWithoutCookie() {
webSession =
new CacheBasedWebSession(
req, res, webSessionManager, authConfig, null, userRequestFactory, accountCache) {};
doReturn(webSession).when(webSessionItem).get();
}
private void initMockedWebSession() {
webSession = mock(WebSession.class);
doReturn(webSession).when(webSessionItem).get();
}
@Test
public void shouldAllowAnonymousRequest() throws Exception {
initMockedWebSession();
res.setStatus(HttpServletResponse.SC_OK);
ProjectBasicAuthFilter basicAuthFilter =
@@ -102,6 +147,7 @@ public class ProjectBasicAuthFilterTest {
@Test
public void shouldRequestAuthenticationForBasicAuthRequest() throws Exception {
initMockedWebSession();
req.addHeader("Authorization", "Basic " + AUTH_USER_B64);
res.setStatus(HttpServletResponse.SC_OK);
@@ -116,16 +162,11 @@ public class ProjectBasicAuthFilterTest {
}
@Test
public void shouldAuthenticateSucessfullyAgainstRealm() throws Exception {
req.addHeader(
"Authorization",
"Basic "
+ B64_ENC.encodeToString(
(AUTH_USER + ":" + AUTH_PASSWORD).getBytes(StandardCharsets.UTF_8)));
public void shouldAuthenticateSucessfullyAgainstRealmAndReturnCookie() throws Exception {
initWebSessionWithoutCookie();
requestBasicAuth(req);
res.setStatus(HttpServletResponse.SC_OK);
AuthResult authSuccessful =
new AuthResult(AUTH_ACCOUNT_ID, ExternalId.Key.create("username", AUTH_USER), false);
doReturn(true).when(account).isActive();
doReturn(authSuccessful).when(accountManager).authenticate(any());
doReturn(GitBasicAuthPolicy.LDAP).when(authConfig).getGitBasicAuthPolicy();
@@ -139,18 +180,51 @@ public class ProjectBasicAuthFilterTest {
verify(chain).doFilter(eq(req), any());
assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(res.getHeader("Set-Cookie")).contains(GERRIT_COOKIE_KEY);
}
@Test
public void shouldValidateUserPasswordAndNotReturnCookie() throws Exception {
initWebSessionWithoutCookie();
requestBasicAuth(req);
res.setStatus(HttpServletResponse.SC_OK);
doReturn(true).when(account).isActive();
doReturn(GitBasicAuthPolicy.HTTP).when(authConfig).getGitBasicAuthPolicy();
ProjectBasicAuthFilter basicAuthFilter =
new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
basicAuthFilter.doFilter(req, res, chain);
verify(accountManager, never()).authenticate(any());
verify(chain).doFilter(eq(req), any());
assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
assertThat(res.getHeader("Set-Cookie")).isNull();
}
@Test
public void shouldNotReauthenticateIfAlreadySignedIn() throws Exception {
req.addHeader(
"Authorization",
"Basic "
+ B64_ENC.encodeToString(
(AUTH_USER + ":" + AUTH_PASSWORD).getBytes(StandardCharsets.UTF_8)));
initMockedWebSession();
doReturn(true).when(webSession).isSignedIn();
requestBasicAuth(req);
res.setStatus(HttpServletResponse.SC_OK);
doReturn(true).when(webSession).isSignedIn();
ProjectBasicAuthFilter basicAuthFilter =
new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
basicAuthFilter.doFilter(req, res, chain);
verify(accountManager, never()).authenticate(any());
verify(chain).doFilter(eq(req), any());
assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@Test
public void shouldNotReauthenticateIfHasExistingCookie() throws Exception {
initWebSessionWithCookie("GerritAccount=" + AUTH_COOKIE_VALUE);
res.setStatus(HttpServletResponse.SC_OK);
ProjectBasicAuthFilter basicAuthFilter =
new ProjectBasicAuthFilter(webSessionItem, accountCache, accountManager, authConfig);
@@ -164,11 +238,8 @@ public class ProjectBasicAuthFilterTest {
@Test
public void shouldFailedAuthenticationAgainstRealm() throws Exception {
req.addHeader(
"Authorization",
"Basic "
+ B64_ENC.encodeToString(
(AUTH_USER + ":" + AUTH_PASSWORD).getBytes(StandardCharsets.UTF_8)));
initMockedWebSession();
requestBasicAuth(req);
doReturn(true).when(account).isActive();
doThrow(new AccountException("Authentication error")).when(accountManager).authenticate(any());
@@ -184,4 +255,12 @@ public class ProjectBasicAuthFilterTest {
verify(chain, never()).doFilter(any(), any());
assertThat(res.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
private void requestBasicAuth(FakeHttpServletRequest fakeReq) {
fakeReq.addHeader(
"Authorization",
"Basic "
+ B64_ENC.encodeToString(
(AUTH_USER + ":" + AUTH_PASSWORD).getBytes(StandardCharsets.UTF_8)));
}
}

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.util.http.testutil;
import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
@@ -257,7 +258,15 @@ public class FakeHttpServletRequest implements HttpServletRequest {
@Override
public Cookie[] getCookies() {
return new Cookie[0];
return Splitter.on(";").splitToList(Strings.nullToEmpty(getHeader("Cookie"))).stream()
.filter(s -> !s.isEmpty())
.map(
(String cookieValue) -> {
String[] kv = cookieValue.split("=");
return new Cookie(kv[0], kv[1]);
})
.collect(toList())
.toArray(new Cookie[0]);
}
@Override

View File

@@ -161,7 +161,7 @@ public class FakeHttpServletResponse implements HttpServletResponse {
@Override
public void addCookie(Cookie cookie) {
throw new UnsupportedOperationException();
addHeader("Set-Cookie", cookie.getName() + "=" + cookie.getValue());
}
@Override