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

@@ -138,6 +138,51 @@ As result a link:#server-info[ServerInfo] entity is returned.
}
----
[[check-consistency]]
=== Check Consistency
--
'POST /config/server/check'
--
Runs consistency checks and returns detected problems.
Input for the consistency checks that should be run must be provided in
the request body inside a
link:#consistency-check-input[ConsistencyCheckInput] entity.
.Request
----
POST /config/server/check HTTP/1.0
Content-Type: application/json; charset=UTF-8
{
"check_account_external_ids": {}
}
----
As result a link:#consistency-check-info[ConsistencyCheckInfo] entity
is returned that contains detected consistency problems.
.Response
----
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
)]}'
{
"results": {
"account_external_id_result": {
"problems": [
{
"status": "ERROR",
"message": "External ID \u0027uuid:ccb8d323-1361-45aa-8874-41987a660c46\u0027 belongs to account that doesn\u0027t exist: 1000012"
}
]
}
}
}
----
[[confirm-email]]
=== Confirm Email
--
@@ -1365,6 +1410,66 @@ link:config-gerrit.html#change.submitWholeTopic[A configuration if
the whole topic is submitted].
|=============================
[[check-account-external-ids-input]]
=== CheckAccountExternalIdsInput
The `CheckAccountExternalIdsInput` entity contains input for the
account external IDs consistency check.
Currently this entity contains no fields.
[[check-account-external-ids-result-info]]
=== CheckAccountExternalIdsResultInfo
The `CheckAccountExternalIdsResultInfo` entity contains the result of
running the account external IDs consistency check.
[options="header",cols="1,6"]
|======================
|Field Name|Description
|`problems`|A list of link:#consistency-problem-info[
ConsistencyProblemInfo] entities.
|======================
[[consistency-check-info]]
=== ConsistencyCheckInfo
The `ConsistencyCheckInfo` entity contains the results of running
consistency checks.
[options="header",cols="1,^1,5"]
|================================================
|Field Name ||Description
|`check_account_external_ids_result`|optional|
The result of running the account external ID consistency check as a
link:#check-account-external-ids-result-info[
CheckAccountExternalIdsResultInfo] entity.
|================================================
[[consistency-check-input]]
=== ConsistencyCheckInput
The `ConsistencyCheckInput` entity contains information about which
consistency checks should be run.
[options="header",cols="1,^1,5"]
|=========================================
|Field Name ||Description
|`check_account_external_ids`|optional|
Input for the account external ID consistency check as
link:#check-account-external-ids-input[CheckAccountExternalIdsInput]
entity.
|=========================================
[[consistency-problem-info]]
=== ConsistencyProblemInfo
The `ConsistencyProblemInfo` entity contains information about a
consistency problem.
[options="header",cols="1,6"]
|======================
|Field Name|Description
|`status` |The status of the consistency problem. +
Possible values are `ERROR` and `WARNING`.
|`message` |Message describing the consistency problem.
|======================
[[download-info]]
=== DownloadInfo
The `DownloadInfo` entity contains information about supported download

View File

@@ -19,6 +19,8 @@ import static com.google.gerrit.acceptance.GitUtil.fetch;
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.group.SystemGroupBackend.REGISTERED_USERS;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.junit.Assert.fail;
import com.github.rholder.retry.BlockStrategy;
@@ -33,7 +35,12 @@ 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.metrics.MetricMaker;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
@@ -54,6 +61,7 @@ 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;
@@ -61,11 +69,13 @@ 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.util.MutableInteger;
import org.junit.Test;
@Sandboxed
@@ -198,6 +208,229 @@ public class ExternalIdIT extends AbstractDaemonTest {
.assertErrorStatus("not allowed to update " + RefNames.REFS_EXTERNAL_IDS);
}
@Test
public void checkConsistency() throws Exception {
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
resetCurrentApiUser();
MutableInteger i = new MutableInteger();
ExternalIdsUpdate u = extIdsUpdate.create();
// create valid external IDs
u.insert(
db,
ExternalId.createWithPassword(
ExternalId.Key.parse(nextId(i)),
admin.id,
"admin.other@example.com",
"secret-password"));
u.insert(db, createExternalIdWithOtherCaseEmail(nextId(i)));
ConsistencyCheckInput input = new ConsistencyCheckInput();
input.checkAccountExternalIds = new CheckAccountExternalIdsInput();
ConsistencyCheckInfo checkInfo = gApi.config().server().checkConsistency(input);
assertThat(checkInfo.checkAccountExternalIdsResult.problems).isEmpty();
// create invalid external IDs
Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
try (Repository repo = repoManager.openRepository(allUsers);
RevWalk rw = new RevWalk(repo)) {
String externalId = nextId(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(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(i));
expectedProblems.add(
consistencyError(
"Invalid external ID config for note '" + noteId + "': Invalid line in config file"));
noteId = insertExternalIdWithEmptyNote(repo, rw, nextId(i));
expectedProblems.add(
consistencyError(
"Invalid external ID config for note '"
+ noteId
+ "': Expected exactly 1 'externalId' section, found 0"));
}
ExternalId extIdForNonExistingAccount = createExternalIdForNonExistingAccount(nextId(i));
u.insert(db, extIdForNonExistingAccount);
expectedProblems.add(
consistencyError(
"External ID '"
+ extIdForNonExistingAccount.key().get()
+ "' belongs to account that doesn't exist: "
+ extIdForNonExistingAccount.accountId().get()));
ExternalId extIdWithInvalidEmail = createExternalIdWithInvalidEmail(nextId(i));
u.insert(db, extIdWithInvalidEmail);
expectedProblems.add(
consistencyError(
"External ID '"
+ extIdWithInvalidEmail.key().get()
+ "' has an invalid email: "
+ extIdWithInvalidEmail.email()));
ExternalId extIdWithDuplicateEmail = createExternalIdWithDuplicateEmail(nextId(i));
u.insert(db, 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(db, extIdWithBadPassword);
expectedProblems.add(
consistencyError(
"External ID '"
+ extIdWithBadPassword.key().get()
+ "' has an invalid password: unrecognized algorithm"));
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("not allowed to run consistency checks");
gApi.config().server().checkConsistency(new ConsistencyCheckInput());
}
private ConsistencyProblemInfo consistencyError(String message) {
return new ConsistencyProblemInfo(ConsistencyProblemInfo.Status.ERROR, message);
}
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(
repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent());
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(
repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent());
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(
repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent());
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(
repo, rw, ins, rev, noteMap, "Add external ID", admin.getIdent(), admin.getIdent());
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(MutableInteger i) {
return "foo:bar" + ++i.value;
}
@Test
public void retryOnLockFailure() throws Exception {
Retryer<RefsMetaExternalIdsUpdate> retryer =

View File

@@ -0,0 +1,64 @@
// 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.extensions.api.config;
import java.util.List;
import java.util.Objects;
public class ConsistencyCheckInfo {
public CheckAccountExternalIdsResultInfo checkAccountExternalIdsResult;
public static class CheckAccountExternalIdsResultInfo {
public List<ConsistencyProblemInfo> problems;
public CheckAccountExternalIdsResultInfo(List<ConsistencyProblemInfo> problems) {
this.problems = problems;
}
}
public static class ConsistencyProblemInfo {
public enum Status {
ERROR,
WARNING,
}
public final Status status;
public final String message;
public ConsistencyProblemInfo(Status status, String message) {
this.status = status;
this.message = message;
}
@Override
public boolean equals(Object o) {
if (o instanceof ConsistencyProblemInfo) {
ConsistencyProblemInfo other = ((ConsistencyProblemInfo) o);
return Objects.equals(status, other.status) && Objects.equals(message, other.message);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(status, message);
}
@Override
public String toString() {
return status.name() + ": " + message;
}
}
}

View File

@@ -0,0 +1,21 @@
// 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.extensions.api.config;
public class ConsistencyCheckInput {
public CheckAccountExternalIdsInput checkAccountExternalIds;
public static class CheckAccountExternalIdsInput {}
}

View File

@@ -34,6 +34,8 @@ public interface Server {
DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) throws RestApiException;
ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) throws RestApiException;
/**
* A default implementation which allows source compatibility when adding new methods to the
* interface.
@@ -68,5 +70,10 @@ public interface Server {
public DiffPreferencesInfo setDefaultDiffPreferences(DiffPreferencesInfo in) {
throw new NotImplementedException();
}
@Override
public ConsistencyCheckInfo checkConsistency(ConsistencyCheckInput in) {
throw new NotImplementedException();
}
}
}

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