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

          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.externalids;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      19             : import static java.nio.charset.StandardCharsets.UTF_8;
      20             : 
      21             : import com.google.auto.value.AutoValue;
      22             : import com.google.auto.value.extension.memoized.Memoized;
      23             : import com.google.common.annotations.VisibleForTesting;
      24             : import com.google.common.base.Strings;
      25             : import com.google.common.collect.ImmutableSet;
      26             : import com.google.common.hash.Hashing;
      27             : import com.google.gerrit.common.Nullable;
      28             : import com.google.gerrit.entities.Account;
      29             : import com.google.gerrit.extensions.client.AuthType;
      30             : import com.google.gerrit.git.ObjectIds;
      31             : import java.io.Serializable;
      32             : import java.util.Collection;
      33             : import java.util.Locale;
      34             : import java.util.Objects;
      35             : import java.util.Optional;
      36             : import java.util.regex.Pattern;
      37             : import java.util.stream.Stream;
      38             : import org.eclipse.jgit.lib.Config;
      39             : import org.eclipse.jgit.lib.ObjectId;
      40             : 
      41             : @AutoValue
      42         153 : public abstract class ExternalId implements Serializable {
      43             :   // If these regular expressions are modified the same modifications should be done to the
      44             :   // corresponding regular expressions in the
      45             :   // com.google.gerrit.client.account.UsernameField class.
      46             :   private static final String USER_NAME_PATTERN_FIRST_REGEX = "[a-zA-Z0-9]";
      47             :   private static final String USER_NAME_PATTERN_REST_REGEX = "[a-zA-Z0-9.!#$%&’*+=?^_`\\{|\\}~@-]";
      48             :   private static final String USER_NAME_PATTERN_LAST_REGEX = "[a-zA-Z0-9]";
      49             : 
      50             :   /** Regular expression that a username must match. */
      51             :   private static final String USER_NAME_PATTERN_REGEX =
      52             :       "^("
      53             :           + //
      54             :           USER_NAME_PATTERN_FIRST_REGEX
      55             :           + //
      56             :           USER_NAME_PATTERN_REST_REGEX
      57             :           + "*"
      58             :           + //
      59             :           USER_NAME_PATTERN_LAST_REGEX
      60             :           + //
      61             :           "|"
      62             :           + //
      63             :           USER_NAME_PATTERN_FIRST_REGEX
      64             :           + //
      65             :           ")$";
      66             : 
      67         153 :   private static final Pattern USER_NAME_PATTERN = Pattern.compile(USER_NAME_PATTERN_REGEX);
      68             : 
      69             :   public static boolean isValidUsername(String username) {
      70          63 :     return USER_NAME_PATTERN.matcher(username).matches();
      71             :   }
      72             : 
      73             :   /**
      74             :    * Returns the ID of the first external ID from the provided external IDs that has the {@link
      75             :    * ExternalId#SCHEME_USERNAME} scheme.
      76             :    *
      77             :    * @param extIds external IDs
      78             :    * @return the ID of the first external ID from the provided external IDs that has the {@link
      79             :    *     ExternalId#SCHEME_USERNAME} scheme
      80             :    */
      81             :   public static Optional<String> getUserName(Collection<ExternalId> extIds) {
      82         152 :     return extIds.stream()
      83         152 :         .filter(e -> e.isScheme(SCHEME_USERNAME))
      84         152 :         .map(e -> e.key().id())
      85         152 :         .filter(u -> !Strings.isNullOrEmpty(u))
      86         152 :         .findFirst();
      87             :   }
      88             : 
      89             :   /**
      90             :    * Returns all IDs of the provided external IDs that have the {@link ExternalId#SCHEME_MAILTO}
      91             :    * scheme as a distinct stream.
      92             :    *
      93             :    * @param extIds external IDs
      94             :    * @return distinct stream of all IDs of the provided external IDs that have the {@link
      95             :    *     ExternalId#SCHEME_MAILTO} scheme
      96             :    */
      97             :   public static Stream<String> getEmails(Collection<ExternalId> extIds) {
      98         135 :     return extIds.stream().filter(e -> e.isScheme(SCHEME_MAILTO)).map(e -> e.key().id()).distinct();
      99             :   }
     100             : 
     101             :   private static final long serialVersionUID = 1L;
     102             : 
     103             :   static final String EXTERNAL_ID_SECTION = "externalId";
     104             :   static final String ACCOUNT_ID_KEY = "accountId";
     105             :   static final String EMAIL_KEY = "email";
     106             :   static final String PASSWORD_KEY = "password";
     107             : 
     108             :   /**
     109             :    * Scheme used to label accounts created, when using the LDAP-based authentication types {@link
     110             :    * AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link AuthType#HTTP_LDAP}, and {@link
     111             :    * AuthType#LDAP_BIND}. The external ID stores the username. Accounts with such an external ID
     112             :    * will be authenticated against the configured LDAP identity provider.
     113             :    *
     114             :    * <p>The name {@code gerrit:} was a very poor choice.
     115             :    *
     116             :    * <p>Scheme names must not contain colons (':').
     117             :    *
     118             :    * <p>Will be handled case insensitive, if auth.userNameCaseInsensitive = true.
     119             :    */
     120             :   public static final String SCHEME_GERRIT = "gerrit";
     121             : 
     122             :   /** Scheme used for randomly created identities constructed by a UUID. */
     123             :   public static final String SCHEME_UUID = "uuid";
     124             : 
     125             :   /** Scheme used to represent only an email address. */
     126             :   public static final String SCHEME_MAILTO = "mailto";
     127             : 
     128             :   /**
     129             :    * Scheme for the username used to authenticate an account, e.g. over SSH.
     130             :    *
     131             :    * <p>Will be handled case insensitive, if auth.userNameCaseInsensitive = true.
     132             :    */
     133             :   public static final String SCHEME_USERNAME = "username";
     134             : 
     135             :   /** Scheme used for GPG public keys. */
     136             :   public static final String SCHEME_GPGKEY = "gpgkey";
     137             : 
     138             :   /** Scheme for imported accounts from other servers with different GerritServerId */
     139             :   public static final String SCHEME_IMPORTED = "imported";
     140             : 
     141             :   /** Scheme for external auth used during authentication, e.g. OAuth Token */
     142             :   public static final String SCHEME_EXTERNAL = "external";
     143             : 
     144             :   /** Scheme for http resources. OpenID in particular makes use of these external IDs. */
     145             :   public static final String SCHEME_HTTP = "http";
     146             : 
     147             :   /** Scheme for https resources. OpenID in particular makes use of these external IDs. */
     148             :   public static final String SCHEME_HTTPS = "https";
     149             : 
     150             :   /** Scheme for xri resources. OpenID in particular makes use of these external IDs. */
     151             :   public static final String SCHEME_XRI = "xri";
     152             : 
     153             :   @AutoValue
     154         153 :   public abstract static class Key implements Serializable {
     155             :     private static final long serialVersionUID = 1L;
     156             : 
     157             :     /**
     158             :      * Creates an external ID key.
     159             :      *
     160             :      * @param scheme the scheme name, must not contain colons (':'), can be {@code null}
     161             :      * @param id the external ID, must not contain colons (':')
     162             :      * @param isCaseInsensitive whether the external ID key is matched case insensitively
     163             :      * @return the created external ID key
     164             :      */
     165             :     @VisibleForTesting
     166             :     public static Key create(@Nullable String scheme, String id, boolean isCaseInsensitive) {
     167         153 :       return new AutoValue_ExternalId_Key(Strings.emptyToNull(scheme), id, isCaseInsensitive);
     168             :     }
     169             : 
     170             :     /**
     171             :      * Parses an external ID key from a string in the format "scheme:id" or "id".
     172             :      *
     173             :      * @return the parsed external ID key
     174             :      */
     175             :     @VisibleForTesting
     176             :     public static Key parse(String externalId, boolean isCaseInsensitive) {
     177           3 :       int c = externalId.indexOf(':');
     178           3 :       if (c < 1 || c >= externalId.length() - 1) {
     179           1 :         return create(null, externalId, isCaseInsensitive);
     180             :       }
     181           3 :       return create(externalId.substring(0, c), externalId.substring(c + 1), isCaseInsensitive);
     182             :     }
     183             : 
     184             :     public abstract @Nullable String scheme();
     185             : 
     186             :     public abstract String id();
     187             : 
     188             :     public abstract boolean isCaseInsensitive();
     189             : 
     190             :     public boolean isScheme(String scheme) {
     191         153 :       return scheme.equals(scheme());
     192             :     }
     193             : 
     194             :     @Memoized
     195             :     public ObjectId sha1() {
     196         152 :       return sha1(isCaseInsensitive());
     197             :     }
     198             : 
     199             :     /**
     200             :      * Returns the SHA1 of the external ID that is used as note ID in the refs/meta/external-ids
     201             :      * notes branch.
     202             :      */
     203             :     @SuppressWarnings("deprecation") // Use Hashing.sha1 for compatibility.
     204             :     private ObjectId sha1(Boolean isCaseInsensitive) {
     205         152 :       String keyString = isCaseInsensitive ? get().toLowerCase(Locale.US) : get();
     206         152 :       return ObjectId.fromRaw(Hashing.sha1().hashString(keyString, UTF_8).asBytes());
     207             :     }
     208             : 
     209             :     @Memoized
     210             :     public ObjectId caseSensitiveSha1() {
     211           2 :       return sha1(false);
     212             :     }
     213             : 
     214             :     /**
     215             :      * Exports this external ID key as string with the format "scheme:id", or "id" if scheme is
     216             :      * null.
     217             :      *
     218             :      * <p>This string representation is used as subsection name in the Git config file that stores
     219             :      * the external ID.
     220             :      */
     221             :     public String get() {
     222         152 :       if (scheme() != null) {
     223         152 :         return scheme() + ":" + id();
     224             :       }
     225           1 :       return id();
     226             :     }
     227             : 
     228             :     @Override
     229             :     public final String toString() {
     230           5 :       return get();
     231             :     }
     232             : 
     233             :     @Override
     234             :     public final boolean equals(Object obj) {
     235         152 :       if (!(obj instanceof ExternalId.Key)) {
     236           0 :         return false;
     237             :       }
     238         152 :       ExternalId.Key o = (ExternalId.Key) obj;
     239             : 
     240         152 :       return sha1().equals(o.sha1());
     241             :     }
     242             : 
     243             :     @Override
     244             :     @Memoized
     245             :     public int hashCode() {
     246         151 :       return Objects.hash(sha1());
     247             :     }
     248             : 
     249             :     public static ImmutableSet<ExternalId.Key> from(Collection<ExternalId> extIds) {
     250         151 :       return extIds.stream().map(ExternalId::key).collect(toImmutableSet());
     251             :     }
     252             :   }
     253             : 
     254             :   @VisibleForTesting
     255             :   public static ExternalId create(
     256             :       Key key,
     257             :       Account.Id accountId,
     258             :       @Nullable String email,
     259             :       @Nullable String hashedPassword,
     260             :       @Nullable ObjectId blobId) {
     261         153 :     return new AutoValue_ExternalId(
     262             :         key,
     263             :         accountId,
     264         153 :         key.isCaseInsensitive(),
     265         153 :         Strings.emptyToNull(email),
     266         153 :         Strings.emptyToNull(hashedPassword),
     267             :         blobId);
     268             :   }
     269             : 
     270             :   public abstract Key key();
     271             : 
     272             :   public abstract Account.Id accountId();
     273             : 
     274             :   public abstract boolean isCaseInsensitive();
     275             : 
     276             :   public abstract @Nullable String email();
     277             : 
     278             :   public abstract @Nullable String password();
     279             : 
     280             :   /**
     281             :    * ID of the note blob in the external IDs branch that stores this external ID. {@code null} if
     282             :    * the external ID was created in code and is not yet stored in Git.
     283             :    */
     284             :   public abstract @Nullable ObjectId blobId();
     285             : 
     286             :   public void checkThatBlobIdIsSet() {
     287         151 :     checkState(blobId() != null, "No blob ID set for external ID %s", key().get());
     288         151 :   }
     289             : 
     290             :   public boolean isScheme(String scheme) {
     291         153 :     return key().isScheme(scheme);
     292             :   }
     293             : 
     294             :   public byte[] toByteArray() {
     295          11 :     checkState(blobId() != null, "Missing blobId in external ID %s", key().get());
     296          11 :     byte[] b = new byte[2 * ObjectIds.STR_LEN + 1];
     297          11 :     key().sha1().copyTo(b, 0);
     298          11 :     b[ObjectIds.STR_LEN] = ':';
     299          11 :     blobId().copyTo(b, ObjectIds.STR_LEN + 1);
     300          11 :     return b;
     301             :   }
     302             : 
     303             :   /**
     304             :    * For checking if two external IDs are equals the blobId is excluded and external IDs that have
     305             :    * different blob IDs but identical other fields are considered equal. This way an external ID
     306             :    * that was loaded from Git can be equal with an external ID that was created from code.
     307             :    */
     308             :   @Override
     309             :   public final boolean equals(Object obj) {
     310         106 :     if (!(obj instanceof ExternalId)) {
     311           0 :       return false;
     312             :     }
     313         106 :     ExternalId o = (ExternalId) obj;
     314         106 :     return Objects.equals(key(), o.key())
     315           4 :         && Objects.equals(accountId(), o.accountId())
     316           4 :         && isCaseInsensitive() == o.isCaseInsensitive()
     317           4 :         && Objects.equals(email(), o.email())
     318         106 :         && Objects.equals(password(), o.password());
     319             :   }
     320             : 
     321             :   @Memoized
     322             :   @Override
     323             :   public int hashCode() {
     324         151 :     return Objects.hash(key(), accountId(), isCaseInsensitive(), email(), password());
     325             :   }
     326             : 
     327             :   /**
     328             :    * Exports this external ID as Git config file text.
     329             :    *
     330             :    * <p>The Git config has exactly one externalId subsection with an accountId and optionally email
     331             :    * and password:
     332             :    *
     333             :    * <pre>
     334             :    * [externalId "username:jdoe"]
     335             :    *   accountId = 1003407
     336             :    *   email = jdoe@example.com
     337             :    *   password = bcrypt:4:LCbmSBDivK/hhGVQMfkDpA==:XcWn0pKYSVU/UJgOvhidkEtmqCp6oKB7
     338             :    * </pre>
     339             :    */
     340             :   @Override
     341             :   @Memoized
     342             :   public String toString() {
     343          14 :     Config c = new Config();
     344          14 :     writeToConfig(c);
     345          14 :     return c.toText();
     346             :   }
     347             : 
     348             :   public void writeToConfig(Config c) {
     349         151 :     String externalIdKey = key().get();
     350             :     // Do not use c.setInt(...) to write the account ID because c.setInt(...) persists integers
     351             :     // that can be expressed in KiB as a unit strings, e.g. "1024000" is stored as "100k". Using
     352             :     // c.setString(...) ensures that account IDs are human readable.
     353         151 :     c.setString(
     354         151 :         EXTERNAL_ID_SECTION, externalIdKey, ACCOUNT_ID_KEY, Integer.toString(accountId().get()));
     355             : 
     356         151 :     if (email() != null) {
     357         150 :       c.setString(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY, email());
     358             :     } else {
     359         151 :       c.unset(EXTERNAL_ID_SECTION, externalIdKey, EMAIL_KEY);
     360             :     }
     361             : 
     362         151 :     if (password() != null) {
     363         139 :       c.setString(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY, password());
     364             :     } else {
     365         151 :       c.unset(EXTERNAL_ID_SECTION, externalIdKey, PASSWORD_KEY);
     366             :     }
     367         151 :   }
     368             : }

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