Store SSH keys in git

The public SSH keys of a user are now stored in an authorized_keys
file in the All-Users repository in the refs/users/CD/ABCD branch of
the user.

Storing SSH keys in an authorized_keys file is the standard way for
SSH to store public keys. Each key is stored on a separate line.

The order of the keys in the file determines the sequence numbers of
the keys.

Invalid keys are marked with the prefix '# INVALID'.

To keep the sequence numbers intact when a key is deleted, a
'# DELETED' line is inserted at the position where the key was
deleted.

Other comment lines are ignored on read, and are not written back when
the file is modified.

Supporting a 2-step live migration for a multi-master Gerrit
installation is not needed since the googlesource.com instances do not
use SSH and there are no other multi-master installations.

On creation of an SSH key, RFC 4716 style keys need to be converted to
OpenSSH style keys. Also before adding a key it should be checked that
the key is parseable. Both of this requires classes from SSH libs that
are only available in the SSH layer. This is why the SSH key creation
must be done there. So far this was done in SshKeyCacheImpl, but since
SshKeyCacheImpl needs VersionedAuthorizedKeys to load keys and to mark
keys as invalid, VersionedAuthorizedKeys cannot not depend on
SshKeyCacheImpl as this would be a cyclic dependency. Instead split
out the SSH key creation from SshKeyCacheImpl into SshKeyCreatorImpl.
This way SshKeyCacheImpl depends on VersionedAuthorizedKeys and
VersionedAuthorizedKeys depends on SshKeyCreatorImpl, and there is no
dependency circle.

Change-Id: I8fcc3c0f27e034fc2c8e8ae3612068099075467d
Signed-off-by: Edwin Kempin <ekempin@google.com>
This commit is contained in:
Edwin Kempin
2016-04-07 14:00:17 +02:00
parent 3cf19f686a
commit 07952c069a
31 changed files with 1136 additions and 221 deletions

View File

@@ -22,13 +22,12 @@ 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.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountByEmailCache;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@@ -47,18 +46,23 @@ import java.util.Map;
public class AccountCreator {
private final Map<String, TestAccount> accounts;
private SchemaFactory<ReviewDb> reviewDbProvider;
private GroupCache groupCache;
private SshKeyCache sshKeyCache;
private AccountCache accountCache;
private AccountByEmailCache byEmailCache;
private final SchemaFactory<ReviewDb> reviewDbProvider;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final GroupCache groupCache;
private final SshKeyCache sshKeyCache;
private final AccountCache accountCache;
private final AccountByEmailCache byEmailCache;
@Inject
AccountCreator(SchemaFactory<ReviewDb> schema, GroupCache groupCache,
SshKeyCache sshKeyCache, AccountCache accountCache,
AccountCreator(SchemaFactory<ReviewDb> schema,
VersionedAuthorizedKeys.Accessor authorizedKeys,
GroupCache groupCache,
SshKeyCache sshKeyCache,
AccountCache accountCache,
AccountByEmailCache byEmailCache) {
accounts = new HashMap<>();
reviewDbProvider = schema;
this.authorizedKeys = authorizedKeys;
this.groupCache = groupCache;
this.sshKeyCache = sshKeyCache;
this.accountCache = accountCache;
@@ -66,17 +70,14 @@ public class AccountCreator {
}
public synchronized TestAccount create(String username, String email,
String fullName, String... groups)
throws OrmException, UnsupportedEncodingException, JSchException {
String fullName, String... groups) throws Exception {
TestAccount account = accounts.get(username);
if (account != null) {
return account;
}
try (ReviewDb db = reviewDbProvider.open()) {
Account.Id id = new Account.Id(db.nextAccountId());
KeyPair sshKey = genSshKey();
AccountSshKey key =
new AccountSshKey(new AccountSshKey.Id(id, 1), publicKey(sshKey, email));
AccountExternalId extUser =
new AccountExternalId(id, new AccountExternalId.Key(
AccountExternalId.SCHEME_USERNAME, username));
@@ -95,8 +96,6 @@ public class AccountCreator {
a.setPreferredEmail(email);
db.accounts().insert(Collections.singleton(a));
db.accountSshKeys().insert(Collections.singleton(key));
if (groups != null) {
for (String n : groups) {
AccountGroup.NameKey k = new AccountGroup.NameKey(n);
@@ -107,7 +106,10 @@ public class AccountCreator {
}
}
KeyPair sshKey = genSshKey();
authorizedKeys.addKey(id, publicKey(sshKey, email));
sshKeyCache.evict(username);
accountCache.evictByUsername(username);
byEmailCache.evict(email);
@@ -118,35 +120,29 @@ public class AccountCreator {
}
}
public TestAccount create(String username, String group)
throws OrmException, UnsupportedEncodingException, JSchException {
public TestAccount create(String username, String group) throws Exception {
return create(username, null, username, group);
}
public TestAccount create(String username)
throws UnsupportedEncodingException, OrmException, JSchException {
public TestAccount create(String username) throws Exception {
return create(username, null, username, (String[]) null);
}
public TestAccount admin()
throws UnsupportedEncodingException, OrmException, JSchException {
public TestAccount admin() throws Exception {
return create("admin", "admin@example.com", "Administrator",
"Administrators");
}
public TestAccount admin2()
throws UnsupportedEncodingException, OrmException, JSchException {
public TestAccount admin2() throws Exception {
return create("admin2", "admin2@example.com", "Administrator2",
"Administrators");
}
public TestAccount user()
throws UnsupportedEncodingException, OrmException, JSchException {
public TestAccount user() throws Exception {
return create("user", "user@example.com", "User");
}
public TestAccount user2()
throws UnsupportedEncodingException, OrmException, JSchException {
public TestAccount user2() throws Exception {
return create("user2", "user2@example.com", "User2");
}
@@ -169,6 +165,6 @@ public class AccountCreator {
throws UnsupportedEncodingException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
sshKey.writePublicKey(out, comment);
return out.toString(US_ASCII.name());
return out.toString(US_ASCII.name()).trim();
}
}

View File

@@ -43,14 +43,17 @@ import java.util.Collections;
public class InitAdminUser implements InitStep {
private final ConsoleUI ui;
private final InitFlags flags;
private final VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory;
private SchemaFactory<ReviewDb> dbFactory;
@Inject
InitAdminUser(
InitFlags flags,
ConsoleUI ui) {
ConsoleUI ui,
VersionedAuthorizedKeysOnInit.Factory authorizedKeysFactory) {
this.flags = flags;
this.ui = ui;
this.authorizedKeysFactory = authorizedKeysFactory;
}
@Override
@@ -110,7 +113,10 @@ public class InitAdminUser implements InitStep {
db.accountGroupMembers().insert(Collections.singleton(m));
if (sshKey != null) {
db.accountSshKeys().insert(Collections.singleton(sshKey));
VersionedAuthorizedKeysOnInit authorizedKeys =
authorizedKeysFactory.create(id).load();
authorizedKeys.addKey(sshKey.getSshPublicKey());
authorizedKeys.save("Added SSH key for initial admin user\n");
}
}
}

View File

@@ -42,6 +42,7 @@ public class InitModule extends FactoryModule {
bind(Libraries.class);
bind(LibraryDownloader.class);
factory(Section.Factory.class);
factory(VersionedAuthorizedKeysOnInit.Factory.class);
// Steps are executed in the order listed here.
//

View File

@@ -0,0 +1,198 @@
// 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.common.base.Preconditions.checkState;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.gerrit.pgm.init.api.InitFlags;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.account.AuthorizedKeys;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.util.FS;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
public class VersionedAuthorizedKeysOnInit extends VersionedMetaData {
public static interface Factory {
VersionedAuthorizedKeysOnInit create(Account.Id accountId);
}
private final Account.Id accountId;
private final String ref;
private final String project;
private final SitePaths site;
private final InitFlags flags;
private List<Optional<AccountSshKey>> keys;
private ObjectId revision;
@Inject
public VersionedAuthorizedKeysOnInit(
AllUsersNameOnInitProvider allUsers,
SitePaths site,
InitFlags flags,
@Assisted Account.Id accountId) {
this.project = allUsers.get();
this.site = site;
this.flags = flags;
this.accountId = accountId;
this.ref = RefNames.refsUsers(accountId);
}
@Override
protected String getRefName() {
return ref;
}
public VersionedAuthorizedKeysOnInit load()
throws IOException, ConfigInvalidException {
File path = getPath();
if (path != null) {
try (Repository repo = new FileRepository(path)) {
load(repo);
}
}
return this;
}
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(project).toFile(), FS.DETECTED);
}
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
revision = getRevision();
keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
}
public AccountSshKey addKey(String pub) {
checkState(keys != null, "SSH keys not loaded yet");
int seq = keys.isEmpty() ? 1 : keys.size() + 1;
AccountSshKey.Id keyId = new AccountSshKey.Id(accountId, seq);
AccountSshKey key =
new VersionedAuthorizedKeys.SimpleSshKeyCreator().create(keyId, pub);
keys.add(Optional.of(key));
return key;
}
public void save(String message) throws IOException {
save(new PersonIdent("Gerrit Initialization", "init@gerrit"), message);
}
private void save(PersonIdent ident, String msg) throws IOException {
File path = getPath();
if (path == null) {
throw new IOException(project + " does not exist.");
}
try (Repository repo = new FileRepository(path);
ObjectInserter i = repo.newObjectInserter();
ObjectReader r = repo.newObjectReader();
RevWalk rw = new RevWalk(reader)) {
inserter = i;
reader = r;
RevTree srcTree = revision != null ? rw.parseTree(revision) : null;
newTree = readTree(srcTree);
CommitBuilder commit = new CommitBuilder();
commit.setAuthor(ident);
commit.setCommitter(ident);
commit.setMessage(msg);
onSave(commit);
ObjectId res = newTree.writeTree(inserter);
if (res.equals(srcTree)) {
return;
}
commit.setTreeId(res);
if (revision != null) {
commit.addParentId(revision);
}
ObjectId newRevision = inserter.insert(commit);
updateRef(repo, ident, newRevision, "commit: " + msg);
revision = newRevision;
} finally {
inserter = null;
reader = null;
}
}
@Override
protected boolean onSave(CommitBuilder commit) throws IOException {
if (Strings.isNullOrEmpty(commit.getMessage())) {
commit.setMessage("Updated SSH keys\n");
}
saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys));
return true;
}
private void updateRef(Repository repo, PersonIdent ident,
ObjectId newRevision, String refLogMsg) throws IOException {
RefUpdate ru = repo.updateRef(getRefName());
ru.setRefLogIdent(ident);
ru.setNewObjectId(newRevision);
ru.setExpectedOldObjectId(revision);
ru.setRefLogMessage(refLogMsg, false);
RefUpdate.Result r = ru.update();
switch(r) {
case FAST_FORWARD:
case NEW:
case NO_CHANGE:
break;
case FORCED:
case IO_FAILURE:
case LOCK_FAILURE:
case NOT_ATTEMPTED:
case REJECTED:
case REJECTED_CURRENT_BRANCH:
case RENAMED:
default:
throw new IOException("Failed to update " + getRefName() + " of "
+ project + ": " + r.name());
}
}
}

View File

@@ -17,6 +17,8 @@ package com.google.gerrit.reviewdb.client;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.IntKey;
import java.util.Objects;
/** An SSH key approved for use by an {@link Account}. */
public final class AccountSshKey {
public static class Id extends IntKey<Account.Id> {
@@ -129,4 +131,20 @@ public final class AccountSshKey {
public void setInvalid() {
valid = false;
}
@Override
public boolean equals(Object o) {
if (o instanceof AccountSshKey) {
AccountSshKey other = (AccountSshKey) o;
return Objects.equals(id, other.id)
&& Objects.equals(sshPublicKey, other.sshPublicKey)
&& Objects.equals(valid, other.valid);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(id, sshPublicKey, valid);
}
}

View File

@@ -1,36 +0,0 @@
// Copyright (C) 2008 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.reviewdb.server;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gwtorm.server.Access;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.PrimaryKey;
import com.google.gwtorm.server.Query;
import com.google.gwtorm.server.ResultSet;
public interface AccountSshKeyAccess extends
Access<AccountSshKey, AccountSshKey.Id> {
@Override
@PrimaryKey("id")
AccountSshKey get(AccountSshKey.Id id) throws OrmException;
@Query("WHERE id.accountId = ?")
ResultSet<AccountSshKey> byAccount(Account.Id id) throws OrmException;
@Query("WHERE id.accountId = ? ORDER BY id.seq DESC LIMIT 1")
ResultSet<AccountSshKey> byAccountLast(Account.Id id) throws OrmException;
}

View File

@@ -53,8 +53,7 @@ public interface ReviewDb extends Schema {
@Relation(id = 7)
AccountExternalIdAccess accountExternalIds();
@Relation(id = 8)
AccountSshKeyAccess accountSshKeys();
// Deleted @Relation(id = 8)
@Relation(id = 10)
AccountGroupAccess accountGroups();

View File

@@ -88,11 +88,6 @@ public class ReviewDbWrapper implements ReviewDb {
return delegate.accountExternalIds();
}
@Override
public AccountSshKeyAccess accountSshKeys() {
return delegate.accountSshKeys();
}
@Override
public AccountGroupAccess accountGroups() {
return delegate.accountGroups();

View File

@@ -47,11 +47,6 @@ CREATE INDEX account_project_watches_byP
ON account_project_watches (project_name);
-- *********************************************************************
-- AccountSshKeyAccess
-- @PrimaryKey covers: byAccount, valid
-- *********************************************************************
-- ApprovalCategoryAccess
-- too small to bother indexing

View File

@@ -54,11 +54,6 @@ CREATE INDEX acc_project_watches_byProject
ON account_project_watches (project_name)
#
-- *********************************************************************
-- AccountSshKeyAccess
-- @PrimaryKey covers: byAccount, valid
-- *********************************************************************
-- ApprovalCategoryAccess
-- too small to bother indexing

View File

@@ -95,11 +95,6 @@ CREATE INDEX account_project_watches_byP
ON account_project_watches (project_name);
-- *********************************************************************
-- AccountSshKeyAccess
-- @PrimaryKey covers: byAccount, valid
-- *********************************************************************
-- ApprovalCategoryAccess
-- too small to bother indexing

View File

@@ -16,7 +16,6 @@ package com.google.gerrit.server.account;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.Iterables;
import com.google.common.io.ByteSource;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.common.errors.InvalidSshKeyException;
@@ -27,24 +26,22 @@ import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AddSshKey.Input;
import com.google.gerrit.server.mail.AddKeySender;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
@Singleton
public class AddSshKey implements RestModifyView<AccountResource, Input> {
@@ -55,22 +52,25 @@ public class AddSshKey implements RestModifyView<AccountResource, Input> {
}
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final SshKeyCache sshKeyCache;
private final AddKeySender.Factory addKeyFactory;
@Inject
AddSshKey(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider,
SshKeyCache sshKeyCache, AddKeySender.Factory addKeyFactory) {
AddSshKey(Provider<CurrentUser> self,
VersionedAuthorizedKeys.Accessor authorizedKeys,
SshKeyCache sshKeyCache,
AddKeySender.Factory addKeyFactory) {
this.self = self;
this.dbProvider = dbProvider;
this.authorizedKeys = authorizedKeys;
this.sshKeyCache = sshKeyCache;
this.addKeyFactory = addKeyFactory;
}
@Override
public Response<SshKeyInfo> apply(AccountResource rsrc, Input input)
throws AuthException, BadRequestException, OrmException, IOException {
throws AuthException, BadRequestException, OrmException, IOException,
ConfigInvalidException {
if (self.get() != rsrc.getUser()
&& !self.get().getCapabilities().canAdministrateServer()) {
throw new AuthException("not allowed to add SSH keys");
@@ -79,7 +79,8 @@ public class AddSshKey implements RestModifyView<AccountResource, Input> {
}
public Response<SshKeyInfo> apply(IdentifiedUser user, Input input)
throws BadRequestException, OrmException, IOException {
throws BadRequestException, IOException,
ConfigInvalidException {
if (input == null) {
input = new Input();
}
@@ -87,11 +88,6 @@ public class AddSshKey implements RestModifyView<AccountResource, Input> {
throw new BadRequestException("SSH public key missing");
}
ResultSet<AccountSshKey> byAccountLast =
dbProvider.get().accountSshKeys().byAccountLast(user.getAccountId());
AccountSshKey last = Iterables.getOnlyElement(byAccountLast, null);
int max = last == null ? 0 : last.getKey().get();
final RawInput rawKey = input.raw;
String sshPublicKey = new ByteSource() {
@Override
@@ -102,15 +98,15 @@ public class AddSshKey implements RestModifyView<AccountResource, Input> {
try {
AccountSshKey sshKey =
sshKeyCache.create(new AccountSshKey.Id(
user.getAccountId(), max + 1), sshPublicKey);
dbProvider.get().accountSshKeys().insert(Collections.singleton(sshKey));
authorizedKeys.addKey(user.getAccountId(), sshPublicKey);
try {
addKeyFactory.create(user, sshKey).send();
} catch (EmailException e) {
log.error("Cannot send SSH key added message to "
+ user.getAccount().getPreferredEmail(), e);
}
sshKeyCache.evict(user.getUserName());
return Response.<SshKeyInfo>created(GetSshKeys.newSshKeyInfo(sshKey));
} catch (InvalidSshKeyException e) {

View File

@@ -0,0 +1,78 @@
// 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 com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class AuthorizedKeys {
public static final String FILE_NAME = "authorized_keys";
@VisibleForTesting
public static final String INVALID_KEY_COMMENT_PREFIX = "# INVALID ";
@VisibleForTesting
public static final String DELETED_KEY_COMMENT = "# DELETED";
public static List<Optional<AccountSshKey>> parse(
Account.Id accountId, String s) {
List<Optional<AccountSshKey>> keys = new ArrayList<>();
int seq = 1;
for (String line : s.split("\\r?\\n")) {
line = line.trim();
if (line.isEmpty()) {
continue;
} else if (line.startsWith(INVALID_KEY_COMMENT_PREFIX)) {
String pub = line.substring(INVALID_KEY_COMMENT_PREFIX.length());
AccountSshKey key =
new AccountSshKey(new AccountSshKey.Id(accountId, seq++), pub);
key.setInvalid();
keys.add(Optional.of(key));
} else if (line.startsWith(DELETED_KEY_COMMENT)) {
keys.add(Optional.<AccountSshKey> absent());
seq++;
} else if (line.startsWith("#")) {
continue;
} else {
AccountSshKey key =
new AccountSshKey(new AccountSshKey.Id(accountId, seq++), line);
keys.add(Optional.of(key));
}
}
return keys;
}
public static String serialize(Collection<Optional<AccountSshKey>> keys) {
StringBuilder b = new StringBuilder();
for (Optional<AccountSshKey> key : keys) {
if (key.isPresent()) {
if (!key.get().isValid()) {
b.append(INVALID_KEY_COMMENT_PREFIX);
}
b.append(key.get().getSshPublicKey().trim());
} else {
b.append(DELETED_KEY_COMMENT);
}
b.append("\n");
}
return b.toString();
}
}

View File

@@ -34,7 +34,6 @@ 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.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.CreateAccount.Input;
@@ -48,7 +47,9 @@ import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.apache.commons.validator.routines.EmailValidator;
import org.eclipse.jgit.errors.ConfigInvalidException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -73,37 +74,45 @@ public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
private final ReviewDb db;
private final Provider<IdentifiedUser> currentUser;
private final GroupsCollection groupsCollection;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final SshKeyCache sshKeyCache;
private final AccountCache accountCache;
private final AccountByEmailCache byEmailCache;
private final AccountLoader.Factory infoLoader;
private final DynamicSet<AccountExternalIdCreator> externalIdCreators;
private final String username;
private final AuditService auditService;
private final String username;
@Inject
CreateAccount(ReviewDb db, Provider<IdentifiedUser> currentUser,
GroupsCollection groupsCollection, SshKeyCache sshKeyCache,
AccountCache accountCache, AccountByEmailCache byEmailCache,
CreateAccount(ReviewDb db,
Provider<IdentifiedUser> currentUser,
GroupsCollection groupsCollection,
VersionedAuthorizedKeys.Accessor authorizedKeys,
SshKeyCache sshKeyCache,
AccountCache accountCache,
AccountByEmailCache byEmailCache,
AccountLoader.Factory infoLoader,
DynamicSet<AccountExternalIdCreator> externalIdCreators,
@Assisted String username, AuditService auditService) {
AuditService auditService,
@Assisted String username) {
this.db = db;
this.currentUser = currentUser;
this.groupsCollection = groupsCollection;
this.authorizedKeys = authorizedKeys;
this.sshKeyCache = sshKeyCache;
this.accountCache = accountCache;
this.byEmailCache = byEmailCache;
this.infoLoader = infoLoader;
this.externalIdCreators = externalIdCreators;
this.username = username;
this.auditService = auditService;
this.username = username;
}
@Override
public Response<AccountInfo> apply(TopLevelResource rsrc, Input input)
throws BadRequestException, ResourceConflictException,
UnprocessableEntityException, OrmException {
UnprocessableEntityException, OrmException, IOException,
ConfigInvalidException {
if (input == null) {
input = new Input();
}
@@ -119,7 +128,6 @@ public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
Set<AccountGroup.Id> groups = parseGroups(input.groups);
Account.Id id = new Account.Id(db.nextAccountId());
AccountSshKey key = createSshKey(id, input.sshKey);
AccountExternalId extUser =
new AccountExternalId(id, new AccountExternalId.Key(
@@ -178,10 +186,6 @@ public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
a.setPreferredEmail(input.email);
db.accounts().insert(Collections.singleton(a));
if (key != null) {
db.accountSshKeys().insert(Collections.singleton(key));
}
for (AccountGroup.Id groupId : groups) {
AccountGroupMember m =
new AccountGroupMember(new AccountGroupMember.Key(id, groupId));
@@ -190,7 +194,15 @@ public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
db.accountGroupMembers().insert(Collections.singleton(m));
}
if (input.sshKey != null) {
try {
authorizedKeys.addKey(id, input.sshKey);
sshKeyCache.evict(username);
} catch (InvalidSshKeyException e) {
throw new BadRequestException(e.getMessage());
}
}
accountCache.evictByUsername(username);
byEmailCache.evict(input.email);
@@ -212,18 +224,6 @@ public class CreateAccount implements RestModifyView<TopLevelResource, Input> {
return groupIds;
}
private AccountSshKey createSshKey(Account.Id id, String sshKey)
throws BadRequestException {
if (sshKey == null) {
return null;
}
try {
return sshKeyCache.create(new AccountSshKey.Id(id, 1), sshKey.trim());
} catch (InvalidSshKeyException e) {
throw new BadRequestException(e.getMessage());
}
}
private AccountExternalId.Key getEmailKey(String email) {
return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
}

View File

@@ -17,7 +17,6 @@ package com.google.gerrit.server.account;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.DeleteSshKey.Input;
import com.google.gerrit.server.ssh.SshKeyCache;
@@ -26,7 +25,10 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Collections;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import java.io.IOException;
@Singleton
public class DeleteSshKey implements
@@ -35,28 +37,31 @@ public class DeleteSshKey implements
}
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final SshKeyCache sshKeyCache;
@Inject
DeleteSshKey(Provider<ReviewDb> dbProvider,
Provider<CurrentUser> self,
DeleteSshKey(Provider<CurrentUser> self,
VersionedAuthorizedKeys.Accessor authorizedKeys,
SshKeyCache sshKeyCache) {
this.self = self;
this.dbProvider = dbProvider;
this.authorizedKeys = authorizedKeys;
this.sshKeyCache = sshKeyCache;
}
@Override
public Response<?> apply(AccountResource.SshKey rsrc, Input input)
throws AuthException, OrmException {
throws AuthException, OrmException, RepositoryNotFoundException,
IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser()
&& !self.get().getCapabilities().canAdministrateServer()) {
throw new AuthException("not allowed to delete SSH keys");
}
dbProvider.get().accountSshKeys()
.deleteKeys(Collections.singleton(rsrc.getSshKey().getKey()));
authorizedKeys.deleteKey(rsrc.getUser().getAccountId(),
rsrc.getSshKey().getKey().get());
sshKeyCache.evict(rsrc.getUser().getUserName());
return Response.none();
}
}

View File

@@ -14,13 +14,13 @@
package com.google.gerrit.server.account;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.common.SshKeyInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gwtorm.server.OrmException;
@@ -28,23 +28,29 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import java.io.IOException;
import java.util.List;
@Singleton
public class GetSshKeys implements RestReadView<AccountResource> {
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
@Inject
GetSshKeys(Provider<CurrentUser> self, Provider<ReviewDb> dbProvider) {
GetSshKeys(Provider<CurrentUser> self,
VersionedAuthorizedKeys.Accessor authorizedKeys) {
this.self = self;
this.dbProvider = dbProvider;
this.authorizedKeys = authorizedKeys;
}
@Override
public List<SshKeyInfo> apply(AccountResource rsrc) throws AuthException,
OrmException {
public List<SshKeyInfo> apply(AccountResource rsrc)
throws AuthException, OrmException, RepositoryNotFoundException,
IOException, ConfigInvalidException {
if (self.get() != rsrc.getUser()
&& !self.get().getCapabilities().canModifyAccount()) {
throw new AuthException("not allowed to get SSH keys");
@@ -52,13 +58,15 @@ public class GetSshKeys implements RestReadView<AccountResource> {
return apply(rsrc.getUser());
}
public List<SshKeyInfo> apply(IdentifiedUser user) throws OrmException {
List<SshKeyInfo> sshKeys = Lists.newArrayList();
for (AccountSshKey sshKey : dbProvider.get().accountSshKeys()
.byAccount(user.getAccountId()).toList()) {
sshKeys.add(newSshKeyInfo(sshKey));
public List<SshKeyInfo> apply(IdentifiedUser user)
throws RepositoryNotFoundException, IOException, ConfigInvalidException {
return Lists.transform(authorizedKeys.getKeys(user.getAccountId()),
new Function<AccountSshKey, SshKeyInfo>() {
@Override
public SshKeyInfo apply(AccountSshKey key) {
return newSshKeyInfo(key);
}
return sshKeys;
});
}
public static SshKeyInfo newSshKeyInfo(AccountSshKey sshKey) {

View File

@@ -20,7 +20,6 @@ import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gwtorm.server.OrmException;
@@ -28,22 +27,26 @@ import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import java.io.IOException;
@Singleton
public class SshKeys implements
ChildCollection<AccountResource, AccountResource.SshKey> {
private final DynamicMap<RestView<AccountResource.SshKey>> views;
private final GetSshKeys list;
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
@Inject
SshKeys(DynamicMap<RestView<AccountResource.SshKey>> views,
GetSshKeys list, Provider<CurrentUser> self,
Provider<ReviewDb> dbProvider) {
VersionedAuthorizedKeys.Accessor authorizedKeys) {
this.views = views;
this.list = list;
this.self = self;
this.dbProvider = dbProvider;
this.authorizedKeys = authorizedKeys;
}
@Override
@@ -53,7 +56,8 @@ public class SshKeys implements
@Override
public AccountResource.SshKey parse(AccountResource rsrc, IdString id)
throws ResourceNotFoundException, OrmException {
throws ResourceNotFoundException, OrmException, IOException,
ConfigInvalidException {
if (self.get() != rsrc.getUser()
&& !self.get().getCapabilities().canModifyAccount()) {
throw new ResourceNotFoundException();
@@ -62,12 +66,10 @@ public class SshKeys implements
}
public AccountResource.SshKey parse(IdentifiedUser user, IdString id)
throws ResourceNotFoundException, OrmException {
throws ResourceNotFoundException, IOException, ConfigInvalidException {
try {
int seq = Integer.parseInt(id.get(), 10);
AccountSshKey sshKey =
dbProvider.get().accountSshKeys()
.get(new AccountSshKey.Id(user.getAccountId(), seq));
AccountSshKey sshKey = authorizedKeys.getKey(user.getAccountId(), seq);
if (sshKey == null) {
throw new ResourceNotFoundException(id);
}

View File

@@ -0,0 +1,316 @@
// 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 com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.client.AccountSshKey.Id;
import com.google.gerrit.reviewdb.client.RefNames;
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.MetaDataUpdate;
import com.google.gerrit.server.git.VersionedMetaData;
import com.google.gerrit.server.ssh.SshKeyCreator;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* 'authorized_keys' file in the refs/users/CD/ABCD branches of the All-Users
* repository.
*
* The `authorized_keys' files stores the public SSH keys of the user. The file
* format matches the standard SSH file format, which means that each key is
* stored on a separate line (see
* https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys).
*
* The order of the keys in the file determines the sequence numbers of the
* keys. The first line corresponds to sequence number 1.
*
* Invalid keys are marked with the prefix <code># INVALID</code>.
*
* To keep the sequence numbers intact when a key is deleted, a
* <code># DELETED</code> line is inserted at the position where the key was
* deleted.
*
* Other comment lines are ignored on read, and are not written back when the
* file is modified.
*/
public class VersionedAuthorizedKeys extends VersionedMetaData
implements AutoCloseable {
@Singleton
public static class Accessor {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final VersionedAuthorizedKeys.Factory authorizedKeysFactory;
private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
private final IdentifiedUser.GenericFactory userFactory;
@Inject
Accessor(
GitRepositoryManager repoManager,
AllUsersName allUsersName,
VersionedAuthorizedKeys.Factory authorizedKeysFactory,
Provider<MetaDataUpdate.User> metaDataUpdateFactory,
IdentifiedUser.GenericFactory userFactory) {
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.authorizedKeysFactory = authorizedKeysFactory;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.userFactory = userFactory;
}
public List<AccountSshKey> getKeys(Account.Id accountId)
throws IOException, ConfigInvalidException {
return read(accountId).getKeys();
}
public AccountSshKey getKey(Account.Id accountId, int seq)
throws IOException, ConfigInvalidException {
return read(accountId).getKey(seq);
}
public AccountSshKey addKey(Account.Id accountId, String pub)
throws IOException, ConfigInvalidException, InvalidSshKeyException {
try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
AccountSshKey key = authorizedKeys.addKey(pub);
commit(authorizedKeys);
return key;
}
}
public void deleteKey(Account.Id accountId, int seq)
throws IOException, ConfigInvalidException {
try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
if (authorizedKeys.deleteKey(seq)) {
commit(authorizedKeys);
}
}
}
public void markKeyInvalid(Account.Id accountId, int seq)
throws IOException, ConfigInvalidException {
try (VersionedAuthorizedKeys authorizedKeys = open(accountId)) {
if (authorizedKeys.markKeyInvalid(seq)) {
commit(authorizedKeys);
}
}
}
private VersionedAuthorizedKeys read(Account.Id accountId)
throws IOException, ConfigInvalidException {
try (Repository git = repoManager.openRepository(allUsersName)) {
VersionedAuthorizedKeys authorizedKeys =
authorizedKeysFactory.create(accountId);
authorizedKeys.load(git);
return authorizedKeys;
}
}
private VersionedAuthorizedKeys open(Account.Id accountId)
throws IOException, ConfigInvalidException {
Repository git = repoManager.openRepository(allUsersName);
VersionedAuthorizedKeys authorizedKeys =
authorizedKeysFactory.create(accountId);
authorizedKeys.load(git);
return authorizedKeys;
}
private void commit(VersionedAuthorizedKeys authorizedKeys)
throws IOException {
try (MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName,
userFactory.create(authorizedKeys.accountId))) {
authorizedKeys.commit(md);
}
}
}
public static class SimpleSshKeyCreator implements SshKeyCreator {
@Override
public AccountSshKey create(Id id, String encoded) {
return new AccountSshKey(id, encoded);
}
}
public static interface Factory {
VersionedAuthorizedKeys create(Account.Id accountId);
}
private final SshKeyCreator sshKeyCreator;
private final Account.Id accountId;
private final String ref;
private Repository git;
private List<Optional<AccountSshKey>> keys;
@Inject
public VersionedAuthorizedKeys(
SshKeyCreator sshKeyCreator,
@Assisted Account.Id accountId) {
this.sshKeyCreator = sshKeyCreator;
this.accountId = accountId;
this.ref = RefNames.refsUsers(accountId);
}
@Override
protected String getRefName() {
return ref;
}
@Override
public void load(Repository git) throws IOException, ConfigInvalidException {
checkState(this.git == null);
this.git = git;
super.load(git);
}
@Override
protected void onLoad() throws IOException {
keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME));
}
@Override
protected boolean onSave(CommitBuilder commit) throws IOException {
if (Strings.isNullOrEmpty(commit.getMessage())) {
commit.setMessage("Updated SSH keys\n");
}
saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys));
return true;
}
/** Returns all SSH keys. */
private List<AccountSshKey> getKeys() {
checkLoaded();
return Lists.newArrayList(Optional.presentInstances(keys));
}
/**
* Returns the SSH key with the given sequence number.
*
* @param seq sequence number
* @return the SSH key, <code>null</code> if there is no SSH key with this
* sequence number, or if the SSH key with this sequence number has
* been deleted
*/
private AccountSshKey getKey(int seq) {
checkLoaded();
Optional<AccountSshKey> key = keys.get(seq - 1);
return key.orNull();
}
/**
* Adds a new public SSH key.
*
* @param pub the public SSH key to be added
* @return the new SSH key
* @throws InvalidSshKeyException
*/
private AccountSshKey addKey(String pub) throws InvalidSshKeyException {
checkLoaded();
int seq = keys.size() + 1;
AccountSshKey.Id keyId = new AccountSshKey.Id(accountId, seq);
AccountSshKey key = sshKeyCreator.create(keyId, pub);
keys.add(Optional.of(key));
return key;
}
/**
* Deletes the SSH key with the given sequence number.
*
* @param seq the sequence number
* @return <code>true</code> if a key with this sequence number was found and
* deleted, <code>false</code> if no key with the given sequence
* number exists
*/
private boolean deleteKey(int seq) {
checkLoaded();
if (seq <= keys.size() && keys.get(seq - 1).isPresent()) {
keys.set(seq - 1, Optional.<AccountSshKey> absent());
return true;
}
return false;
}
/**
* Marks the SSH key with the given sequence number as invalid.
*
* @param seq the sequence number
* @return <code>true</code> if a key with this sequence number was found and
* marked as invalid, <code>false</code> if no key with the given
* sequence number exists or if the key was already marked as invalid
*/
private boolean markKeyInvalid(int seq) {
checkLoaded();
AccountSshKey key = getKey(seq);
if (key != null && key.isValid()) {
key.setInvalid();
return true;
}
return false;
}
/**
* Sets new SSH keys.
*
* The existing SSH keys are overwritten.
*
* @param newKeys the new public SSH keys
*/
public void setKeys(Collection<AccountSshKey> newKeys) {
Ordering<AccountSshKey> o =
Ordering.natural().onResultOf(new Function<AccountSshKey, Integer>() {
@Override
public Integer apply(AccountSshKey sshKey) {
return sshKey.getKey().get();
}
});
keys = Collections.nCopies(o.max(newKeys).getKey().get(),
Optional.<AccountSshKey> absent());
for (AccountSshKey key : newKeys) {
keys.set(key.getKey().get() - 1, Optional.of(key));
}
}
@Override
public void close() {
if (git != null) {
git.close();
}
}
private void checkLoaded() {
checkNotNull(keys, "SSH keys not loaded yet");
}
}

View File

@@ -233,7 +233,7 @@ public class AccountApiImpl implements AccountApi {
public List<SshKeyInfo> listSshKeys() throws RestApiException {
try {
return getSshKeys.apply(account);
} catch (OrmException e) {
} catch (OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot list SSH keys", e);
}
}
@@ -244,7 +244,7 @@ public class AccountApiImpl implements AccountApi {
in.raw = RawInputUtil.create(key);
try {
return addSshKey.apply(account, in).value();
} catch (OrmException | IOException e) {
} catch (OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot add SSH key", e);
}
}
@@ -255,7 +255,7 @@ public class AccountApiImpl implements AccountApi {
AccountResource.SshKey sshKeyRes =
sshKeys.parse(account, IdString.fromDecoded(Integer.toString(seq)));
deleteSshKey.apply(sshKeyRes, null);
} catch (OrmException e) {
} catch (OrmException | IOException | ConfigInvalidException e) {
throw new RestApiException("Cannot delete SSH key", e);
}
}

View File

@@ -70,6 +70,7 @@ import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.account.GroupDetailFactory;
import com.google.gerrit.server.account.GroupIncludeCacheImpl;
import com.google.gerrit.server.account.GroupMembers;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.api.accounts.AccountExternalIdCreator;
import com.google.gerrit.server.auth.AuthBackend;
import com.google.gerrit.server.auth.UniversalAuthBackend;
@@ -330,6 +331,7 @@ public class GerritGlobalModule extends FactoryModule {
factory(SubmoduleSectionParser.Factory.class);
factory(ReplaceOp.Factory.class);
factory(GitModules.Factory.class);
factory(VersionedAuthorizedKeys.Factory.class);
bind(AccountManager.class);
factory(ChangeUserName.Factory.class);

View File

@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
public static final Class<Schema_123> C = Schema_123.class;
public static final Class<Schema_124> C = Schema_124.class;
public static int getBinaryVersion() {
return guessVersion(C);

View File

@@ -0,0 +1,119 @@
// 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.schema;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.account.VersionedAuthorizedKeys.SimpleSshKeyCreator;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.NullProgressMonitor;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collection;
import java.util.Map;
public class Schema_124 extends SchemaVersion {
private final GitRepositoryManager repoManager;
private final AllUsersName allUsersName;
private final PersonIdent serverUser;
@Inject
Schema_124(Provider<Schema_123> prior,
GitRepositoryManager repoManager,
AllUsersName allUsersName,
@GerritPersonIdent PersonIdent serverUser) {
super(prior);
this.repoManager = repoManager;
this.allUsersName = allUsersName;
this.serverUser = serverUser;
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui)
throws OrmException, SQLException {
Multimap<Account.Id, AccountSshKey> imports = ArrayListMultimap.create();
try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT "
+ "account_id, "
+ "seq, "
+ "ssh_public_key, "
+ "valid "
+ "FROM account_ssh_keys")) {
while (rs.next()) {
Account.Id accountId = new Account.Id(rs.getInt(1));
int seq = rs.getInt(2);
String sshPublicKey = rs.getString(3);
AccountSshKey key = new AccountSshKey(
new AccountSshKey.Id(accountId, seq), sshPublicKey);
boolean valid = rs.getBoolean(4);
if (!valid) {
key.setInvalid();
}
imports.put(accountId, key);
}
}
if (imports.isEmpty()) {
return;
}
try (Repository git = repoManager.openRepository(allUsersName);
RevWalk rw = new RevWalk(git)) {
BatchRefUpdate bru = git.getRefDatabase().newBatchUpdate();
for (Map.Entry<Account.Id, Collection<AccountSshKey>> e : imports.asMap()
.entrySet()) {
try (MetaDataUpdate md = new MetaDataUpdate(
GitReferenceUpdated.DISABLED, allUsersName, git, bru);
VersionedAuthorizedKeys authorizedKeys =
new VersionedAuthorizedKeys(
new SimpleSshKeyCreator(), e.getKey())) {
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
authorizedKeys.load(md);
authorizedKeys.setKeys(e.getValue());
authorizedKeys.commit(md);
}
}
bru.execute(rw, NullProgressMonitor.INSTANCE);
} catch (ConfigInvalidException | IOException ex) {
throw new OrmException(ex);
}
}
}

View File

@@ -20,13 +20,14 @@ import com.google.inject.AbstractModule;
import com.google.inject.Module;
public class NoSshKeyCache implements SshKeyCache {
public class NoSshKeyCache implements SshKeyCache, SshKeyCreator {
public static Module module() {
return new AbstractModule() {
@Override
protected void configure() {
bind(SshKeyCache.class).to(NoSshKeyCache.class);
bind(SshKeyCreator.class).to(NoSshKeyCache.class);
}
};
}

View File

@@ -14,13 +14,7 @@
package com.google.gerrit.server.ssh;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.reviewdb.client.AccountSshKey;
/** Permits controlling the contents of the SSH key cache area. */
public interface SshKeyCache {
void evict(String username);
AccountSshKey create(AccountSshKey.Id id, String encoded)
throws InvalidSshKeyException;
}

View File

@@ -0,0 +1,23 @@
// 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.ssh;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.reviewdb.client.AccountSshKey;
public interface SshKeyCreator {
AccountSshKey create(AccountSshKey.Id id, String encoded)
throws InvalidSshKeyException;
}

View File

@@ -0,0 +1,174 @@
// 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.truth.Truth.assertThat;
import com.google.common.base.Optional;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class AuthorizedKeysTest {
private static final String KEY1 =
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgug5VyMXQGnem2H1KVC4/HcRcD4zzBqS"
+ "uJBRWVonSSoz3RoAZ7bWXCVVGwchtXwUURD689wFYdiPecOrWOUgeeyRq754YWRhU+W28"
+ "vf8IZixgjCmiBhaL2gt3wff6pP+NXJpTSA4aeWE5DfNK5tZlxlSxqkKOS8JRSUeNQov5T"
+ "w== john.doe@example.com";
private static final String KEY2 =
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDm5yP7FmEoqzQRDyskX+9+N0q9GrvZeh5"
+ "RG52EUpE4ms/Ujm3ewV1LoGzc/lYKJAIbdcZQNJ9+06EfWZaIRA3oOwAPe1eCnX+aLr8E"
+ "6Tw2gDMQOGc5e9HfyXpC2pDvzauoZNYqLALOG3y/1xjo7IH8GYRS2B7zO/Mf9DdCcCKSf"
+ "w== john.doe@example.com";
private static final String KEY3 =
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCaS7RHEcZ/zjl9hkWkqnm29RNr2OQ/TZ5"
+ "jk2qBVMH3BgzPsTsEs+7ag9tfD8OCj+vOcwm626mQBZoR2e3niHa/9gnHBHFtOrGfzKbp"
+ "RjTWtiOZbB9HF+rqMVD+Dawo/oicX/dDg7VAgOFSPothe6RMhbgWf84UcK5aQd5eP5y+t"
+ "Q== john.doe@example.com";
private static final String KEY4 =
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDIJzW9BaAeO+upFletwwEBnGS15lJmS5i"
+ "08/NiFef0jXtNNKcLtnd13bq8jOi5VA2is0bwof1c8YbwcvUkdFa8RL5aXoyZBpfYZsWs"
+ "/YBLZGiHy5rjooMZQMnH37A50cBPnXr0AQz0WRBxLDBDyOZho+O/DfYAKv4rzPSQ3yw4+"
+ "w== john.doe@example.com";
private static final String KEY5 =
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCgBRKGhiXvY6D9sM+Vbth5Kate57YF7kD"
+ "rqIyUiYIMJK93/AXc8qR/J/p3OIFQAxvLz1qozAur3j5HaiwvxVU19IiSA0vafdhaDLRi"
+ "zRuEL5e/QOu9yGq9xkWApCmg6edpWAHG+Bx4AldU78MiZvzoB7gMMdxc9RmZ1gYj/DjxV"
+ "w== john.doe@example.com";
@Test
public void test() throws Exception {
List<Optional<AccountSshKey>> keys = new ArrayList<>();
StringBuilder expected = new StringBuilder();
assertSerialization(keys, expected);
assertParse(expected, keys);
expected.append(addKey(keys, KEY1));
assertSerialization(keys, expected);
assertParse(expected, keys);
expected.append(addKey(keys, KEY2));
assertSerialization(keys, expected);
assertParse(expected, keys);
expected.append(addInvalidKey(keys, KEY3));
assertSerialization(keys, expected);
assertParse(expected, keys);
expected.append(addKey(keys, KEY4));
assertSerialization(keys, expected);
assertParse(expected, keys);
expected.append(addDeletedKey(keys));
assertSerialization(keys, expected);
assertParse(expected, keys);
expected.append(addKey(keys, KEY5));
assertSerialization(keys, expected);
assertParse(expected, keys);
}
@Test
public void testParseWindowsLineEndings() throws Exception {
List<Optional<AccountSshKey>> keys = new ArrayList<>();
StringBuilder authorizedKeys = new StringBuilder();
authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY1)));
assertParse(authorizedKeys, keys);
authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY2)));
assertParse(authorizedKeys, keys);
authorizedKeys.append(toWindowsLineEndings(addInvalidKey(keys, KEY3)));
assertParse(authorizedKeys, keys);
authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY4)));
assertParse(authorizedKeys, keys);
authorizedKeys.append(toWindowsLineEndings(addDeletedKey(keys)));
assertParse(authorizedKeys, keys);
authorizedKeys.append(toWindowsLineEndings(addKey(keys, KEY5)));
assertParse(authorizedKeys, keys);
}
private static String toWindowsLineEndings(String s) {
return s.replaceAll("\n", "\r\n");
}
private static void assertSerialization(List<Optional<AccountSshKey>> keys,
StringBuilder expected) {
assertThat(AuthorizedKeys.serialize(keys)).isEqualTo(expected.toString());
}
private static void assertParse(StringBuilder authorizedKeys,
List<Optional<AccountSshKey>> expectedKeys) {
Account.Id accountId = new Account.Id(1);
List<Optional<AccountSshKey>> parsedKeys =
AuthorizedKeys.parse(accountId, authorizedKeys.toString());
assertThat(parsedKeys).containsExactlyElementsIn(expectedKeys);
int seq = 1;
for(Optional<AccountSshKey> sshKey : parsedKeys) {
if (sshKey.isPresent()) {
assertThat(sshKey.get().getAccount()).isEqualTo(accountId);
assertThat(sshKey.get().getKey().get()).isEqualTo(seq);
}
seq++;
}
}
/**
* Adds the given public key as new SSH key to the given list.
*
* @return the expected line for this key in the authorized_keys file
*/
private static String addKey(List<Optional<AccountSshKey>> keys, String pub) {
AccountSshKey.Id keyId =
new AccountSshKey.Id(new Account.Id(1), keys.size() + 1);
AccountSshKey key = new AccountSshKey(keyId, pub);
keys.add(Optional.of(key));
return key.getSshPublicKey() + "\n";
}
/**
* Adds the given public key as invalid SSH key to the given list.
*
* @return the expected line for this key in the authorized_keys file
*/
private static String addInvalidKey(List<Optional<AccountSshKey>> keys,
String pub) {
AccountSshKey.Id keyId =
new AccountSshKey.Id(new Account.Id(1), keys.size() + 1);
AccountSshKey key = new AccountSshKey(keyId, pub);
key.setInvalid();
keys.add(Optional.of(key));
return AuthorizedKeys.INVALID_KEY_COMMENT_PREFIX
+ key.getSshPublicKey() + "\n";
}
/**
* Adds a deleted SSH key to the given list.
*
* @return the expected line for this key in the authorized_keys file
*/
private static String addDeletedKey(List<Optional<AccountSshKey>> keys) {
keys.add(Optional.<AccountSshKey> absent());
return AuthorizedKeys.DELETED_KEY_COMMENT + "\n";
}
}

View File

@@ -24,7 +24,6 @@ import com.google.gerrit.reviewdb.server.AccountGroupMemberAuditAccess;
import com.google.gerrit.reviewdb.server.AccountGroupNameAccess;
import com.google.gerrit.reviewdb.server.AccountPatchReviewAccess;
import com.google.gerrit.reviewdb.server.AccountProjectWatchAccess;
import com.google.gerrit.reviewdb.server.AccountSshKeyAccess;
import com.google.gerrit.reviewdb.server.ChangeAccess;
import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
@@ -96,11 +95,6 @@ public class DisabledReviewDb implements ReviewDb {
throw new Disabled();
}
@Override
public AccountSshKeyAccess accountSshKeys() {
throw new Disabled();
}
@Override
public AccountGroupAccess accountGroups() {
throw new Disabled();

View File

@@ -18,13 +18,13 @@ import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAM
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.ssh.SshKeyCache;
import com.google.gwtorm.server.OrmException;
import com.google.gerrit.server.ssh.SshKeyCreator;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.Module;
@@ -32,12 +32,11 @@ import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Named;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -64,6 +63,7 @@ public class SshKeyCacheImpl implements SshKeyCache {
.loader(Loader.class);
bind(SshKeyCacheImpl.class);
bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
bind(SshKeyCreator.class).to(SshKeyCreatorImpl.class);
}
};
}
@@ -97,48 +97,34 @@ public class SshKeyCacheImpl implements SshKeyCache {
}
}
@Override
public AccountSshKey create(AccountSshKey.Id id, String encoded)
throws InvalidSshKeyException {
try {
final AccountSshKey key =
new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
SshUtil.parse(key);
return key;
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new InvalidSshKeyException();
} catch (NoSuchProviderException e) {
log.error("Cannot parse SSH key", e);
throw new InvalidSshKeyException();
}
}
static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
private final SchemaFactory<ReviewDb> schema;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
@Inject
Loader(SchemaFactory<ReviewDb> schema) {
Loader(SchemaFactory<ReviewDb> schema,
VersionedAuthorizedKeys.Accessor authorizedKeys) {
this.schema = schema;
this.authorizedKeys = authorizedKeys;
}
@Override
public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
try (ReviewDb db = schema.open()) {
final AccountExternalId.Key key =
AccountExternalId.Key key =
new AccountExternalId.Key(SCHEME_USERNAME, username);
final AccountExternalId user = db.accountExternalIds().get(key);
AccountExternalId user = db.accountExternalIds().get(key);
if (user == null) {
return NO_SUCH_USER;
}
final List<SshKeyCacheEntry> kl = new ArrayList<>(4);
for (AccountSshKey k : db.accountSshKeys().byAccount(
user.getAccountId())) {
List<SshKeyCacheEntry> kl = new ArrayList<>(4);
for (AccountSshKey k : authorizedKeys.getKeys(user.getAccountId())) {
if (k.isValid()) {
add(db, kl, k);
add(kl, k);
}
}
if (kl.isEmpty()) {
return NO_KEYS;
}
@@ -146,7 +132,7 @@ public class SshKeyCacheImpl implements SshKeyCache {
}
}
private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
private void add(List<SshKeyCacheEntry> kl, AccountSshKey k) {
try {
kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
} catch (OutOfMemoryError e) {
@@ -155,16 +141,16 @@ public class SshKeyCacheImpl implements SshKeyCache {
//
throw e;
} catch (Throwable e) {
markInvalid(db, k);
markInvalid(k);
}
}
private void markInvalid(final ReviewDb db, final AccountSshKey k) {
private void markInvalid(AccountSshKey k) {
try {
log.info("Flagging SSH key " + k.getKey() + " invalid");
authorizedKeys.markKeyInvalid(k.getAccount(), k.getKey().get());
k.setInvalid();
db.accountSshKeys().update(Collections.singleton(k));
} catch (OrmException e) {
} catch (IOException | ConfigInvalidException e) {
log.error("Failed to mark SSH key" + k.getKey() + " invalid", e);
}
}

View File

@@ -0,0 +1,48 @@
// 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.sshd;
import com.google.gerrit.common.errors.InvalidSshKeyException;
import com.google.gerrit.reviewdb.client.AccountSshKey;
import com.google.gerrit.server.ssh.SshKeyCreator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.spec.InvalidKeySpecException;
public class SshKeyCreatorImpl implements SshKeyCreator {
private static final Logger log =
LoggerFactory.getLogger(SshKeyCreatorImpl.class);
@Override
public AccountSshKey create(AccountSshKey.Id id, String encoded)
throws InvalidSshKeyException {
try {
AccountSshKey key =
new AccountSshKey(id, SshUtil.toOpenSshPublicKey(encoded));
SshUtil.parse(key);
return key;
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new InvalidSshKeyException();
} catch (NoSuchProviderException e) {
log.error("Cannot parse SSH key", e);
throw new InvalidSshKeyException();
}
}
}

View File

@@ -29,6 +29,7 @@ import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -64,7 +65,8 @@ final class CreateAccountCommand extends SshCommand {
private CreateAccount.Factory createAccountFactory;
@Override
protected void run() throws OrmException, IOException, UnloggedFailure {
protected void run() throws OrmException, IOException, ConfigInvalidException,
UnloggedFailure {
CreateAccount.Input input = new CreateAccount.Input();
input.username = username;
input.email = email;

View File

@@ -47,6 +47,8 @@ import com.google.gerrit.sshd.SshCommand;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -167,7 +169,8 @@ final class SetAccountCommand extends SshCommand {
}
}
private void setAccount() throws OrmException, IOException, UnloggedFailure {
private void setAccount() throws OrmException, IOException, UnloggedFailure,
ConfigInvalidException {
user = genericUserFactory.create(id);
rsrc = new AccountResource(user);
try {
@@ -220,7 +223,7 @@ final class SetAccountCommand extends SshCommand {
}
private void addSshKeys(List<String> sshKeys) throws RestApiException,
OrmException, IOException {
OrmException, IOException, ConfigInvalidException {
for (final String sshKey : sshKeys) {
AddSshKey.Input in = new AddSshKey.Input();
in.raw = RawInputUtil.create(sshKey.getBytes(), "plain/text");
@@ -228,8 +231,9 @@ final class SetAccountCommand extends SshCommand {
}
}
private void deleteSshKeys(List<String> sshKeys) throws RestApiException,
OrmException {
private void deleteSshKeys(List<String> sshKeys)
throws RestApiException, OrmException, RepositoryNotFoundException,
IOException, ConfigInvalidException {
List<SshKeyInfo> infos = getSshKeys.apply(rsrc);
if (sshKeys.contains("ALL")) {
for (SshKeyInfo i : infos) {
@@ -247,7 +251,8 @@ final class SetAccountCommand extends SshCommand {
}
}
private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException {
private void deleteSshKey(SshKeyInfo i) throws AuthException, OrmException,
RepositoryNotFoundException, IOException, ConfigInvalidException {
AccountSshKey sshKey = new AccountSshKey(
new AccountSshKey.Id(user.getAccountId(), i.seq), i.sshPublicKey);
deleteSshKey.apply(