Add REST endpoint to check consistency of external IDs

The REST endpoint is generic so that further consistency checks can be
added later. Each consistency check has a specific input entity so that
sepcific options for a check can be set. At the moment the consistency
check for external IDs doesn't support any input options, but we may add
options later, e.g. to tell the consistency check to automatically fix
certain inconsistencies.

Change-Id: I2ae76ea9254798744d8d5408d1ba640931319ed8
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin
2017-03-24 15:32:03 +01:00
parent a2b6bd9648
commit 54fd1d362a
10 changed files with 672 additions and 8 deletions

View File

@@ -215,21 +215,21 @@ public abstract class ExternalId implements Serializable {
throw invalidConfig(
noteId,
String.format(
"Expected exactly 1 %s section, found %d",
"Expected exactly 1 '%s' section, found %d",
EXTERNAL_ID_SECTION, externalIdKeys.size()));
}
String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
Key externalIdKey = Key.parse(externalIdKeyStr);
if (externalIdKey == null) {
throw invalidConfig(noteId, String.format("Invalid external id: %s", externalIdKeyStr));
throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
}
if (!externalIdKey.sha1().getName().equals(noteId)) {
throw invalidConfig(
noteId,
String.format(
"SHA1 of external ID %s does not match note ID %s", externalIdKeyStr, noteId));
"SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
}
String email = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, EMAIL_KEY);
@@ -252,7 +252,7 @@ public abstract class ExternalId implements Serializable {
throw invalidConfig(
noteId,
String.format(
"Value for %s.%s.%s is missing, expected account ID",
"Value for '%s.%s.%s' is missing, expected account ID",
EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
}
@@ -263,7 +263,7 @@ public abstract class ExternalId implements Serializable {
throw invalidConfig(
noteId,
String.format(
"Value %s for %s.%s.%s is invalid, expected account ID",
"Value %s for '%s.%s.%s' is invalid, expected account ID",
accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
}
return accountId;
@@ -271,14 +271,14 @@ public abstract class ExternalId implements Serializable {
throw invalidConfig(
noteId,
String.format(
"Value %s for %s.%s.%s is invalid, expected account ID",
"Value %s for '%s.%s.%s' is invalid, expected account ID",
accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
}
}
private static ConfigInvalidException invalidConfig(String noteId, String message) {
return new ConfigInvalidException(
String.format("Invalid external id config for note %s: %s", noteId, message));
String.format("Invalid external ID config for note '%s': %s", noteId, message));
}
public static ExternalId from(AccountExternalId externalId) {

View File

@@ -0,0 +1,150 @@
// 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.server.account.externalids;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import static java.util.stream.Collectors.joining;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.HashedPassword;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.codec.DecoderException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.Note;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
@Singleton
public class ExternalIdsConsistencyChecker {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsers;
private final AccountCache accountCache;
@Inject
ExternalIdsConsistencyChecker(
GitRepositoryManager repoManager, AllUsersName allUsers, AccountCache accountCache) {
this.repoManager = repoManager;
this.allUsers = allUsers;
this.accountCache = accountCache;
}
public List<ConsistencyProblemInfo> check() throws IOException {
try (Repository repo = repoManager.openRepository(allUsers)) {
return check(repo, ExternalIdReader.readRevision(repo));
}
}
public List<ConsistencyProblemInfo> check(ObjectId rev) throws IOException {
try (Repository repo = repoManager.openRepository(allUsers)) {
return check(repo, rev);
}
}
private List<ConsistencyProblemInfo> check(Repository repo, ObjectId commit) throws IOException {
List<ConsistencyProblemInfo> problems = new ArrayList<>();
ListMultimap<String, ExternalId.Key> emails =
MultimapBuilder.hashKeys().arrayListValues().build();
try (RevWalk rw = new RevWalk(repo)) {
NoteMap noteMap = ExternalIdReader.readNoteMap(rw, commit);
for (Note note : noteMap) {
byte[] raw =
rw.getObjectReader()
.open(note.getData(), OBJ_BLOB)
.getCachedBytes(ExternalIdReader.MAX_NOTE_SZ);
try {
ExternalId extId = ExternalId.parse(note.getName(), raw);
problems.addAll(validateExternalId(extId));
if (extId.email() != null) {
emails.put(extId.email(), extId.key());
}
} catch (ConfigInvalidException e) {
addError(String.format(e.getMessage()), problems);
}
}
}
emails
.asMap()
.entrySet()
.stream()
.filter(e -> e.getValue().size() > 1)
.forEach(
e ->
addError(
String.format(
"Email '%s' is not unique, it's used by the following external IDs: %s",
e.getKey(),
e.getValue()
.stream()
.map(k -> "'" + k.get() + "'")
.sorted()
.collect(joining(", "))),
problems));
return problems;
}
private List<ConsistencyProblemInfo> validateExternalId(ExternalId extId) {
List<ConsistencyProblemInfo> problems = new ArrayList<>();
if (accountCache.getIfPresent(extId.accountId()) == null) {
addError(
String.format(
"External ID '%s' belongs to account that doesn't exist: %s",
extId.key().get(), extId.accountId().get()),
problems);
}
if (extId.email() != null && !OutgoingEmailValidator.isValid(extId.email())) {
addError(
String.format(
"External ID '%s' has an invalid email: %s", extId.key().get(), extId.email()),
problems);
}
if (extId.password() != null && extId.isScheme(SCHEME_USERNAME)) {
try {
HashedPassword.decode(extId.password());
} catch (DecoderException e) {
addError(
String.format(
"External ID '%s' has an invalid password: %s", extId.key().get(), e.getMessage()),
problems);
}
}
return problems;
}
private static void addError(String error, List<ConsistencyProblemInfo> problems) {
problems.add(new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, error));
}
}

View File

@@ -15,11 +15,14 @@
package com.google.gerrit.server.api.config;
import com.google.gerrit.common.Version;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
import com.google.gerrit.extensions.api.config.Server;
import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.common.ServerInfo;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.config.CheckConsistency;
import com.google.gerrit.server.config.ConfigResource;
import com.google.gerrit.server.config.GetDiffPreferences;
import com.google.gerrit.server.config.GetPreferences;
@@ -27,6 +30,7 @@ import com.google.gerrit.server.config.GetServerInfo;
import com.google.gerrit.server.config.SetDiffPreferences;
import com.google.gerrit.server.config.SetPreferences;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -38,6 +42,7 @@ public class ServerImpl implements Server {
private final GetDiffPreferences getDiffPreferences;
private final SetDiffPreferences setDiffPreferences;
private final GetServerInfo getServerInfo;
private final Provider<CheckConsistency> checkConsistency;
@Inject
ServerImpl(
@@ -45,12 +50,14 @@ public class ServerImpl implements Server {
SetPreferences setPreferences,
GetDiffPreferences getDiffPreferences,
SetDiffPreferences setDiffPreferences,
GetServerInfo getServerInfo) {
GetServerInfo getServerInfo,
Provider<CheckConsistency> checkConsistency) {
this.getPreferences = getPreferences;
this.setPreferences = setPreferences;
this.getDiffPreferences = getDiffPreferences;
this.setDiffPreferences = setDiffPreferences;
this.getServerInfo = getServerInfo;
this.checkConsistency = checkConsistency;
}
@Override
@@ -104,4 +111,13 @@ public class ServerImpl implements Server {
throw new RestApiException("Cannot set default diff preferences", e);
}
}
@Override
public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException {
try {
return checkConsistency.get().apply(new ConfigResource(), in);
} catch (IOException e) {
throw new RestApiException("Cannot check consistency", e);
}
}
}

View File

@@ -0,0 +1,67 @@
// 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.server.config;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.CheckAccountExternalIdsResultInfo;
import com.google.gerrit.extensions.api.config.ConsistencyCheckInput;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
@Singleton
public class CheckConsistency implements RestModifyView<ConfigResource, ConsistencyCheckInput> {
private final Provider<IdentifiedUser> userProvider;
private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
@Inject
CheckConsistency(
Provider<IdentifiedUser> currentUser,
ExternalIdsConsistencyChecker externalIdsConsistencyChecker) {
this.userProvider = currentUser;
this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
}
@Override
public ConsistencyCheckInfo apply(ConfigResource resource, ConsistencyCheckInput input)
throws RestApiException, IOException {
IdentifiedUser user = userProvider.get();
if (!user.isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
if (!user.getCapabilities().canAccessDatabase()) {
throw new AuthException("not allowed to run consistency checks");
}
if (input == null || input.checkAccountExternalIds == null) {
throw new BadRequestException("input required");
}
ConsistencyCheckInfo consistencyCheckInfo = new ConsistencyCheckInfo();
if (input.checkAccountExternalIds != null) {
consistencyCheckInfo.checkAccountExternalIdsResult =
new CheckAccountExternalIdsResultInfo(externalIdsConsistencyChecker.check());
}
return consistencyCheckInfo;
}
}

View File

@@ -36,6 +36,7 @@ public class Module extends RestApiModule {
child(CONFIG_KIND, "top-menus").to(TopMenuCollection.class);
get(CONFIG_KIND, "version").to(GetVersion.class);
get(CONFIG_KIND, "info").to(GetServerInfo.class);
post(CONFIG_KIND, "check").to(CheckConsistency.class);
get(CONFIG_KIND, "preferences").to(GetPreferences.class);
put(CONFIG_KIND, "preferences").to(SetPreferences.class);
get(CONFIG_KIND, "preferences.diff").to(GetDiffPreferences.class);