974 lines
37 KiB
Java
974 lines
37 KiB
Java
// Copyright (C) 2017 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.acceptance.rest.account;
|
|
|
|
import static com.google.common.truth.Truth.assertThat;
|
|
import static com.google.gerrit.acceptance.GitUtil.fetch;
|
|
import static com.google.gerrit.acceptance.GitUtil.pushHead;
|
|
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
|
|
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
|
|
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
|
|
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
|
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
|
import static java.util.stream.Collectors.toList;
|
|
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
|
|
import static org.junit.Assert.fail;
|
|
|
|
import com.github.rholder.retry.BlockStrategy;
|
|
import com.github.rholder.retry.Retryer;
|
|
import com.github.rholder.retry.RetryerBuilder;
|
|
import com.github.rholder.retry.StopStrategies;
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.ImmutableSet;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
|
import com.google.gerrit.acceptance.RestResponse;
|
|
import com.google.gerrit.acceptance.Sandboxed;
|
|
import com.google.gerrit.common.data.GlobalCapability;
|
|
import com.google.gerrit.common.data.Permission;
|
|
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
|
|
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
|
|
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
|
|
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput.CheckAccountExternalIdsInput;
|
|
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
|
|
import com.google.gerrit.extensions.restapi.AuthException;
|
|
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
|
|
import com.google.gerrit.metrics.MetricMaker;
|
|
import com.google.gerrit.reviewdb.client.Account;
|
|
import com.google.gerrit.reviewdb.client.RefNames;
|
|
import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
|
|
import com.google.gerrit.server.account.externalids.ExternalId;
|
|
import com.google.gerrit.server.account.externalids.ExternalIdReader;
|
|
import com.google.gerrit.server.account.externalids.ExternalIds;
|
|
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
|
|
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate.RefsMetaExternalIdsUpdate;
|
|
import com.google.gerrit.server.config.AllUsersName;
|
|
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
|
import com.google.gerrit.server.git.LockFailureException;
|
|
import com.google.gson.reflect.TypeToken;
|
|
import com.google.gwtorm.server.OrmDuplicateKeyException;
|
|
import com.google.gwtorm.server.OrmException;
|
|
import com.google.inject.Inject;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collection;
|
|
import java.util.Collections;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Set;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import org.eclipse.jgit.api.errors.TransportException;
|
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
|
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
|
|
import org.eclipse.jgit.junit.TestRepository;
|
|
import org.eclipse.jgit.lib.Config;
|
|
import org.eclipse.jgit.lib.ObjectId;
|
|
import org.eclipse.jgit.lib.ObjectInserter;
|
|
import org.eclipse.jgit.lib.Repository;
|
|
import org.eclipse.jgit.notes.NoteMap;
|
|
import org.eclipse.jgit.revwalk.RevWalk;
|
|
import org.eclipse.jgit.transport.PushResult;
|
|
import org.eclipse.jgit.transport.RemoteRefUpdate;
|
|
import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
|
|
import org.eclipse.jgit.util.MutableInteger;
|
|
import org.junit.Test;
|
|
|
|
@Sandboxed
|
|
public class ExternalIdIT extends AbstractDaemonTest {
|
|
@Inject private AllUsersName allUsers;
|
|
@Inject private ExternalIdsUpdate.Server extIdsUpdate;
|
|
@Inject private ExternalIds externalIds;
|
|
@Inject private ExternalIdReader externalIdReader;
|
|
@Inject private MetricMaker metricMaker;
|
|
|
|
@Test
|
|
public void getExternalIds() throws Exception {
|
|
Collection<ExternalId> expectedIds = accountCache.get(user.getId()).getExternalIds();
|
|
List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
|
|
|
|
RestResponse response = userRestSession.get("/accounts/self/external.ids");
|
|
response.assertOK();
|
|
|
|
List<AccountExternalIdInfo> results =
|
|
newGson()
|
|
.fromJson(
|
|
response.getReader(), new TypeToken<List<AccountExternalIdInfo>>() {}.getType());
|
|
|
|
Collections.sort(expectedIdInfos);
|
|
Collections.sort(results);
|
|
assertThat(results).containsExactlyElementsIn(expectedIdInfos);
|
|
}
|
|
|
|
@Test
|
|
public void getExternalIdsOfOtherUserNotAllowed() throws Exception {
|
|
setApiUser(user);
|
|
exception.expect(AuthException.class);
|
|
exception.expectMessage("access database not permitted");
|
|
gApi.accounts().id(admin.id.get()).getExternalIds();
|
|
}
|
|
|
|
@Test
|
|
public void getExternalIdsOfOtherUserWithAccessDatabase() throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
Collection<ExternalId> expectedIds = accountCache.get(admin.getId()).getExternalIds();
|
|
List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
|
|
|
|
RestResponse response = userRestSession.get("/accounts/" + admin.id + "/external.ids");
|
|
response.assertOK();
|
|
|
|
List<AccountExternalIdInfo> results =
|
|
newGson()
|
|
.fromJson(
|
|
response.getReader(), new TypeToken<List<AccountExternalIdInfo>>() {}.getType());
|
|
|
|
Collections.sort(expectedIdInfos);
|
|
Collections.sort(results);
|
|
assertThat(results).containsExactlyElementsIn(expectedIdInfos);
|
|
}
|
|
|
|
@Test
|
|
public void deleteExternalIds() throws Exception {
|
|
setApiUser(user);
|
|
List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
|
|
|
|
List<String> toDelete = new ArrayList<>();
|
|
List<AccountExternalIdInfo> expectedIds = new ArrayList<>();
|
|
for (AccountExternalIdInfo id : externalIds) {
|
|
if (id.canDelete != null && id.canDelete) {
|
|
toDelete.add(id.identity);
|
|
continue;
|
|
}
|
|
expectedIds.add(id);
|
|
}
|
|
|
|
assertThat(toDelete).hasSize(1);
|
|
|
|
RestResponse response = userRestSession.post("/accounts/self/external.ids:delete", toDelete);
|
|
response.assertNoContent();
|
|
List<AccountExternalIdInfo> results = gApi.accounts().self().getExternalIds();
|
|
// The external ID in WebSession will not be set for tests, resulting that
|
|
// "mailto:user@example.com" can be deleted while "username:user" can't.
|
|
assertThat(results).hasSize(1);
|
|
assertThat(results).containsExactlyElementsIn(expectedIds);
|
|
}
|
|
|
|
@Test
|
|
public void deleteExternalIdsOfOtherUserNotAllowed() throws Exception {
|
|
List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
|
|
setApiUser(user);
|
|
exception.expect(AuthException.class);
|
|
exception.expectMessage("access database not permitted");
|
|
gApi.accounts()
|
|
.id(admin.id.get())
|
|
.deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList()));
|
|
}
|
|
|
|
@Test
|
|
public void deleteExternalIdOfOtherUserUnderOwnAccount_UnprocessableEntity() throws Exception {
|
|
List<AccountExternalIdInfo> extIds = gApi.accounts().self().getExternalIds();
|
|
setApiUser(user);
|
|
exception.expect(UnprocessableEntityException.class);
|
|
exception.expectMessage(String.format("External id %s does not exist", extIds.get(0).identity));
|
|
gApi.accounts()
|
|
.self()
|
|
.deleteExternalIds(extIds.stream().map(e -> e.identity).collect(toList()));
|
|
}
|
|
|
|
@Test
|
|
public void deleteExternalIdsOfOtherUserWithAccessDatabase() throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
List<AccountExternalIdInfo> externalIds = gApi.accounts().self().getExternalIds();
|
|
|
|
List<String> toDelete = new ArrayList<>();
|
|
List<AccountExternalIdInfo> expectedIds = new ArrayList<>();
|
|
for (AccountExternalIdInfo id : externalIds) {
|
|
if (id.canDelete != null && id.canDelete) {
|
|
toDelete.add(id.identity);
|
|
continue;
|
|
}
|
|
expectedIds.add(id);
|
|
}
|
|
|
|
assertThat(toDelete).hasSize(1);
|
|
|
|
setApiUser(user);
|
|
RestResponse response =
|
|
userRestSession.post("/accounts/" + admin.id + "/external.ids:delete", toDelete);
|
|
response.assertNoContent();
|
|
List<AccountExternalIdInfo> results = gApi.accounts().id(admin.id.get()).getExternalIds();
|
|
// The external ID in WebSession will not be set for tests, resulting that
|
|
// "mailto:user@example.com" can be deleted while "username:user" can't.
|
|
assertThat(results).hasSize(1);
|
|
assertThat(results).containsExactlyElementsIn(expectedIds);
|
|
}
|
|
|
|
@Test
|
|
public void deleteExternalIdOfPreferredEmail() throws Exception {
|
|
String preferredEmail = gApi.accounts().self().get().email;
|
|
assertThat(preferredEmail).isNotNull();
|
|
|
|
gApi.accounts()
|
|
.self()
|
|
.deleteExternalIds(
|
|
ImmutableList.of(ExternalId.Key.create(SCHEME_MAILTO, preferredEmail).get()));
|
|
assertThat(gApi.accounts().self().get().email).isNull();
|
|
}
|
|
|
|
@Test
|
|
public void deleteExternalIds_Conflict() throws Exception {
|
|
List<String> toDelete = new ArrayList<>();
|
|
String externalIdStr = "username:" + user.username;
|
|
toDelete.add(externalIdStr);
|
|
RestResponse response = userRestSession.post("/accounts/self/external.ids:delete", toDelete);
|
|
response.assertConflict();
|
|
assertThat(response.getEntityContent())
|
|
.isEqualTo(String.format("External id %s cannot be deleted", externalIdStr));
|
|
}
|
|
|
|
@Test
|
|
public void deleteExternalIds_UnprocessableEntity() throws Exception {
|
|
List<String> toDelete = new ArrayList<>();
|
|
String externalIdStr = "mailto:user@domain.com";
|
|
toDelete.add(externalIdStr);
|
|
RestResponse response = userRestSession.post("/accounts/self/external.ids:delete", toDelete);
|
|
response.assertUnprocessableEntity();
|
|
assertThat(response.getEntityContent())
|
|
.isEqualTo(String.format("External id %s does not exist", externalIdStr));
|
|
}
|
|
|
|
@Test
|
|
public void fetchExternalIdsBranch() throws Exception {
|
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
|
|
|
|
// refs/meta/external-ids is only visible to users with the 'Access Database' capability
|
|
try {
|
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
|
fail("expected TransportException");
|
|
} catch (TransportException e) {
|
|
assertThat(e.getMessage())
|
|
.isEqualTo(
|
|
"Remote does not have " + RefNames.REFS_EXTERNAL_IDS + " available for fetch.");
|
|
}
|
|
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
// re-clone to get new request context, otherwise the old global capabilities are still cached
|
|
// in the IdentifiedUser object
|
|
allUsersRepo = cloneProject(allUsers, user);
|
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranch() throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
// different case email is allowed
|
|
ExternalId newExtId = createExternalIdWithOtherCaseEmail("foo:bar");
|
|
addExtId(allUsersRepo, newExtId);
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
List<AccountExternalIdInfo> extIdsBefore = gApi.accounts().self().getExternalIds();
|
|
|
|
allowPushOfExternalIds();
|
|
PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
|
assertThat(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS).getStatus()).isEqualTo(Status.OK);
|
|
|
|
List<AccountExternalIdInfo> extIdsAfter = gApi.accounts().self().getExternalIds();
|
|
assertThat(extIdsAfter)
|
|
.containsExactlyElementsIn(
|
|
Iterables.concat(extIdsBefore, ImmutableSet.of(toExternalIdInfo(newExtId))));
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranchRejectsExternalIdWithoutAccountId() throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
insertExternalIdWithoutAccountId(
|
|
allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
allowPushOfExternalIds();
|
|
PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
|
assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranchRejectsExternalIdWithKeyThatDoesntMatchTheNoteId()
|
|
throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
insertExternalIdWithKeyThatDoesntMatchNoteId(
|
|
allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
allowPushOfExternalIds();
|
|
PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
|
assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranchRejectsExternalIdWithInvalidConfig() throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
insertExternalIdWithInvalidConfig(
|
|
allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
allowPushOfExternalIds();
|
|
PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
|
assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranchRejectsExternalIdWithEmptyNote() throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
insertExternalIdWithEmptyNote(
|
|
allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
allowPushOfExternalIds();
|
|
PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
|
assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranchRejectsExternalIdForNonExistingAccount() throws Exception {
|
|
testPushToExternalIdsBranchRejectsInvalidExternalId(
|
|
createExternalIdForNonExistingAccount("foo:bar"));
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranchRejectsExternalIdWithInvalidEmail() throws Exception {
|
|
testPushToExternalIdsBranchRejectsInvalidExternalId(
|
|
createExternalIdWithInvalidEmail("foo:bar"));
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranchRejectsDuplicateEmails() throws Exception {
|
|
testPushToExternalIdsBranchRejectsInvalidExternalId(
|
|
createExternalIdWithDuplicateEmail("foo:bar"));
|
|
}
|
|
|
|
@Test
|
|
public void pushToExternalIdsBranchRejectsBadPassword() throws Exception {
|
|
testPushToExternalIdsBranchRejectsInvalidExternalId(createExternalIdWithBadPassword("foo"));
|
|
}
|
|
|
|
private void testPushToExternalIdsBranchRejectsInvalidExternalId(ExternalId invalidExtId)
|
|
throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
|
|
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
|
|
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
addExtId(allUsersRepo, invalidExtId);
|
|
allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
|
|
|
|
allowPushOfExternalIds();
|
|
PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
|
|
assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
|
|
}
|
|
|
|
@Test
|
|
public void readExternalIdsWhenInvalidExternalIdsExist() throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
resetCurrentApiUser();
|
|
|
|
insertValidExternalIds();
|
|
insertInvalidButParsableExternalIds();
|
|
|
|
Set<ExternalId> parseableExtIds = externalIds.all();
|
|
|
|
insertNonParsableExternalIds();
|
|
|
|
Set<ExternalId> extIds = externalIds.all();
|
|
assertThat(extIds).containsExactlyElementsIn(parseableExtIds);
|
|
|
|
for (ExternalId parseableExtId : parseableExtIds) {
|
|
ExternalId extId = externalIds.get(parseableExtId.key());
|
|
assertThat(extId).isEqualTo(parseableExtId);
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void checkConsistency() throws Exception {
|
|
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
|
|
resetCurrentApiUser();
|
|
|
|
insertValidExternalIds();
|
|
|
|
ConsistencyCheckInput input = new ConsistencyCheckInput();
|
|
input.checkAccountExternalIds = new CheckAccountExternalIdsInput();
|
|
ConsistencyCheckInfo checkInfo = gApi.config().server().checkConsistency(input);
|
|
assertThat(checkInfo.checkAccountExternalIdsResult.problems).isEmpty();
|
|
|
|
Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
|
|
expectedProblems.addAll(insertInvalidButParsableExternalIds());
|
|
expectedProblems.addAll(insertNonParsableExternalIds());
|
|
|
|
checkInfo = gApi.config().server().checkConsistency(input);
|
|
assertThat(checkInfo.checkAccountExternalIdsResult.problems).hasSize(expectedProblems.size());
|
|
assertThat(checkInfo.checkAccountExternalIdsResult.problems)
|
|
.containsExactlyElementsIn(expectedProblems);
|
|
}
|
|
|
|
@Test
|
|
public void checkConsistencyNotAllowed() throws Exception {
|
|
exception.expect(AuthException.class);
|
|
exception.expectMessage("access database not permitted");
|
|
gApi.config().server().checkConsistency(new ConsistencyCheckInput());
|
|
}
|
|
|
|
private ConsistencyProblemInfo consistencyError(String message) {
|
|
return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, message);
|
|
}
|
|
|
|
private void insertValidExternalIds() throws IOException, ConfigInvalidException, OrmException {
|
|
MutableInteger i = new MutableInteger();
|
|
String scheme = "valid";
|
|
ExternalIdsUpdate u = extIdsUpdate.create();
|
|
|
|
// create valid external IDs
|
|
u.insert(
|
|
ExternalId.createWithPassword(
|
|
ExternalId.Key.parse(nextId(scheme, i)),
|
|
admin.id,
|
|
"admin.other@example.com",
|
|
"secret-password"));
|
|
u.insert(createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
|
|
}
|
|
|
|
private Set<ConsistencyProblemInfo> insertInvalidButParsableExternalIds()
|
|
throws IOException, ConfigInvalidException, OrmException {
|
|
MutableInteger i = new MutableInteger();
|
|
String scheme = "invalid";
|
|
ExternalIdsUpdate u = extIdsUpdate.create();
|
|
|
|
Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
|
|
ExternalId extIdForNonExistingAccount =
|
|
createExternalIdForNonExistingAccount(nextId(scheme, i));
|
|
u.insert(extIdForNonExistingAccount);
|
|
expectedProblems.add(
|
|
consistencyError(
|
|
"External ID '"
|
|
+ extIdForNonExistingAccount.key().get()
|
|
+ "' belongs to account that doesn't exist: "
|
|
+ extIdForNonExistingAccount.accountId().get()));
|
|
|
|
ExternalId extIdWithInvalidEmail = createExternalIdWithInvalidEmail(nextId(scheme, i));
|
|
u.insert(extIdWithInvalidEmail);
|
|
expectedProblems.add(
|
|
consistencyError(
|
|
"External ID '"
|
|
+ extIdWithInvalidEmail.key().get()
|
|
+ "' has an invalid email: "
|
|
+ extIdWithInvalidEmail.email()));
|
|
|
|
ExternalId extIdWithDuplicateEmail = createExternalIdWithDuplicateEmail(nextId(scheme, i));
|
|
u.insert(extIdWithDuplicateEmail);
|
|
expectedProblems.add(
|
|
consistencyError(
|
|
"Email '"
|
|
+ extIdWithDuplicateEmail.email()
|
|
+ "' is not unique, it's used by the following external IDs: '"
|
|
+ extIdWithDuplicateEmail.key().get()
|
|
+ "', 'mailto:"
|
|
+ extIdWithDuplicateEmail.email()
|
|
+ "'"));
|
|
|
|
ExternalId extIdWithBadPassword = createExternalIdWithBadPassword("admin-username");
|
|
u.insert(extIdWithBadPassword);
|
|
expectedProblems.add(
|
|
consistencyError(
|
|
"External ID '"
|
|
+ extIdWithBadPassword.key().get()
|
|
+ "' has an invalid password: unrecognized algorithm"));
|
|
|
|
return expectedProblems;
|
|
}
|
|
|
|
private Set<ConsistencyProblemInfo> insertNonParsableExternalIds() throws IOException {
|
|
MutableInteger i = new MutableInteger();
|
|
String scheme = "corrupt";
|
|
|
|
Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
|
|
try (Repository repo = repoManager.openRepository(allUsers);
|
|
RevWalk rw = new RevWalk(repo)) {
|
|
String externalId = nextId(scheme, i);
|
|
String noteId = insertExternalIdWithoutAccountId(repo, rw, externalId);
|
|
expectedProblems.add(
|
|
consistencyError(
|
|
"Invalid external ID config for note '"
|
|
+ noteId
|
|
+ "': Value for 'externalId."
|
|
+ externalId
|
|
+ ".accountId' is missing, expected account ID"));
|
|
|
|
externalId = nextId(scheme, i);
|
|
noteId = insertExternalIdWithKeyThatDoesntMatchNoteId(repo, rw, externalId);
|
|
expectedProblems.add(
|
|
consistencyError(
|
|
"Invalid external ID config for note '"
|
|
+ noteId
|
|
+ "': SHA1 of external ID '"
|
|
+ externalId
|
|
+ "' does not match note ID '"
|
|
+ noteId
|
|
+ "'"));
|
|
|
|
noteId = insertExternalIdWithInvalidConfig(repo, rw, nextId(scheme, i));
|
|
expectedProblems.add(
|
|
consistencyError(
|
|
"Invalid external ID config for note '" + noteId + "': Invalid line in config file"));
|
|
|
|
noteId = insertExternalIdWithEmptyNote(repo, rw, nextId(scheme, i));
|
|
expectedProblems.add(
|
|
consistencyError(
|
|
"Invalid external ID config for note '"
|
|
+ noteId
|
|
+ "': Expected exactly 1 'externalId' section, found 0"));
|
|
}
|
|
|
|
return expectedProblems;
|
|
}
|
|
|
|
private ExternalId createExternalIdWithOtherCaseEmail(String externalId) {
|
|
return ExternalId.createWithPassword(
|
|
ExternalId.Key.parse(externalId), admin.id, admin.email.toUpperCase(Locale.US), "password");
|
|
}
|
|
|
|
private String insertExternalIdWithoutAccountId(Repository repo, RevWalk rw, String externalId)
|
|
throws IOException {
|
|
ObjectId rev = ExternalIdReader.readRevision(repo);
|
|
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
|
|
|
|
ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
|
|
|
|
try (ObjectInserter ins = repo.newObjectInserter()) {
|
|
ObjectId noteId = extId.key().sha1();
|
|
Config c = new Config();
|
|
extId.writeToConfig(c);
|
|
c.unset("externalId", extId.key().get(), "accountId");
|
|
byte[] raw = c.toText().getBytes(UTF_8);
|
|
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
|
|
noteMap.set(noteId, dataBlob);
|
|
|
|
ExternalIdsUpdate.commit(
|
|
allUsers,
|
|
repo,
|
|
rw,
|
|
ins,
|
|
rev,
|
|
noteMap,
|
|
"Add external ID",
|
|
admin.getIdent(),
|
|
admin.getIdent(),
|
|
null,
|
|
GitReferenceUpdated.DISABLED);
|
|
return noteId.getName();
|
|
}
|
|
}
|
|
|
|
private String insertExternalIdWithKeyThatDoesntMatchNoteId(
|
|
Repository repo, RevWalk rw, String externalId) throws IOException {
|
|
ObjectId rev = ExternalIdReader.readRevision(repo);
|
|
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
|
|
|
|
ExternalId extId = ExternalId.create(ExternalId.Key.parse(externalId), admin.id);
|
|
|
|
try (ObjectInserter ins = repo.newObjectInserter()) {
|
|
ObjectId noteId = ExternalId.Key.parse(externalId + "x").sha1();
|
|
Config c = new Config();
|
|
extId.writeToConfig(c);
|
|
byte[] raw = c.toText().getBytes(UTF_8);
|
|
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
|
|
noteMap.set(noteId, dataBlob);
|
|
|
|
ExternalIdsUpdate.commit(
|
|
allUsers,
|
|
repo,
|
|
rw,
|
|
ins,
|
|
rev,
|
|
noteMap,
|
|
"Add external ID",
|
|
admin.getIdent(),
|
|
admin.getIdent(),
|
|
null,
|
|
GitReferenceUpdated.DISABLED);
|
|
return noteId.getName();
|
|
}
|
|
}
|
|
|
|
private String insertExternalIdWithInvalidConfig(Repository repo, RevWalk rw, String externalId)
|
|
throws IOException {
|
|
ObjectId rev = ExternalIdReader.readRevision(repo);
|
|
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
|
|
|
|
try (ObjectInserter ins = repo.newObjectInserter()) {
|
|
ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
|
|
byte[] raw = "bad-config".getBytes(UTF_8);
|
|
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
|
|
noteMap.set(noteId, dataBlob);
|
|
|
|
ExternalIdsUpdate.commit(
|
|
allUsers,
|
|
repo,
|
|
rw,
|
|
ins,
|
|
rev,
|
|
noteMap,
|
|
"Add external ID",
|
|
admin.getIdent(),
|
|
admin.getIdent(),
|
|
null,
|
|
GitReferenceUpdated.DISABLED);
|
|
return noteId.getName();
|
|
}
|
|
}
|
|
|
|
private String insertExternalIdWithEmptyNote(Repository repo, RevWalk rw, String externalId)
|
|
throws IOException {
|
|
ObjectId rev = ExternalIdReader.readRevision(repo);
|
|
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
|
|
|
|
try (ObjectInserter ins = repo.newObjectInserter()) {
|
|
ObjectId noteId = ExternalId.Key.parse(externalId).sha1();
|
|
byte[] raw = "".getBytes(UTF_8);
|
|
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
|
|
noteMap.set(noteId, dataBlob);
|
|
|
|
ExternalIdsUpdate.commit(
|
|
allUsers,
|
|
repo,
|
|
rw,
|
|
ins,
|
|
rev,
|
|
noteMap,
|
|
"Add external ID",
|
|
admin.getIdent(),
|
|
admin.getIdent(),
|
|
null,
|
|
GitReferenceUpdated.DISABLED);
|
|
return noteId.getName();
|
|
}
|
|
}
|
|
|
|
private ExternalId createExternalIdForNonExistingAccount(String externalId) {
|
|
return ExternalId.create(ExternalId.Key.parse(externalId), new Account.Id(1));
|
|
}
|
|
|
|
private ExternalId createExternalIdWithInvalidEmail(String externalId) {
|
|
return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, "invalid-email");
|
|
}
|
|
|
|
private ExternalId createExternalIdWithDuplicateEmail(String externalId) {
|
|
return ExternalId.createWithEmail(ExternalId.Key.parse(externalId), admin.id, admin.email);
|
|
}
|
|
|
|
private ExternalId createExternalIdWithBadPassword(String username) {
|
|
return ExternalId.create(
|
|
ExternalId.Key.create(SCHEME_USERNAME, username),
|
|
admin.id,
|
|
null,
|
|
"non-hashed-password-is-not-allowed");
|
|
}
|
|
|
|
private static String nextId(String scheme, MutableInteger i) {
|
|
return scheme + ":foo" + ++i.value;
|
|
}
|
|
|
|
@Test
|
|
public void retryOnLockFailure() throws Exception {
|
|
Retryer<RefsMetaExternalIdsUpdate> retryer =
|
|
ExternalIdsUpdate.retryerBuilder()
|
|
.withBlockStrategy(
|
|
new BlockStrategy() {
|
|
@Override
|
|
public void block(long sleepTime) {
|
|
// Don't sleep in tests.
|
|
}
|
|
})
|
|
.build();
|
|
|
|
ExternalId.Key fooId = ExternalId.Key.create("foo", "foo");
|
|
ExternalId.Key barId = ExternalId.Key.create("bar", "bar");
|
|
|
|
final AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
|
|
ExternalIdsUpdate update =
|
|
new ExternalIdsUpdate(
|
|
repoManager,
|
|
accountCache,
|
|
allUsers,
|
|
metricMaker,
|
|
externalIds,
|
|
new DisabledExternalIdCache(),
|
|
serverIdent.get(),
|
|
serverIdent.get(),
|
|
null,
|
|
GitReferenceUpdated.DISABLED,
|
|
() -> {
|
|
if (!doneBgUpdate.getAndSet(true)) {
|
|
try {
|
|
extIdsUpdate.create().insert(ExternalId.create(barId, admin.id));
|
|
} catch (IOException | ConfigInvalidException | OrmException e) {
|
|
// Ignore, the successful insertion of the external ID is asserted later
|
|
}
|
|
}
|
|
},
|
|
retryer);
|
|
assertThat(doneBgUpdate.get()).isFalse();
|
|
update.insert(ExternalId.create(fooId, admin.id));
|
|
assertThat(doneBgUpdate.get()).isTrue();
|
|
|
|
assertThat(externalIds.get(fooId)).isNotNull();
|
|
assertThat(externalIds.get(barId)).isNotNull();
|
|
}
|
|
|
|
@Test
|
|
public void failAfterRetryerGivesUp() throws Exception {
|
|
ExternalId.Key[] extIdsKeys = {
|
|
ExternalId.Key.create("foo", "foo"),
|
|
ExternalId.Key.create("bar", "bar"),
|
|
ExternalId.Key.create("baz", "baz")
|
|
};
|
|
final AtomicInteger bgCounter = new AtomicInteger(0);
|
|
ExternalIdsUpdate update =
|
|
new ExternalIdsUpdate(
|
|
repoManager,
|
|
accountCache,
|
|
allUsers,
|
|
metricMaker,
|
|
externalIds,
|
|
new DisabledExternalIdCache(),
|
|
serverIdent.get(),
|
|
serverIdent.get(),
|
|
null,
|
|
GitReferenceUpdated.DISABLED,
|
|
() -> {
|
|
try {
|
|
extIdsUpdate
|
|
.create()
|
|
.insert(ExternalId.create(extIdsKeys[bgCounter.getAndAdd(1)], admin.id));
|
|
} catch (IOException | ConfigInvalidException | OrmException e) {
|
|
// Ignore, the successful insertion of the external ID is asserted later
|
|
}
|
|
},
|
|
RetryerBuilder.<RefsMetaExternalIdsUpdate>newBuilder()
|
|
.retryIfException(e -> e instanceof LockFailureException)
|
|
.withStopStrategy(StopStrategies.stopAfterAttempt(extIdsKeys.length))
|
|
.build());
|
|
assertThat(bgCounter.get()).isEqualTo(0);
|
|
try {
|
|
update.insert(ExternalId.create(ExternalId.Key.create("abc", "abc"), admin.id));
|
|
fail("expected LockFailureException");
|
|
} catch (LockFailureException e) {
|
|
// Ignore, expected
|
|
}
|
|
assertThat(bgCounter.get()).isEqualTo(extIdsKeys.length);
|
|
for (ExternalId.Key extIdKey : extIdsKeys) {
|
|
assertThat(externalIds.get(extIdKey)).isNotNull();
|
|
}
|
|
}
|
|
|
|
@Test
|
|
public void readExternalIdWithAccountIdThatCanBeExpressedInKiB() throws Exception {
|
|
ExternalId.Key extIdKey = ExternalId.Key.parse("foo:bar");
|
|
Account.Id accountId = new Account.Id(1024 * 100);
|
|
extIdsUpdate.create().insert(ExternalId.create(extIdKey, accountId));
|
|
ExternalId extId = externalIds.get(extIdKey);
|
|
assertThat(extId.accountId()).isEqualTo(accountId);
|
|
}
|
|
|
|
@Test
|
|
public void checkNoReloadAfterUpdate() throws Exception {
|
|
Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(admin.id));
|
|
externalIdReader.setFailOnLoad(true);
|
|
|
|
// insert external ID
|
|
ExternalId extId = ExternalId.create("foo", "bar", admin.id);
|
|
extIdsUpdate.create().insert(extId);
|
|
expectedExtIds.add(extId);
|
|
assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
|
|
|
|
// update external ID
|
|
expectedExtIds.remove(extId);
|
|
extId = ExternalId.createWithEmail("foo", "bar", admin.id, "foo.bar@example.com");
|
|
extIdsUpdate.create().upsert(extId);
|
|
expectedExtIds.add(extId);
|
|
assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
|
|
|
|
// delete external ID
|
|
extIdsUpdate.create().delete(extId);
|
|
expectedExtIds.remove(extId);
|
|
assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
|
|
}
|
|
|
|
@Test
|
|
public void byAccountFailIfReadingExternalIdsFails() throws Exception {
|
|
externalIdReader.setFailOnLoad(true);
|
|
|
|
// update external ID branch so that external IDs need to be reloaded
|
|
insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
|
|
|
|
exception.expect(IOException.class);
|
|
externalIds.byAccount(admin.id);
|
|
}
|
|
|
|
@Test
|
|
public void byEmailFailIfReadingExternalIdsFails() throws Exception {
|
|
externalIdReader.setFailOnLoad(true);
|
|
|
|
// update external ID branch so that external IDs need to be reloaded
|
|
insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
|
|
|
|
exception.expect(IOException.class);
|
|
externalIds.byEmail(admin.email);
|
|
}
|
|
|
|
@Test
|
|
public void byAccountUpdateExternalIdsBehindGerritsBack() throws Exception {
|
|
Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(admin.id));
|
|
ExternalId newExtId = ExternalId.create("foo", "bar", admin.id);
|
|
insertExtIdBehindGerritsBack(newExtId);
|
|
expectedExternalIds.add(newExtId);
|
|
assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExternalIds);
|
|
}
|
|
|
|
@Test
|
|
public void unsetEmail() throws Exception {
|
|
ExternalId extId = ExternalId.createWithEmail("x", "1", user.id, "x@example.com");
|
|
extIdsUpdate.create().insert(extId);
|
|
|
|
ExternalId extIdWithoutEmail = ExternalId.create("x", "1", user.id);
|
|
extIdsUpdate.create().upsert(extIdWithoutEmail);
|
|
|
|
assertThat(externalIds.get(extId.key())).isEqualTo(extIdWithoutEmail);
|
|
}
|
|
|
|
@Test
|
|
public void unsetHttpPassword() throws Exception {
|
|
ExternalId extId =
|
|
ExternalId.createWithPassword(ExternalId.Key.create("y", "1"), user.id, null, "secret");
|
|
extIdsUpdate.create().insert(extId);
|
|
|
|
ExternalId extIdWithoutPassword = ExternalId.create("y", "1", user.id);
|
|
extIdsUpdate.create().upsert(extIdWithoutPassword);
|
|
|
|
assertThat(externalIds.get(extId.key())).isEqualTo(extIdWithoutPassword);
|
|
}
|
|
|
|
private void insertExtIdBehindGerritsBack(ExternalId extId) throws Exception {
|
|
try (Repository repo = repoManager.openRepository(allUsers);
|
|
RevWalk rw = new RevWalk(repo);
|
|
ObjectInserter ins = repo.newObjectInserter()) {
|
|
ObjectId rev = ExternalIdReader.readRevision(repo);
|
|
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, rev);
|
|
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
|
|
ExternalIdsUpdate.commit(
|
|
allUsers,
|
|
repo,
|
|
rw,
|
|
ins,
|
|
rev,
|
|
noteMap,
|
|
"insert new ID",
|
|
serverIdent.get(),
|
|
serverIdent.get(),
|
|
null,
|
|
GitReferenceUpdated.DISABLED);
|
|
}
|
|
}
|
|
|
|
private void addExtId(TestRepository<?> testRepo, ExternalId... extIds)
|
|
throws IOException, OrmDuplicateKeyException, ConfigInvalidException {
|
|
ObjectId rev = ExternalIdReader.readRevision(testRepo.getRepository());
|
|
|
|
try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) {
|
|
NoteMap noteMap = ExternalIdReader.readNoteMap(testRepo.getRevWalk(), rev);
|
|
for (ExternalId extId : extIds) {
|
|
ExternalIdsUpdate.insert(testRepo.getRevWalk(), ins, noteMap, extId);
|
|
}
|
|
|
|
ExternalIdsUpdate.commit(
|
|
allUsers,
|
|
testRepo.getRepository(),
|
|
testRepo.getRevWalk(),
|
|
ins,
|
|
rev,
|
|
noteMap,
|
|
"Add external ID",
|
|
admin.getIdent(),
|
|
admin.getIdent(),
|
|
null,
|
|
GitReferenceUpdated.DISABLED);
|
|
}
|
|
}
|
|
|
|
private List<AccountExternalIdInfo> toExternalIdInfos(Collection<ExternalId> extIds) {
|
|
return extIds.stream().map(this::toExternalIdInfo).collect(toList());
|
|
}
|
|
|
|
private AccountExternalIdInfo toExternalIdInfo(ExternalId extId) {
|
|
AccountExternalIdInfo info = new AccountExternalIdInfo();
|
|
info.identity = extId.key().get();
|
|
info.emailAddress = extId.email();
|
|
info.canDelete = !extId.isScheme(SCHEME_USERNAME) ? true : null;
|
|
info.trusted =
|
|
extId.isScheme(SCHEME_MAILTO)
|
|
|| extId.isScheme(SCHEME_UUID)
|
|
|| extId.isScheme(SCHEME_USERNAME)
|
|
? true
|
|
: null;
|
|
return info;
|
|
}
|
|
|
|
private void allowPushOfExternalIds() throws IOException, ConfigInvalidException {
|
|
grant(allUsers, RefNames.REFS_EXTERNAL_IDS, Permission.READ);
|
|
grant(allUsers, RefNames.REFS_EXTERNAL_IDS, Permission.PUSH);
|
|
}
|
|
|
|
private void assertRefUpdateFailure(RemoteRefUpdate update, String msg) {
|
|
assertThat(update.getStatus()).isEqualTo(Status.REJECTED_OTHER_REASON);
|
|
assertThat(update.getMessage()).contains(msg);
|
|
}
|
|
}
|