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:
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user