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