LCOV - code coverage report
Current view: top level - server - IdentifiedUser.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 135 149 90.6 %
Date: 2022-11-19 15:00:39 Functions: 40 42 95.2 %

          Line data    Source code
       1             : // Copyright (C) 2009 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;
      16             : 
      17             : import static com.google.common.base.MoreObjects.firstNonNull;
      18             : import static com.google.common.collect.ImmutableSet.toImmutableSet;
      19             : import static com.google.common.flogger.LazyArgs.lazy;
      20             : 
      21             : import com.google.common.annotations.VisibleForTesting;
      22             : import com.google.common.base.Strings;
      23             : import com.google.common.collect.ImmutableSet;
      24             : import com.google.common.collect.Sets;
      25             : import com.google.common.flogger.FluentLogger;
      26             : import com.google.gerrit.common.Nullable;
      27             : import com.google.gerrit.common.UsedAt;
      28             : import com.google.gerrit.entities.Account;
      29             : import com.google.gerrit.server.account.AccountCache;
      30             : import com.google.gerrit.server.account.AccountState;
      31             : import com.google.gerrit.server.account.GroupBackend;
      32             : import com.google.gerrit.server.account.GroupMembership;
      33             : import com.google.gerrit.server.account.ListGroupMembership;
      34             : import com.google.gerrit.server.account.Realm;
      35             : import com.google.gerrit.server.account.externalids.ExternalId;
      36             : import com.google.gerrit.server.config.AnonymousCowardName;
      37             : import com.google.gerrit.server.config.AuthConfig;
      38             : import com.google.gerrit.server.config.CanonicalWebUrl;
      39             : import com.google.gerrit.server.config.EnablePeerIPInReflogRecord;
      40             : import com.google.gerrit.server.group.SystemGroupBackend;
      41             : import com.google.inject.Inject;
      42             : import com.google.inject.OutOfScopeException;
      43             : import com.google.inject.Provider;
      44             : import com.google.inject.ProvisionException;
      45             : import com.google.inject.Singleton;
      46             : import com.google.inject.util.Providers;
      47             : import java.net.InetAddress;
      48             : import java.net.InetSocketAddress;
      49             : import java.net.MalformedURLException;
      50             : import java.net.SocketAddress;
      51             : import java.net.URL;
      52             : import java.time.Instant;
      53             : import java.time.ZoneId;
      54             : import java.util.Optional;
      55             : import java.util.Set;
      56             : import org.eclipse.jgit.lib.PersonIdent;
      57             : import org.eclipse.jgit.util.SystemReader;
      58             : 
      59             : /** An authenticated user. */
      60             : public class IdentifiedUser extends CurrentUser {
      61         152 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      62             : 
      63             :   /** Create an IdentifiedUser, ignoring any per-request state. */
      64             :   @Singleton
      65             :   public static class GenericFactory {
      66             :     private final AuthConfig authConfig;
      67             :     private final Realm realm;
      68             :     private final String anonymousCowardName;
      69             :     private final Provider<String> canonicalUrl;
      70             :     private final AccountCache accountCache;
      71             :     private final GroupBackend groupBackend;
      72             :     private final Boolean enablePeerIPInReflogRecord;
      73             : 
      74             :     @Inject
      75             :     public GenericFactory(
      76             :         AuthConfig authConfig,
      77             :         Realm realm,
      78             :         @AnonymousCowardName String anonymousCowardName,
      79             :         @CanonicalWebUrl Provider<String> canonicalUrl,
      80             :         @EnablePeerIPInReflogRecord Boolean enablePeerIPInReflogRecord,
      81             :         AccountCache accountCache,
      82         152 :         GroupBackend groupBackend) {
      83         152 :       this.authConfig = authConfig;
      84         152 :       this.realm = realm;
      85         152 :       this.anonymousCowardName = anonymousCowardName;
      86         152 :       this.canonicalUrl = canonicalUrl;
      87         152 :       this.accountCache = accountCache;
      88         152 :       this.groupBackend = groupBackend;
      89         152 :       this.enablePeerIPInReflogRecord = enablePeerIPInReflogRecord;
      90         152 :     }
      91             : 
      92             :     public IdentifiedUser create(AccountState state) {
      93         147 :       return new IdentifiedUser(
      94             :           authConfig,
      95             :           realm,
      96             :           anonymousCowardName,
      97             :           canonicalUrl,
      98             :           accountCache,
      99             :           groupBackend,
     100             :           enablePeerIPInReflogRecord,
     101         147 :           Providers.of(null),
     102             :           state,
     103             :           null);
     104             :     }
     105             : 
     106             :     public IdentifiedUser create(Account.Id id) {
     107         151 :       return create(null, id);
     108             :     }
     109             : 
     110             :     @VisibleForTesting
     111             :     @UsedAt(UsedAt.Project.GOOGLE)
     112             :     public IdentifiedUser forTest(Account.Id id, PropertyMap properties) {
     113           0 :       return runAs(null, id, null, properties);
     114             :     }
     115             : 
     116             :     public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
     117         151 :       return runAs(remotePeer, id, null);
     118             :     }
     119             : 
     120             :     public IdentifiedUser runAs(
     121             :         SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) {
     122         151 :       return runAs(remotePeer, id, caller, PropertyMap.EMPTY);
     123             :     }
     124             : 
     125             :     private IdentifiedUser runAs(
     126             :         SocketAddress remotePeer,
     127             :         Account.Id id,
     128             :         @Nullable CurrentUser caller,
     129             :         PropertyMap properties) {
     130         151 :       return new IdentifiedUser(
     131             :           authConfig,
     132             :           realm,
     133             :           anonymousCowardName,
     134             :           canonicalUrl,
     135             :           accountCache,
     136             :           groupBackend,
     137             :           enablePeerIPInReflogRecord,
     138         151 :           Providers.of(remotePeer),
     139             :           id,
     140             :           caller,
     141             :           properties);
     142             :     }
     143             :   }
     144             : 
     145             :   /**
     146             :    * Create an IdentifiedUser, relying on current request state.
     147             :    *
     148             :    * <p>Can only be used from within a module that has defined a request scoped {@code @RemotePeer
     149             :    * SocketAddress} provider.
     150             :    */
     151             :   @Singleton
     152             :   public static class RequestFactory {
     153             :     private final AuthConfig authConfig;
     154             :     private final Realm realm;
     155             :     private final String anonymousCowardName;
     156             :     private final Provider<String> canonicalUrl;
     157             :     private final AccountCache accountCache;
     158             :     private final GroupBackend groupBackend;
     159             :     private final Boolean enablePeerIPInReflogRecord;
     160             :     private final Provider<SocketAddress> remotePeerProvider;
     161             : 
     162             :     @Inject
     163             :     RequestFactory(
     164             :         AuthConfig authConfig,
     165             :         Realm realm,
     166             :         @AnonymousCowardName String anonymousCowardName,
     167             :         @CanonicalWebUrl Provider<String> canonicalUrl,
     168             :         AccountCache accountCache,
     169             :         GroupBackend groupBackend,
     170             :         @EnablePeerIPInReflogRecord Boolean enablePeerIPInReflogRecord,
     171         138 :         @RemotePeer Provider<SocketAddress> remotePeerProvider) {
     172         138 :       this.authConfig = authConfig;
     173         138 :       this.realm = realm;
     174         138 :       this.anonymousCowardName = anonymousCowardName;
     175         138 :       this.canonicalUrl = canonicalUrl;
     176         138 :       this.accountCache = accountCache;
     177         138 :       this.groupBackend = groupBackend;
     178         138 :       this.enablePeerIPInReflogRecord = enablePeerIPInReflogRecord;
     179         138 :       this.remotePeerProvider = remotePeerProvider;
     180         138 :     }
     181             : 
     182             :     public IdentifiedUser create(Account.Id id) {
     183          10 :       return create(id, PropertyMap.EMPTY);
     184             :     }
     185             : 
     186             :     public <T> IdentifiedUser create(Account.Id id, PropertyMap properties) {
     187          11 :       return new IdentifiedUser(
     188             :           authConfig,
     189             :           realm,
     190             :           anonymousCowardName,
     191             :           canonicalUrl,
     192             :           accountCache,
     193             :           groupBackend,
     194             :           enablePeerIPInReflogRecord,
     195             :           remotePeerProvider,
     196             :           id,
     197             :           null,
     198             :           properties);
     199             :     }
     200             : 
     201             :     public IdentifiedUser runAs(Account.Id id, CurrentUser caller, PropertyMap properties) {
     202          37 :       return new IdentifiedUser(
     203             :           authConfig,
     204             :           realm,
     205             :           anonymousCowardName,
     206             :           canonicalUrl,
     207             :           accountCache,
     208             :           groupBackend,
     209             :           enablePeerIPInReflogRecord,
     210             :           remotePeerProvider,
     211             :           id,
     212             :           caller,
     213             :           properties);
     214             :     }
     215             :   }
     216             : 
     217         152 :   private static final GroupMembership registeredGroups =
     218             :       new ListGroupMembership(
     219         152 :           ImmutableSet.of(SystemGroupBackend.ANONYMOUS_USERS, SystemGroupBackend.REGISTERED_USERS));
     220             : 
     221             :   private final Provider<String> canonicalUrl;
     222             :   private final AccountCache accountCache;
     223             :   private final AuthConfig authConfig;
     224             :   private final Realm realm;
     225             :   private final GroupBackend groupBackend;
     226             :   private final String anonymousCowardName;
     227             :   private final Boolean enablePeerIPInReflogRecord;
     228         151 :   private final Set<String> validEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
     229             :   private final CurrentUser realUser; // Must be final since cached properties depend on it.
     230             : 
     231             :   private final Provider<SocketAddress> remotePeerProvider;
     232             :   private final Account.Id accountId;
     233             : 
     234             :   private AccountState state;
     235             :   private boolean loadedAllEmails;
     236             :   private Set<String> invalidEmails;
     237             :   private GroupMembership effectiveGroups;
     238             : 
     239             :   private IdentifiedUser(
     240             :       AuthConfig authConfig,
     241             :       Realm realm,
     242             :       String anonymousCowardName,
     243             :       Provider<String> canonicalUrl,
     244             :       AccountCache accountCache,
     245             :       GroupBackend groupBackend,
     246             :       Boolean enablePeerIPInReflogRecord,
     247             :       @Nullable Provider<SocketAddress> remotePeerProvider,
     248             :       AccountState state,
     249             :       @Nullable CurrentUser realUser) {
     250         147 :     this(
     251             :         authConfig,
     252             :         realm,
     253             :         anonymousCowardName,
     254             :         canonicalUrl,
     255             :         accountCache,
     256             :         groupBackend,
     257             :         enablePeerIPInReflogRecord,
     258             :         remotePeerProvider,
     259         147 :         state.account().id(),
     260             :         realUser,
     261             :         PropertyMap.EMPTY);
     262         147 :     this.state = state;
     263         147 :   }
     264             : 
     265             :   private IdentifiedUser(
     266             :       AuthConfig authConfig,
     267             :       Realm realm,
     268             :       String anonymousCowardName,
     269             :       Provider<String> canonicalUrl,
     270             :       AccountCache accountCache,
     271             :       GroupBackend groupBackend,
     272             :       Boolean enablePeerIPInReflogRecord,
     273             :       @Nullable Provider<SocketAddress> remotePeerProvider,
     274             :       Account.Id id,
     275             :       @Nullable CurrentUser realUser,
     276             :       PropertyMap properties) {
     277         151 :     super(properties);
     278         151 :     this.canonicalUrl = canonicalUrl;
     279         151 :     this.accountCache = accountCache;
     280         151 :     this.groupBackend = groupBackend;
     281         151 :     this.authConfig = authConfig;
     282         151 :     this.realm = realm;
     283         151 :     this.anonymousCowardName = anonymousCowardName;
     284         151 :     this.enablePeerIPInReflogRecord = enablePeerIPInReflogRecord;
     285         151 :     this.remotePeerProvider = remotePeerProvider;
     286         151 :     this.accountId = id;
     287         151 :     this.realUser = realUser != null ? realUser : this;
     288         151 :   }
     289             : 
     290             :   @Override
     291             :   public CurrentUser getRealUser() {
     292         103 :     return realUser;
     293             :   }
     294             : 
     295             :   @Override
     296             :   public boolean isImpersonating() {
     297         150 :     if (realUser == this) {
     298         150 :       return false;
     299             :     }
     300           2 :     if (realUser.isIdentifiedUser()) {
     301           2 :       if (realUser.getAccountId().equals(getAccountId())) {
     302             :         // Impersonating another copy of this user is allowed.
     303           0 :         return false;
     304             :       }
     305             :     }
     306           2 :     return true;
     307             :   }
     308             : 
     309             :   /**
     310             :    * Returns the account state of the identified user.
     311             :    *
     312             :    * @return the account state of the identified user, an empty account state if the account is
     313             :    *     missing
     314             :    */
     315             :   public AccountState state() {
     316         151 :     if (state == null) {
     317             :       // TODO(ekempin):
     318             :       // Ideally we would only create IdentifiedUser instances for existing accounts. To ensure
     319             :       // this we could load the account state eagerly on the creation of IdentifiedUser and fail is
     320             :       // the account is missing. In most cases, e.g. when creating an IdentifiedUser for a request
     321             :       // context, we really want to fail early if the account is missing. However there are some
     322             :       // usages where an IdentifiedUser may be instantiated for a missing account. We may go
     323             :       // through all of them and ensure that they never try to create an IdentifiedUser for a
     324             :       // missing account or make this explicit by adding a createEvenIfMissing method to
     325             :       // IdentifiedUser.GenericFactory. However since this is a lot of effort we stick with calling
     326             :       // AccountCache#getEvenIfMissing(Account.Id) for now.
     327             :       // Alternatively we could be could also return an Optional<AccountState> from the state()
     328             :       // method and let callers handle the missing account case explicitly. But this would be a lot
     329             :       // of work too.
     330         151 :       state = accountCache.getEvenIfMissing(getAccountId());
     331             :     }
     332         151 :     return state;
     333             :   }
     334             : 
     335             :   @Override
     336             :   public IdentifiedUser asIdentifiedUser() {
     337         150 :     return this;
     338             :   }
     339             : 
     340             :   @Override
     341             :   public Account.Id getAccountId() {
     342         151 :     return accountId;
     343             :   }
     344             : 
     345             :   /**
     346             :    * Returns the user's user name; null if one has not been selected/assigned or if the user name is
     347             :    * empty.
     348             :    */
     349             :   @Override
     350             :   public Optional<String> getUserName() {
     351         151 :     return state().userName();
     352             :   }
     353             : 
     354             :   /** Returns unique name of the user for logging, never {@code null} */
     355             :   @Override
     356             :   public String getLoggableName() {
     357         150 :     return getUserName()
     358         150 :         .orElseGet(() -> firstNonNull(getAccount().preferredEmail(), "a/" + getAccountId().get()));
     359             :   }
     360             : 
     361             :   /**
     362             :    * Returns the account of the identified user.
     363             :    *
     364             :    * @return the account of the identified user, an empty account if the account is missing
     365             :    */
     366             :   public Account getAccount() {
     367         151 :     return state().account();
     368             :   }
     369             : 
     370             :   public boolean hasEmailAddress(String email) {
     371         110 :     if (validEmails.contains(email)) {
     372         101 :       return true;
     373         108 :     } else if (invalidEmails != null && invalidEmails.contains(email)) {
     374          28 :       return false;
     375         108 :     } else if (realm.hasEmailAddress(this, email)) {
     376         104 :       validEmails.add(email);
     377         104 :       return true;
     378          38 :     } else if (invalidEmails == null) {
     379          38 :       invalidEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER);
     380             :     }
     381          38 :     invalidEmails.add(email);
     382          38 :     return false;
     383             :   }
     384             : 
     385             :   @Override
     386             :   public ImmutableSet<String> getEmailAddresses() {
     387          52 :     if (!loadedAllEmails) {
     388          52 :       validEmails.addAll(realm.getEmailAddresses(this));
     389          52 :       loadedAllEmails = true;
     390             :     }
     391          52 :     return ImmutableSet.copyOf(validEmails);
     392             :   }
     393             : 
     394             :   @Override
     395             :   public ImmutableSet<ExternalId.Key> getExternalIdKeys() {
     396           2 :     return state().externalIds().stream().map(ExternalId::key).collect(toImmutableSet());
     397             :   }
     398             : 
     399             :   public String getName() {
     400          41 :     return getAccount().getName();
     401             :   }
     402             : 
     403             :   public String getNameEmail() {
     404           6 :     return getAccount().getNameEmail(anonymousCowardName);
     405             :   }
     406             : 
     407             :   @Override
     408             :   public GroupMembership getEffectiveGroups() {
     409         150 :     if (effectiveGroups == null) {
     410         150 :       if (authConfig.isIdentityTrustable(state().externalIds())) {
     411         150 :         effectiveGroups = groupBackend.membershipsOf(this);
     412         150 :         logger.atFinest().log(
     413         150 :             "Known groups of %s: %s", getLoggableName(), lazy(effectiveGroups::getKnownGroups));
     414             :       } else {
     415           3 :         effectiveGroups = registeredGroups;
     416           3 :         logger.atFinest().log(
     417             :             "%s has a non-trusted identity, falling back to %s as known groups",
     418           3 :             getLoggableName(), lazy(registeredGroups::getKnownGroups));
     419             :       }
     420             :     }
     421         150 :     return effectiveGroups;
     422             :   }
     423             : 
     424             :   @Override
     425             :   public Object getCacheKey() {
     426         145 :     return getAccountId();
     427             :   }
     428             : 
     429             :   public PersonIdent newRefLogIdent() {
     430         100 :     return newRefLogIdent(Instant.now(), ZoneId.systemDefault());
     431             :   }
     432             : 
     433             :   public PersonIdent newRefLogIdent(Instant when, ZoneId zoneId) {
     434         113 :     final Account ua = getAccount();
     435             : 
     436         113 :     String name = ua.fullName();
     437         113 :     if (name == null || name.isEmpty()) {
     438          15 :       name = ua.preferredEmail();
     439             :     }
     440         113 :     if (name == null || name.isEmpty()) {
     441          13 :       name = anonymousCowardName;
     442             :     }
     443             : 
     444             :     String user;
     445         113 :     if (enablePeerIPInReflogRecord) {
     446           1 :       user = constructMailAddress(ua, guessHost());
     447             :     } else {
     448             :       user =
     449         113 :           Strings.isNullOrEmpty(ua.preferredEmail())
     450          13 :               ? constructMailAddress(ua, "unknown")
     451         113 :               : ua.preferredEmail();
     452             :     }
     453             : 
     454         113 :     return new PersonIdent(name, user, when, zoneId);
     455             :   }
     456             : 
     457             :   private String constructMailAddress(Account ua, String host) {
     458          14 :     return getUserName().orElse("") + "|account-" + ua.id().toString() + "@" + host;
     459             :   }
     460             : 
     461             :   public PersonIdent newCommitterIdent(PersonIdent ident) {
     462         151 :     return newCommitterIdent(ident.getWhenAsInstant(), ident.getZoneId());
     463             :   }
     464             : 
     465             :   public PersonIdent newCommitterIdent(Instant when, ZoneId zoneId) {
     466         151 :     final Account ua = getAccount();
     467         151 :     String name = ua.fullName();
     468         151 :     String email = ua.preferredEmail();
     469             : 
     470         151 :     if (email == null || email.isEmpty()) {
     471             :       // No preferred email is configured. Use a generic identity so we
     472             :       // don't leak an address the user may have given us, but doesn't
     473             :       // necessarily want to publish through Git records.
     474             :       //
     475          17 :       String user = getUserName().orElseGet(() -> "account-" + ua.id().toString());
     476             : 
     477             :       String host;
     478          17 :       if (canonicalUrl.get() != null) {
     479             :         try {
     480          17 :           host = new URL(canonicalUrl.get()).getHost();
     481           0 :         } catch (MalformedURLException e) {
     482           0 :           host = SystemReader.getInstance().getHostname();
     483          17 :         }
     484             :       } else {
     485           0 :         host = SystemReader.getInstance().getHostname();
     486             :       }
     487             : 
     488          17 :       email = user + "@" + host;
     489             :     }
     490             : 
     491         151 :     if (name == null || name.isEmpty()) {
     492          18 :       final int at = email.indexOf('@');
     493          18 :       if (0 < at) {
     494          18 :         name = email.substring(0, at);
     495             :       } else {
     496           0 :         name = anonymousCowardName;
     497             :       }
     498             :     }
     499             : 
     500         151 :     return new PersonIdent(name, email, when, zoneId);
     501             :   }
     502             : 
     503             :   @Override
     504             :   public String toString() {
     505           5 :     return "IdentifiedUser[account " + getAccountId() + "]";
     506             :   }
     507             : 
     508             :   /** Check if user is the IdentifiedUser */
     509             :   @Override
     510             :   public boolean isIdentifiedUser() {
     511         150 :     return true;
     512             :   }
     513             : 
     514             :   /**
     515             :    * Returns a materialized copy of the user with all dependencies.
     516             :    *
     517             :    * <p>Invoke all providers and factories of dependent objects and store the references to a copy
     518             :    * of the current identified user.
     519             :    *
     520             :    * @return copy of the identified user
     521             :    */
     522             :   public IdentifiedUser materializedCopy() {
     523             :     Provider<SocketAddress> remotePeer;
     524             :     try {
     525          96 :       remotePeer = Providers.of(remotePeerProvider.get());
     526           0 :     } catch (OutOfScopeException | ProvisionException e) {
     527           0 :       remotePeer =
     528             :           () -> {
     529           0 :             throw e;
     530             :           };
     531          96 :     }
     532          96 :     return new IdentifiedUser(
     533             :         authConfig,
     534             :         realm,
     535             :         anonymousCowardName,
     536          96 :         Providers.of(canonicalUrl.get()),
     537             :         accountCache,
     538             :         groupBackend,
     539             :         enablePeerIPInReflogRecord,
     540             :         remotePeer,
     541             :         state,
     542             :         realUser);
     543             :   }
     544             : 
     545             :   @Override
     546             :   public boolean hasSameAccountId(CurrentUser other) {
     547          28 :     return getAccountId().get() == other.getAccountId().get();
     548             :   }
     549             : 
     550             :   private String guessHost() {
     551           1 :     String host = null;
     552           1 :     SocketAddress remotePeer = null;
     553             :     try {
     554           1 :       remotePeer = remotePeerProvider.get();
     555           0 :     } catch (OutOfScopeException | ProvisionException e) {
     556             :       // Leave null.
     557           1 :     }
     558           1 :     if (remotePeer instanceof InetSocketAddress) {
     559           0 :       InetSocketAddress sa = (InetSocketAddress) remotePeer;
     560           0 :       InetAddress in = sa.getAddress();
     561           0 :       host = in != null ? in.getHostAddress() : sa.getHostName();
     562             :     }
     563           1 :     if (Strings.isNullOrEmpty(host)) {
     564           1 :       return "unknown";
     565             :     }
     566           0 :     return host;
     567             :   }
     568             : }

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