| // 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.errorprone.annotations.CanIgnoreReturnValue; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.exceptions.InvalidSshKeyException; |
| 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 { |
| |
| /** Read/write SSH keys by user ID. */ |
| @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); |
| } |
| |
| @CanIgnoreReturnValue |
| 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 |
| */ |
| @Nullable |
| 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 |
| */ |
| 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"); |
| } |
| } |