LCOV - code coverage report
Current view: top level - server/account/externalids - ExternalIdFactory.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 61 72 84.7 %
Date: 2022-11-19 15:00:39 Functions: 15 15 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2021 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.externalids;
      16             : 
      17             : import static java.nio.charset.StandardCharsets.UTF_8;
      18             : import static java.util.Objects.requireNonNull;
      19             : 
      20             : import com.google.common.base.Strings;
      21             : import com.google.common.collect.Iterables;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.entities.Account;
      24             : import com.google.gerrit.server.account.HashedPassword;
      25             : import com.google.gerrit.server.config.AuthConfig;
      26             : import java.util.Set;
      27             : import javax.inject.Inject;
      28             : import javax.inject.Singleton;
      29             : import org.eclipse.jgit.errors.ConfigInvalidException;
      30             : import org.eclipse.jgit.lib.Config;
      31             : import org.eclipse.jgit.lib.ObjectId;
      32             : 
      33             : @Singleton
      34             : public class ExternalIdFactory {
      35             :   private final ExternalIdKeyFactory externalIdKeyFactory;
      36             :   private AuthConfig authConfig;
      37             : 
      38             :   @Inject
      39         153 :   public ExternalIdFactory(ExternalIdKeyFactory externalIdKeyFactory, AuthConfig authConfig) {
      40         153 :     this.externalIdKeyFactory = externalIdKeyFactory;
      41         153 :     this.authConfig = authConfig;
      42         153 :   }
      43             : 
      44             :   /**
      45             :    * Creates an external ID.
      46             :    *
      47             :    * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
      48             :    *     ExternalId#SCHEME_USERNAME}.
      49             :    * @param id the external ID, must not contain colons (':')
      50             :    * @param accountId the ID of the account to which the external ID belongs
      51             :    * @return the created external ID
      52             :    */
      53             :   public ExternalId create(String scheme, String id, Account.Id accountId) {
      54          19 :     return create(externalIdKeyFactory.create(scheme, id), accountId, null, null);
      55             :   }
      56             : 
      57             :   /**
      58             :    * Creates an external ID.
      59             :    *
      60             :    * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
      61             :    *     ExternalId#SCHEME_USERNAME}.
      62             :    * @param id the external ID, must not contain colons (':')
      63             :    * @param accountId the ID of the account to which the external ID belongs
      64             :    * @param email the email of the external ID, may be {@code null}
      65             :    * @param hashedPassword the hashed password of the external ID, may be {@code null}
      66             :    * @return the created external ID
      67             :    */
      68             :   public ExternalId create(
      69             :       String scheme,
      70             :       String id,
      71             :       Account.Id accountId,
      72             :       @Nullable String email,
      73             :       @Nullable String hashedPassword) {
      74           2 :     return create(externalIdKeyFactory.create(scheme, id), accountId, email, hashedPassword);
      75             :   }
      76             : 
      77             :   /**
      78             :    * Creates an external ID.
      79             :    *
      80             :    * @param key the external Id key
      81             :    * @param accountId the ID of the account to which the external ID belongs
      82             :    * @return the created external ID
      83             :    */
      84             :   public ExternalId create(ExternalId.Key key, Account.Id accountId) {
      85           5 :     return create(key, accountId, null, null);
      86             :   }
      87             : 
      88             :   /**
      89             :    * Creates an external ID.
      90             :    *
      91             :    * @param key the external Id key
      92             :    * @param accountId the ID of the account to which the external ID belongs
      93             :    * @param email the email of the external ID, may be {@code null}
      94             :    * @param hashedPassword the hashed password of the external ID, may be {@code null}
      95             :    * @return the created external ID
      96             :    */
      97             :   public ExternalId create(
      98             :       ExternalId.Key key,
      99             :       Account.Id accountId,
     100             :       @Nullable String email,
     101             :       @Nullable String hashedPassword) {
     102         153 :     return create(
     103         153 :         key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), null);
     104             :   }
     105             : 
     106             :   /**
     107             :    * Creates an external ID adding a hashed password computed from a plain password.
     108             :    *
     109             :    * @param key the external Id key
     110             :    * @param accountId the ID of the account to which the external ID belongs
     111             :    * @param email the email of the external ID, may be {@code null}
     112             :    * @param plainPassword the plain HTTP password, may be {@code null}
     113             :    * @return the created external ID
     114             :    */
     115             :   public ExternalId createWithPassword(
     116             :       ExternalId.Key key,
     117             :       Account.Id accountId,
     118             :       @Nullable String email,
     119             :       @Nullable String plainPassword) {
     120         141 :     plainPassword = Strings.emptyToNull(plainPassword);
     121             :     String hashedPassword =
     122         141 :         plainPassword != null ? HashedPassword.fromPassword(plainPassword).encode() : null;
     123         141 :     return create(key, accountId, email, hashedPassword);
     124             :   }
     125             : 
     126             :   /**
     127             :    * Create a external ID for a username (scheme "username").
     128             :    *
     129             :    * @param id the external ID, must not contain colons (':')
     130             :    * @param accountId the ID of the account to which the external ID belongs
     131             :    * @param plainPassword the plain HTTP password, may be {@code null}
     132             :    * @return the created external ID
     133             :    */
     134             :   public ExternalId createUsername(
     135             :       String id, Account.Id accountId, @Nullable String plainPassword) {
     136         140 :     return createWithPassword(
     137         140 :         externalIdKeyFactory.create(ExternalId.SCHEME_USERNAME, id),
     138             :         accountId,
     139             :         null,
     140             :         plainPassword);
     141             :   }
     142             : 
     143             :   /**
     144             :    * Creates an external ID with an email.
     145             :    *
     146             :    * @param scheme the scheme name, must not contain colons (':'). E.g. {@link
     147             :    *     ExternalId#SCHEME_USERNAME}.
     148             :    * @param id the external ID, must not contain colons (':')
     149             :    * @param accountId the ID of the account to which the external ID belongs
     150             :    * @param email the email of the external ID, may be {@code null}
     151             :    * @return the created external ID
     152             :    */
     153             :   public ExternalId createWithEmail(
     154             :       String scheme, String id, Account.Id accountId, @Nullable String email) {
     155         146 :     return createWithEmail(externalIdKeyFactory.create(scheme, id), accountId, email);
     156             :   }
     157             : 
     158             :   /**
     159             :    * Creates an external ID with an email.
     160             :    *
     161             :    * @param key the external Id key
     162             :    * @param accountId the ID of the account to which the external ID belongs
     163             :    * @param email the email of the external ID, may be {@code null}
     164             :    * @return the created external ID
     165             :    */
     166             :   public ExternalId createWithEmail(
     167             :       ExternalId.Key key, Account.Id accountId, @Nullable String email) {
     168         151 :     return create(key, accountId, Strings.emptyToNull(email), null);
     169             :   }
     170             : 
     171             :   /**
     172             :    * Creates an external ID using the `mailto`-scheme.
     173             :    *
     174             :    * @param accountId the ID of the account to which the external ID belongs
     175             :    * @param email the email of the external ID, may be {@code null}
     176             :    * @return the created external ID
     177             :    */
     178             :   public ExternalId createEmail(Account.Id accountId, String email) {
     179         146 :     return createWithEmail(ExternalId.SCHEME_MAILTO, email, accountId, requireNonNull(email));
     180             :   }
     181             : 
     182             :   ExternalId create(ExternalId extId, @Nullable ObjectId blobId) {
     183         151 :     return create(extId.key(), extId.accountId(), extId.email(), extId.password(), blobId);
     184             :   }
     185             : 
     186             :   /**
     187             :    * Creates an external ID.
     188             :    *
     189             :    * @param key the external Id key
     190             :    * @param accountId the ID of the account to which the external ID belongs
     191             :    * @param email the email of the external ID, may be {@code null}
     192             :    * @param hashedPassword the hashed password of the external ID, may be {@code null}
     193             :    * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
     194             :    *     {@code null} if the external ID was created in code and is not yet stored in Git.
     195             :    * @return the created external ID
     196             :    */
     197             :   public ExternalId create(
     198             :       ExternalId.Key key,
     199             :       Account.Id accountId,
     200             :       @Nullable String email,
     201             :       @Nullable String hashedPassword,
     202             :       @Nullable ObjectId blobId) {
     203         153 :     return ExternalId.create(
     204         153 :         key, accountId, Strings.emptyToNull(email), Strings.emptyToNull(hashedPassword), blobId);
     205             :   }
     206             : 
     207             :   /**
     208             :    * Parses an external ID from a byte array that contains the external ID as a Git config file
     209             :    * text.
     210             :    *
     211             :    * <p>The Git config must have exactly one externalId subsection with an accountId and optionally
     212             :    * email and password:
     213             :    *
     214             :    * <pre>
     215             :    * [externalId "username:jdoe"]
     216             :    *   accountId = 1003407
     217             :    *   email = jdoe@example.com
     218             :    *   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
     219             :    * </pre>
     220             :    *
     221             :    * @param noteId the SHA-1 sum of the external ID used as the note's ID
     222             :    * @param raw a byte array that contains the external ID as a Git config file text.
     223             :    * @param blobId the ID of the note blob in the external IDs branch that stores this external ID.
     224             :    *     {@code null} if the external ID was created in code and is not yet stored in Git.
     225             :    * @return the parsed external ID
     226             :    */
     227             :   public ExternalId parse(String noteId, byte[] raw, ObjectId blobId)
     228             :       throws ConfigInvalidException {
     229         151 :     requireNonNull(blobId);
     230             : 
     231         151 :     Config externalIdConfig = new Config();
     232             :     try {
     233         151 :       externalIdConfig.fromText(new String(raw, UTF_8));
     234           1 :     } catch (ConfigInvalidException e) {
     235           1 :       throw invalidConfig(noteId, e.getMessage());
     236         151 :     }
     237             : 
     238         151 :     Set<String> externalIdKeys = externalIdConfig.getSubsections(ExternalId.EXTERNAL_ID_SECTION);
     239         151 :     if (externalIdKeys.size() != 1) {
     240           1 :       throw invalidConfig(
     241             :           noteId,
     242           1 :           String.format(
     243             :               "Expected exactly 1 '%s' section, found %d",
     244           1 :               ExternalId.EXTERNAL_ID_SECTION, externalIdKeys.size()));
     245             :     }
     246             : 
     247         151 :     String externalIdKeyStr = Iterables.getOnlyElement(externalIdKeys);
     248         151 :     ExternalId.Key externalIdKey = externalIdKeyFactory.parse(externalIdKeyStr);
     249         151 :     if (externalIdKey == null) {
     250           0 :       throw invalidConfig(noteId, String.format("External ID %s is invalid", externalIdKeyStr));
     251             :     }
     252             : 
     253         151 :     if (!externalIdKey.sha1().getName().equals(noteId)) {
     254           3 :       if (!authConfig.isUserNameCaseInsensitiveMigrationMode()) {
     255           3 :         throw invalidConfig(
     256             :             noteId,
     257           3 :             String.format(
     258             :                 "SHA1 of external ID '%s' does not match note ID '%s'", externalIdKeyStr, noteId));
     259             :       }
     260             : 
     261           2 :       if (!externalIdKey.caseSensitiveSha1().getName().equals(noteId)) {
     262           0 :         throw invalidConfig(
     263             :             noteId,
     264           0 :             String.format(
     265             :                 "Neither case sensitive nor case insensitive SHA1 of external ID '%s' match note ID"
     266             :                     + " '%s'",
     267             :                 externalIdKeyStr, noteId));
     268             :       }
     269           2 :       externalIdKey =
     270           2 :           externalIdKeyFactory.create(externalIdKey.scheme(), externalIdKey.id(), false);
     271             :     }
     272             : 
     273         151 :     String email =
     274         151 :         externalIdConfig.getString(
     275             :             ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.EMAIL_KEY);
     276         151 :     String password =
     277         151 :         externalIdConfig.getString(
     278             :             ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.PASSWORD_KEY);
     279         151 :     int accountId = readAccountId(noteId, externalIdConfig, externalIdKeyStr);
     280             : 
     281         151 :     return create(
     282             :         externalIdKey,
     283         151 :         Account.id(accountId),
     284         151 :         Strings.emptyToNull(email),
     285         151 :         Strings.emptyToNull(password),
     286             :         blobId);
     287             :   }
     288             : 
     289             :   private static int readAccountId(String noteId, Config externalIdConfig, String externalIdKeyStr)
     290             :       throws ConfigInvalidException {
     291         151 :     String accountIdStr =
     292         151 :         externalIdConfig.getString(
     293             :             ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY);
     294         151 :     if (accountIdStr == null) {
     295           1 :       throw invalidConfig(
     296             :           noteId,
     297           1 :           String.format(
     298             :               "Value for '%s.%s.%s' is missing, expected account ID",
     299             :               ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY));
     300             :     }
     301             : 
     302             :     try {
     303         151 :       int accountId =
     304         151 :           externalIdConfig.getInt(
     305             :               ExternalId.EXTERNAL_ID_SECTION, externalIdKeyStr, ExternalId.ACCOUNT_ID_KEY, -1);
     306         151 :       if (accountId < 0) {
     307           0 :         throw invalidConfig(
     308             :             noteId,
     309           0 :             String.format(
     310             :                 "Value %s for '%s.%s.%s' is invalid, expected account ID",
     311             :                 accountIdStr,
     312             :                 ExternalId.EXTERNAL_ID_SECTION,
     313             :                 externalIdKeyStr,
     314             :                 ExternalId.ACCOUNT_ID_KEY));
     315             :       }
     316         151 :       return accountId;
     317           0 :     } catch (IllegalArgumentException e) {
     318           0 :       ConfigInvalidException newException =
     319           0 :           invalidConfig(
     320             :               noteId,
     321           0 :               String.format(
     322             :                   "Value %s for '%s.%s.%s' is invalid, expected account ID",
     323             :                   accountIdStr,
     324             :                   ExternalId.EXTERNAL_ID_SECTION,
     325             :                   externalIdKeyStr,
     326             :                   ExternalId.ACCOUNT_ID_KEY));
     327           0 :       newException.initCause(e);
     328           0 :       throw newException;
     329             :     }
     330             :   }
     331             : 
     332             :   private static ConfigInvalidException invalidConfig(String noteId, String message) {
     333           3 :     return new ConfigInvalidException(
     334           3 :         String.format("Invalid external ID config for note '%s': %s", noteId, message));
     335             :   }
     336             : }

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