diff options
Diffstat (limited to 'java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java')
-rw-r--r-- | java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java | 277 |
1 files changed, 277 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java new file mode 100644 index 0000000000..965f1ba068 --- /dev/null +++ b/java/com/google/gerrit/server/account/VersionedAuthorizedKeys.java @@ -0,0 +1,277 @@ +// 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.checkState; +import static java.util.Comparator.comparing; +import static java.util.stream.Collectors.toList; + +import com.google.common.base.Strings; +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.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.meta.MetaDataUpdate; +import com.google.gerrit.server.git.meta.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 java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Repository; + +/** + * 'authorized_keys' file in the refs/users/CD/ABCD branches of the All-Users repository. + * + * <p>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). + * + * <p>The order of the keys in the file determines the sequence numbers of the keys. The first line + * corresponds to sequence number 1. + * + * <p>Invalid keys are marked with the prefix <code># INVALID</code>. + * + * <p>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. + * + * <p>Other comment lines are ignored on read, and are not written back when the file is modified. + */ +public class VersionedAuthorizedKeys extends VersionedMetaData { + @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 synchronized AccountSshKey addKey(Account.Id accountId, String pub) + throws IOException, ConfigInvalidException, InvalidSshKeyException { + VersionedAuthorizedKeys authorizedKeys = read(accountId); + AccountSshKey key = authorizedKeys.addKey(pub); + commit(authorizedKeys); + return key; + } + + public synchronized void deleteKey(Account.Id accountId, int seq) + throws IOException, ConfigInvalidException { + VersionedAuthorizedKeys authorizedKeys = read(accountId); + if (authorizedKeys.deleteKey(seq)) { + commit(authorizedKeys); + } + } + + public synchronized void markKeyInvalid(Account.Id accountId, int seq) + throws IOException, ConfigInvalidException { + VersionedAuthorizedKeys authorizedKeys = read(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(allUsersName, 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(Account.Id accountId, int seq, String encoded) { + return AccountSshKey.create(accountId, seq, encoded); + } + } + + public interface Factory { + VersionedAuthorizedKeys create(Account.Id accountId); + } + + private final SshKeyCreator sshKeyCreator; + private final Account.Id accountId; + private final String ref; + 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 + 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 keys.stream().filter(Optional::isPresent).map(Optional::get).collect(toList()); + } + + /** + * 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(); + return keys.get(seq - 1).orElse(null); + } + + /** + * Adds a new public SSH key. + * + * <p>If the specified public key exists already, the existing key is returned. + * + * @param pub the public SSH key to be added + * @return the new SSH key + * @throws InvalidSshKeyException + */ + private AccountSshKey addKey(String pub) throws InvalidSshKeyException { + checkLoaded(); + + for (Optional<AccountSshKey> key : keys) { + if (key.isPresent() && key.get().sshPublicKey().trim().equals(pub.trim())) { + return key.get(); + } + } + + int seq = keys.size() + 1; + AccountSshKey key = sshKeyCreator.create(accountId, seq, 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.empty()); + 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(); + + Optional<AccountSshKey> key = keys.get(seq - 1); + if (key.isPresent() && key.get().valid()) { + keys.set(seq - 1, Optional.of(AccountSshKey.createInvalid(key.get()))); + return true; + } + return false; + } + + /** + * Sets new SSH keys. + * + * <p>The existing SSH keys are overwritten. + * + * @param newKeys the new public SSH keys + */ + public void setKeys(Collection<AccountSshKey> newKeys) { + Ordering<AccountSshKey> o = Ordering.from(comparing(AccountSshKey::seq)); + keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).seq(), Optional.empty())); + for (AccountSshKey key : newKeys) { + keys.set(key.seq() - 1, Optional.of(key)); + } + } + + private void checkLoaded() { + checkState(keys != null, "SSH keys not loaded yet"); + } +} |