LCOV - code coverage report
Current view: top level - server/account - VersionedAuthorizedKeys.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 66 76 86.8 %
Date: 2022-11-19 15:00:39 Functions: 18 21 85.7 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750