Migrate external IDs to NoteDb (part 1)

In NoteDb external IDs are stored in the All-Users repository in a Git
Notes branch called refs/meta/external-ids where the sha1 of the
external ID is used as note name. Each note content is a Git config
file that contains an external ID. It has exactly one externalId
subsection with an accountId and optionally email and password:

  [externalId "username:jdoe"]
     accountId = 1003407
     email = jdoe@example.com
     password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7

Storing the external IDs in a Git Notes branch with using the sha1 of
the external ID as note name ensures that external IDs are unique and
are only assigned to a single account. If it is tried to assign the
same external ID concurrently to different accounts, only one Git
update succeeds while the other Git updates fail with LOCK_FAILURE.
This means assigning external IDs is also safe in a multimaster setup
if a consensus algorithm for updating Git refs is implemented (which
is needed for multimaster in any case). Alternatively it was
considered to store the external IDs per account as Git config file in
the refs/users/<sharded-id> user branches in the All-Users repository
(see abandoned change 9f9f07ef). This approach was given up because in
race conditions it allowed to assign the same external ID to different
accounts by updating different branches in Git.

To support a live migration on a multi-master Gerrit installation, the
migration of external IDs from ReviewDb to NoteDb is done in 2 steps:

- part 1 (this change):
  * always write to both backends (ReviewDb and NoteDb)
  * always read external IDs from ReviewDb
  * upgraded instances write to both backends, old instances only
    write to ReviewDb
  * after upgrading all instances (all still read from ReviewDb)
    run a batch to copy all external IDs from the ReviewDb to NoteDb
- part 2 (next change):
  * bump the database schema version
  * migrate the external IDs from ReviewDb to NoteDb (for single instance
    Gerrit servers)
  * read external IDs from NoteDb
  * delete the database table

With this change reading external IDs from NoteDb is not implemented
yet. This is because the storage format of external IDs in NoteDb
doesn't support efficient lookup of external IDs by account and this
problem is only addressed in the follow-up change (it adds a cache for
external IDs, but this cache uses the revision of the notes branch as
key, and hence can be only implemented once the external IDs are fully
migrated to NoteDb and storing external IDs in ReviewDb is dropped).

The ExternalIdsUpdate class implements updating of external IDs in
both NoteDb and ReviewDb. It provides various methods to update
external IDs (e.g. insert, upsert, delete, replace). For NoteDb each
method invocation leads to one commit in the Git notes branch.
ExternalIdsUpdate has two factories, User and Server. This allows to
record either the calling user or the Gerrit server identity as
committer for an update of the external Ids.

External IDs are now represented by a new AutoValue class called
ExternalId. This class replaces the usage of the old gwtorm entity
AccountExternalId class. For ExternalId scheme names are the same as for
AccountExternalId but no longer include the trailing ':'.

The class ExternalIdsOnInit makes it possible to update external IDs
during the init phase. This is required for inserting external IDs for
the initial admin user which is created by InitAdminUser. We need a
special class for this since not all dependencies of ExternalIdsUpdate
are available during init.

The class ExternalIdsBatchUpdate allows to do batch updates to
external IDs. For NoteDb all updates will result in a single commit to
the refs/meta/external-ids Git notes branch.

LocalUsernamesToLowerCase is now always converting the usernames in a
single thread only. This allows us to get a single commit for the
username convertion in NoteDb (this would not be possible if workers
do updates in parallel). Since LocalUsernamesToLowerCase is rather
light-weight being able to parallelize work is not really needed and
removing the workers simplifies the code significantly.

To protect the refs/meta/external-ids Git notes branch in the All-Users
repository read access for this ref is only allowed to users that have
the 'Access Database' global capability assigned. In addition
there is a commit validator that disallows updating the
refs/meta/external-ids branch by push. This is to prevent that the
external IDs in NoteDb diverge from the external IDs in ReviewDb while
the migration to NoteDb is not fully done yet.

Change-Id: Ic9bd5791e84ee8d332ccb1f709970b59ee66b308
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin 2017-02-15 11:10:59 +01:00
parent 84d830b5b3
commit 744d2b8967
64 changed files with 2022 additions and 625 deletions

View File

@ -9,7 +9,6 @@ account to lower case
-- --
_java_ -jar gerrit.war _LocalUsernamesToLowerCase _java_ -jar gerrit.war _LocalUsernamesToLowerCase
-d <SITE_PATH> -d <SITE_PATH>
[--threads]
-- --
== DESCRIPTION == DESCRIPTION
@ -40,10 +39,6 @@ must be run by itself.
Location of the gerrit.config file, and all other per-site Location of the gerrit.config file, and all other per-site
configuration data, supporting libraries and log files. configuration data, supporting libraries and log files.
--threads::
Number of threads to perform the scan work with. Defaults to
twice the number of CPUs available.
== CONTEXT == CONTEXT
This command can only be run on a server which has direct This command can only be run on a server which has direct
connectivity to the metadata database. connectivity to the metadata database.

View File

@ -20,14 +20,14 @@ import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountByEmailCache; import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gerrit.server.account.GroupCache; import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.HashedPassword;
import com.google.gerrit.server.account.VersionedAuthorizedKeys; import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.index.account.AccountIndexer; import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gerrit.server.ssh.SshKeyCache;
@ -40,8 +40,10 @@ import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.KeyPair; import com.jcraft.jsch.KeyPair;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
@Singleton @Singleton
@ -55,6 +57,7 @@ public class AccountCreator {
private final AccountCache accountCache; private final AccountCache accountCache;
private final AccountByEmailCache byEmailCache; private final AccountByEmailCache byEmailCache;
private final AccountIndexer indexer; private final AccountIndexer indexer;
private final ExternalIdsUpdate.Server externalIdsUpdate;
@Inject @Inject
AccountCreator( AccountCreator(
@ -64,7 +67,8 @@ public class AccountCreator {
SshKeyCache sshKeyCache, SshKeyCache sshKeyCache,
AccountCache accountCache, AccountCache accountCache,
AccountByEmailCache byEmailCache, AccountByEmailCache byEmailCache,
AccountIndexer indexer) { AccountIndexer indexer,
ExternalIdsUpdate.Server externalIdsUpdate) {
accounts = new HashMap<>(); accounts = new HashMap<>();
reviewDbProvider = schema; reviewDbProvider = schema;
this.authorizedKeys = authorizedKeys; this.authorizedKeys = authorizedKeys;
@ -73,6 +77,7 @@ public class AccountCreator {
this.accountCache = accountCache; this.accountCache = accountCache;
this.byEmailCache = byEmailCache; this.byEmailCache = byEmailCache;
this.indexer = indexer; this.indexer = indexer;
this.externalIdsUpdate = externalIdsUpdate;
} }
public synchronized TestAccount create( public synchronized TestAccount create(
@ -84,19 +89,14 @@ public class AccountCreator {
try (ReviewDb db = reviewDbProvider.open()) { try (ReviewDb db = reviewDbProvider.open()) {
Account.Id id = new Account.Id(db.nextAccountId()); Account.Id id = new Account.Id(db.nextAccountId());
AccountExternalId extUser = List<ExternalId> extIds = new ArrayList<>(2);
new AccountExternalId(
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
String httpPass = "http-pass"; String httpPass = "http-pass";
extUser.setPassword(HashedPassword.fromPassword(httpPass).encode()); extIds.add(ExternalId.createUsername(username, id, httpPass));
db.accountExternalIds().insert(Collections.singleton(extUser));
if (email != null) { if (email != null) {
AccountExternalId extMailto = new AccountExternalId(id, getEmailKey(email)); extIds.add(ExternalId.createEmail(id, email));
extMailto.setEmailAddress(email);
db.accountExternalIds().insert(Collections.singleton(extMailto));
} }
externalIdsUpdate.create().insert(db, extIds);
Account a = new Account(id, TimeUtil.nowTs()); Account a = new Account(id, TimeUtil.nowTs());
a.setFullName(fullName); a.setFullName(fullName);
@ -159,10 +159,6 @@ public class AccountCreator {
return checkNotNull(accounts.get(username), "No TestAccount created for %s", username); return checkNotNull(accounts.get(username), "No TestAccount created for %s", username);
} }
private AccountExternalId.Key getEmailKey(String email) {
return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
}
public static KeyPair genSshKey() throws JSchException { public static KeyPair genSshKey() throws JSchException {
JSch jsch = new JSch(); JSch jsch = new JSch();
return KeyPair.genKeyPair(jsch, KeyPair.RSA); return KeyPair.genKeyPair(jsch, KeyPair.RSA);

View File

@ -62,9 +62,10 @@ import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.server.GpgKeys; import com.google.gerrit.gpg.server.GpgKeys;
import com.google.gerrit.gpg.testutil.TestKey; import com.google.gerrit.gpg.testutil.TestKey;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AccountByEmailCache; import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gerrit.server.account.WatchConfig; import com.google.gerrit.server.account.WatchConfig;
import com.google.gerrit.server.account.WatchConfig.NotifyType; import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.AllUsersName;
@ -79,7 +80,6 @@ import java.io.ByteArrayOutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -116,10 +116,15 @@ public class AccountIT extends AbstractDaemonTest {
@Inject private AccountByEmailCache byEmailCache; @Inject private AccountByEmailCache byEmailCache;
private List<AccountExternalId> savedExternalIds; @Inject private ExternalIdsUpdate.User externalIdsUpdateFactory;
private ExternalIdsUpdate externalIdsUpdate;
private List<ExternalId> savedExternalIds;
@Before @Before
public void saveExternalIds() throws Exception { public void saveExternalIds() throws Exception {
externalIdsUpdate = externalIdsUpdateFactory.create();
savedExternalIds = new ArrayList<>(); savedExternalIds = new ArrayList<>();
savedExternalIds.addAll(getExternalIds(admin)); savedExternalIds.addAll(getExternalIds(admin));
savedExternalIds.addAll(getExternalIds(user)); savedExternalIds.addAll(getExternalIds(user));
@ -131,9 +136,9 @@ public class AccountIT extends AbstractDaemonTest {
// savedExternalIds is null when we don't run SSH tests and the assume in // savedExternalIds is null when we don't run SSH tests and the assume in
// @Before in AbstractDaemonTest prevents this class' @Before method from // @Before in AbstractDaemonTest prevents this class' @Before method from
// being executed. // being executed.
db.accountExternalIds().delete(getExternalIds(admin)); externalIdsUpdate.delete(db, getExternalIds(admin));
db.accountExternalIds().delete(getExternalIds(user)); externalIdsUpdate.delete(db, getExternalIds(user));
db.accountExternalIds().insert(savedExternalIds); externalIdsUpdate.insert(db, savedExternalIds);
} }
accountCache.evict(admin.getId()); accountCache.evict(admin.getId());
accountCache.evict(user.getId()); accountCache.evict(user.getId());
@ -151,7 +156,7 @@ public class AccountIT extends AbstractDaemonTest {
} }
} }
private Collection<AccountExternalId> getExternalIds(TestAccount account) throws Exception { private Collection<ExternalId> getExternalIds(TestAccount account) throws Exception {
return accountCache.get(account.getId()).getExternalIds(); return accountCache.get(account.getId()).getExternalIds();
} }
@ -445,11 +450,11 @@ public class AccountIT extends AbstractDaemonTest {
String email = "foo.bar@example.com"; String email = "foo.bar@example.com";
String extId1 = "foo:bar"; String extId1 = "foo:bar";
String extId2 = "foo:baz"; String extId2 = "foo:baz";
db.accountExternalIds() List<ExternalId> extIds =
.insert( ImmutableList.of(
ImmutableList.of( ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email),
createExternalIdWithEmail(extId1, email), ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email));
createExternalIdWithEmail(extId2, email))); externalIdsUpdateFactory.create().insert(db, extIds);
accountCache.evict(admin.id); accountCache.evict(admin.id);
assertThat( assertThat(
gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet())) gApi.accounts().self().getExternalIds().stream().map(e -> e.identity).collect(toSet()))
@ -498,7 +503,9 @@ public class AccountIT extends AbstractDaemonTest {
// exact match with other scheme // exact match with other scheme
String email = "foo.bar@example.com"; String email = "foo.bar@example.com";
db.accountExternalIds().insert(ImmutableList.of(createExternalIdWithEmail("foo:bar", email))); externalIdsUpdateFactory
.create()
.insert(db, ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email));
accountCache.evict(admin.id); accountCache.evict(admin.id);
assertEmail(byEmailCache.get(email), admin); assertEmail(byEmailCache.get(email), admin);
@ -706,10 +713,7 @@ public class AccountIT extends AbstractDaemonTest {
public void addOtherUsersGpgKey_Conflict() throws Exception { public void addOtherUsersGpgKey_Conflict() throws Exception {
// Both users have a matching external ID for this key. // Both users have a matching external ID for this key.
addExternalIdEmail(admin, "test5@example.com"); addExternalIdEmail(admin, "test5@example.com");
AccountExternalId extId = externalIdsUpdate.insert(db, ExternalId.create("foo", "myId", user.getId()));
new AccountExternalId(user.getId(), new AccountExternalId.Key("foo:myId"));
db.accountExternalIds().insert(Collections.singleton(extId));
accountCache.evict(user.getId()); accountCache.evict(user.getId());
TestKey key = validKeyWithSecondUserId(); TestKey key = validKeyWithSecondUserId();
@ -909,7 +913,7 @@ public class AccountIT extends AbstractDaemonTest {
Iterable<String> expectedFps = Iterable<String> expectedFps =
expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint())); expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
Iterable<String> actualFps = Iterable<String> actualFps =
GpgKeys.getGpgExtIds(db, currAccountId).transform(AccountExternalId::getSchemeRest); GpgKeys.getGpgExtIds(db, currAccountId).transform(e -> e.key().id());
assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps); assertThat(actualFps).named("external IDs in database").containsExactlyElementsIn(expectedFps);
// Check raw stored keys. // Check raw stored keys.
@ -934,11 +938,9 @@ public class AccountIT extends AbstractDaemonTest {
private void addExternalIdEmail(TestAccount account, String email) throws Exception { private void addExternalIdEmail(TestAccount account, String email) throws Exception {
checkNotNull(email); checkNotNull(email);
AccountExternalId extId = externalIdsUpdate.insert(
new AccountExternalId(account.getId(), new AccountExternalId.Key(name("test"), email)); db, ExternalId.createWithEmail(name("test"), email, account.getId(), email));
extId.setEmailAddress(email); // Clear saved AccountState and ExternalIds.
db.accountExternalIds().insert(Collections.singleton(extId));
// Clear saved AccountState and AccountExternalIds.
accountCache.evict(account.getId()); accountCache.evict(account.getId());
setApiUser(account); setApiUser(account);
} }
@ -958,12 +960,6 @@ public class AccountIT extends AbstractDaemonTest {
return gApi.accounts().self().getEmails().stream().map(e -> e.email).collect(toSet()); return gApi.accounts().self().getEmails().stream().map(e -> e.email).collect(toSet());
} }
private AccountExternalId createExternalIdWithEmail(String id, String email) {
AccountExternalId extId = new AccountExternalId(admin.id, new AccountExternalId.Key(id));
extId.setEmailAddress(email);
return extId;
}
private void assertEmail(Set<Account.Id> accounts, TestAccount expectedAccount) { private void assertEmail(Set<Account.Id> accounts, TestAccount expectedAccount) {
assertThat(accounts).hasSize(1); assertThat(accounts).hasSize(1);
assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId()); assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.getId());

View File

@ -15,30 +15,64 @@
package com.google.gerrit.acceptance.rest.account; package com.google.gerrit.acceptance.rest.account;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.GitUtil.fetch;
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static org.junit.Assert.fail;
import com.github.rholder.retry.BlockStrategy;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.Sandboxed; 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.common.AccountExternalIdInfo; import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIds;
import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.LockFailureException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.junit.Test; import org.junit.Test;
@Sandboxed @Sandboxed
public class ExternalIdIT extends AbstractDaemonTest { public class ExternalIdIT extends AbstractDaemonTest {
@Inject private AllUsersName allUsers;
@Inject private ExternalIdsUpdate.Server extIdsUpdate;
@Inject private ExternalIds externalIds;
@Test @Test
public void getExternalIDs() throws Exception { public void getExternalIDs() throws Exception {
Collection<AccountExternalId> expectedIds = accountCache.get(user.getId()).getExternalIds(); Collection<ExternalId> expectedIds = accountCache.get(user.getId()).getExternalIds();
List<AccountExternalIdInfo> expectedIdInfos = new ArrayList<>(); List<AccountExternalIdInfo> expectedIdInfos = new ArrayList<>();
for (AccountExternalId id : expectedIds) { for (ExternalId id : expectedIds) {
id.setCanDelete(!id.getExternalId().equals("username:" + user.username)); AccountExternalIdInfo info = new AccountExternalIdInfo();
id.setTrusted(true); info.identity = id.key().get();
expectedIdInfos.add(toInfo(id)); info.emailAddress = id.email();
info.canDelete = !id.isScheme(SCHEME_USERNAME) ? true : null;
info.trusted = true;
expectedIdInfos.add(info);
} }
RestResponse response = userRestSession.get("/accounts/self/external.ids"); RestResponse response = userRestSession.get("/accounts/self/external.ids");
@ -102,12 +136,119 @@ public class ExternalIdIT extends AbstractDaemonTest {
.isEqualTo(String.format("External id %s does not exist", externalIdStr)); .isEqualTo(String.format("External id %s does not exist", externalIdStr));
} }
private static AccountExternalIdInfo toInfo(AccountExternalId id) { @Test
AccountExternalIdInfo info = new AccountExternalIdInfo(); public void fetchExternalIdsBranch() throws Exception {
info.identity = id.getExternalId(); TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers, user);
info.emailAddress = id.getEmailAddress();
info.trusted = id.isTrusted() ? true : null; // refs/meta/external-ids is only visible to users with the 'Access Database' capability
info.canDelete = id.canDelete() ? true : null; try {
return info; fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
fail("expected TransportException");
} catch (TransportException e) {
assertThat(e.getMessage())
.isEqualTo(
"Remote does not have " + RefNames.REFS_EXTERNAL_IDS + " available for fetch.");
}
allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
// re-clone to get new request context, otherwise the old global capabilities are still cached
// in the IdentifiedUser object
allUsersRepo = cloneProject(allUsers, user);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
}
@Test
public void pushToExternalIdsBranch() throws Exception {
grant(Permission.READ, allUsers, RefNames.REFS_EXTERNAL_IDS);
grant(Permission.PUSH, allUsers, RefNames.REFS_EXTERNAL_IDS);
TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":externalIds");
allUsersRepo.reset("externalIds");
PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
push.to(RefNames.REFS_EXTERNAL_IDS)
.assertErrorStatus("not allowed to update " + RefNames.REFS_EXTERNAL_IDS);
}
@Test
public void retryOnLockFailure() throws Exception {
Retryer<Void> retryer =
ExternalIdsUpdate.retryerBuilder()
.withBlockStrategy(
new BlockStrategy() {
@Override
public void block(long sleepTime) {
// Don't sleep in tests.
}
})
.build();
ExternalId.Key fooId = ExternalId.Key.create("foo", "foo");
ExternalId.Key barId = ExternalId.Key.create("bar", "bar");
final AtomicBoolean doneBgUpdate = new AtomicBoolean(false);
ExternalIdsUpdate update =
new ExternalIdsUpdate(
repoManager,
allUsers,
serverIdent.get(),
serverIdent.get(),
() -> {
if (!doneBgUpdate.getAndSet(true)) {
try {
extIdsUpdate.create().insert(db, ExternalId.create(barId, admin.id));
} catch (IOException | ConfigInvalidException | OrmException e) {
// Ignore, the successful insertion of the external ID is asserted later
}
}
},
retryer);
assertThat(doneBgUpdate.get()).isFalse();
update.insert(db, ExternalId.create(fooId, admin.id));
assertThat(doneBgUpdate.get()).isTrue();
assertThat(externalIds.get(fooId)).isNotNull();
assertThat(externalIds.get(barId)).isNotNull();
}
@Test
public void failAfterRetryerGivesUp() throws Exception {
ExternalId.Key[] extIdsKeys = {
ExternalId.Key.create("foo", "foo"),
ExternalId.Key.create("bar", "bar"),
ExternalId.Key.create("baz", "baz")
};
final AtomicInteger bgCounter = new AtomicInteger(0);
ExternalIdsUpdate update =
new ExternalIdsUpdate(
repoManager,
allUsers,
serverIdent.get(),
serverIdent.get(),
() -> {
try {
extIdsUpdate
.create()
.insert(db, ExternalId.create(extIdsKeys[bgCounter.getAndAdd(1)], admin.id));
} catch (IOException | ConfigInvalidException | OrmException e) {
// Ignore, the successful insertion of the external ID is asserted later
}
},
RetryerBuilder.<Void>newBuilder()
.retryIfException(e -> e instanceof LockFailureException)
.withStopStrategy(StopStrategies.stopAfterAttempt(extIdsKeys.length))
.build());
assertThat(bgCounter.get()).isEqualTo(0);
try {
update.insert(db, ExternalId.create(ExternalId.Key.create("abc", "abc"), admin.id));
fail("expected LockFailureException");
} catch (LockFailureException e) {
// Ignore, expected
}
assertThat(bgCounter.get()).isEqualTo(extIdsKeys.length);
for (ExternalId.Key extIdKey : extIdsKeys) {
assertThat(externalIds.get(extIdKey)).isNotNull();
}
} }
} }

View File

@ -15,16 +15,16 @@
package com.google.gerrit.gpg; package com.google.gerrit.gpg;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY; import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
import com.google.common.base.CharMatcher; import com.google.common.base.CharMatcher;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gerrit.server.query.account.InternalAccountQuery;
@ -155,8 +155,7 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
} }
private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException { private CheckResult checkIdsForArbitraryUser(PGPPublicKey key) throws PGPException, OrmException {
List<AccountState> accountStates = List<AccountState> accountStates = accountQueryProvider.get().byExternalId(toExtIdKey(key));
accountQueryProvider.get().byExternalId(toExtIdKey(key).get());
if (accountStates.isEmpty()) { if (accountStates.isEmpty()) {
return CheckResult.bad("Key is not associated with any users"); return CheckResult.bad("Key is not associated with any users");
} }
@ -202,11 +201,11 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
private Set<String> getAllowedUserIds(IdentifiedUser user) { private Set<String> getAllowedUserIds(IdentifiedUser user) {
Set<String> result = new HashSet<>(); Set<String> result = new HashSet<>();
result.addAll(user.getEmailAddresses()); result.addAll(user.getEmailAddresses());
for (AccountExternalId extId : user.state().getExternalIds()) { for (ExternalId extId : user.state().getExternalIds()) {
if (extId.isScheme(SCHEME_GPGKEY)) { if (extId.isScheme(SCHEME_GPGKEY)) {
continue; // Omit GPG keys. continue; // Omit GPG keys.
} }
result.add(extId.getExternalId()); result.add(extId.key().get());
} }
return result; return result;
} }
@ -248,8 +247,7 @@ public class GerritPublicKeyChecker extends PublicKeyChecker {
return sb.toString(); return sb.toString();
} }
static AccountExternalId.Key toExtIdKey(PGPPublicKey key) { static ExternalId.Key toExtIdKey(PGPPublicKey key) {
return new AccountExternalId.Key( return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint()));
} }
} }

View File

@ -33,6 +33,7 @@ import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.transport.PushCertificate; import org.eclipse.jgit.transport.PushCertificate;
import org.eclipse.jgit.transport.PushCertificateParser; import org.eclipse.jgit.transport.PushCertificateParser;
@ -78,7 +79,7 @@ public class GpgApiAdapterImpl implements GpgApiAdapter {
in.delete = delete; in.delete = delete;
try { try {
return postGpgKeys.apply(account, in); return postGpgKeys.apply(account, in);
} catch (PGPException | OrmException | IOException e) { } catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
throw new GpgException(e); throw new GpgException(e);
} }
} }

View File

@ -25,6 +25,7 @@ import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject; import com.google.inject.assistedinject.AssistedInject;
import java.io.IOException; import java.io.IOException;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class GpgKeyApiImpl implements GpgKeyApi { public class GpgKeyApiImpl implements GpgKeyApi {
public interface Factory { public interface Factory {
@ -55,7 +56,7 @@ public class GpgKeyApiImpl implements GpgKeyApi {
public void delete() throws RestApiException { public void delete() throws RestApiException {
try { try {
delete.apply(rsrc, new DeleteGpgKey.Input()); delete.apply(rsrc, new DeleteGpgKey.Input());
} catch (PGPException | OrmException | IOException e) { } catch (PGPException | OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot delete GPG key", e); throw new RestApiException("Cannot delete GPG key", e);
} }
} }

View File

@ -15,6 +15,7 @@
package com.google.gerrit.gpg.server; package com.google.gerrit.gpg.server;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
import com.google.common.io.BaseEncoding; import com.google.common.io.BaseEncoding;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
@ -22,17 +23,18 @@ import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.gpg.PublicKeyStore; import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.server.DeleteGpgKey.Input; import com.google.gerrit.gpg.server.DeleteGpgKey.Input;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKey;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate;
@ -44,27 +46,34 @@ public class DeleteGpgKey implements RestModifyView<GpgKey, Input> {
private final Provider<ReviewDb> db; private final Provider<ReviewDb> db;
private final Provider<PublicKeyStore> storeProvider; private final Provider<PublicKeyStore> storeProvider;
private final AccountCache accountCache; private final AccountCache accountCache;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@Inject @Inject
DeleteGpgKey( DeleteGpgKey(
@GerritPersonIdent Provider<PersonIdent> serverIdent, @GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<ReviewDb> db, Provider<ReviewDb> db,
Provider<PublicKeyStore> storeProvider, Provider<PublicKeyStore> storeProvider,
AccountCache accountCache) { AccountCache accountCache,
ExternalIdsUpdate.User externalIdsUpdateFactory) {
this.serverIdent = serverIdent; this.serverIdent = serverIdent;
this.db = db; this.db = db;
this.storeProvider = storeProvider; this.storeProvider = storeProvider;
this.accountCache = accountCache; this.accountCache = accountCache;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
} }
@Override @Override
public Response<?> apply(GpgKey rsrc, Input input) public Response<?> apply(GpgKey rsrc, Input input)
throws ResourceConflictException, PGPException, OrmException, IOException { throws ResourceConflictException, PGPException, OrmException, IOException,
ConfigInvalidException {
PGPPublicKey key = rsrc.getKeyRing().getPublicKey(); PGPPublicKey key = rsrc.getKeyRing().getPublicKey();
AccountExternalId.Key extIdKey = externalIdsUpdateFactory
new AccountExternalId.Key( .create()
AccountExternalId.SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())); .delete(
db.get().accountExternalIds().deleteKeys(Collections.singleton(extIdKey)); db.get(),
rsrc.getUser().getAccountId(),
ExternalId.Key.create(
SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
accountCache.evict(rsrc.getUser().getAccountId()); accountCache.evict(rsrc.getUser().getAccountId());
try (PublicKeyStore store = storeProvider.get()) { try (PublicKeyStore store = storeProvider.get()) {

View File

@ -14,7 +14,7 @@
package com.google.gerrit.gpg.server; package com.google.gerrit.gpg.server;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY; import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
@ -37,10 +37,10 @@ import com.google.gerrit.gpg.GerritPublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyChecker; import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore; import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.ExternalId;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
@ -114,7 +114,7 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
throw new ResourceNotFoundException(id); throw new ResourceNotFoundException(id);
} }
static byte[] parseFingerprint(String str, Iterable<AccountExternalId> existingExtIds) static byte[] parseFingerprint(String str, Iterable<ExternalId> existingExtIds)
throws ResourceNotFoundException { throws ResourceNotFoundException {
str = CharMatcher.whitespace().removeFrom(str).toUpperCase(); str = CharMatcher.whitespace().removeFrom(str).toUpperCase();
if ((str.length() != 8 && str.length() != 40) if ((str.length() != 8 && str.length() != 40)
@ -122,8 +122,8 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
throw new ResourceNotFoundException(str); throw new ResourceNotFoundException(str);
} }
byte[] fp = null; byte[] fp = null;
for (AccountExternalId extId : existingExtIds) { for (ExternalId extId : existingExtIds) {
String fpStr = extId.getSchemeRest(); String fpStr = extId.key().id();
if (!fpStr.endsWith(str)) { if (!fpStr.endsWith(str)) {
continue; continue;
} else if (fp != null) { } else if (fp != null) {
@ -152,8 +152,8 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
checkVisible(self, rsrc); checkVisible(self, rsrc);
Map<String, GpgKeyInfo> keys = new HashMap<>(); Map<String, GpgKeyInfo> keys = new HashMap<>();
try (PublicKeyStore store = storeProvider.get()) { try (PublicKeyStore store = storeProvider.get()) {
for (AccountExternalId extId : getGpgExtIds(rsrc)) { for (ExternalId extId : getGpgExtIds(rsrc)) {
String fpStr = extId.getSchemeRest(); String fpStr = extId.key().id();
byte[] fp = BaseEncoding.base16().decode(fpStr); byte[] fp = BaseEncoding.base16().decode(fpStr);
boolean found = false; boolean found = false;
for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) { for (PGPPublicKeyRing keyRing : store.get(keyId(fp))) {
@ -199,13 +199,14 @@ public class GpgKeys implements ChildCollection<AccountResource, GpgKey> {
} }
@VisibleForTesting @VisibleForTesting
public static FluentIterable<AccountExternalId> getGpgExtIds(ReviewDb db, Account.Id accountId) public static FluentIterable<ExternalId> getGpgExtIds(ReviewDb db, Account.Id accountId)
throws OrmException { throws OrmException {
return FluentIterable.from(db.accountExternalIds().byAccount(accountId)) return FluentIterable.from(
ExternalId.from(db.accountExternalIds().byAccount(accountId).toList()))
.filter(in -> in.isScheme(SCHEME_GPGKEY)); .filter(in -> in.isScheme(SCHEME_GPGKEY));
} }
private Iterable<AccountExternalId> getGpgExtIds(AccountResource rsrc) throws OrmException { private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws OrmException {
return getGpgExtIds(db.get(), rsrc.getUser().getAccountId()); return getGpgExtIds(db.get(), rsrc.getUser().getAccountId());
} }

View File

@ -16,12 +16,13 @@ package com.google.gerrit.gpg.server;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString; import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.server.account.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
@ -39,7 +40,6 @@ import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore; import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.server.PostGpgKeys.Input; import com.google.gerrit.gpg.server.PostGpgKeys.Input;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.GerritPersonIdent;
@ -47,6 +47,8 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResource; import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gerrit.server.mail.send.AddKeySender; import com.google.gerrit.server.mail.send.AddKeySender;
import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
@ -66,6 +68,7 @@ import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RefUpdate;
@ -88,6 +91,7 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
private final AddKeySender.Factory addKeyFactory; private final AddKeySender.Factory addKeyFactory;
private final AccountCache accountCache; private final AccountCache accountCache;
private final Provider<InternalAccountQuery> accountQueryProvider; private final Provider<InternalAccountQuery> accountQueryProvider;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@Inject @Inject
PostGpgKeys( PostGpgKeys(
@ -98,7 +102,8 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
GerritPublicKeyChecker.Factory checkerFactory, GerritPublicKeyChecker.Factory checkerFactory,
AddKeySender.Factory addKeyFactory, AddKeySender.Factory addKeyFactory,
AccountCache accountCache, AccountCache accountCache,
Provider<InternalAccountQuery> accountQueryProvider) { Provider<InternalAccountQuery> accountQueryProvider,
ExternalIdsUpdate.User externalIdsUpdateFactory) {
this.serverIdent = serverIdent; this.serverIdent = serverIdent;
this.db = db; this.db = db;
this.self = self; this.self = self;
@ -107,48 +112,48 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
this.addKeyFactory = addKeyFactory; this.addKeyFactory = addKeyFactory;
this.accountCache = accountCache; this.accountCache = accountCache;
this.accountQueryProvider = accountQueryProvider; this.accountQueryProvider = accountQueryProvider;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
} }
@Override @Override
public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input) public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input)
throws ResourceNotFoundException, BadRequestException, ResourceConflictException, throws ResourceNotFoundException, BadRequestException, ResourceConflictException,
PGPException, OrmException, IOException { PGPException, OrmException, IOException, ConfigInvalidException {
GpgKeys.checkVisible(self, rsrc); GpgKeys.checkVisible(self, rsrc);
List<AccountExternalId> existingExtIds = Collection<ExternalId> existingExtIds =
GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList(); GpgKeys.getGpgExtIds(db.get(), rsrc.getUser().getAccountId()).toList();
try (PublicKeyStore store = storeProvider.get()) { try (PublicKeyStore store = storeProvider.get()) {
Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds); Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove); List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
List<AccountExternalId> newExtIds = new ArrayList<>(existingExtIds.size()); List<ExternalId> newExtIds = new ArrayList<>(existingExtIds.size());
for (PGPPublicKeyRing keyRing : newKeys) { for (PGPPublicKeyRing keyRing : newKeys) {
PGPPublicKey key = keyRing.getPublicKey(); PGPPublicKey key = keyRing.getPublicKey();
AccountExternalId.Key extIdKey = toExtIdKey(key.getFingerprint()); ExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
Account account = getAccountByExternalId(extIdKey.get()); Account account = getAccountByExternalId(extIdKey);
if (account != null) { if (account != null) {
if (!account.getId().equals(rsrc.getUser().getAccountId())) { if (!account.getId().equals(rsrc.getUser().getAccountId())) {
throw new ResourceConflictException("GPG key already associated with another account"); throw new ResourceConflictException("GPG key already associated with another account");
} }
} else { } else {
newExtIds.add(new AccountExternalId(rsrc.getUser().getAccountId(), extIdKey)); newExtIds.add(ExternalId.create(extIdKey, rsrc.getUser().getAccountId()));
} }
} }
storeKeys(rsrc, newKeys, toRemove); storeKeys(rsrc, newKeys, toRemove);
if (!newExtIds.isEmpty()) {
db.get().accountExternalIds().insert(newExtIds); List<ExternalId.Key> extIdKeysToRemove =
} toRemove.stream().map(fp -> toExtIdKey(fp.get())).collect(toList());
db.get() externalIdsUpdateFactory
.accountExternalIds() .create()
.deleteKeys(Iterables.transform(toRemove, fp -> toExtIdKey(fp.get()))); .replace(db.get(), rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds);
accountCache.evict(rsrc.getUser().getAccountId()); accountCache.evict(rsrc.getUser().getAccountId());
return toJson(newKeys, toRemove, store, rsrc.getUser()); return toJson(newKeys, toRemove, store, rsrc.getUser());
} }
} }
private Set<Fingerprint> readKeysToRemove(Input input, List<AccountExternalId> existingExtIds) { private Set<Fingerprint> readKeysToRemove(Input input, Collection<ExternalId> existingExtIds) {
if (input.delete == null || input.delete.isEmpty()) { if (input.delete == null || input.delete.isEmpty()) {
return ImmutableSet.of(); return ImmutableSet.of();
} }
@ -243,13 +248,12 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
} }
} }
private AccountExternalId.Key toExtIdKey(byte[] fp) { private ExternalId.Key toExtIdKey(byte[] fp) {
return new AccountExternalId.Key( return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
AccountExternalId.SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
} }
private Account getAccountByExternalId(String externalId) throws OrmException { private Account getAccountByExternalId(ExternalId.Key extIdKey) throws OrmException {
List<AccountState> accountStates = accountQueryProvider.get().byExternalId(externalId); List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);
if (accountStates.isEmpty()) { if (accountStates.isEmpty()) {
return null; return null;
@ -257,7 +261,7 @@ public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
if (accountStates.size() > 1) { if (accountStates.size() > 1) {
StringBuilder msg = new StringBuilder(); StringBuilder msg = new StringBuilder();
msg.append("GPG key ").append(externalId).append(" associated with multiple accounts: "); msg.append("GPG key ").append(extIdKey.get()).append(" associated with multiple accounts: ");
Joiner.on(", ") Joiner.on(", ")
.appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION)); .appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
log.error(msg.toString()); log.error(msg.toString());

View File

@ -23,7 +23,6 @@ import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC; import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyD; import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyD;
import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyE; import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyE;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD; import static org.eclipse.jgit.lib.RefUpdate.Result.FAST_FORWARD;
import static org.eclipse.jgit.lib.RefUpdate.Result.FORCED; import static org.eclipse.jgit.lib.RefUpdate.Result.FORCED;
import static org.eclipse.jgit.lib.RefUpdate.Result.NEW; import static org.eclipse.jgit.lib.RefUpdate.Result.NEW;
@ -34,13 +33,14 @@ import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
import com.google.gerrit.gpg.testutil.TestKey; import com.google.gerrit.gpg.testutil.TestKey;
import com.google.gerrit.lifecycle.LifecycleManager; import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gerrit.server.schema.SchemaCreator; import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.util.RequestContext; import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext; import com.google.gerrit.server.util.ThreadLocalRequestContext;
@ -55,7 +55,6 @@ import com.google.inject.util.Providers;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRing;
@ -86,6 +85,8 @@ public class GerritPublicKeyCheckerTest {
@Inject private ThreadLocalRequestContext requestContext; @Inject private ThreadLocalRequestContext requestContext;
@Inject private ExternalIdsUpdate.Server externalIdsUpdateFactory;
private LifecycleManager lifecycle; private LifecycleManager lifecycle;
private ReviewDb db; private ReviewDb db;
private Account.Id userId; private Account.Id userId;
@ -221,7 +222,8 @@ public class GerritPublicKeyCheckerTest {
@Test @Test
public void noExternalIds() throws Exception { public void noExternalIds() throws Exception {
db.accountExternalIds().delete(db.accountExternalIds().byAccount(user.getAccountId())); ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
externalIdsUpdate.deleteAll(db, user.getAccountId());
reloadUser(); reloadUser();
TestKey key = validKeyWithSecondUserId(); TestKey key = validKeyWithSecondUserId();
@ -234,11 +236,8 @@ public class GerritPublicKeyCheckerTest {
checker = checkerFactory.create().setStore(store).disableTrust(); checker = checkerFactory.create().setStore(store).disableTrust();
assertProblems( assertProblems(
checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users"); checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users");
externalIdsUpdate.insert(
db.accountExternalIds() db, ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId()));
.insert(
Collections.singleton(
new AccountExternalId(user.getAccountId(), toExtIdKey(key.getPublicKey()))));
reloadUser(); reloadUser();
assertProblems(checker.check(key.getPublicKey()), Status.BAD, "No identities found for user"); assertProblems(checker.check(key.getPublicKey()), Status.BAD, "No identities found for user");
} }
@ -389,18 +388,15 @@ public class GerritPublicKeyCheckerTest {
private void add(PGPPublicKeyRing kr, IdentifiedUser user) throws Exception { private void add(PGPPublicKeyRing kr, IdentifiedUser user) throws Exception {
Account.Id id = user.getAccountId(); Account.Id id = user.getAccountId();
List<AccountExternalId> newExtIds = new ArrayList<>(2); List<ExternalId> newExtIds = new ArrayList<>(2);
newExtIds.add(new AccountExternalId(id, toExtIdKey(kr.getPublicKey()))); newExtIds.add(ExternalId.create(toExtIdKey(kr.getPublicKey()), id));
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
String userId = (String) Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null); String userId = (String) Iterators.getOnlyElement(kr.getPublicKey().getUserIDs(), null);
if (userId != null) { if (userId != null) {
String email = PushCertificateIdent.parse(userId).getEmailAddress(); String email = PushCertificateIdent.parse(userId).getEmailAddress();
assertThat(email).contains("@"); assertThat(email).contains("@");
AccountExternalId mailto = newExtIds.add(ExternalId.createEmail(id, email));
new AccountExternalId(id, new AccountExternalId.Key(SCHEME_MAILTO, email));
mailto.setEmailAddress(email);
newExtIds.add(mailto);
} }
store.add(kr); store.add(kr);
@ -410,7 +406,7 @@ public class GerritPublicKeyCheckerTest {
cb.setCommitter(ident); cb.setCommitter(ident);
assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED); assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
db.accountExternalIds().insert(newExtIds); externalIdsUpdateFactory.create().insert(db, newExtIds);
accountCache.evict(user.getAccountId()); accountCache.evict(user.getAccountId());
} }
@ -434,12 +430,9 @@ public class GerritPublicKeyCheckerTest {
} }
private void addExternalId(String scheme, String id, String email) throws Exception { private void addExternalId(String scheme, String id, String email) throws Exception {
AccountExternalId extId = externalIdsUpdateFactory
new AccountExternalId(user.getAccountId(), new AccountExternalId.Key(scheme, id)); .create()
if (email != null) { .insert(db, ExternalId.createWithEmail(scheme, id, user.getAccountId(), email));
extId.setEmailAddress(email);
}
db.accountExternalIds().insert(Collections.singleton(extId));
reloadUser(); reloadUser();
} }
} }

View File

@ -22,12 +22,12 @@ import com.google.gerrit.common.data.HostPageData;
import com.google.gerrit.httpd.WebSessionManager.Key; import com.google.gerrit.httpd.WebSessionManager.Key;
import com.google.gerrit.httpd.WebSessionManager.Val; import com.google.gerrit.httpd.WebSessionManager.Val;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser; import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AuthResult; import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.servlet.RequestScoped; import com.google.inject.servlet.RequestScoped;
@ -132,7 +132,7 @@ public abstract class CacheBasedWebSession implements WebSession {
} }
@Override @Override
public AccountExternalId.Key getLastLoginExternalId() { public ExternalId.Key getLastLoginExternalId() {
return val != null ? val.getExternalId() : null; return val != null ? val.getExternalId() : null;
} }
@ -149,9 +149,9 @@ public abstract class CacheBasedWebSession implements WebSession {
} }
@Override @Override
public void login(final AuthResult res, final boolean rememberMe) { public void login(AuthResult res, boolean rememberMe) {
final Account.Id id = res.getAccountId(); Account.Id id = res.getAccountId();
final AccountExternalId.Key identity = res.getExternalId(); ExternalId.Key identity = res.getExternalId();
if (val != null) { if (val != null) {
manager.destroy(key); manager.destroy(key);

View File

@ -16,10 +16,10 @@ package com.google.gerrit.httpd;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.AccessPath; import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AuthResult; import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
public interface WebSession { public interface WebSession {
boolean isSignedIn(); boolean isSignedIn();
@ -29,7 +29,7 @@ public interface WebSession {
boolean isValidXGerritAuth(String keyIn); boolean isValidXGerritAuth(String keyIn);
AccountExternalId.Key getLastLoginExternalId(); ExternalId.Key getLastLoginExternalId();
CurrentUser getUser(); CurrentUser getUser();

View File

@ -30,7 +30,7 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.cache.Cache; import com.google.common.cache.Cache;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.GerritServerConfig;
import com.google.inject.Inject; import com.google.inject.Inject;
@ -98,18 +98,18 @@ public class WebSessionManager {
} }
} }
Val createVal(final Key key, final Val val) { Val createVal(Key key, Val val) {
final Account.Id who = val.getAccountId(); Account.Id who = val.getAccountId();
final boolean remember = val.isPersistentCookie(); boolean remember = val.isPersistentCookie();
final AccountExternalId.Key lastLogin = val.getExternalId(); ExternalId.Key lastLogin = val.getExternalId();
return createVal(key, who, remember, lastLogin, val.sessionId, val.auth); return createVal(key, who, remember, lastLogin, val.sessionId, val.auth);
} }
Val createVal( Val createVal(
final Key key, Key key,
final Account.Id who, Account.Id who,
final boolean remember, boolean remember,
final AccountExternalId.Key lastLogin, ExternalId.Key lastLogin,
String sid, String sid,
String auth) { String auth) {
// Refresh the cookie every hour or when it is half-expired. // Refresh the cookie every hour or when it is half-expired.
@ -191,19 +191,19 @@ public class WebSessionManager {
private transient Account.Id accountId; private transient Account.Id accountId;
private transient long refreshCookieAt; private transient long refreshCookieAt;
private transient boolean persistentCookie; private transient boolean persistentCookie;
private transient AccountExternalId.Key externalId; private transient ExternalId.Key externalId;
private transient long expiresAt; private transient long expiresAt;
private transient String sessionId; private transient String sessionId;
private transient String auth; private transient String auth;
Val( Val(
final Account.Id accountId, Account.Id accountId,
final long refreshCookieAt, long refreshCookieAt,
final boolean persistentCookie, boolean persistentCookie,
final AccountExternalId.Key externalId, ExternalId.Key externalId,
final long expiresAt, long expiresAt,
final String sessionId, String sessionId,
final String auth) { String auth) {
this.accountId = accountId; this.accountId = accountId;
this.refreshCookieAt = refreshCookieAt; this.refreshCookieAt = refreshCookieAt;
this.persistentCookie = persistentCookie; this.persistentCookie = persistentCookie;
@ -221,7 +221,7 @@ public class WebSessionManager {
return accountId; return accountId;
} }
AccountExternalId.Key getExternalId() { ExternalId.Key getExternalId() {
return externalId; return externalId;
} }
@ -253,7 +253,7 @@ public class WebSessionManager {
if (externalId != null) { if (externalId != null) {
writeVarInt32(out, 4); writeVarInt32(out, 4);
writeString(out, externalId.get()); writeString(out, externalId.toString());
} }
if (sessionId != null) { if (sessionId != null) {
@ -289,7 +289,7 @@ public class WebSessionManager {
persistentCookie = readVarInt32(in) != 0; persistentCookie = readVarInt32(in) != 0;
continue; continue;
case 4: case 4:
externalId = new AccountExternalId.Key(readString(in)); externalId = ExternalId.Key.parse(readString(in));
continue; continue;
case 5: case 5:
sessionId = readString(in); sessionId = readString(in);

View File

@ -14,7 +14,8 @@
package com.google.gerrit.httpd.auth.become; package com.google.gerrit.httpd.auth.become;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import static com.google.gerrit.server.account.ExternalId.SCHEME_UUID;
import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.PageLinks;
import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.registration.DynamicItem;
@ -23,13 +24,13 @@ import com.google.gerrit.httpd.LoginUrlToken;
import com.google.gerrit.httpd.WebSession; import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.httpd.template.SiteHeaderFooter; import com.google.gerrit.httpd.template.SiteHeaderFooter;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult; import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
@ -179,17 +180,16 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
return null; return null;
} }
private AuthResult auth(final AccountExternalId account) { private AuthResult auth(Account.Id account) {
if (account != null) { if (account != null) {
return new AuthResult(account.getAccountId(), null, false); return new AuthResult(account, null, false);
} }
return null; return null;
} }
private AuthResult byUserName(final String userName) { private AuthResult byUserName(final String userName) {
try { try {
AccountExternalId.Key extKey = new AccountExternalId.Key(SCHEME_USERNAME, userName); List<AccountState> accountStates = accountQuery.byExternalId(SCHEME_USERNAME, userName);
List<AccountState> accountStates = accountQuery.byExternalId(extKey.get());
if (accountStates.isEmpty()) { if (accountStates.isEmpty()) {
getServletContext().log("No accounts with username " + userName + " found"); getServletContext().log("No accounts with username " + userName + " found");
return null; return null;
@ -198,7 +198,7 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
getServletContext().log("Multiple accounts with username " + userName + " found"); getServletContext().log("Multiple accounts with username " + userName + " found");
return null; return null;
} }
return auth(new AccountExternalId(accountStates.get(0).getAccount().getId(), extKey)); return auth(accountStates.get(0).getAccount().getId());
} catch (OrmException e) { } catch (OrmException e) {
getServletContext().log("cannot query account index", e); getServletContext().log("cannot query account index", e);
return null; return null;
@ -231,9 +231,9 @@ class BecomeAnyAccountLoginServlet extends HttpServlet {
} }
private AuthResult create() throws IOException { private AuthResult create() throws IOException {
String fakeId = AccountExternalId.SCHEME_UUID + UUID.randomUUID();
try { try {
return accountManager.authenticate(new AuthRequest(fakeId)); return accountManager.authenticate(
new AuthRequest(ExternalId.Key.create(SCHEME_UUID, UUID.randomUUID().toString())));
} catch (AccountException e) { } catch (AccountException e) {
getServletContext().log("cannot create new account", e); getServletContext().log("cannot create new account", e);
return null; return null;

View File

@ -17,7 +17,7 @@ package com.google.gerrit.httpd.auth.container;
import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Strings.emptyToNull; import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.net.HttpHeaders.AUTHORIZATION; import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT; import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
import static java.nio.charset.StandardCharsets.ISO_8859_1; import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@ -26,7 +26,7 @@ import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.httpd.RemoteUserUtil; import com.google.gerrit.httpd.RemoteUserUtil;
import com.google.gerrit.httpd.WebSession; import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.httpd.raw.HostPageServlet; import com.google.gerrit.httpd.raw.HostPageServlet;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtjsonrpc.server.RPCServletUtils; import com.google.gwtjsonrpc.server.RPCServletUtils;
@ -127,8 +127,8 @@ class HttpAuthFilter implements Filter {
} }
private static boolean correctUser(String user, WebSession session) { private static boolean correctUser(String user, WebSession session) {
AccountExternalId.Key id = session.getLastLoginExternalId(); ExternalId.Key id = session.getLastLoginExternalId();
return id != null && id.equals(new AccountExternalId.Key(SCHEME_GERRIT, user)); return id != null && id.equals(ExternalId.Key.create(SCHEME_GERRIT, user));
} }
String getRemoteUser(HttpServletRequest req) { String getRemoteUser(HttpServletRequest req) {

View File

@ -14,7 +14,7 @@
package com.google.gerrit.httpd.auth.container; package com.google.gerrit.httpd.auth.container;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL; import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.PageLinks;
@ -23,11 +23,11 @@ import com.google.gerrit.httpd.CanonicalWebUrl;
import com.google.gerrit.httpd.HtmlDomUtil; import com.google.gerrit.httpd.HtmlDomUtil;
import com.google.gerrit.httpd.LoginUrlToken; import com.google.gerrit.httpd.LoginUrlToken;
import com.google.gerrit.httpd.WebSession; import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult; import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfig;
import com.google.gwtexpui.server.CacheHeaders; import com.google.gwtexpui.server.CacheHeaders;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
@ -39,6 +39,7 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.w3c.dom.Document; import org.w3c.dom.Document;
@ -127,7 +128,7 @@ class HttpLoginServlet extends HttpServlet {
try { try {
log.debug("Associating external identity \"{}\" to user \"{}\"", remoteExternalId, user); log.debug("Associating external identity \"{}\" to user \"{}\"", remoteExternalId, user);
updateRemoteExternalId(arsp, remoteExternalId); updateRemoteExternalId(arsp, remoteExternalId);
} catch (AccountException | OrmException e) { } catch (AccountException | OrmException | ConfigInvalidException e) {
log.error( log.error(
"Unable to associate external identity \"" "Unable to associate external identity \""
+ remoteExternalId + remoteExternalId
@ -156,12 +157,10 @@ class HttpLoginServlet extends HttpServlet {
} }
private void updateRemoteExternalId(AuthResult arsp, String remoteAuthToken) private void updateRemoteExternalId(AuthResult arsp, String remoteAuthToken)
throws AccountException, OrmException, IOException { throws AccountException, OrmException, IOException, ConfigInvalidException {
AccountExternalId remoteAuthExtId =
new AccountExternalId(
arsp.getAccountId(), new AccountExternalId.Key(SCHEME_EXTERNAL, remoteAuthToken));
accountManager.updateLink( accountManager.updateLink(
arsp.getAccountId(), new AuthRequest(remoteAuthExtId.getExternalId())); arsp.getAccountId(),
new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, remoteAuthToken)));
} }
private void replace(Document doc, String name, String value) { private void replace(Document doc, String name, String value) {

View File

@ -22,6 +22,7 @@ java_library(
"//lib/commons:codec", "//lib/commons:codec",
"//lib/guice", "//lib/guice",
"//lib/guice:guice-servlet", "//lib/guice:guice-servlet",
"//lib/jgit/org.eclipse.jgit:jgit",
"//lib/log:api", "//lib/log:api",
], ],
) )

View File

@ -31,6 +31,7 @@ import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.AuthResult; import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.auth.oauth.OAuthTokenCache; import com.google.gerrit.server.auth.oauth.OAuthTokenCache;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
@ -44,6 +45,7 @@ import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -124,7 +126,7 @@ class OAuthSession {
private void authenticateAndRedirect( private void authenticateAndRedirect(
HttpServletRequest req, HttpServletResponse rsp, OAuthToken token) throws IOException { HttpServletRequest req, HttpServletResponse rsp, OAuthToken token) throws IOException {
AuthRequest areq = new AuthRequest(user.getExternalId()); AuthRequest areq = new AuthRequest(ExternalId.Key.parse(user.getExternalId()));
AuthResult arsp; AuthResult arsp;
try { try {
String claimedIdentifier = user.getClaimedIdentity(); String claimedIdentifier = user.getClaimedIdentity();
@ -190,7 +192,7 @@ class OAuthSession {
log.info("OAuth2: linking claimed identity to {}", claimedId.get().toString()); log.info("OAuth2: linking claimed identity to {}", claimedId.get().toString());
try { try {
accountManager.link(claimedId.get(), req); accountManager.link(claimedId.get(), req);
} catch (OrmException e) { } catch (OrmException | ConfigInvalidException e) {
log.error( log.error(
"Cannot link: " "Cannot link: "
+ user.getExternalId() + user.getExternalId()
@ -210,7 +212,7 @@ class OAuthSession {
throws AccountException, IOException { throws AccountException, IOException {
try { try {
accountManager.link(identifiedUser.get().getAccountId(), areq); accountManager.link(identifiedUser.get().getAccountId(), areq);
} catch (OrmException e) { } catch (OrmException | ConfigInvalidException e) {
log.error( log.error(
"Cannot link: " "Cannot link: "
+ user.getExternalId() + user.getExternalId()

View File

@ -31,6 +31,7 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthResult; import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.ExternalId;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
@ -43,6 +44,7 @@ import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -116,7 +118,8 @@ class OAuthSessionOverOpenID {
private void authenticateAndRedirect(HttpServletRequest req, HttpServletResponse rsp) private void authenticateAndRedirect(HttpServletRequest req, HttpServletResponse rsp)
throws IOException { throws IOException {
com.google.gerrit.server.account.AuthRequest areq = com.google.gerrit.server.account.AuthRequest areq =
new com.google.gerrit.server.account.AuthRequest(user.getExternalId()); new com.google.gerrit.server.account.AuthRequest(
ExternalId.Key.parse(user.getExternalId()));
AuthResult arsp = null; AuthResult arsp = null;
try { try {
String claimedIdentifier = user.getClaimedIdentity(); String claimedIdentifier = user.getClaimedIdentity();
@ -167,7 +170,7 @@ class OAuthSessionOverOpenID {
log.debug("Claimed account already exists: link to it."); log.debug("Claimed account already exists: link to it.");
try { try {
accountManager.link(claimedId.get(), areq); accountManager.link(claimedId.get(), areq);
} catch (OrmException e) { } catch (OrmException | ConfigInvalidException e) {
log.error( log.error(
"Cannot link: " "Cannot link: "
+ user.getExternalId() + user.getExternalId()
@ -186,7 +189,7 @@ class OAuthSessionOverOpenID {
try { try {
log.debug("Linking \"{}\" to \"{}\"", user.getExternalId(), accountId); log.debug("Linking \"{}\" to \"{}\"", user.getExternalId(), accountId);
accountManager.link(accountId, areq); accountManager.link(accountId, areq);
} catch (OrmException e) { } catch (OrmException | ConfigInvalidException e) {
log.error("Cannot link: " + user.getExternalId() + " to user identity: " + accountId); log.error("Cannot link: " + user.getExternalId() + " to user identity: " + accountId);
rsp.sendError(HttpServletResponse.SC_FORBIDDEN); rsp.sendError(HttpServletResponse.SC_FORBIDDEN);
return; return;

View File

@ -26,6 +26,7 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.UrlEncoded; import com.google.gerrit.server.UrlEncoded;
import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern; import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfig;
import com.google.gerrit.server.config.ConfigUtil; import com.google.gerrit.server.config.ConfigUtil;
@ -314,7 +315,7 @@ class OpenIdServiceImpl {
} }
final com.google.gerrit.server.account.AuthRequest areq = final com.google.gerrit.server.account.AuthRequest areq =
new com.google.gerrit.server.account.AuthRequest(openidIdentifier); new com.google.gerrit.server.account.AuthRequest(ExternalId.Key.parse(openidIdentifier));
if (sregRsp != null) { if (sregRsp != null) {
areq.setDisplayName(sregRsp.getAttributeValue("fullname")); areq.setDisplayName(sregRsp.getAttributeValue("fullname"));
@ -369,7 +370,7 @@ class OpenIdServiceImpl {
// link between the two, so set one up if not present. // link between the two, so set one up if not present.
// //
Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier); Optional<Account.Id> claimedId = accountManager.lookup(claimedIdentifier);
Optional<Account.Id> actualId = accountManager.lookup(areq.getExternalId()); Optional<Account.Id> actualId = accountManager.lookup(areq.getExternalIdKey().get());
if (claimedId.isPresent() && actualId.isPresent()) { if (claimedId.isPresent() && actualId.isPresent()) {
if (claimedId.get().equals(actualId.get())) { if (claimedId.get().equals(actualId.get())) {
@ -388,7 +389,7 @@ class OpenIdServiceImpl {
+ " Delgate ID: " + " Delgate ID: "
+ actualId.get() + actualId.get()
+ " is " + " is "
+ areq.getExternalId()); + areq.getExternalIdKey());
cancelWithError(req, rsp, "Contact site administrator"); cancelWithError(req, rsp, "Contact site administrator");
return; return;
} }
@ -398,7 +399,8 @@ class OpenIdServiceImpl {
// was missing due to a bug in Gerrit. Link the claimed. // was missing due to a bug in Gerrit. Link the claimed.
// //
final com.google.gerrit.server.account.AuthRequest linkReq = final com.google.gerrit.server.account.AuthRequest linkReq =
new com.google.gerrit.server.account.AuthRequest(claimedIdentifier); new com.google.gerrit.server.account.AuthRequest(
ExternalId.Key.parse(claimedIdentifier));
linkReq.setDisplayName(areq.getDisplayName()); linkReq.setDisplayName(areq.getDisplayName());
linkReq.setEmailAddress(areq.getEmailAddress()); linkReq.setEmailAddress(areq.getEmailAddress());
accountManager.link(actualId.get(), linkReq); accountManager.link(actualId.get(), linkReq);
@ -434,7 +436,8 @@ class OpenIdServiceImpl {
webSession.get().login(arsp, remember); webSession.get().login(arsp, remember);
if (arsp.isNew() && claimedIdentifier != null) { if (arsp.isNew() && claimedIdentifier != null) {
final com.google.gerrit.server.account.AuthRequest linkReq = final com.google.gerrit.server.account.AuthRequest linkReq =
new com.google.gerrit.server.account.AuthRequest(claimedIdentifier); new com.google.gerrit.server.account.AuthRequest(
ExternalId.Key.parse(claimedIdentifier));
linkReq.setDisplayName(areq.getDisplayName()); linkReq.setDisplayName(areq.getDisplayName());
linkReq.setEmailAddress(areq.getEmailAddress()); linkReq.setEmailAddress(areq.getEmailAddress());
accountManager.link(arsp.getAccountId(), linkReq); accountManager.link(arsp.getAccountId(), linkReq);

View File

@ -14,115 +14,67 @@
package com.google.gerrit.pgm; package com.google.gerrit.pgm;
import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER; import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
import com.google.gerrit.lifecycle.LifecycleManager; import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.pgm.util.SiteProgram; import com.google.gerrit.pgm.util.SiteProgram;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIdsBatchUpdate;
import com.google.gerrit.server.schema.SchemaVersionCheck; import com.google.gerrit.server.schema.SchemaVersionCheck;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory; import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Injector; import com.google.inject.Injector;
import java.util.ArrayList; import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.eclipse.jgit.lib.TextProgressMonitor; import org.eclipse.jgit.lib.TextProgressMonitor;
import org.kohsuke.args4j.Option;
/** Converts the local username for all accounts to lower case */ /** Converts the local username for all accounts to lower case */
public class LocalUsernamesToLowerCase extends SiteProgram { public class LocalUsernamesToLowerCase extends SiteProgram {
@Option(name = "--threads", usage = "Number of concurrent threads to run")
private int threads = 2;
private final LifecycleManager manager = new LifecycleManager(); private final LifecycleManager manager = new LifecycleManager();
private final TextProgressMonitor monitor = new TextProgressMonitor(); private final TextProgressMonitor monitor = new TextProgressMonitor();
private List<AccountExternalId> todo;
private Injector dbInjector;
@Inject private SchemaFactory<ReviewDb> database; @Inject private SchemaFactory<ReviewDb> database;
@Inject private ExternalIdsBatchUpdate externalIdsBatchUpdate;
@Override @Override
public int run() throws Exception { public int run() throws Exception {
if (threads <= 0) { Injector dbInjector = createDbInjector(MULTI_USER);
threads = 1;
}
dbInjector = createDbInjector(MULTI_USER);
manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module())); manager.add(dbInjector, dbInjector.createChildInjector(SchemaVersionCheck.module()));
manager.start(); manager.start();
dbInjector.injectMembers(this); dbInjector.injectMembers(this);
try (ReviewDb db = database.open()) { try (ReviewDb db = database.open()) {
todo = db.accountExternalIds().all().toList(); Collection<ExternalId> todo = ExternalId.from(db.accountExternalIds().all().toList());
synchronized (monitor) { monitor.beginTask("Converting local usernames", todo.size());
monitor.beginTask("Converting local usernames", todo.size());
}
}
final List<Worker> workers = new ArrayList<>(threads); for (ExternalId extId : todo) {
for (int tid = 0; tid < threads; tid++) { convertLocalUserToLowerCase(extId);
Worker t = new Worker(); monitor.update(1);
t.start(); }
workers.add(t);
} externalIdsBatchUpdate.commit(db, "Convert local usernames to lower case");
for (Worker t : workers) {
t.join();
}
synchronized (monitor) {
monitor.endTask();
} }
monitor.endTask();
manager.stop(); manager.stop();
return 0; return 0;
} }
private void convertLocalUserToLowerCase(final ReviewDb db, final AccountExternalId extId) { private void convertLocalUserToLowerCase(ExternalId extId) {
if (extId.isScheme(AccountExternalId.SCHEME_GERRIT)) { if (extId.isScheme(SCHEME_GERRIT)) {
final String localUser = extId.getSchemeRest(); String localUser = extId.key().id();
final String localUserLowerCase = localUser.toLowerCase(Locale.US); String localUserLowerCase = localUser.toLowerCase(Locale.US);
if (!localUser.equals(localUserLowerCase)) { if (!localUser.equals(localUserLowerCase)) {
final AccountExternalId.Key extIdKeyLowerCase = ExternalId extIdLowerCase =
new AccountExternalId.Key(AccountExternalId.SCHEME_GERRIT, localUserLowerCase); ExternalId.create(
final AccountExternalId extIdLowerCase = SCHEME_GERRIT,
new AccountExternalId(extId.getAccountId(), extIdKeyLowerCase); localUserLowerCase,
try { extId.accountId(),
db.accountExternalIds().insert(Collections.singleton(extIdLowerCase)); extId.email(),
db.accountExternalIds().delete(Collections.singleton(extId)); extId.password());
} catch (OrmException error) { externalIdsBatchUpdate.replace(extId, extIdLowerCase);
System.err.println("ERR " + error.getMessage());
}
}
}
}
private AccountExternalId next() {
synchronized (todo) {
if (todo.isEmpty()) {
return null;
}
return todo.remove(todo.size() - 1);
}
}
private class Worker extends Thread {
@Override
public void run() {
try (ReviewDb db = database.open()) {
for (; ; ) {
final AccountExternalId extId = next();
if (extId == null) {
break;
}
convertLocalUserToLowerCase(db, extId);
synchronized (monitor) {
monitor.update(1);
}
}
} catch (OrmException e) {
e.printStackTrace();
} }
} }
} }

View File

@ -0,0 +1,85 @@
// Copyright (C) 2016 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.pgm.init;
import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds;
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdentProvider;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.ExternalIds;
import com.google.gerrit.server.account.ExternalIdsUpdate;
import com.google.gerrit.server.config.SitePaths;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FS;
public class ExternalIdsOnInit {
private final InitFlags flags;
private final SitePaths site;
private final String allUsers;
@Inject
public ExternalIdsOnInit(InitFlags flags, SitePaths site, AllUsersNameOnInitProvider allUsers) {
this.flags = flags;
this.site = site;
this.allUsers = allUsers.get();
}
public synchronized void insert(ReviewDb db, String commitMessage, Collection<ExternalId> extIds)
throws OrmException, IOException, ConfigInvalidException {
db.accountExternalIds().insert(toAccountExternalIds(extIds));
File path = getPath();
if (path != null) {
try (Repository repo = new FileRepository(path);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId rev = ExternalIds.readRevision(repo);
NoteMap noteMap = ExternalIds.readNoteMap(rw, rev);
for (ExternalId extId : extIds) {
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
}
PersonIdent serverIdent = new GerritPersonIdentProvider(flags.cfg).get();
ExternalIdsUpdate.commit(
repo, rw, ins, rev, noteMap, commitMessage, serverIdent, serverIdent);
}
}
}
private File getPath() {
Path basePath = site.resolve(flags.cfg.getString("gerrit", null, "basePath"));
if (basePath == null) {
throw new IllegalStateException("gerrit.basePath must be configured");
}
return FileKey.resolve(basePath.resolve(allUsers).toFile(), FS.DETECTED);
}
}

View File

@ -23,14 +23,13 @@ import com.google.gerrit.pgm.init.api.ConsoleUI;
import com.google.gerrit.pgm.init.api.InitFlags; import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.pgm.init.api.InitStep; import com.google.gerrit.pgm.init.api.InitStep;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountGroupName; import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.client.AccountSshKey; import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.HashedPassword; import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.index.account.AccountIndex; import com.google.gerrit.server.index.account.AccountIndex;
import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gwtorm.server.SchemaFactory; import com.google.gwtorm.server.SchemaFactory;
@ -49,15 +48,20 @@ public class InitAdminUser implements InitStep {
private final ConsoleUI ui; private final ConsoleUI ui;
private final InitFlags flags; private final InitFlags flags;
private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory; private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
private final ExternalIdsOnInit externalIds;
private SchemaFactory<ReviewDb> dbFactory; private SchemaFactory<ReviewDb> dbFactory;
private AccountIndexCollection indexCollection; private AccountIndexCollection indexCollection;
@Inject @Inject
InitAdminUser( InitAdminUser(
InitFlags flags, ConsoleUI ui, VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory) { InitFlags flags,
ConsoleUI ui,
VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory,
ExternalIdsOnInit externalIds) {
this.flags = flags; this.flags = flags;
this.ui = ui; this.ui = ui;
this.authorizedKeysFactory = authorizedKeysFactory; this.authorizedKeysFactory = authorizedKeysFactory;
this.externalIds = externalIds;
} }
@Override @Override
@ -91,24 +95,13 @@ public class InitAdminUser implements InitStep {
AccountSshKey sshKey = readSshKey(id); AccountSshKey sshKey = readSshKey(id);
String email = readEmail(sshKey); String email = readEmail(sshKey);
List<AccountExternalId> extIds = new ArrayList<>(2); List<ExternalId> extIds = new ArrayList<>(2);
AccountExternalId extUser = extIds.add(ExternalId.createUsername(username, id, httpPassword));
new AccountExternalId(
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
if (!Strings.isNullOrEmpty(httpPassword)) {
extUser.setPassword(HashedPassword.fromPassword(httpPassword).encode());
}
extIds.add(extUser);
db.accountExternalIds().insert(Collections.singleton(extUser));
if (email != null) { if (email != null) {
AccountExternalId extMailto = extIds.add(ExternalId.createEmail(id, email));
new AccountExternalId(
id, new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email));
extMailto.setEmailAddress(email);
extIds.add(extMailto);
db.accountExternalIds().insert(Collections.singleton(extMailto));
} }
externalIds.insert(db, "Add external IDs for initial admin user", extIds);
Account a = new Account(id, TimeUtil.nowTs()); Account a = new Account(id, TimeUtil.nowTs());
a.setFullName(name); a.setFullName(name);
@ -124,7 +117,7 @@ public class InitAdminUser implements InitStep {
if (sshKey != null) { if (sshKey != null) {
VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load(); VersionedAuthorizedKeysOnInit authorizedKeys = authorizedKeysFactory.create(id).load();
authorizedKeys.addKey(sshKey.getSshPublicKey()); authorizedKeys.addKey(sshKey.getSshPublicKey());
authorizedKeys.save("Added SSH key for initial admin user\n"); authorizedKeys.save("Add SSH key for initial admin user\n");
} }
AccountGroup adminGroup = db.accountGroups().get(adminGroupName.getId()); AccountGroup adminGroup = db.accountGroups().get(adminGroupName.getId());

View File

@ -25,16 +25,16 @@ import java.sql.Timestamp;
/** /**
* Information about a single user. * Information about a single user.
* *
* <p>A user may have multiple identities they can use to login to Gerrit (see {@link * <p>A user may have multiple identities they can use to login to Gerrit (see ExternalId), but in
* AccountExternalId}), but in such cases they always map back to a single Account entity. * such cases they always map back to a single Account entity.
* *
* <p>Entities "owned" by an Account (that is, their primary key contains the {@link Account.Id} key * <p>Entities "owned" by an Account (that is, their primary key contains the {@link Account.Id} key
* as part of their key structure): * as part of their key structure):
* *
* <ul> * <ul>
* <li>{@link AccountExternalId}: OpenID identities and email addresses known to be registered to * <li>ExternalId: OpenID identities and email addresses known to be registered to this user.
* this user. Multiple records can exist when the user has more than one public identity, such * Multiple records can exist when the user has more than one public identity, such as a work
* as a work and a personal email address. * and a personal email address.
* <li>{@link AccountGroupMember}: membership of the user in a specific human managed {@link * <li>{@link AccountGroupMember}: membership of the user in a specific human managed {@link
* AccountGroup}. Multiple records can exist when the user is a member of more than one group. * AccountGroup}. Multiple records can exist when the user is a member of more than one group.
* <li>{@link AccountSshKey}: user's public SSH keys, for authentication through the internal SSH * <li>{@link AccountSshKey}: user's public SSH keys, for authentication through the internal SSH

View File

@ -17,6 +17,7 @@ package com.google.gerrit.reviewdb.client;
import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.client.AuthType;
import com.google.gwtorm.client.Column; import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.StringKey; import com.google.gwtorm.client.StringKey;
import java.util.Objects;
/** Association of an external account identifier to a local {@link Account}. */ /** Association of an external account identifier to a local {@link Account}. */
public final class AccountExternalId { public final class AccountExternalId {
@ -165,4 +166,21 @@ public final class AccountExternalId {
public void setCanDelete(final boolean t) { public void setCanDelete(final boolean t) {
canDelete = t; canDelete = t;
} }
@Override
public boolean equals(Object o) {
if (o instanceof AccountExternalId) {
AccountExternalId extId = (AccountExternalId) o;
return Objects.equals(key, extId.key)
&& Objects.equals(accountId, extId.accountId)
&& Objects.equals(emailAddress, extId.emailAddress)
&& Objects.equals(password, extId.password);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(key, accountId, emailAddress, password);
}
} }

View File

@ -34,6 +34,9 @@ public class RefNames {
/** Configuration settings for a project {@code refs/meta/config} */ /** Configuration settings for a project {@code refs/meta/config} */
public static final String REFS_CONFIG = "refs/meta/config"; public static final String REFS_CONFIG = "refs/meta/config";
/** Note tree listing external IDs */
public static final String REFS_EXTERNAL_IDS = "refs/meta/external-ids";
/** Preference settings for a user {@code refs/users} */ /** Preference settings for a user {@code refs/users} */
public static final String REFS_USERS = "refs/users/"; public static final String REFS_USERS = "refs/users/";

View File

@ -16,8 +16,8 @@ package com.google.gerrit.server;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.account.CapabilityControl; import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.GroupMembership; import com.google.gerrit.server.account.GroupMembership;
import com.google.inject.servlet.RequestScoped; import com.google.inject.servlet.RequestScoped;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -44,7 +44,7 @@ public abstract class CurrentUser {
private AccessPath accessPath = AccessPath.UNKNOWN; private AccessPath accessPath = AccessPath.UNKNOWN;
private CapabilityControl capabilities; private CapabilityControl capabilities;
private PropertyKey<AccountExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create(); private PropertyKey<ExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create();
protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) { protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) {
this.capabilityControlFactory = capabilityControlFactory; this.capabilityControlFactory = capabilityControlFactory;
@ -151,11 +151,11 @@ public abstract class CurrentUser {
*/ */
public <T> void put(PropertyKey<T> key, @Nullable T value) {} public <T> void put(PropertyKey<T> key, @Nullable T value) {}
public void setLastLoginExternalIdKey(AccountExternalId.Key externalIdKey) { public void setLastLoginExternalIdKey(ExternalId.Key externalIdKey) {
put(lastLoginExternalIdPropertyKey, externalIdKey); put(lastLoginExternalIdPropertyKey, externalIdKey);
} }
public AccountExternalId.Key getLastLoginExternalIdKey() { public ExternalId.Key getLastLoginExternalIdKey() {
return get(lastLoginExternalIdPropertyKey); return get(lastLoginExternalIdPropertyKey);
} }
} }

View File

@ -17,7 +17,6 @@ package com.google.gerrit.server.account;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.extensions.client.AccountFieldName;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.mail.send.EmailSender; import com.google.gerrit.server.mail.send.EmailSender;
import com.google.inject.Inject; import com.google.inject.Inject;
@ -53,8 +52,8 @@ public abstract class AbstractRealm implements Realm {
@Override @Override
public boolean hasEmailAddress(IdentifiedUser user, String email) { public boolean hasEmailAddress(IdentifiedUser user, String email) {
for (AccountExternalId ext : user.state().getExternalIds()) { for (ExternalId ext : user.state().getExternalIds()) {
if (email != null && email.equalsIgnoreCase(ext.getEmailAddress())) { if (email != null && email.equalsIgnoreCase(ext.email())) {
return true; return true;
} }
} }
@ -63,11 +62,11 @@ public abstract class AbstractRealm implements Realm {
@Override @Override
public Set<String> getEmailAddresses(IdentifiedUser user) { public Set<String> getEmailAddresses(IdentifiedUser user) {
Collection<AccountExternalId> ids = user.state().getExternalIds(); Collection<ExternalId> ids = user.state().getExternalIds();
Set<String> emails = Sets.newHashSetWithExpectedSize(ids.size()); Set<String> emails = Sets.newHashSetWithExpectedSize(ids.size());
for (AccountExternalId ext : ids) { for (ExternalId ext : ids) {
if (!Strings.isNullOrEmpty(ext.getEmailAddress())) { if (!Strings.isNullOrEmpty(ext.email())) {
emails.add(ext.getEmailAddress()); emails.add(ext.email());
} }
} }
return emails; return emails;

View File

@ -97,7 +97,7 @@ public class AccountByEmailCacheImpl implements AccountByEmailCache {
if (accountState if (accountState
.getExternalIds() .getExternalIds()
.stream() .stream()
.filter(e -> email.equals(e.getEmailAddress())) .filter(e -> email.equals(e.email()))
.findAny() .findAny()
.isPresent()) { .isPresent()) {
r.add(accountState.getAccount().getId()); r.add(accountState.getAccount().getId());

View File

@ -14,13 +14,14 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import com.google.common.cache.CacheLoader; import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache; import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo; import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
@ -38,7 +39,6 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral; import com.google.inject.TypeLiteral;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -138,9 +138,9 @@ public class AccountCacheImpl implements AccountCache {
private static AccountState missing(Account.Id accountId) { private static AccountState missing(Account.Id accountId) {
Account account = new Account(accountId, TimeUtil.nowTs()); Account account = new Account(accountId, TimeUtil.nowTs());
account.setActive(false); account.setActive(false);
Collection<AccountExternalId> ids = Collections.emptySet();
Set<AccountGroup.UUID> anon = ImmutableSet.of(); Set<AccountGroup.UUID> anon = ImmutableSet.of();
return new AccountState(account, anon, ids, new HashMap<ProjectWatchKey, Set<NotifyType>>()); return new AccountState(
account, anon, Collections.emptySet(), new HashMap<ProjectWatchKey, Set<NotifyType>>());
} }
static class ByIdLoader extends CacheLoader<Account.Id, AccountState> { static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
@ -184,8 +184,8 @@ public class AccountCacheImpl implements AccountCache {
return missing(who); return missing(who);
} }
Collection<AccountExternalId> externalIds = Set<ExternalId> externalIds =
Collections.unmodifiableCollection(db.accountExternalIds().byAccount(who).toList()); ExternalId.from(db.accountExternalIds().byAccount(who).toList());
Set<AccountGroup.UUID> internalGroups = new HashSet<>(); Set<AccountGroup.UUID> internalGroups = new HashSet<>();
for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) { for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
@ -219,11 +219,8 @@ public class AccountCacheImpl implements AccountCache {
@Override @Override
public Optional<Account.Id> load(String username) throws Exception { public Optional<Account.Id> load(String username) throws Exception {
AccountExternalId.Key key = AccountState accountState =
new AccountExternalId.Key( // accountQueryProvider.get().oneByExternalId(SCHEME_USERNAME, username);
AccountExternalId.SCHEME_USERNAME, //
username);
AccountState accountState = accountQueryProvider.get().oneByExternalId(key.get());
return Optional.ofNullable(accountState).map(s -> s.getAccount().getId()); return Optional.ofNullable(accountState).map(s -> s.getAccount().getId());
} }
} }

View File

@ -14,6 +14,8 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static java.util.stream.Collectors.toSet;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.gerrit.audit.AuditService; import com.google.gerrit.audit.AuditService;
import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.TimeUtil;
@ -23,7 +25,6 @@ import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.extensions.client.AccountFieldName;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
@ -31,17 +32,16 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.account.InternalAccountQuery; import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.gwtorm.server.SchemaFactory; import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -60,6 +60,7 @@ public class AccountManager {
private final AtomicBoolean awaitsFirstAccountCheck; private final AtomicBoolean awaitsFirstAccountCheck;
private final AuditService auditService; private final AuditService auditService;
private final Provider<InternalAccountQuery> accountQueryProvider; private final Provider<InternalAccountQuery> accountQueryProvider;
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
@Inject @Inject
AccountManager( AccountManager(
@ -71,7 +72,8 @@ public class AccountManager {
ChangeUserName.Factory changeUserNameFactory, ChangeUserName.Factory changeUserNameFactory,
ProjectCache projectCache, ProjectCache projectCache,
AuditService auditService, AuditService auditService,
Provider<InternalAccountQuery> accountQueryProvider) { Provider<InternalAccountQuery> accountQueryProvider,
ExternalIdsUpdate.Server externalIdsUpdateFactory) {
this.schema = schema; this.schema = schema;
this.byIdCache = byIdCache; this.byIdCache = byIdCache;
this.byEmailCache = byEmailCache; this.byEmailCache = byEmailCache;
@ -82,6 +84,7 @@ public class AccountManager {
this.awaitsFirstAccountCheck = new AtomicBoolean(true); this.awaitsFirstAccountCheck = new AtomicBoolean(true);
this.auditService = auditService; this.auditService = auditService;
this.accountQueryProvider = accountQueryProvider; this.accountQueryProvider = accountQueryProvider;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
} }
/** @return user identified by this external identity string */ /** @return user identified by this external identity string */
@ -108,8 +111,7 @@ public class AccountManager {
who = realm.authenticate(who); who = realm.authenticate(who);
try { try {
try (ReviewDb db = schema.open()) { try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = id(who); ExternalId id = findExternalId(who.getExternalIdKey());
AccountExternalId id = getAccountExternalId(key);
if (id == null) { if (id == null) {
// New account, automatically create and return. // New account, automatically create and return.
// //
@ -117,25 +119,25 @@ public class AccountManager {
} }
// Account exists // Account exists
Account act = byIdCache.get(id.getAccountId()).getAccount(); Account act = byIdCache.get(id.accountId()).getAccount();
if (!act.isActive()) { if (!act.isActive()) {
throw new AccountException("Authentication error, account inactive"); throw new AccountException("Authentication error, account inactive");
} }
// return the identity to the caller. // return the identity to the caller.
update(db, who, id); update(db, who, id);
return new AuthResult(id.getAccountId(), key, false); return new AuthResult(id.accountId(), who.getExternalIdKey(), false);
} }
} catch (OrmException e) { } catch (OrmException | ConfigInvalidException e) {
throw new AccountException("Authentication error", e); throw new AccountException("Authentication error", e);
} }
} }
private AccountExternalId getAccountExternalId(AccountExternalId.Key key) throws OrmException { private ExternalId findExternalId(ExternalId.Key key) throws OrmException {
AccountState accountState = accountQueryProvider.get().oneByExternalId(key.get()); AccountState accountState = accountQueryProvider.get().oneByExternalId(key);
if (accountState != null) { if (accountState != null) {
for (AccountExternalId extId : accountState.getExternalIds()) { for (ExternalId extId : accountState.getExternalIds()) {
if (extId.getKey().equals(key)) { if (extId.key().equals(key)) {
return extId; return extId;
} }
} }
@ -143,24 +145,28 @@ public class AccountManager {
return null; return null;
} }
private void update(ReviewDb db, AuthRequest who, AccountExternalId extId) private void update(ReviewDb db, AuthRequest who, ExternalId extId)
throws OrmException, IOException { throws OrmException, IOException, ConfigInvalidException {
IdentifiedUser user = userFactory.create(extId.getAccountId()); IdentifiedUser user = userFactory.create(extId.accountId());
Account toUpdate = null; Account toUpdate = null;
// If the email address was modified by the authentication provider, // If the email address was modified by the authentication provider,
// update our records to match the changed email. // update our records to match the changed email.
// //
String newEmail = who.getEmailAddress(); String newEmail = who.getEmailAddress();
String oldEmail = extId.getEmailAddress(); String oldEmail = extId.email();
if (newEmail != null && !newEmail.equals(oldEmail)) { if (newEmail != null && !newEmail.equals(oldEmail)) {
if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) { if (oldEmail != null && oldEmail.equals(user.getAccount().getPreferredEmail())) {
toUpdate = load(toUpdate, user.getAccountId(), db); toUpdate = load(toUpdate, user.getAccountId(), db);
toUpdate.setPreferredEmail(newEmail); toUpdate.setPreferredEmail(newEmail);
} }
extId.setEmailAddress(newEmail); externalIdsUpdateFactory
db.accountExternalIds().update(Collections.singleton(extId)); .create()
.replace(
db,
extId,
ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password()));
} }
if (!realm.allowsEdit(AccountFieldName.FULL_NAME) if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
@ -206,14 +212,14 @@ public class AccountManager {
} }
private AuthResult create(ReviewDb db, AuthRequest who) private AuthResult create(ReviewDb db, AuthRequest who)
throws OrmException, AccountException, IOException { throws OrmException, AccountException, IOException, ConfigInvalidException {
Account.Id newId = new Account.Id(db.nextAccountId()); Account.Id newId = new Account.Id(db.nextAccountId());
Account account = new Account(newId, TimeUtil.nowTs()); Account account = new Account(newId, TimeUtil.nowTs());
AccountExternalId extId = createId(newId, who);
extId.setEmailAddress(who.getEmailAddress()); ExternalId extId =
ExternalId.createWithEmail(who.getExternalIdKey(), newId, who.getEmailAddress());
account.setFullName(who.getDisplayName()); account.setFullName(who.getDisplayName());
account.setPreferredEmail(extId.getEmailAddress()); account.setPreferredEmail(extId.email());
boolean isFirstAccount = boolean isFirstAccount =
awaitsFirstAccountCheck.getAndSet(false) && db.accounts().anyAccounts().toList().isEmpty(); awaitsFirstAccountCheck.getAndSet(false) && db.accounts().anyAccounts().toList().isEmpty();
@ -221,18 +227,19 @@ public class AccountManager {
try { try {
db.accounts().upsert(Collections.singleton(account)); db.accounts().upsert(Collections.singleton(account));
AccountExternalId existingExtId = db.accountExternalIds().get(extId.getKey()); ExternalId existingExtId =
if (existingExtId != null && !existingExtId.getAccountId().equals(extId.getAccountId())) { ExternalId.from(db.accountExternalIds().get(extId.key().asAccountExternalIdKey()));
if (existingExtId != null && !existingExtId.accountId().equals(extId.accountId())) {
// external ID is assigned to another account, do not overwrite // external ID is assigned to another account, do not overwrite
db.accounts().delete(Collections.singleton(account)); db.accounts().delete(Collections.singleton(account));
throw new AccountException( throw new AccountException(
"Cannot assign external ID \"" "Cannot assign external ID \""
+ extId.getExternalId() + extId.key().get()
+ "\" to account " + "\" to account "
+ newId + newId
+ "; external ID already in use."); + "; external ID already in use.");
} }
db.accountExternalIds().upsert(Collections.singleton(extId)); externalIdsUpdateFactory.create().upsert(db, extId);
} finally { } finally {
// If adding the account failed, it may be that it actually was the // If adding the account failed, it may be that it actually was the
// first account. So we reset the 'check for first account'-guard, as // first account. So we reset the 'check for first account'-guard, as
@ -291,7 +298,7 @@ public class AccountManager {
byEmailCache.evict(account.getPreferredEmail()); byEmailCache.evict(account.getPreferredEmail());
byIdCache.evict(account.getId()); byIdCache.evict(account.getId());
realm.onCreateAccount(who, account); realm.onCreateAccount(who, account);
return new AuthResult(newId, extId.getKey(), true); return new AuthResult(newId, extId.key(), true);
} }
/** /**
@ -313,11 +320,11 @@ public class AccountManager {
private void handleSettingUserNameFailure( private void handleSettingUserNameFailure(
ReviewDb db, ReviewDb db,
Account account, Account account,
AccountExternalId extId, ExternalId extId,
String errorMessage, String errorMessage,
Exception e, Exception e,
boolean logException) boolean logException)
throws AccountUserNameException, OrmException { throws AccountUserNameException, OrmException, IOException, ConfigInvalidException {
if (logException) { if (logException) {
log.error(errorMessage, e); log.error(errorMessage, e);
} else { } else {
@ -333,16 +340,11 @@ public class AccountManager {
// this is why the best we can do here is to fail early and cleanup // this is why the best we can do here is to fail early and cleanup
// the database // the database
db.accounts().delete(Collections.singleton(account)); db.accounts().delete(Collections.singleton(account));
db.accountExternalIds().delete(Collections.singleton(extId)); externalIdsUpdateFactory.create().delete(db, extId);
throw new AccountUserNameException(errorMessage, e); throw new AccountUserNameException(errorMessage, e);
} }
} }
private static AccountExternalId createId(Account.Id newId, AuthRequest who) {
String ext = who.getExternalId();
return new AccountExternalId(newId, new AccountExternalId.Key(ext));
}
/** /**
* Link another authentication identity to an existing account. * Link another authentication identity to an existing account.
* *
@ -353,19 +355,19 @@ public class AccountManager {
* this time. * this time.
*/ */
public AuthResult link(Account.Id to, AuthRequest who) public AuthResult link(Account.Id to, AuthRequest who)
throws AccountException, OrmException, IOException { throws AccountException, OrmException, IOException, ConfigInvalidException {
try (ReviewDb db = schema.open()) { try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = id(who); ExternalId extId = findExternalId(who.getExternalIdKey());
AccountExternalId extId = getAccountExternalId(key);
if (extId != null) { if (extId != null) {
if (!extId.getAccountId().equals(to)) { if (!extId.accountId().equals(to)) {
throw new AccountException("Identity in use by another account"); throw new AccountException("Identity in use by another account");
} }
update(db, who, extId); update(db, who, extId);
} else { } else {
extId = createId(to, who); externalIdsUpdateFactory
extId.setEmailAddress(who.getEmailAddress()); .create()
db.accountExternalIds().insert(Collections.singleton(extId)); .insert(
db, ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
if (who.getEmailAddress() != null) { if (who.getEmailAddress() != null) {
Account a = db.accounts().get(to); Account a = db.accounts().get(to);
@ -381,7 +383,7 @@ public class AccountManager {
byIdCache.evict(to); byIdCache.evict(to);
} }
return new AuthResult(to, key, false); return new AuthResult(to, who.getExternalIdKey(), false);
} }
} }
@ -399,31 +401,28 @@ public class AccountManager {
* this time. * this time.
*/ */
public AuthResult updateLink(Account.Id to, AuthRequest who) public AuthResult updateLink(Account.Id to, AuthRequest who)
throws OrmException, AccountException, IOException { throws OrmException, AccountException, IOException, ConfigInvalidException {
try (ReviewDb db = schema.open()) { try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = id(who); Collection<ExternalId> filteredExtIdsByScheme =
List<AccountExternalId.Key> filteredKeysByScheme = ExternalId.from(db.accountExternalIds().byAccount(to).toList())
filterKeysByScheme(key.getScheme(), db.accountExternalIds().byAccount(to)); .stream()
if (!filteredKeysByScheme.isEmpty() .filter(e -> e.isScheme(who.getExternalIdKey().scheme()))
&& (filteredKeysByScheme.size() > 1 || !filteredKeysByScheme.contains(key))) { .collect(toSet());
db.accountExternalIds().deleteKeys(filteredKeysByScheme);
if (!filteredExtIdsByScheme.isEmpty()
&& (filteredExtIdsByScheme.size() > 1
|| !filteredExtIdsByScheme
.stream()
.filter(e -> e.key().equals(who.getExternalIdKey()))
.findAny()
.isPresent())) {
externalIdsUpdateFactory.create().delete(db, filteredExtIdsByScheme);
} }
byIdCache.evict(to); byIdCache.evict(to);
return link(to, who); return link(to, who);
} }
} }
private List<AccountExternalId.Key> filterKeysByScheme(
String keyScheme, ResultSet<AccountExternalId> externalIds) {
List<AccountExternalId.Key> filteredExternalIds = new ArrayList<>();
for (AccountExternalId accountExternalId : externalIds) {
if (accountExternalId.isScheme(keyScheme)) {
filteredExternalIds.add(accountExternalId.getKey());
}
}
return filteredExternalIds;
}
/** /**
* Unlink an authentication identity from an existing account. * Unlink an authentication identity from an existing account.
* *
@ -434,15 +433,15 @@ public class AccountManager {
* at this time. * at this time.
*/ */
public AuthResult unlink(Account.Id from, AuthRequest who) public AuthResult unlink(Account.Id from, AuthRequest who)
throws AccountException, OrmException, IOException { throws AccountException, OrmException, IOException, ConfigInvalidException {
try (ReviewDb db = schema.open()) { try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = id(who); ExternalId extId = findExternalId(who.getExternalIdKey());
AccountExternalId extId = getAccountExternalId(key);
if (extId != null) { if (extId != null) {
if (!extId.getAccountId().equals(from)) { if (!extId.accountId().equals(from)) {
throw new AccountException("Identity '" + key.get() + "' in use by another account"); throw new AccountException(
"Identity '" + who.getExternalIdKey().get() + "' in use by another account");
} }
db.accountExternalIds().delete(Collections.singleton(extId)); externalIdsUpdateFactory.create().delete(db, extId);
if (who.getEmailAddress() != null) { if (who.getEmailAddress() != null) {
Account a = db.accounts().get(from); Account a = db.accounts().get(from);
@ -456,14 +455,10 @@ public class AccountManager {
} }
} else { } else {
throw new AccountException("Identity '" + key.get() + "' not found"); throw new AccountException("Identity '" + who.getExternalIdKey().get() + "' not found");
} }
return new AuthResult(from, key, false); return new AuthResult(from, who.getExternalIdKey(), false);
} }
} }
private static AccountExternalId.Key id(AuthRequest who) {
return new AccountExternalId.Key(who.getExternalId());
}
} }

View File

@ -14,8 +14,8 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO; import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Strings; import com.google.common.base.Strings;
@ -23,7 +23,6 @@ import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheBuilder;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser.PropertyKey; import com.google.gerrit.server.CurrentUser.PropertyKey;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
@ -45,14 +44,14 @@ public class AccountState {
private final Account account; private final Account account;
private final Set<AccountGroup.UUID> internalGroups; private final Set<AccountGroup.UUID> internalGroups;
private final Collection<AccountExternalId> externalIds; private final Collection<ExternalId> externalIds;
private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches; private final Map<ProjectWatchKey, Set<NotifyType>> projectWatches;
private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties; private Cache<IdentifiedUser.PropertyKey<Object>, Object> properties;
public AccountState( public AccountState(
Account account, Account account,
Set<AccountGroup.UUID> actualGroups, Set<AccountGroup.UUID> actualGroups,
Collection<AccountExternalId> externalIds, Collection<ExternalId> externalIds,
Map<ProjectWatchKey, Set<NotifyType>> projectWatches) { Map<ProjectWatchKey, Set<NotifyType>> projectWatches) {
this.account = account; this.account = account;
this.internalGroups = actualGroups; this.internalGroups = actualGroups;
@ -69,8 +68,7 @@ public class AccountState {
/** /**
* Get the username, if one has been declared for this user. * Get the username, if one has been declared for this user.
* *
* <p>The username is the {@link AccountExternalId} using the scheme {@link * <p>The username is the {@link ExternalId} using the scheme {@link ExternalId#SCHEME_USERNAME}.
* AccountExternalId#SCHEME_USERNAME}.
*/ */
public String getUserName() { public String getUserName() {
return account.getUserName(); return account.getUserName();
@ -80,13 +78,13 @@ public class AccountState {
if (password == null) { if (password == null) {
return false; return false;
} }
for (AccountExternalId id : getExternalIds()) { for (ExternalId id : getExternalIds()) {
// Only process the "username:$USER" entry, which is unique. // Only process the "username:$USER" entry, which is unique.
if (!id.isScheme(AccountExternalId.SCHEME_USERNAME) || !username.equals(id.getSchemeRest())) { if (!id.isScheme(SCHEME_USERNAME) || !username.equals(id.key().id())) {
continue; continue;
} }
String hashedStr = id.getPassword(); String hashedStr = id.password();
if (!Strings.isNullOrEmpty(hashedStr)) { if (!Strings.isNullOrEmpty(hashedStr)) {
try { try {
return HashedPassword.decode(hashedStr).checkPassword(password); return HashedPassword.decode(hashedStr).checkPassword(password);
@ -101,7 +99,7 @@ public class AccountState {
} }
/** The external identities that identify the account holder. */ /** The external identities that identify the account holder. */
public Collection<AccountExternalId> getExternalIds() { public Collection<ExternalId> getExternalIds() {
return externalIds; return externalIds;
} }
@ -115,20 +113,20 @@ public class AccountState {
return internalGroups; return internalGroups;
} }
public static String getUserName(Collection<AccountExternalId> ids) { public static String getUserName(Collection<ExternalId> ids) {
for (AccountExternalId id : ids) { for (ExternalId extId : ids) {
if (id.isScheme(SCHEME_USERNAME)) { if (extId.isScheme(SCHEME_USERNAME)) {
return id.getSchemeRest(); return extId.key().id();
} }
} }
return null; return null;
} }
public static Set<String> getEmails(Collection<AccountExternalId> ids) { public static Set<String> getEmails(Collection<ExternalId> ids) {
Set<String> emails = new HashSet<>(); Set<String> emails = new HashSet<>();
for (AccountExternalId id : ids) { for (ExternalId extId : ids) {
if (id.isScheme(SCHEME_MAILTO)) { if (extId.isScheme(SCHEME_MAILTO)) {
emails.add(id.getSchemeRest()); emails.add(extId.key().id());
} }
} }
return emails; return emails;

View File

@ -14,11 +14,9 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_EXTERNAL; import static com.google.gerrit.server.account.ExternalId.SCHEME_EXTERNAL;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT; import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO; import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
import com.google.gerrit.reviewdb.client.AccountExternalId;
/** /**
* Information for {@link AccountManager#authenticate(AuthRequest)}. * Information for {@link AccountManager#authenticate(AuthRequest)}.
@ -30,17 +28,15 @@ import com.google.gerrit.reviewdb.client.AccountExternalId;
*/ */
public class AuthRequest { public class AuthRequest {
/** Create a request for a local username, such as from LDAP. */ /** Create a request for a local username, such as from LDAP. */
public static AuthRequest forUser(final String username) { public static AuthRequest forUser(String username) {
final AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_GERRIT, username); AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_GERRIT, username));
final AuthRequest r = new AuthRequest(i.get());
r.setUserName(username); r.setUserName(username);
return r; return r;
} }
/** Create a request for an external username. */ /** Create a request for an external username. */
public static AuthRequest forExternalUser(String username) { public static AuthRequest forExternalUser(String username) {
AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_EXTERNAL, username); AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_EXTERNAL, username));
AuthRequest r = new AuthRequest(i.get());
r.setUserName(username); r.setUserName(username);
return r; return r;
} }
@ -51,14 +47,13 @@ public class AuthRequest {
* <p>This type of request should be used only to attach a new email address to an existing user * <p>This type of request should be used only to attach a new email address to an existing user
* account. * account.
*/ */
public static AuthRequest forEmail(final String email) { public static AuthRequest forEmail(String email) {
final AccountExternalId.Key i = new AccountExternalId.Key(SCHEME_MAILTO, email); AuthRequest r = new AuthRequest(ExternalId.Key.create(SCHEME_MAILTO, email));
final AuthRequest r = new AuthRequest(i.get());
r.setEmailAddress(email); r.setEmailAddress(email);
return r; return r;
} }
private String externalId; private ExternalId.Key externalId;
private String password; private String password;
private String displayName; private String displayName;
private String emailAddress; private String emailAddress;
@ -67,29 +62,24 @@ public class AuthRequest {
private String authPlugin; private String authPlugin;
private String authProvider; private String authProvider;
public AuthRequest(final String externalId) { public AuthRequest(ExternalId.Key externalId) {
this.externalId = externalId; this.externalId = externalId;
} }
public String getExternalId() { public ExternalId.Key getExternalIdKey() {
return externalId; return externalId;
} }
public boolean isScheme(final String scheme) {
return getExternalId().startsWith(scheme);
}
public String getLocalUser() { public String getLocalUser() {
if (isScheme(SCHEME_GERRIT)) { if (externalId.isScheme(SCHEME_GERRIT)) {
return getExternalId().substring(SCHEME_GERRIT.length()); return externalId.id();
} }
return null; return null;
} }
public void setLocalUser(final String localUser) { public void setLocalUser(String localUser) {
if (isScheme(SCHEME_GERRIT)) { if (externalId.isScheme(SCHEME_GERRIT)) {
final AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_GERRIT, localUser); externalId = ExternalId.Key.create(SCHEME_GERRIT, localUser);
externalId = key.get();
} }
} }

View File

@ -15,16 +15,14 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
/** Result from {@link AccountManager#authenticate(AuthRequest)}. */ /** Result from {@link AccountManager#authenticate(AuthRequest)}. */
public class AuthResult { public class AuthResult {
private final Account.Id accountId; private final Account.Id accountId;
private final AccountExternalId.Key externalId; private final ExternalId.Key externalId;
private final boolean isNew; private final boolean isNew;
public AuthResult( public AuthResult(Account.Id accountId, ExternalId.Key externalId, boolean isNew) {
final Account.Id accountId, final AccountExternalId.Key externalId, final boolean isNew) {
this.accountId = accountId; this.accountId = accountId;
this.externalId = externalId; this.externalId = externalId;
this.isNew = isNew; this.isNew = isNew;
@ -36,7 +34,7 @@ public class AuthResult {
} }
/** External identity used to authenticate the user. */ /** External identity used to authenticate the user. */
public AccountExternalId.Key getExternalId() { public ExternalId.Key getExternalId() {
return externalId; return externalId;
} }

View File

@ -14,12 +14,12 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import static java.util.stream.Collectors.toSet;
import com.google.gerrit.common.Nullable; import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.errors.NameAlreadyUsedException; import com.google.gerrit.common.errors.NameAlreadyUsedException;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gerrit.server.ssh.SshKeyCache;
@ -29,11 +29,10 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.Assisted;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.eclipse.jgit.errors.ConfigInvalidException;
/** Operation to change the username of an account. */ /** Operation to change the username of an account. */
public class ChangeUserName implements Callable<VoidResult> { public class ChangeUserName implements Callable<VoidResult> {
@ -48,6 +47,7 @@ public class ChangeUserName implements Callable<VoidResult> {
private final AccountCache accountCache; private final AccountCache accountCache;
private final SshKeyCache sshKeyCache; private final SshKeyCache sshKeyCache;
private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
private final ReviewDb db; private final ReviewDb db;
private final IdentifiedUser user; private final IdentifiedUser user;
@ -55,14 +55,15 @@ public class ChangeUserName implements Callable<VoidResult> {
@Inject @Inject
ChangeUserName( ChangeUserName(
final AccountCache accountCache, AccountCache accountCache,
final SshKeyCache sshKeyCache, SshKeyCache sshKeyCache,
@Assisted final ReviewDb db, ExternalIdsUpdate.Server externalIdsUpdateFactory,
@Assisted final IdentifiedUser user, @Assisted ReviewDb db,
@Nullable @Assisted final String newUsername) { @Assisted IdentifiedUser user,
@Nullable @Assisted String newUsername) {
this.accountCache = accountCache; this.accountCache = accountCache;
this.sshKeyCache = sshKeyCache; this.sshKeyCache = sshKeyCache;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.db = db; this.db = db;
this.user = user; this.user = user;
this.newUsername = newUsername; this.newUsername = newUsername;
@ -70,33 +71,38 @@ public class ChangeUserName implements Callable<VoidResult> {
@Override @Override
public VoidResult call() public VoidResult call()
throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException { throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException,
final Collection<AccountExternalId> old = old(); ConfigInvalidException {
Collection<ExternalId> old =
ExternalId.from(db.accountExternalIds().byAccount(user.getAccountId()).toList())
.stream()
.filter(e -> e.isScheme(SCHEME_USERNAME))
.collect(toSet());
if (!old.isEmpty()) { if (!old.isEmpty()) {
throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED); throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
} }
ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
if (newUsername != null && !newUsername.isEmpty()) { if (newUsername != null && !newUsername.isEmpty()) {
if (!USER_NAME_PATTERN.matcher(newUsername).matches()) { if (!USER_NAME_PATTERN.matcher(newUsername).matches()) {
throw new InvalidUserNameException(); throw new InvalidUserNameException();
} }
final AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_USERNAME, newUsername); ExternalId.Key key = ExternalId.Key.create(SCHEME_USERNAME, newUsername);
try { try {
final AccountExternalId id = new AccountExternalId(user.getAccountId(), key); String password = null;
for (ExternalId i : old) {
for (AccountExternalId i : old) { if (i.password() != null) {
if (i.getPassword() != null) { password = i.password();
id.setPassword(i.getPassword());
} }
} }
externalIdsUpdate.insert(db, ExternalId.create(key, user.getAccountId(), null, password));
db.accountExternalIds().insert(Collections.singleton(id));
} catch (OrmDuplicateKeyException dupeErr) { } catch (OrmDuplicateKeyException dupeErr) {
// If we are using this identity, don't report the exception. // If we are using this identity, don't report the exception.
// //
AccountExternalId other = db.accountExternalIds().get(key); ExternalId other =
if (other != null && other.getAccountId().equals(user.getAccountId())) { ExternalId.from(db.accountExternalIds().get(key.asAccountExternalIdKey()));
if (other != null && other.accountId().equals(user.getAccountId())) {
return VoidResult.INSTANCE; return VoidResult.INSTANCE;
} }
@ -108,10 +114,10 @@ public class ChangeUserName implements Callable<VoidResult> {
// If we have any older user names, remove them. // If we have any older user names, remove them.
// //
db.accountExternalIds().delete(old); externalIdsUpdate.delete(db, old);
for (AccountExternalId i : old) { for (ExternalId extId : old) {
sshKeyCache.evict(i.getSchemeRest()); sshKeyCache.evict(extId.key().id());
accountCache.evictByUsername(i.getSchemeRest()); accountCache.evictByUsername(extId.key().id());
} }
accountCache.evict(user.getAccountId()); accountCache.evict(user.getAccountId());
@ -119,14 +125,4 @@ public class ChangeUserName implements Callable<VoidResult> {
sshKeyCache.evict(newUsername); sshKeyCache.evict(newUsername);
return VoidResult.INSTANCE; return VoidResult.INSTANCE;
} }
private Collection<AccountExternalId> old() throws OrmException {
final Collection<AccountExternalId> r = new ArrayList<>(1);
for (AccountExternalId i : db.accountExternalIds().byAccount(user.getAccountId())) {
if (i.isScheme(SCHEME_USERNAME)) {
r.add(i);
}
}
return r;
}
} }

View File

@ -14,6 +14,8 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
import com.google.gerrit.audit.AuditService; import com.google.gerrit.audit.AuditService;
import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.GlobalCapability;
@ -30,7 +32,6 @@ import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupMember; import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
@ -70,6 +71,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
private final AccountLoader.Factory infoLoader; private final AccountLoader.Factory infoLoader;
private final DynamicSet<AccountExternalIdCreator> externalIdCreators; private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
private final AuditService auditService; private final AuditService auditService;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
private final String username; private final String username;
@Inject @Inject
@ -85,6 +87,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
AccountLoader.Factory infoLoader, AccountLoader.Factory infoLoader,
DynamicSet<AccountExternalIdCreator> externalIdCreators, DynamicSet<AccountExternalIdCreator> externalIdCreators,
AuditService auditService, AuditService auditService,
ExternalIdsUpdate.User externalIdsUpdateFactory,
@Assisted String username) { @Assisted String username) {
this.db = db; this.db = db;
this.currentUser = currentUser; this.currentUser = currentUser;
@ -97,6 +100,7 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
this.infoLoader = infoLoader; this.infoLoader = infoLoader;
this.externalIdCreators = externalIdCreators; this.externalIdCreators = externalIdCreators;
this.auditService = auditService; this.auditService = auditService;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.username = username; this.username = username;
} }
@ -120,19 +124,14 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
Account.Id id = new Account.Id(db.nextAccountId()); Account.Id id = new Account.Id(db.nextAccountId());
AccountExternalId extUser = ExternalId extUser = ExternalId.createUsername(username, id, input.httpPassword);
new AccountExternalId( if (db.accountExternalIds().get(extUser.key().asAccountExternalIdKey()) != null) {
id, new AccountExternalId.Key(AccountExternalId.SCHEME_USERNAME, username));
if (input.httpPassword != null) {
extUser.setPassword(HashedPassword.fromPassword(input.httpPassword).encode());
}
if (db.accountExternalIds().get(extUser.getKey()) != null) {
throw new ResourceConflictException("username '" + username + "' already exists"); throw new ResourceConflictException("username '" + username + "' already exists");
} }
if (input.email != null) { if (input.email != null) {
if (db.accountExternalIds().get(getEmailKey(input.email)) != null) { if (db.accountExternalIds()
.get(ExternalId.Key.create(SCHEME_MAILTO, input.email).asAccountExternalIdKey())
!= null) {
throw new UnprocessableEntityException("email '" + input.email + "' already exists"); throw new UnprocessableEntityException("email '" + input.email + "' already exists");
} }
if (!OutgoingEmailValidator.isValid(input.email)) { if (!OutgoingEmailValidator.isValid(input.email)) {
@ -140,27 +139,26 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
} }
} }
List<AccountExternalId> externalIds = new ArrayList<>(); List<ExternalId> extIds = new ArrayList<>();
externalIds.add(extUser); extIds.add(extUser);
for (AccountExternalIdCreator c : externalIdCreators) { for (AccountExternalIdCreator c : externalIdCreators) {
externalIds.addAll(c.create(id, username, input.email)); extIds.addAll(c.create(id, username, input.email));
} }
ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
try { try {
db.accountExternalIds().insert(externalIds); externalIdsUpdate.insert(db, extIds);
} catch (OrmDuplicateKeyException duplicateKey) { } catch (OrmDuplicateKeyException duplicateKey) {
throw new ResourceConflictException("username '" + username + "' already exists"); throw new ResourceConflictException("username '" + username + "' already exists");
} }
if (input.email != null) { if (input.email != null) {
AccountExternalId extMailto = new AccountExternalId(id, getEmailKey(input.email));
extMailto.setEmailAddress(input.email);
try { try {
db.accountExternalIds().insert(Collections.singleton(extMailto)); externalIdsUpdate.insert(db, ExternalId.createEmail(id, input.email));
} catch (OrmDuplicateKeyException duplicateKey) { } catch (OrmDuplicateKeyException duplicateKey) {
try { try {
db.accountExternalIds().delete(Collections.singleton(extUser)); externalIdsUpdate.delete(db, extUser);
} catch (OrmException cleanupError) { } catch (IOException | ConfigInvalidException | OrmException cleanupError) {
// Ignored // Ignored
} }
throw new UnprocessableEntityException("email '" + input.email + "' already exists"); throw new UnprocessableEntityException("email '" + input.email + "' already exists");
@ -208,8 +206,4 @@ public class CreateAccount implements RestModifyView<TopLevelResource, AccountIn
} }
return groupIds; return groupIds;
} }
private AccountExternalId.Key getEmailKey(String email) {
return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
}
} }

View File

@ -37,6 +37,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.Assisted;
import java.io.IOException; import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -77,7 +78,7 @@ public class CreateEmail implements RestModifyView<AccountResource, EmailInput>
public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input) public Response<EmailInfo> apply(AccountResource rsrc, EmailInput input)
throws AuthException, BadRequestException, ResourceConflictException, throws AuthException, BadRequestException, ResourceConflictException,
ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException, ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
IOException { IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
throw new AuthException("not allowed to add email address"); throw new AuthException("not allowed to add email address");
} }
@ -104,7 +105,7 @@ public class CreateEmail implements RestModifyView<AccountResource, EmailInput>
public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input) public Response<EmailInfo> apply(IdentifiedUser user, EmailInput input)
throws AuthException, BadRequestException, ResourceConflictException, throws AuthException, BadRequestException, ResourceConflictException,
ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException, ResourceNotFoundException, OrmException, EmailException, MethodNotAllowedException,
IOException { IOException, ConfigInvalidException {
if (input.email != null && !email.equals(input.email)) { if (input.email != null && !email.equals(input.email)) {
throw new BadRequestException("email address must match URL"); throw new BadRequestException("email address must match URL");
} }

View File

@ -23,7 +23,6 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
@ -34,6 +33,7 @@ import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.util.Set; import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton @Singleton
public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> { public class DeleteEmail implements RestModifyView<AccountResource.Email, Input> {
@ -59,7 +59,7 @@ public class DeleteEmail implements RestModifyView<AccountResource.Email, Input>
@Override @Override
public Response<?> apply(AccountResource.Email rsrc, Input input) public Response<?> apply(AccountResource.Email rsrc, Input input)
throws AuthException, ResourceNotFoundException, ResourceConflictException, throws AuthException, ResourceNotFoundException, ResourceConflictException,
MethodNotAllowedException, OrmException, IOException { MethodNotAllowedException, OrmException, IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) { if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canModifyAccount()) {
throw new AuthException("not allowed to delete email address"); throw new AuthException("not allowed to delete email address");
} }
@ -68,27 +68,28 @@ public class DeleteEmail implements RestModifyView<AccountResource.Email, Input>
public Response<?> apply(IdentifiedUser user, String email) public Response<?> apply(IdentifiedUser user, String email)
throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException, throws ResourceNotFoundException, ResourceConflictException, MethodNotAllowedException,
OrmException, IOException { OrmException, IOException, ConfigInvalidException {
if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) { if (!realm.allowsEdit(AccountFieldName.REGISTER_NEW_EMAIL)) {
throw new MethodNotAllowedException("realm does not allow deleting emails"); throw new MethodNotAllowedException("realm does not allow deleting emails");
} }
Set<AccountExternalId> extIds = Set<ExternalId> extIds =
dbProvider dbProvider
.get() .get()
.accountExternalIds() .accountExternalIds()
.byAccount(user.getAccountId()) .byAccount(user.getAccountId())
.toList() .toList()
.stream() .stream()
.filter(e -> email.equals(e.getEmailAddress())) .map(ExternalId::from)
.filter(e -> email.equals(e.email()))
.collect(toSet()); .collect(toSet());
if (extIds.isEmpty()) { if (extIds.isEmpty()) {
throw new ResourceNotFoundException(email); throw new ResourceNotFoundException(email);
} }
try { try {
for (AccountExternalId extId : extIds) { for (ExternalId extId : extIds) {
AuthRequest authRequest = new AuthRequest(extId.getKey().get()); AuthRequest authRequest = new AuthRequest(extId.key());
authRequest.setEmailAddress(email); authRequest.setEmailAddress(email);
accountManager.unlink(user.getAccountId(), authRequest); accountManager.unlink(user.getAccountId(), authRequest);
} }

View File

@ -14,7 +14,7 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.BadRequestException;
@ -24,44 +24,42 @@ import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class DeleteExternalIds implements RestModifyView<AccountResource, List<String>> { public class DeleteExternalIds implements RestModifyView<AccountResource, List<String>> {
private final Provider<ReviewDb> db;
private final AccountByEmailCache accountByEmailCache; private final AccountByEmailCache accountByEmailCache;
private final AccountCache accountCache; private final AccountCache accountCache;
private final ExternalIdsUpdate.User externalIdsUpdateFactory;
private final Provider<CurrentUser> self; private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider; private final Provider<ReviewDb> dbProvider;
@Inject @Inject
DeleteExternalIds( DeleteExternalIds(
Provider<ReviewDb> db,
AccountByEmailCache accountByEmailCache, AccountByEmailCache accountByEmailCache,
AccountCache accountCache, AccountCache accountCache,
ExternalIdsUpdate.User externalIdsUpdateFactory,
Provider<CurrentUser> self, Provider<CurrentUser> self,
Provider<ReviewDb> dbProvider) { Provider<ReviewDb> dbProvider) {
this.db = db;
this.accountByEmailCache = accountByEmailCache; this.accountByEmailCache = accountByEmailCache;
this.accountCache = accountCache; this.accountCache = accountCache;
this.externalIdsUpdateFactory = externalIdsUpdateFactory;
this.self = self; this.self = self;
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
} }
@Override @Override
public Response<?> apply(AccountResource resource, List<String> externalIds) public Response<?> apply(AccountResource resource, List<String> externalIds)
throws RestApiException, IOException, OrmException { throws RestApiException, IOException, OrmException, ConfigInvalidException {
if (self.get() != resource.getUser()) { if (self.get() != resource.getUser()) {
throw new AuthException("not allowed to delete external IDs"); throw new AuthException("not allowed to delete external IDs");
} }
@ -71,18 +69,20 @@ public class DeleteExternalIds implements RestModifyView<AccountResource, List<S
} }
Account.Id accountId = resource.getUser().getAccountId(); Account.Id accountId = resource.getUser().getAccountId();
Map<AccountExternalId.Key, AccountExternalId> externalIdMap = Map<ExternalId.Key, ExternalId> externalIdMap =
db.get() dbProvider
.get()
.accountExternalIds() .accountExternalIds()
.byAccount(resource.getUser().getAccountId()) .byAccount(resource.getUser().getAccountId())
.toList() .toList()
.stream() .stream()
.collect(Collectors.toMap(i -> i.getKey(), i -> i)); .map(ExternalId::from)
.collect(Collectors.toMap(i -> i.key(), i -> i));
List<AccountExternalId> toDelete = new ArrayList<>(); List<ExternalId> toDelete = new ArrayList<>();
AccountExternalId.Key last = resource.getUser().getLastLoginExternalIdKey(); ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
for (String externalIdStr : externalIds) { for (String externalIdStr : externalIds) {
AccountExternalId id = externalIdMap.get(new AccountExternalId.Key(externalIdStr)); ExternalId id = externalIdMap.get(ExternalId.Key.parse(externalIdStr));
if (id == null) { if (id == null) {
throw new UnprocessableEntityException( throw new UnprocessableEntityException(
@ -90,7 +90,7 @@ public class DeleteExternalIds implements RestModifyView<AccountResource, List<S
} }
if ((!id.isScheme(SCHEME_USERNAME)) if ((!id.isScheme(SCHEME_USERNAME))
&& ((last == null) || (!last.get().equals(id.getExternalId())))) { && ((last == null) || (!last.get().equals(id.key().get())))) {
toDelete.add(id); toDelete.add(id);
} else { } else {
throw new ResourceConflictException( throw new ResourceConflictException(
@ -99,10 +99,10 @@ public class DeleteExternalIds implements RestModifyView<AccountResource, List<S
} }
if (!toDelete.isEmpty()) { if (!toDelete.isEmpty()) {
dbProvider.get().accountExternalIds().delete(toDelete); externalIdsUpdateFactory.create().delete(dbProvider.get(), toDelete);
accountCache.evict(accountId); accountCache.evict(accountId);
for (AccountExternalId e : toDelete) { for (ExternalId e : toDelete) {
accountByEmailCache.evict(e.getEmailAddress()); accountByEmailCache.evict(e.email());
} }
} }

View File

@ -0,0 +1,321 @@
// Copyright (C) 2016 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;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import com.google.auto.value.AutoValue;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.hash.Hashing;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import java.util.Collection;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
@AutoValue
public abstract class ExternalId {
private static final String EXTERNAL_ID_SECTION = "externalId";
private static final String ACCOUNT_ID_KEY = "accountId";
private static final String EMAIL_KEY = "email";
private static final String PASSWORD_KEY = "password";
/**
* Scheme used for {@link AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link
* AuthType#HTTP_LDAP}, and {@link AuthType#LDAP_BIND} usernames.
*
* <p>The name {@code gerrit:} was a very poor choice.
*/
public static final String SCHEME_GERRIT = "gerrit";
/** Scheme used for randomly created identities constructed by a UUID. */
public static final String SCHEME_UUID = "uuid";
/** Scheme used to represent only an email address. */
public static final String SCHEME_MAILTO = "mailto";
/** Scheme for the username used to authenticate an account, e.g. over SSH. */
public static final String SCHEME_USERNAME = "username";
/** Scheme used for GPG public keys. */
public static final String SCHEME_GPGKEY = "gpgkey";
/** Scheme for external auth used during authentication, e.g. OAuth Token */
public static final String SCHEME_EXTERNAL = "external";
@AutoValue
public abstract static class Key {
public static Key create(@Nullable String scheme, String id) {
return new AutoValue_ExternalId_Key(scheme, id);
}
public static ExternalId.Key from(AccountExternalId.Key externalIdKey) {
return parse(externalIdKey.get());
}
/**
* Parses an external ID key from a string in the format "scheme:id" or "id".
*
* @return the parsed external ID key
*/
public static Key parse(String externalId) {
int c = externalId.indexOf(':');
if (c < 1 || c >= externalId.length() - 1) {
return create(null, externalId);
}
return create(externalId.substring(0, c), externalId.substring(c + 1));
}
public static Set<AccountExternalId.Key> toAccountExternalIdKeys(
Collection<ExternalId.Key> extIdKeys) {
return extIdKeys.stream().map(k -> k.asAccountExternalIdKey()).collect(toSet());
}
public abstract @Nullable String scheme();
public abstract String id();
public boolean isScheme(String scheme) {
return scheme.equals(scheme());
}
public AccountExternalId.Key asAccountExternalIdKey() {
if (scheme() != null) {
return new AccountExternalId.Key(scheme(), id());
}
return new AccountExternalId.Key(id());
}
/**
* Returns the SHA1 of the external ID that is used as note ID in the refs/meta/external-ids
* notes branch.
*/
public ObjectId sha1() {
return ObjectId.fromRaw(Hashing.sha1().hashString(get(), UTF_8).asBytes());
}
/**
* Exports this external ID key as string with the format "scheme:id", or "id" id scheme is
* null.
*
* <p>This string representation is used as subsection name in the Git config file that stores
* the external ID.
*/
public String get() {
if (scheme() != null) {
return scheme() + ":" + id();
}
return id();
}
@Override
public String toString() {
return get();
}
}
public static ExternalId create(String scheme, String id, Account.Id accountId) {
return new AutoValue_ExternalId(Key.create(scheme, id), accountId, null, null);
}
public static ExternalId create(
String scheme,
String id,
Account.Id accountId,
@Nullable String email,
@Nullable String hashedPassword) {
return create(Key.create(scheme, id), accountId, email, hashedPassword);
}
public static ExternalId create(Key key, Account.Id accountId) {
return create(key, accountId, null, null);
}
public static ExternalId create(
Key key, Account.Id accountId, @Nullable String email, @Nullable String hashedPassword) {
return new AutoValue_ExternalId(key, accountId, email, hashedPassword);
}
public static ExternalId createWithPassword(
Key key, Account.Id accountId, @Nullable String email, String plainPassword) {
plainPassword = Strings.emptyToNull(plainPassword);
String hashedPassword =
plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
return create(key, accountId, email, hashedPassword);
}
public static ExternalId createUsername(String id, Account.Id accountId, String plainPassword) {
return createWithPassword(Key.create(SCHEME_USERNAME, id), accountId, null, plainPassword);
}
public static ExternalId createWithEmail(
String scheme, String id, Account.Id accountId, String email) {
return createWithEmail(Key.create(scheme, id), accountId, email);
}
public static ExternalId createWithEmail(Key key, Account.Id accountId, String email) {
return new AutoValue_ExternalId(key, accountId, email, null);
}
public static ExternalId createEmail(Account.Id accountId, String email) {
return createWithEmail(SCHEME_MAILTO, email, accountId, email);
}
/**
* Parses an external ID from a byte array that contain the external ID as an Git config file
* text.
*
* <p>The Git config must have exactly one externalId subsection with an accountId and optionally
* email and password:
*
* <pre>
* [externalId "username:jdoe"]
* accountId = 1003407
* email = jdoe@example.com
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
* </pre>
*/
public static ExternalId parse(String noteId, byte[] raw) throws ConfigInvalidException {
Config externalIdConfig = new Config();
try {
externalIdConfig.fromText(new String(raw, UTF_8));
} catch (ConfigInvalidException e) {
throw invalidConfig(noteId, e.getMessage());
}
Set<String> externalIdKeys = externalIdConfig.getSubsections(EXTERNAL_ID_SECTION);
if (externalIdKeys.size() != 1) {
throw invalidConfig(
noteId,
String.format(
"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));
}
String accountIdStr =
externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY);
String email = externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, EMAIL_KEY);
String password =
externalIdConfig.getString(EXTERNAL_ID_SECTION, externalIdKeyStr, PASSWORD_KEY);
if (accountIdStr == null) {
throw invalidConfig(
noteId,
String.format(
"Missing value for %s.%s.%s", EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
}
Integer accountId = Ints.tryParse(accountIdStr);
if (accountId == null) {
throw invalidConfig(
noteId,
String.format(
"Value %s for %s.%s.%s is invalid, expected account ID",
accountIdStr, EXTERNAL_ID_SECTION, externalIdKeyStr, ACCOUNT_ID_KEY));
}
return new AutoValue_ExternalId(externalIdKey, new Account.Id(accountId), email, password);
}
private static ConfigInvalidException invalidConfig(String noteId, String message) {
return new ConfigInvalidException(
String.format("Invalid external id config for note %s: %s", noteId, message));
}
public static ExternalId from(AccountExternalId externalId) {
if (externalId == null) {
return null;
}
return new AutoValue_ExternalId(
ExternalId.Key.parse(externalId.getExternalId()),
externalId.getAccountId(),
externalId.getEmailAddress(),
externalId.getPassword());
}
public static Set<ExternalId> from(Collection<AccountExternalId> externalIds) {
if (externalIds == null) {
return ImmutableSet.of();
}
return externalIds.stream().map(ExternalId::from).collect(toSet());
}
public static Set<AccountExternalId> toAccountExternalIds(Collection<ExternalId> extIds) {
return extIds.stream().map(e -> e.asAccountExternalId()).collect(toSet());
}
public abstract Key key();
public abstract Account.Id accountId();
public abstract @Nullable String email();
public abstract @Nullable String password();
public boolean isScheme(String scheme) {
return key().isScheme(scheme);
}
public AccountExternalId asAccountExternalId() {
AccountExternalId extId = new AccountExternalId(accountId(), key().asAccountExternalIdKey());
extId.setEmailAddress(email());
extId.setPassword(password());
return extId;
}
/**
* Exports this external ID as Git config file text.
*
* <p>The Git config has exactly one externalId subsection with an accountId and optionally email
* and password:
*
* <pre>
* [externalId "username:jdoe"]
* accountId = 1003407
* email = jdoe@example.com
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
* </pre>
*/
@Override
public String toString() {
Config c = new Config();
writeToConfig(c);
return c.toText();
}
public void writeToConfig(Config c) {
String externalIdKey = key().get();
c.setInt(EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, accountId().get());
if (email() != null) {
c.setString(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY, email());
}
if (password() != null) {
c.setString(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY, password());
}
}
}

View File

@ -0,0 +1,105 @@
// Copyright (C) 2016 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;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* Class to read external IDs from NoteDb.
*
* <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
* refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
* is a git config file that contains an external ID. It has exactly one externalId subsection with
* an accountId and optionally email and password:
*
* <pre>
* [externalId "username:jdoe"]
* accountId = 1003407
* email = jdoe@example.com
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
* </pre>
*/
@Singleton
public class ExternalIds {
public static final int MAX_NOTE_SZ = 1 << 19;
public static ObjectId readRevision(Repository repo) throws IOException {
Ref ref = repo.exactRef(RefNames.REFS_EXTERNAL_IDS);
return ref != null ? ref.getObjectId() : ObjectId.zeroId();
}
public static NoteMap readNoteMap(RevWalk rw, ObjectId rev) throws IOException {
if (!rev.equals(ObjectId.zeroId())) {
return NoteMap.read(rw.getObjectReader(), rw.parseCommit(rev));
}
return NoteMap.newEmptyMap();
}
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
@Inject
public ExternalIds(GitRepositoryManager repoManager, AllUsersName allUsersName) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
}
public ObjectId readRevision() throws IOException {
try (Repository repo = repoManager.openRepository(allUsersName)) {
return readRevision(repo);
}
}
/** Reads and returns the specified external ID. */
@Nullable
public ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException {
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo)) {
ObjectId rev = readRevision(repo);
if (rev.equals(ObjectId.zeroId())) {
return null;
}
return parse(key, rw, rev);
}
}
private ExternalId parse(ExternalId.Key key, RevWalk rw, ObjectId rev)
throws IOException, ConfigInvalidException {
NoteMap noteMap = readNoteMap(rw, rev);
ObjectId noteId = key.sha1();
if (!noteMap.contains(noteId)) {
return null;
}
byte[] raw =
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
return ExternalId.parse(noteId.name(), raw);
}
}

View File

@ -0,0 +1,116 @@
// Copyright (C) 2016 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;
import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* This class allows to do batch updates to external IDs.
*
* <p>For NoteDb all updates will result in a single commit to the refs/meta/external-ids branch.
* This means callers can prepare many updates by invoking {@link #replace(ExternalId, ExternalId)}
* multiple times and when {@link ExternalIdsBatchUpdate#commit(ReviewDb, String)} is invoked a
* single NoteDb commit is created that contains all the prepared updates.
*/
public class ExternalIdsBatchUpdate {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final PersonIdent serverIdent;
private final Set<ExternalId> toAdd = new HashSet<>();
private final Set<ExternalId> toDelete = new HashSet<>();
@Inject
public ExternalIdsBatchUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@GerritPersonIdent PersonIdent serverIdent) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.serverIdent = serverIdent;
}
/**
* Adds an external ID replacement to the batch.
*
* <p>The actual replacement is only done when {@link #commit(ReviewDb, String)} is invoked.
*/
public void replace(ExternalId extIdToDelete, ExternalId extIdToAdd) {
ExternalIdsUpdate.checkSameAccount(ImmutableSet.of(extIdToDelete, extIdToAdd));
toAdd.add(extIdToAdd);
toDelete.add(extIdToDelete);
}
/**
* Commits this batch.
*
* <p>This means external ID replacements which were prepared by invoking {@link
* #replace(ExternalId, ExternalId)} are now executed. Deletion of external IDs is done before
* adding the new external IDs. This means if an external ID is specified for deletion and an
* external ID with the same key is specified to be added, the old external ID with that key is
* deleted first and then the new external ID is added (so the external ID for that key is
* replaced).
*
* <p>For NoteDb a single commit is created that contains all the external ID updates.
*/
public void commit(ReviewDb db, String commitMessage)
throws IOException, OrmException, ConfigInvalidException {
if (toDelete.isEmpty() && toAdd.isEmpty()) {
return;
}
db.accountExternalIds().delete(toAccountExternalIds(toDelete));
db.accountExternalIds().insert(toAccountExternalIds(toAdd));
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
ObjectId rev = ExternalIds.readRevision(repo);
NoteMap noteMap = ExternalIds.readNoteMap(rw, rev);
for (ExternalId extId : toDelete) {
ExternalIdsUpdate.remove(rw, noteMap, extId);
}
for (ExternalId extId : toAdd) {
ExternalIdsUpdate.insert(rw, ins, noteMap, extId);
}
ExternalIdsUpdate.commit(
repo, rw, ins, rev, noteMap, commitMessage, serverIdent, serverIdent);
}
toAdd.clear();
toDelete.clear();
}
}

View File

@ -0,0 +1,636 @@
// Copyright (C) 2016 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;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.account.ExternalId.Key.toAccountExternalIdKeys;
import static com.google.gerrit.server.account.ExternalId.toAccountExternalIds;
import static com.google.gerrit.server.account.ExternalIds.MAX_NOTE_SZ;
import static com.google.gerrit.server.account.ExternalIds.readNoteMap;
import static com.google.gerrit.server.account.ExternalIds.readRevision;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import static org.eclipse.jgit.lib.Constants.OBJ_TREE;
import com.github.rholder.retry.RetryException;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Runnables;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.LockFailureException;
import com.google.gwtorm.server.OrmDuplicateKeyException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
/**
* Updates externalIds in ReviewDb and NoteDb.
*
* <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
* refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
* is a git config file that contains an external ID. It has exactly one externalId subsection with
* an accountId and optionally email and password:
*
* <pre>
* [externalId "username:jdoe"]
* accountId = 1003407
* email = jdoe@example.com
* password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
* </pre>
*
* For NoteDb each method call results in one commit on refs/meta/external-ids branch.
*/
public class ExternalIdsUpdate {
private static final String COMMIT_MSG = "Update external IDs";
/**
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the Gerrit server.
*
* <p>The Gerrit server identity will be used as author and committer for all commits that update
* the external IDs.
*/
@Singleton
public static class Server {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final Provider<PersonIdent> serverIdent;
@Inject
public Server(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@GerritPersonIdent Provider<PersonIdent> serverIdent) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.serverIdent = serverIdent;
}
public ExternalIdsUpdate create() {
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(repoManager, allUsersName, i, i);
}
}
/**
* Factory to create an ExternalIdsUpdate instance for updating external IDs by the current user.
*
* <p>The identity of the current user will be used as author for all commits that update the
* external IDs. The Gerrit server identity will be used as committer.
*/
@Singleton
public static class User {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final Provider<PersonIdent> serverIdent;
private final Provider<IdentifiedUser> identifiedUser;
@Inject
public User(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<IdentifiedUser> identifiedUser) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.serverIdent = serverIdent;
this.identifiedUser = identifiedUser;
}
public ExternalIdsUpdate create() {
PersonIdent i = serverIdent.get();
return new ExternalIdsUpdate(
repoManager, allUsersName, createPersonIdent(i, identifiedUser.get()), i);
}
private PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone());
}
}
@VisibleForTesting
public static RetryerBuilder<Void> retryerBuilder() {
return RetryerBuilder.<Void>newBuilder()
.retryIfException(e -> e instanceof LockFailureException)
.withWaitStrategy(
WaitStrategies.join(
WaitStrategies.exponentialWait(2, TimeUnit.SECONDS),
WaitStrategies.randomWait(50, TimeUnit.MILLISECONDS)))
.withStopStrategy(StopStrategies.stopAfterDelay(10, TimeUnit.SECONDS));
}
private static final Retryer<Void> RETRYER = retryerBuilder().build();
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final PersonIdent committerIdent;
private final PersonIdent authorIdent;
private final Runnable afterReadRevision;
private final Retryer<Void> retryer;
private ExternalIdsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
PersonIdent committerIdent,
PersonIdent authorIdent) {
this(repoManager, allUsersName, committerIdent, authorIdent, Runnables.doNothing(), RETRYER);
}
@VisibleForTesting
public ExternalIdsUpdate(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
PersonIdent committerIdent,
PersonIdent authorIdent,
Runnable afterReadRevision,
Retryer<Void> retryer) {
this.repoManager = checkNotNull(repoManager, "repoManager");
this.allUsersName = checkNotNull(allUsersName, "allUsersName");
this.committerIdent = checkNotNull(committerIdent, "committerIdent");
this.authorIdent = checkNotNull(authorIdent, "authorIdent");
this.afterReadRevision = checkNotNull(afterReadRevision, "afterReadRevision");
this.retryer = checkNotNull(retryer, "retryer");
}
/**
* Inserts a new external ID.
*
* <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
*/
public void insert(ReviewDb db, ExternalId extId)
throws IOException, ConfigInvalidException, OrmException {
insert(db, Collections.singleton(extId));
}
/**
* Inserts new external IDs.
*
* <p>If any of the external ID already exists, the insert fails with {@link
* OrmDuplicateKeyException}.
*/
public void insert(ReviewDb db, Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
db.accountExternalIds().insert(toAccountExternalIds(extIds));
updateNoteMap(
o -> {
for (ExternalId extId : extIds) {
insert(o.rw(), o.ins(), o.noteMap(), extId);
}
});
}
/**
* Inserts or updates an external ID.
*
* <p>If the external ID already exists, it is overwritten, otherwise it is inserted.
*/
public void upsert(ReviewDb db, ExternalId extId)
throws IOException, ConfigInvalidException, OrmException {
upsert(db, Collections.singleton(extId));
}
/**
* Inserts or updates external IDs.
*
* <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted.
*/
public void upsert(ReviewDb db, Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
db.accountExternalIds().upsert(toAccountExternalIds(extIds));
updateNoteMap(
o -> {
for (ExternalId extId : extIds) {
upsert(o.rw(), o.ins(), o.noteMap(), extId);
}
});
}
/**
* Deletes an external ID.
*
* <p>The deletion fails with {@link IllegalStateException} if there is an existing external ID
* that has the same key, but otherwise doesn't match the specified external ID.
*/
public void delete(ReviewDb db, ExternalId extId)
throws IOException, ConfigInvalidException, OrmException {
delete(db, Collections.singleton(extId));
}
/**
* Deletes external IDs.
*
* <p>The deletion fails with {@link IllegalStateException} if there is an existing external ID
* that has the same key as any of the external IDs that should be deleted, but otherwise doesn't
* match the that external ID.
*/
public void delete(ReviewDb db, Collection<ExternalId> extIds)
throws IOException, ConfigInvalidException, OrmException {
db.accountExternalIds().delete(toAccountExternalIds(extIds));
updateNoteMap(
o -> {
for (ExternalId extId : extIds) {
remove(o.rw(), o.noteMap(), extId);
}
});
}
/**
* Delete an external ID by key.
*
* <p>The external ID is only deleted if it belongs to the specified account. If it belongs to
* another account the deletion fails with {@link IllegalStateException}.
*/
public void delete(ReviewDb db, Account.Id accountId, ExternalId.Key extIdKey)
throws IOException, ConfigInvalidException, OrmException {
delete(db, accountId, Collections.singleton(extIdKey));
}
/**
* Delete external IDs by external ID key.
*
* <p>The external IDs are only deleted if they belongs to the specified account. If any of the
* external IDs belongs to another account the deletion fails with {@link IllegalStateException}.
*/
public void delete(ReviewDb db, Account.Id accountId, Collection<ExternalId.Key> extIdKeys)
throws IOException, ConfigInvalidException, OrmException {
db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(extIdKeys));
updateNoteMap(
o -> {
for (ExternalId.Key extIdKey : extIdKeys) {
remove(o.rw(), o.noteMap(), accountId, extIdKey);
}
});
}
/** Deletes all external IDs of the specified account. */
public void deleteAll(ReviewDb db, Account.Id accountId)
throws IOException, ConfigInvalidException, OrmException {
delete(db, ExternalId.from(db.accountExternalIds().byAccount(accountId).toList()));
}
/**
* Replaces external IDs for an account by external ID keys.
*
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
* external ID key is specified for deletion and an external ID with the same key is specified to
* be added, the old external ID with that key is deleted first and then the new external ID is
* added (so the external ID for that key is replaced).
*
* <p>If any of the specified external IDs belongs to another account the replacement fails with
* {@link IllegalStateException}.
*/
public void replace(
ReviewDb db,
Account.Id accountId,
Collection<ExternalId.Key> toDelete,
Collection<ExternalId> toAdd)
throws IOException, ConfigInvalidException, OrmException {
checkSameAccount(toAdd, accountId);
db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(toDelete));
db.accountExternalIds().insert(toAccountExternalIds(toAdd));
updateNoteMap(
o -> {
for (ExternalId.Key extIdKey : toDelete) {
remove(o.rw(), o.noteMap(), accountId, extIdKey);
}
for (ExternalId extId : toAdd) {
insert(o.rw(), o.ins(), o.noteMap(), extId);
}
});
}
/**
* Replaces an external ID.
*
* <p>If the specified external IDs belongs to different accounts the replacement fails with
* {@link IllegalStateException}.
*/
public void replace(ReviewDb db, ExternalId toDelete, ExternalId toAdd)
throws IOException, ConfigInvalidException, OrmException {
replace(db, Collections.singleton(toDelete), Collections.singleton(toAdd));
}
/**
* Replaces external IDs.
*
* <p>Deletion of external IDs is done before adding the new external IDs. This means if an
* external ID is specified for deletion and an external ID with the same key is specified to be
* added, the old external ID with that key is deleted first and then the new external ID is added
* (so the external ID for that key is replaced).
*
* <p>If the specified external IDs belong to different accounts the replacement fails with {@link
* IllegalStateException}.
*/
public void replace(ReviewDb db, Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
throws IOException, ConfigInvalidException, OrmException {
Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
if (accountId == null) {
// toDelete and toAdd are empty -> nothing to do
return;
}
replace(db, accountId, toDelete.stream().map(e -> e.key()).collect(toSet()), toAdd);
}
/**
* Checks that all specified external IDs belong to the same account.
*
* @return the ID of the account to which all specified external IDs belong.
*/
public static Account.Id checkSameAccount(Iterable<ExternalId> extIds) {
return checkSameAccount(extIds, null);
}
/**
* Checks that all specified external IDs belong to specified account. If no account is specified
* it is checked that all specified external IDs belong to the same account.
*
* @return the ID of the account to which all specified external IDs belong.
*/
public static Account.Id checkSameAccount(
Iterable<ExternalId> extIds, @Nullable Account.Id accountId) {
for (ExternalId extId : extIds) {
if (accountId == null) {
accountId = extId.accountId();
continue;
}
checkState(
accountId.equals(extId.accountId()),
"external id %s belongs to account %s, expected account %s",
extId.key().get(),
extId.accountId().get(),
accountId.get());
}
return accountId;
}
/**
* Inserts a new external ID and sets it in the note map.
*
* <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
*/
public static void insert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
throws OrmDuplicateKeyException, ConfigInvalidException, IOException {
if (noteMap.contains(extId.key().sha1())) {
throw new OrmDuplicateKeyException(
String.format("external id %s already exists", extId.key().get()));
}
upsert(rw, ins, noteMap, extId);
}
/**
* Insert or updates an new external ID and sets it in the note map.
*
* <p>If the external ID already exists it is overwritten.
*/
private static void upsert(RevWalk rw, ObjectInserter ins, NoteMap noteMap, ExternalId extId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
Config c = new Config();
if (noteMap.contains(extId.key().sha1())) {
byte[] raw =
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
try {
c.fromText(new String(raw, UTF_8));
} catch (ConfigInvalidException e) {
throw new ConfigInvalidException(
String.format("Invalid external id config for note %s: %s", noteId, e.getMessage()));
}
}
extId.writeToConfig(c);
byte[] raw = c.toText().getBytes(UTF_8);
ObjectId dataBlob = ins.insert(OBJ_BLOB, raw);
noteMap.set(noteId, dataBlob);
}
/**
* Removes an external ID from the note map.
*
* <p>The removal fails with {@link IllegalStateException} if there is an existing external ID
* that has the same key, but otherwise doesn't match the specified external ID.
*/
public static void remove(RevWalk rw, NoteMap noteMap, ExternalId extId)
throws IOException, ConfigInvalidException {
ObjectId noteId = extId.key().sha1();
if (!noteMap.contains(noteId)) {
return;
}
byte[] raw =
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
ExternalId actualExtId = ExternalId.parse(noteId.name(), raw);
checkState(
extId.equals(actualExtId),
"external id %s should be removed, but it's not matching the actual external id %s",
extId.toString(),
actualExtId.toString());
noteMap.remove(noteId);
}
/**
* Removes an external ID from the note map by external ID key.
*
* <p>The external ID is only deleted if it belongs to the specified account. If the external IDs
* belongs to another account the deletion fails with {@link IllegalStateException}.
*/
private static void remove(
RevWalk rw, NoteMap noteMap, Account.Id accountId, ExternalId.Key extIdKey)
throws IOException, ConfigInvalidException {
ObjectId noteId = extIdKey.sha1();
if (!noteMap.contains(noteId)) {
return;
}
byte[] raw =
rw.getObjectReader().open(noteMap.get(noteId), OBJ_BLOB).getCachedBytes(MAX_NOTE_SZ);
ExternalId extId = ExternalId.parse(noteId.name(), raw);
checkState(
accountId.equals(extId.accountId()),
"external id %s should be removed for account %s,"
+ " but external id belongs to account %s",
extIdKey.get(),
accountId.get(),
extId.accountId().get());
noteMap.remove(noteId);
}
private void updateNoteMap(MyConsumer<OpenRepo> update)
throws IOException, ConfigInvalidException, OrmException {
try (Repository repo = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(repo);
ObjectInserter ins = repo.newObjectInserter()) {
retryer.call(new TryNoteMapUpdate(repo, rw, ins, update));
} catch (ExecutionException | RetryException e) {
if (e.getCause() != null) {
Throwables.throwIfInstanceOf(e.getCause(), IOException.class);
Throwables.throwIfInstanceOf(e.getCause(), ConfigInvalidException.class);
Throwables.throwIfInstanceOf(e.getCause(), OrmException.class);
}
throw new OrmException(e);
}
}
private void commit(
Repository repo, RevWalk rw, ObjectInserter ins, ObjectId rev, NoteMap noteMap)
throws IOException {
commit(repo, rw, ins, rev, noteMap, COMMIT_MSG, committerIdent, authorIdent);
}
/** Commits updates to the external IDs. */
public static void commit(
Repository repo,
RevWalk rw,
ObjectInserter ins,
ObjectId rev,
NoteMap noteMap,
String commitMessage,
PersonIdent committerIdent,
PersonIdent authorIdent)
throws IOException {
CommitBuilder cb = new CommitBuilder();
cb.setMessage(commitMessage);
cb.setTreeId(noteMap.writeTree(ins));
cb.setAuthor(authorIdent);
cb.setCommitter(committerIdent);
if (!rev.equals(ObjectId.zeroId())) {
cb.setParentId(rev);
} else {
cb.setParentIds(); // Ref is currently nonexistent, commit has no parents.
}
if (cb.getTreeId() == null) {
if (rev.equals(ObjectId.zeroId())) {
cb.setTreeId(emptyTree(ins)); // No parent, assume empty tree.
} else {
RevCommit p = rw.parseCommit(rev);
cb.setTreeId(p.getTree()); // Copy tree from parent.
}
}
ObjectId commitId = ins.insert(cb);
ins.flush();
RefUpdate u = repo.updateRef(RefNames.REFS_EXTERNAL_IDS);
u.setRefLogIdent(committerIdent);
u.setRefLogMessage("Update external IDs", false);
u.setExpectedOldObjectId(rev);
u.setNewObjectId(commitId);
RefUpdate.Result res = u.update();
switch (res) {
case NEW:
case FAST_FORWARD:
case NO_CHANGE:
case RENAMED:
case FORCED:
break;
case LOCK_FAILURE:
throw new LockFailureException("Updating external IDs failed with " + res);
case IO_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
default:
throw new IOException("Updating external IDs failed with " + res);
}
}
private static ObjectId emptyTree(ObjectInserter ins) throws IOException {
return ins.insert(OBJ_TREE, new byte[] {});
}
private static interface MyConsumer<T> {
void accept(T t) throws IOException, ConfigInvalidException, OrmException;
}
@AutoValue
abstract static class OpenRepo {
static OpenRepo create(Repository repo, RevWalk rw, ObjectInserter ins, NoteMap noteMap) {
return new AutoValue_ExternalIdsUpdate_OpenRepo(repo, rw, ins, noteMap);
}
abstract Repository repo();
abstract RevWalk rw();
abstract ObjectInserter ins();
abstract NoteMap noteMap();
}
private class TryNoteMapUpdate implements Callable<Void> {
private final Repository repo;
private final RevWalk rw;
private final ObjectInserter ins;
private final MyConsumer<OpenRepo> update;
private TryNoteMapUpdate(
Repository repo, RevWalk rw, ObjectInserter ins, MyConsumer<OpenRepo> update) {
this.repo = repo;
this.rw = rw;
this.ins = ins;
this.update = update;
}
@Override
public Void call() throws Exception {
ObjectId rev = readRevision(repo);
afterReadRevision.run();
NoteMap noteMap = readNoteMap(rw, rev);
update.accept(OpenRepo.create(repo, rw, ins, noteMap));
commit(repo, rw, ins, rev, noteMap);
return null;
}
}
}

View File

@ -14,7 +14,7 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
@ -22,7 +22,6 @@ import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView; import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfig;
@ -54,22 +53,23 @@ public class GetExternalIds implements RestReadView<AccountResource> {
throw new AuthException("not allowed to get external IDs"); throw new AuthException("not allowed to get external IDs");
} }
Collection<AccountExternalId> ids = Collection<ExternalId> ids =
db.get().accountExternalIds().byAccount(resource.getUser().getAccountId()).toList(); ExternalId.from(
db.get().accountExternalIds().byAccount(resource.getUser().getAccountId()).toList());
if (ids.isEmpty()) { if (ids.isEmpty()) {
return ImmutableList.of(); return ImmutableList.of();
} }
List<AccountExternalIdInfo> result = Lists.newArrayListWithCapacity(ids.size()); List<AccountExternalIdInfo> result = Lists.newArrayListWithCapacity(ids.size());
for (AccountExternalId id : ids) { for (ExternalId id : ids) {
AccountExternalIdInfo info = new AccountExternalIdInfo(); AccountExternalIdInfo info = new AccountExternalIdInfo();
info.identity = id.getExternalId(); info.identity = id.key().get();
info.emailAddress = id.getEmailAddress(); info.emailAddress = id.email();
info.trusted = toBoolean(authConfig.isIdentityTrustable(Collections.singleton(id))); info.trusted = toBoolean(authConfig.isIdentityTrustable(Collections.singleton(id)));
// The identity can be deleted only if its not the one used to // The identity can be deleted only if its not the one used to
// establish this web session, and if only if an identity was // establish this web session, and if only if an identity was
// actually used to establish this web session. // actually used to establish this web session.
if (!id.isScheme(SCHEME_USERNAME)) { if (!id.isScheme(SCHEME_USERNAME)) {
AccountExternalId.Key last = resource.getUser().getLastLoginExternalIdKey(); ExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
info.canDelete = toBoolean(last == null || !last.get().equals(info.identity)); info.canDelete = toBoolean(last == null || !last.get().equals(info.identity));
} }
result.add(info); result.add(info);

View File

@ -20,7 +20,6 @@ import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AvatarInfo; import com.google.gerrit.extensions.common.AvatarInfo;
import com.google.gerrit.extensions.registration.DynamicItem; import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.avatar.AvatarProvider; import com.google.gerrit.server.avatar.AvatarProvider;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
@ -74,7 +73,7 @@ public class InternalAccountDirectory extends AccountDirectory {
private void fill( private void fill(
AccountInfo info, AccountInfo info,
Account account, Account account,
@Nullable Collection<AccountExternalId> externalIds, @Nullable Collection<ExternalId> externalIds,
Set<FillOptions> options) { Set<FillOptions> options) {
if (options.contains(FillOptions.ID)) { if (options.contains(FillOptions.ID)) {
info._accountId = account.getId().get(); info._accountId = account.getId().get();
@ -124,8 +123,7 @@ public class InternalAccountDirectory extends AccountDirectory {
} }
} }
public List<String> getSecondaryEmails( public List<String> getSecondaryEmails(Account account, Collection<ExternalId> externalIds) {
Account account, Collection<AccountExternalId> externalIds) {
List<String> emails = new ArrayList<>(AccountState.getEmails(externalIds)); List<String> emails = new ArrayList<>(AccountState.getEmails(externalIds));
if (account.getPreferredEmail() != null) { if (account.getPreferredEmail() != null) {
emails.remove(account.getPreferredEmail()); emails.remove(account.getPreferredEmail());

View File

@ -14,7 +14,7 @@
package com.google.gerrit.server.account; package com.google.gerrit.server.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.AuthException;
@ -22,7 +22,6 @@ import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response; import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
@ -30,14 +29,12 @@ import com.google.gerrit.server.account.PutHttpPassword.Input;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collections;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton
public class PutHttpPassword implements RestModifyView<AccountResource, Input> { public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
public static class Input { public static class Input {
public String httpPassword; public String httpPassword;
@ -58,19 +55,24 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
private final Provider<CurrentUser> self; private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider; private final Provider<ReviewDb> dbProvider;
private final AccountCache accountCache; private final AccountCache accountCache;
private final ExternalIdsUpdate.User externalIdsUpdate;
@Inject @Inject
PutHttpPassword( PutHttpPassword(
Provider<CurrentUser> self, Provider<ReviewDb> dbProvider, AccountCache accountCache) { Provider<CurrentUser> self,
Provider<ReviewDb> dbProvider,
AccountCache accountCache,
ExternalIdsUpdate.User externalIdsUpdate) {
this.self = self; this.self = self;
this.dbProvider = dbProvider; this.dbProvider = dbProvider;
this.accountCache = accountCache; this.accountCache = accountCache;
this.externalIdsUpdate = externalIdsUpdate;
} }
@Override @Override
public Response<String> apply(AccountResource rsrc, Input input) public Response<String> apply(AccountResource rsrc, Input input)
throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException, throws AuthException, ResourceNotFoundException, ResourceConflictException, OrmException,
IOException { IOException, ConfigInvalidException {
if (input == null) { if (input == null) {
input = new Input(); input = new Input();
} }
@ -100,22 +102,26 @@ public class PutHttpPassword implements RestModifyView<AccountResource, Input> {
} }
public Response<String> apply(IdentifiedUser user, String newPassword) public Response<String> apply(IdentifiedUser user, String newPassword)
throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException { throws ResourceNotFoundException, ResourceConflictException, OrmException, IOException,
ConfigInvalidException {
if (user.getUserName() == null) { if (user.getUserName() == null) {
throw new ResourceConflictException("username must be set"); throw new ResourceConflictException("username must be set");
} }
AccountExternalId id = ExternalId extId =
dbProvider ExternalId.from(
.get() dbProvider
.accountExternalIds() .get()
.get(new AccountExternalId.Key(SCHEME_USERNAME, user.getUserName())); .accountExternalIds()
if (id == null) { .get(
ExternalId.Key.create(SCHEME_USERNAME, user.getUserName())
.asAccountExternalIdKey()));
if (extId == null) {
throw new ResourceNotFoundException(); throw new ResourceNotFoundException();
} }
id.setPassword(HashedPassword.fromPassword(newPassword).encode()); ExternalId newExtId =
ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword);
dbProvider.get().accountExternalIds().update(Collections.singleton(id)); externalIdsUpdate.create().upsert(dbProvider.get(), newExtId);
accountCache.evict(user.getAccountId()); accountCache.evict(user.getAccountId());
return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword); return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword);

View File

@ -30,6 +30,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton @Singleton
public class PutUsername implements RestModifyView<AccountResource, Input> { public class PutUsername implements RestModifyView<AccountResource, Input> {
@ -57,7 +58,7 @@ public class PutUsername implements RestModifyView<AccountResource, Input> {
@Override @Override
public String apply(AccountResource rsrc, Input input) public String apply(AccountResource rsrc, Input input)
throws AuthException, MethodNotAllowedException, UnprocessableEntityException, throws AuthException, MethodNotAllowedException, UnprocessableEntityException,
ResourceConflictException, OrmException, IOException { ResourceConflictException, OrmException, IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) { if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
throw new AuthException("not allowed to set username"); throw new AuthException("not allowed to set username");
} }

View File

@ -372,7 +372,7 @@ public class AccountApiImpl implements AccountApi {
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email); AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), input.email);
try { try {
createEmailFactory.create(input.email).apply(rsrc, input); createEmailFactory.create(input.email).apply(rsrc, input);
} catch (EmailException | OrmException | IOException e) { } catch (EmailException | OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot add email", e); throw new RestApiException("Cannot add email", e);
} }
} }
@ -382,7 +382,7 @@ public class AccountApiImpl implements AccountApi {
AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email); AccountResource.Email rsrc = new AccountResource.Email(account.getUser(), email);
try { try {
deleteEmail.apply(rsrc, null); deleteEmail.apply(rsrc, null);
} catch (OrmException | IOException e) { } catch (OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot delete email", e); throw new RestApiException("Cannot delete email", e);
} }
} }
@ -494,7 +494,7 @@ public class AccountApiImpl implements AccountApi {
public void deleteExternalIds(List<String> externalIds) throws RestApiException { public void deleteExternalIds(List<String> externalIds) throws RestApiException {
try { try {
deleteExternalIds.apply(account, externalIds); deleteExternalIds.apply(account, externalIds);
} catch (IOException | OrmException e) { } catch (IOException | OrmException | ConfigInvalidException e) {
throw new RestApiException("Cannot delete external IDs", e); throw new RestApiException("Cannot delete external IDs", e);
} }
} }

View File

@ -15,7 +15,7 @@
package com.google.gerrit.server.api.accounts; package com.google.gerrit.server.api.accounts;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.server.account.ExternalId;
import java.util.List; import java.util.List;
public interface AccountExternalIdCreator { public interface AccountExternalIdCreator {
@ -28,5 +28,5 @@ public interface AccountExternalIdCreator {
* @param email an optional email address to assign to the external identifiers, or {@code null}. * @param email an optional email address to assign to the external identifiers, or {@code null}.
* @return a list of external identifiers, or an empty list. * @return a list of external identifiers, or an empty list.
*/ */
List<AccountExternalId> create(Account.Id id, String username, String email); List<ExternalId> create(Account.Id id, String username, String email);
} }

View File

@ -14,6 +14,7 @@
package com.google.gerrit.server.auth.ldap; package com.google.gerrit.server.auth.ldap;
import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR; import static com.google.gerrit.server.account.GroupBackends.GROUP_REF_NAME_COMPARATOR;
import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID; import static com.google.gerrit.server.auth.ldap.Helper.LDAP_UUID;
import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE; import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
@ -25,10 +26,10 @@ import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GroupDescription; import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference; import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.ParameterizedString; import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.GroupBackend; import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupMembership; import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.auth.ldap.Helper.LdapSchema; import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
@ -180,10 +181,10 @@ public class LdapGroupBackend implements GroupBackend {
return new LdapGroupMembership(membershipCache, projectCache, id); return new LdapGroupMembership(membershipCache, projectCache, id);
} }
private static String findId(final Collection<AccountExternalId> ids) { private static String findId(Collection<ExternalId> extIds) {
for (final AccountExternalId i : ids) { for (ExternalId extId : extIds) {
if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) { if (extId.isScheme(SCHEME_GERRIT)) {
return i.getSchemeRest(); return extId.key().id();
} }
} }
return null; return null;

View File

@ -14,7 +14,7 @@
package com.google.gerrit.server.auth.ldap; package com.google.gerrit.server.auth.ldap;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT; import static com.google.gerrit.server.account.ExternalId.SCHEME_GERRIT;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.cache.CacheLoader; import com.google.common.cache.CacheLoader;
@ -24,13 +24,13 @@ import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.extensions.client.AccountFieldName; import com.google.gerrit.extensions.client.AccountFieldName;
import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AbstractRealm; import com.google.gerrit.server.account.AbstractRealm;
import com.google.gerrit.server.account.AccountException; import com.google.gerrit.server.account.AccountException;
import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.account.EmailExpander; import com.google.gerrit.server.account.EmailExpander;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.GroupBackends; import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.auth.AuthenticationUnavailableException; import com.google.gerrit.server.auth.AuthenticationUnavailableException;
import com.google.gerrit.server.config.AuthConfig; import com.google.gerrit.server.config.AuthConfig;
@ -329,8 +329,12 @@ class LdapRealm extends AbstractRealm {
public Optional<Account.Id> load(String username) throws Exception { public Optional<Account.Id> load(String username) throws Exception {
try (ReviewDb db = schema.open()) { try (ReviewDb db = schema.open()) {
return Optional.ofNullable( return Optional.ofNullable(
db.accountExternalIds().get(new AccountExternalId.Key(SCHEME_GERRIT, username))) ExternalId.from(
.map(AccountExternalId::getAccountId); db.accountExternalIds()
.get(
ExternalId.Key.create(SCHEME_GERRIT, username)
.asAccountExternalIdKey())))
.map(ExternalId::accountId);
} }
} }
} }

View File

@ -14,7 +14,7 @@
package com.google.gerrit.server.auth.openid; package com.google.gerrit.server.auth.openid;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.server.account.ExternalId;
public class OpenIdProviderPattern { public class OpenIdProviderPattern {
public static OpenIdProviderPattern create(String pattern) { public static OpenIdProviderPattern create(String pattern) {
@ -33,8 +33,8 @@ public class OpenIdProviderPattern {
return regex ? id.matches(pattern) : id.startsWith(pattern); return regex ? id.matches(pattern) : id.startsWith(pattern);
} }
public boolean matches(AccountExternalId id) { public boolean matches(ExternalId extId) {
return matches(id.getExternalId()); return matches(extId.key().get());
} }
@Override @Override

View File

@ -14,9 +14,13 @@
package com.google.gerrit.server.config; package com.google.gerrit.server.config;
import static com.google.gerrit.server.account.ExternalId.SCHEME_MAILTO;
import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import static com.google.gerrit.server.account.ExternalId.SCHEME_UUID;
import com.google.gerrit.extensions.client.AuthType; import com.google.gerrit.extensions.client.AuthType;
import com.google.gerrit.extensions.client.GitBasicAuthPolicy; import com.google.gerrit.extensions.client.GitBasicAuthPolicy;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.auth.openid.OpenIdProviderPattern; import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
import com.google.gwtjsonrpc.server.SignedToken; import com.google.gwtjsonrpc.server.SignedToken;
import com.google.gwtjsonrpc.server.XsrfException; import com.google.gwtjsonrpc.server.XsrfException;
@ -230,7 +234,7 @@ public class AuthConfig {
return useContributorAgreements; return useContributorAgreements;
} }
public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) { public boolean isIdentityTrustable(Collection<ExternalId> ids) {
switch (getAuthType()) { switch (getAuthType()) {
case DEVELOPMENT_BECOME_ANY_ACCOUNT: case DEVELOPMENT_BECOME_ANY_ACCOUNT:
case HTTP: case HTTP:
@ -251,7 +255,7 @@ public class AuthConfig {
case OPENID: case OPENID:
// All identities must be trusted in order to trust the account. // All identities must be trusted in order to trust the account.
// //
for (final AccountExternalId e : ids) { for (ExternalId e : ids) {
if (!isTrusted(e)) { if (!isTrusted(e)) {
return false; return false;
} }
@ -265,8 +269,8 @@ public class AuthConfig {
} }
} }
private boolean isTrusted(final AccountExternalId id) { private boolean isTrusted(ExternalId id) {
if (id.isScheme(AccountExternalId.SCHEME_MAILTO)) { if (id.isScheme(SCHEME_MAILTO)) {
// mailto identities are created by sending a unique validation // mailto identities are created by sending a unique validation
// token to the address and asking them to come back to the site // token to the address and asking them to come back to the site
// with that token. // with that token.
@ -274,20 +278,20 @@ public class AuthConfig {
return true; return true;
} }
if (id.isScheme(AccountExternalId.SCHEME_UUID)) { if (id.isScheme(SCHEME_UUID)) {
// UUID identities are absolutely meaningless and cannot be // UUID identities are absolutely meaningless and cannot be
// constructed through any normal login process we use. // constructed through any normal login process we use.
// //
return true; return true;
} }
if (id.isScheme(AccountExternalId.SCHEME_USERNAME)) { if (id.isScheme(SCHEME_USERNAME)) {
// We can trust their username, its local to our server only. // We can trust their username, its local to our server only.
// //
return true; return true;
} }
for (final OpenIdProviderPattern p : trustedOpenIDs) { for (OpenIdProviderPattern p : trustedOpenIDs) {
if (p.matches(id)) { if (p.matches(id)) {
return true; return true;
} }

View File

@ -30,6 +30,7 @@ import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
@Singleton @Singleton
public class ConfirmEmail implements RestModifyView<ConfigResource, Input> { public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
@ -54,7 +55,7 @@ public class ConfirmEmail implements RestModifyView<ConfigResource, Input> {
@Override @Override
public Response<?> apply(ConfigResource rsrc, Input input) public Response<?> apply(ConfigResource rsrc, Input input)
throws AuthException, UnprocessableEntityException, AccountException, OrmException, throws AuthException, UnprocessableEntityException, AccountException, OrmException,
IOException { IOException, ConfigInvalidException {
CurrentUser user = self.get(); CurrentUser user = self.get();
if (!user.isIdentifiedUser()) { if (!user.isIdentifiedUser()) {
throw new AuthException("Authentication required"); throw new AuthException("Authentication required");

View File

@ -110,7 +110,8 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
String name = ref.getName(); String name = ref.getName();
Change.Id changeId; Change.Id changeId;
Account.Id accountId; Account.Id accountId;
if (name.startsWith(REFS_CACHE_AUTOMERGE) || (!showMetadata && isMetadata(name))) { if (name.startsWith(REFS_CACHE_AUTOMERGE)
|| (!showMetadata && isMetadata(projectCtl, name))) {
continue; continue;
} else if (RefNames.isRefsEdit(name)) { } else if (RefNames.isRefsEdit(name)) {
// Edits are visible only to the owning user, if change is visible. // Edits are visible only to the owning user, if change is visible.
@ -138,6 +139,12 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
if (viewMetadata) { if (viewMetadata) {
result.put(name, ref); result.put(name, ref);
} }
} else if (projectCtl.getProjectState().isAllUsers()
&& name.equals(RefNames.REFS_EXTERNAL_IDS)) {
// The notes branch with the external IDs of all users must not be exposed to normal users.
if (viewMetadata) {
result.put(name, ref);
}
} else if (projectCtl.controlForRef(ref.getLeaf().getName()).isVisible()) { } else if (projectCtl.controlForRef(ref.getLeaf().getName()).isVisible()) {
// Use the leaf to lookup the control data. If the reference is // Use the leaf to lookup the control data. If the reference is
// symbolic we want the control around the final target. If its // symbolic we want the control around the final target. If its
@ -264,8 +271,10 @@ public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
} }
} }
private static boolean isMetadata(String name) { private static boolean isMetadata(ProjectControl projectCtl, String name) {
return name.startsWith(REFS_CHANGES) || RefNames.isRefsEdit(name); return name.startsWith(REFS_CHANGES)
|| RefNames.isRefsEdit(name)
|| (projectCtl.getProjectState().isAllUsers() && name.equals(RefNames.REFS_EXTERNAL_IDS));
} }
private static boolean isTag(Ref ref) { private static boolean isTag(Ref ref) {

View File

@ -134,7 +134,8 @@ public class CommitValidators {
refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo), refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
new ConfigValidator(refControl, repo, allUsers), new ConfigValidator(refControl, repo, allUsers),
new BannedCommitsValidator(rejectCommits), new BannedCommitsValidator(rejectCommits),
new PluginCommitValidationListener(pluginValidators))); new PluginCommitValidationListener(pluginValidators),
new BlockExternalIdUpdateListener(allUsers)));
} }
} }
@ -149,7 +150,8 @@ public class CommitValidators {
new ChangeIdValidator( new ChangeIdValidator(
refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo), refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
new ConfigValidator(refControl, repo, allUsers), new ConfigValidator(refControl, repo, allUsers),
new PluginCommitValidationListener(pluginValidators))); new PluginCommitValidationListener(pluginValidators),
new BlockExternalIdUpdateListener(allUsers)));
} }
private CommitValidators forMergedCommits(RefControl refControl) { private CommitValidators forMergedCommits(RefControl refControl) {
@ -617,6 +619,25 @@ public class CommitValidators {
} }
} }
/** Blocks any update to refs/meta/external-ids */
public static class BlockExternalIdUpdateListener implements CommitValidationListener {
private final AllUsersName allUsers;
public BlockExternalIdUpdateListener(AllUsersName allUsers) {
this.allUsers = allUsers;
}
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
if (allUsers.equals(receiveEvent.project.getNameKey())
&& RefNames.REFS_EXTERNAL_IDS.equals(receiveEvent.refName)) {
throw new CommitValidationException("not allowed to update " + RefNames.REFS_EXTERNAL_IDS);
}
return Collections.emptyList();
}
}
private static CommitValidationMessage getInvalidEmailError( private static CommitValidationMessage getInvalidEmailError(
RevCommit c, RevCommit c,
String type, String type,

View File

@ -18,8 +18,8 @@ import com.google.common.base.Predicates;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.FluentIterable; import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.index.FieldDef; import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldType; import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.SchemaUtil; import com.google.gerrit.server.index.SchemaUtil;
@ -42,7 +42,7 @@ public class AccountField {
new FieldDef.Repeatable<AccountState, String>("external_id", FieldType.EXACT, false) { new FieldDef.Repeatable<AccountState, String>("external_id", FieldType.EXACT, false) {
@Override @Override
public Iterable<String> get(AccountState input, FillArgs args) { public Iterable<String> get(AccountState input, FillArgs args) {
return Iterables.transform(input.getExternalIds(), id -> id.getKey().get()); return Iterables.transform(input.getExternalIds(), id -> id.key().get());
} }
}; };
@ -54,8 +54,7 @@ public class AccountField {
String fullName = input.getAccount().getFullName(); String fullName = input.getAccount().getFullName();
Set<String> parts = Set<String> parts =
SchemaUtil.getNameParts( SchemaUtil.getNameParts(
fullName, fullName, Iterables.transform(input.getExternalIds(), ExternalId::email));
Iterables.transform(input.getExternalIds(), AccountExternalId::getEmailAddress));
// Additional values not currently added by getPersonParts. // Additional values not currently added by getPersonParts.
// TODO(dborowitz): Move to getPersonParts and remove this hack. // TODO(dborowitz): Move to getPersonParts and remove this hack.
@ -87,7 +86,7 @@ public class AccountField {
@Override @Override
public Iterable<String> get(AccountState input, FillArgs args) { public Iterable<String> get(AccountState input, FillArgs args) {
return FluentIterable.from(input.getExternalIds()) return FluentIterable.from(input.getExternalIds())
.transform(AccountExternalId::getEmailAddress) .transform(ExternalId::email)
.append(Collections.singleton(input.getAccount().getPreferredEmail())) .append(Collections.singleton(input.getAccount().getPreferredEmail()))
.filter(Predicates.notNull()) .filter(Predicates.notNull())
.transform(String::toLowerCase) .transform(String::toLowerCase)

View File

@ -18,6 +18,7 @@ import com.google.common.base.Joiner;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.index.IndexConfig; import com.google.gerrit.server.index.IndexConfig;
import com.google.gerrit.server.index.account.AccountIndexCollection; import com.google.gerrit.server.index.account.AccountIndexCollection;
import com.google.gerrit.server.query.InternalQuery; import com.google.gerrit.server.query.InternalQuery;
@ -71,11 +72,23 @@ public class InternalAccountQuery extends InternalQuery<AccountState> {
return query(AccountPredicates.email(emailPrefix)); return query(AccountPredicates.email(emailPrefix));
} }
public List<AccountState> byExternalId(String externalId) throws OrmException { public List<AccountState> byExternalId(String scheme, String id) throws OrmException {
return query(AccountPredicates.externalId(externalId)); return byExternalId(ExternalId.Key.create(scheme, id));
}
public List<AccountState> byExternalId(ExternalId.Key externalId) throws OrmException {
return query(AccountPredicates.externalId(externalId.toString()));
} }
public AccountState oneByExternalId(String externalId) throws OrmException { public AccountState oneByExternalId(String externalId) throws OrmException {
return oneByExternalId(ExternalId.Key.parse(externalId));
}
public AccountState oneByExternalId(String scheme, String id) throws OrmException {
return oneByExternalId(ExternalId.Key.create(scheme, id));
}
public AccountState oneByExternalId(ExternalId.Key externalId) throws OrmException {
List<AccountState> accountStates = byExternalId(externalId); List<AccountState> accountStates = byExternalId(externalId);
if (accountStates.size() == 1) { if (accountStates.size() == 1) {
return accountStates.get(0); return accountStates.get(0);

View File

@ -56,6 +56,18 @@ public class AclUtil {
} }
} }
public static void block(
ProjectConfig config, AccessSection section, String permission, GroupReference... groupList) {
Permission p = section.getPermission(permission, true);
for (GroupReference group : groupList) {
if (group != null) {
PermissionRule r = rule(config, group);
r.setBlock();
p.add(r);
}
}
}
public static void grant( public static void grant(
ProjectConfig config, ProjectConfig config,
AccessSection section, AccessSection section,

View File

@ -23,18 +23,13 @@ import static org.easymock.EasyMock.verify;
import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import com.google.gerrit.server.mail.Address; import com.google.gerrit.server.mail.Address;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Set;
import org.eclipse.jgit.lib.Config; import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.PersonIdent;
import org.junit.Before; import org.junit.Before;
@ -388,9 +383,6 @@ public class FromAddressGeneratorProviderTest {
account.setFullName(name); account.setFullName(name);
account.setPreferredEmail(email); account.setPreferredEmail(email);
return new AccountState( return new AccountState(
account, account, Collections.emptySet(), Collections.emptySet(), new HashMap<>());
Collections.<AccountGroup.UUID>emptySet(),
Collections.<AccountExternalId>emptySet(),
new HashMap<ProjectWatchKey, Set<NotifyType>>());
} }
} }

View File

@ -17,15 +17,10 @@ package com.google.gerrit.testutil;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.account.AccountCache; import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState; import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.WatchConfig.NotifyType;
import com.google.gerrit.server.account.WatchConfig.ProjectWatchKey;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** Fake implementation of {@link AccountCache} for testing. */ /** Fake implementation of {@link AccountCache} for testing. */
public class FakeAccountCache implements AccountCache { public class FakeAccountCache implements AccountCache {
@ -81,10 +76,6 @@ public class FakeAccountCache implements AccountCache {
} }
private static AccountState newState(Account account) { private static AccountState newState(Account account) {
return new AccountState( return new AccountState(account, ImmutableSet.of(), ImmutableSet.of(), new HashMap<>());
account,
ImmutableSet.<AccountGroup.UUID>of(),
ImmutableSet.<AccountExternalId>of(),
new HashMap<ProjectWatchKey, Set<NotifyType>>());
} }
} }

View File

@ -14,13 +14,13 @@
package com.google.gerrit.sshd; package com.google.gerrit.sshd;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME; import static com.google.gerrit.server.account.ExternalId.SCHEME_USERNAME;
import com.google.common.cache.CacheLoader; import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache; import com.google.common.cache.LoadingCache;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountSshKey; import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.ExternalId;
import com.google.gerrit.server.account.VersionedAuthorizedKeys; import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.cache.CacheModule; import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.ssh.SshKeyCache; import com.google.gerrit.server.ssh.SshKeyCache;
@ -103,14 +103,17 @@ public class SshKeyCacheImpl implements SshKeyCache {
@Override @Override
public Iterable<SshKeyCacheEntry> load(String username) throws Exception { public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
try (ReviewDb db = schema.open()) { try (ReviewDb db = schema.open()) {
AccountExternalId.Key key = new AccountExternalId.Key(SCHEME_USERNAME, username); ExternalId user =
AccountExternalId user = db.accountExternalIds().get(key); ExternalId.from(
db.accountExternalIds()
.get(
ExternalId.Key.create(SCHEME_USERNAME, username).asAccountExternalIdKey()));
if (user == null) { if (user == null) {
return NO_SUCH_USER; return NO_SUCH_USER;
} }
List<SshKeyCacheEntry> kl = new ArrayList<>(4); List<SshKeyCacheEntry> kl = new ArrayList<>(4);
for (AccountSshKey k : authorizedKeys.getKeys(user.getAccountId())) { for (AccountSshKey k : authorizedKeys.getKeys(user.accountId())) {
if (k.isValid()) { if (k.isValid()) {
add(kl, k); add(kl, k);
} }

View File

@ -263,7 +263,7 @@ final class SetAccountCommand extends SshCommand {
} }
private void addEmail(String email) private void addEmail(String email)
throws UnloggedFailure, RestApiException, OrmException, IOException { throws UnloggedFailure, RestApiException, OrmException, IOException, ConfigInvalidException {
EmailInput in = new EmailInput(); EmailInput in = new EmailInput();
in.email = email; in.email = email;
in.noConfirmation = true; in.noConfirmation = true;
@ -274,7 +274,8 @@ final class SetAccountCommand extends SshCommand {
} }
} }
private void deleteEmail(String email) throws RestApiException, OrmException, IOException { private void deleteEmail(String email)
throws RestApiException, OrmException, IOException, ConfigInvalidException {
if (email.equals("ALL")) { if (email.equals("ALL")) {
List<EmailInfo> emails = getEmails.apply(rsrc); List<EmailInfo> emails = getEmails.apply(rsrc);
for (EmailInfo e : emails) { for (EmailInfo e : emails) {