Line data Source code
1 : // Copyright (C) 2016 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.account; 16 : 17 : import static com.google.common.base.Preconditions.checkState; 18 : import static java.util.Comparator.comparing; 19 : import static java.util.stream.Collectors.toList; 20 : 21 : import com.google.common.base.Strings; 22 : import com.google.common.collect.Ordering; 23 : import com.google.gerrit.common.Nullable; 24 : import com.google.gerrit.entities.Account; 25 : import com.google.gerrit.entities.RefNames; 26 : import com.google.gerrit.exceptions.InvalidSshKeyException; 27 : import com.google.gerrit.server.IdentifiedUser; 28 : import com.google.gerrit.server.config.AllUsersName; 29 : import com.google.gerrit.server.git.GitRepositoryManager; 30 : import com.google.gerrit.server.git.meta.MetaDataUpdate; 31 : import com.google.gerrit.server.git.meta.VersionedMetaData; 32 : import com.google.gerrit.server.ssh.SshKeyCreator; 33 : import com.google.inject.Inject; 34 : import com.google.inject.Provider; 35 : import com.google.inject.Singleton; 36 : import com.google.inject.assistedinject.Assisted; 37 : import java.io.IOException; 38 : import java.util.ArrayList; 39 : import java.util.Collection; 40 : import java.util.Collections; 41 : import java.util.List; 42 : import java.util.Optional; 43 : import org.eclipse.jgit.errors.ConfigInvalidException; 44 : import org.eclipse.jgit.lib.CommitBuilder; 45 : import org.eclipse.jgit.lib.Repository; 46 : 47 : /** 48 : * 'authorized_keys' file in the refs/users/CD/ABCD branches of the All-Users repository. 49 : * 50 : * <p>The `authorized_keys' files stores the public SSH keys of the user. The file format matches 51 : * the standard SSH file format, which means that each key is stored on a separate line (see 52 : * https://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys). 53 : * 54 : * <p>The order of the keys in the file determines the sequence numbers of the keys. The first line 55 : * corresponds to sequence number 1. 56 : * 57 : * <p>Invalid keys are marked with the prefix <code># INVALID</code>. 58 : * 59 : * <p>To keep the sequence numbers intact when a key is deleted, a <code># DELETED</code> line is 60 : * inserted at the position where the key was deleted. 61 : * 62 : * <p>Other comment lines are ignored on read, and are not written back when the file is modified. 63 : */ 64 : public class VersionedAuthorizedKeys extends VersionedMetaData { 65 : 66 : /** Read/write SSH keys by user ID. */ 67 : @Singleton 68 : public static class Accessor { 69 : private final GitRepositoryManager repoManager; 70 : private final AllUsersName allUsersName; 71 : private final VersionedAuthorizedKeys.Factory authorizedKeysFactory; 72 : private final Provider<MetaDataUpdate.User> metaDataUpdateFactory; 73 : private final IdentifiedUser.GenericFactory userFactory; 74 : 75 : @Inject 76 : Accessor( 77 : GitRepositoryManager repoManager, 78 : AllUsersName allUsersName, 79 : VersionedAuthorizedKeys.Factory authorizedKeysFactory, 80 : Provider<MetaDataUpdate.User> metaDataUpdateFactory, 81 149 : IdentifiedUser.GenericFactory userFactory) { 82 149 : this.repoManager = repoManager; 83 149 : this.allUsersName = allUsersName; 84 149 : this.authorizedKeysFactory = authorizedKeysFactory; 85 149 : this.metaDataUpdateFactory = metaDataUpdateFactory; 86 149 : this.userFactory = userFactory; 87 149 : } 88 : 89 : public List<AccountSshKey> getKeys(Account.Id accountId) 90 : throws IOException, ConfigInvalidException { 91 16 : return read(accountId).getKeys(); 92 : } 93 : 94 : public AccountSshKey getKey(Account.Id accountId, int seq) 95 : throws IOException, ConfigInvalidException { 96 3 : return read(accountId).getKey(seq); 97 : } 98 : 99 : public synchronized AccountSshKey addKey(Account.Id accountId, String pub) 100 : throws IOException, ConfigInvalidException, InvalidSshKeyException { 101 16 : VersionedAuthorizedKeys authorizedKeys = read(accountId); 102 16 : AccountSshKey key = authorizedKeys.addKey(pub); 103 16 : commit(authorizedKeys); 104 16 : return key; 105 : } 106 : 107 : public synchronized void deleteKey(Account.Id accountId, int seq) 108 : throws IOException, ConfigInvalidException { 109 3 : VersionedAuthorizedKeys authorizedKeys = read(accountId); 110 3 : if (authorizedKeys.deleteKey(seq)) { 111 3 : commit(authorizedKeys); 112 : } 113 3 : } 114 : 115 : public synchronized void markKeyInvalid(Account.Id accountId, int seq) 116 : throws IOException, ConfigInvalidException { 117 1 : VersionedAuthorizedKeys authorizedKeys = read(accountId); 118 1 : if (authorizedKeys.markKeyInvalid(seq)) { 119 1 : commit(authorizedKeys); 120 : } 121 1 : } 122 : 123 : private VersionedAuthorizedKeys read(Account.Id accountId) 124 : throws IOException, ConfigInvalidException { 125 16 : try (Repository git = repoManager.openRepository(allUsersName)) { 126 16 : VersionedAuthorizedKeys authorizedKeys = authorizedKeysFactory.create(accountId); 127 16 : authorizedKeys.load(allUsersName, git); 128 16 : return authorizedKeys; 129 : } 130 : } 131 : 132 : private void commit(VersionedAuthorizedKeys authorizedKeys) throws IOException { 133 16 : try (MetaDataUpdate md = 134 : metaDataUpdateFactory 135 16 : .get() 136 16 : .create(allUsersName, userFactory.create(authorizedKeys.accountId))) { 137 16 : authorizedKeys.commit(md); 138 : } 139 16 : } 140 : } 141 : 142 0 : public static class SimpleSshKeyCreator implements SshKeyCreator { 143 : @Override 144 : public AccountSshKey create(Account.Id accountId, int seq, String encoded) { 145 0 : return AccountSshKey.create(accountId, seq, encoded); 146 : } 147 : } 148 : 149 : public interface Factory { 150 : VersionedAuthorizedKeys create(Account.Id accountId); 151 : } 152 : 153 : private final SshKeyCreator sshKeyCreator; 154 : private final Account.Id accountId; 155 : private final String ref; 156 : private List<Optional<AccountSshKey>> keys; 157 : 158 : @Inject 159 16 : public VersionedAuthorizedKeys(SshKeyCreator sshKeyCreator, @Assisted Account.Id accountId) { 160 16 : this.sshKeyCreator = sshKeyCreator; 161 16 : this.accountId = accountId; 162 16 : this.ref = RefNames.refsUsers(accountId); 163 16 : } 164 : 165 : @Override 166 : protected String getRefName() { 167 16 : return ref; 168 : } 169 : 170 : @Override 171 : protected void onLoad() throws IOException { 172 16 : keys = AuthorizedKeys.parse(accountId, readUTF8(AuthorizedKeys.FILE_NAME)); 173 16 : } 174 : 175 : @Override 176 : protected boolean onSave(CommitBuilder commit) throws IOException { 177 16 : if (Strings.isNullOrEmpty(commit.getMessage())) { 178 16 : commit.setMessage("Updated SSH keys\n"); 179 : } 180 : 181 16 : saveUTF8(AuthorizedKeys.FILE_NAME, AuthorizedKeys.serialize(keys)); 182 16 : return true; 183 : } 184 : 185 : /** Returns all SSH keys. */ 186 : private List<AccountSshKey> getKeys() { 187 16 : checkLoaded(); 188 16 : return keys.stream().filter(Optional::isPresent).map(Optional::get).collect(toList()); 189 : } 190 : 191 : /** 192 : * Returns the SSH key with the given sequence number. 193 : * 194 : * @param seq sequence number 195 : * @return the SSH key, <code>null</code> if there is no SSH key with this sequence number, or if 196 : * the SSH key with this sequence number has been deleted 197 : */ 198 : @Nullable 199 : private AccountSshKey getKey(int seq) { 200 3 : checkLoaded(); 201 3 : return keys.get(seq - 1).orElse(null); 202 : } 203 : 204 : /** 205 : * Adds a new public SSH key. 206 : * 207 : * <p>If the specified public key exists already, the existing key is returned. 208 : * 209 : * @param pub the public SSH key to be added 210 : * @return the new SSH key 211 : */ 212 : private AccountSshKey addKey(String pub) throws InvalidSshKeyException { 213 16 : checkLoaded(); 214 : 215 16 : for (Optional<AccountSshKey> key : keys) { 216 3 : if (key.isPresent() && key.get().sshPublicKey().trim().equals(pub.trim())) { 217 2 : return key.get(); 218 : } 219 3 : } 220 : 221 16 : int seq = keys.size() + 1; 222 16 : AccountSshKey key = sshKeyCreator.create(accountId, seq, pub); 223 16 : keys.add(Optional.of(key)); 224 16 : return key; 225 : } 226 : 227 : /** 228 : * Deletes the SSH key with the given sequence number. 229 : * 230 : * @param seq the sequence number 231 : * @return <code>true</code> if a key with this sequence number was found and deleted, <code>false 232 : * </code> if no key with the given sequence number exists 233 : */ 234 : private boolean deleteKey(int seq) { 235 3 : checkLoaded(); 236 3 : if (seq <= keys.size() && keys.get(seq - 1).isPresent()) { 237 3 : keys.set(seq - 1, Optional.empty()); 238 3 : return true; 239 : } 240 0 : return false; 241 : } 242 : 243 : /** 244 : * Marks the SSH key with the given sequence number as invalid. 245 : * 246 : * @param seq the sequence number 247 : * @return <code>true</code> if a key with this sequence number was found and marked as invalid, 248 : * <code>false</code> if no key with the given sequence number exists or if the key was 249 : * already marked as invalid 250 : */ 251 : private boolean markKeyInvalid(int seq) { 252 1 : checkLoaded(); 253 : 254 1 : Optional<AccountSshKey> key = keys.get(seq - 1); 255 1 : if (key.isPresent() && key.get().valid()) { 256 1 : keys.set(seq - 1, Optional.of(AccountSshKey.createInvalid(key.get()))); 257 1 : return true; 258 : } 259 0 : return false; 260 : } 261 : 262 : /** 263 : * Sets new SSH keys. 264 : * 265 : * <p>The existing SSH keys are overwritten. 266 : * 267 : * @param newKeys the new public SSH keys 268 : */ 269 : public void setKeys(Collection<AccountSshKey> newKeys) { 270 0 : Ordering<AccountSshKey> o = Ordering.from(comparing(AccountSshKey::seq)); 271 0 : keys = new ArrayList<>(Collections.nCopies(o.max(newKeys).seq(), Optional.empty())); 272 0 : for (AccountSshKey key : newKeys) { 273 0 : keys.set(key.seq() - 1, Optional.of(key)); 274 0 : } 275 0 : } 276 : 277 : private void checkLoaded() { 278 16 : checkState(keys != null, "SSH keys not loaded yet"); 279 16 : } 280 : }