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