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:
@@ -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
|
||||
|
@@ -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 =
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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 {}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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