blob: 61199384a8b97825f769d912fd05a2af6288f10e [file] [log] [blame]
// 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");
}
}