| // 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); |
| } |
| } |