diff options
Diffstat (limited to 'java/com/google/gerrit/server/IdentifiedUser.java')
-rw-r--r-- | java/com/google/gerrit/server/IdentifiedUser.java | 565 |
1 files changed, 565 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java new file mode 100644 index 0000000000..d9a4cae71a --- /dev/null +++ b/java/com/google/gerrit/server/IdentifiedUser.java @@ -0,0 +1,565 @@ +// Copyright (C) 2009 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.server; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.flogger.LazyArgs.lazy; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.common.flogger.FluentLogger; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.GroupBackend; +import com.google.gerrit.server.account.GroupMembership; +import com.google.gerrit.server.account.ListGroupMembership; +import com.google.gerrit.server.account.Realm; +import com.google.gerrit.server.config.AnonymousCowardName; +import com.google.gerrit.server.config.AuthConfig; +import com.google.gerrit.server.config.CanonicalWebUrl; +import com.google.gerrit.server.config.DisableReverseDnsLookup; +import com.google.gerrit.server.group.SystemGroupBackend; +import com.google.inject.Inject; +import com.google.inject.OutOfScopeException; +import com.google.inject.Provider; +import com.google.inject.ProvisionException; +import com.google.inject.Singleton; +import com.google.inject.util.Providers; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.SocketAddress; +import java.net.URL; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.TimeZone; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.util.SystemReader; + +/** An authenticated user. */ +public class IdentifiedUser extends CurrentUser { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + /** Create an IdentifiedUser, ignoring any per-request state. */ + @Singleton + public static class GenericFactory { + private final AuthConfig authConfig; + private final Realm realm; + private final String anonymousCowardName; + private final Provider<String> canonicalUrl; + private final AccountCache accountCache; + private final GroupBackend groupBackend; + private final Boolean disableReverseDnsLookup; + + @Inject + public GenericFactory( + AuthConfig authConfig, + Realm realm, + @AnonymousCowardName String anonymousCowardName, + @CanonicalWebUrl Provider<String> canonicalUrl, + @DisableReverseDnsLookup Boolean disableReverseDnsLookup, + AccountCache accountCache, + GroupBackend groupBackend) { + this.authConfig = authConfig; + this.realm = realm; + this.anonymousCowardName = anonymousCowardName; + this.canonicalUrl = canonicalUrl; + this.accountCache = accountCache; + this.groupBackend = groupBackend; + this.disableReverseDnsLookup = disableReverseDnsLookup; + } + + public IdentifiedUser create(AccountState state) { + return new IdentifiedUser( + authConfig, + realm, + anonymousCowardName, + canonicalUrl, + accountCache, + groupBackend, + disableReverseDnsLookup, + Providers.of((SocketAddress) null), + state, + null); + } + + public IdentifiedUser create(Account.Id id) { + return create((SocketAddress) null, id); + } + + public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) { + return runAs(remotePeer, id, null); + } + + public IdentifiedUser runAs( + SocketAddress remotePeer, Account.Id id, @Nullable CurrentUser caller) { + return new IdentifiedUser( + authConfig, + realm, + anonymousCowardName, + canonicalUrl, + accountCache, + groupBackend, + disableReverseDnsLookup, + Providers.of(remotePeer), + id, + caller); + } + } + + /** + * Create an IdentifiedUser, relying on current request state. + * + * <p>Can only be used from within a module that has defined request scoped {@code @RemotePeer + * SocketAddress} and {@code ReviewDb} providers. + */ + @Singleton + public static class RequestFactory { + private final AuthConfig authConfig; + private final Realm realm; + private final String anonymousCowardName; + private final Provider<String> canonicalUrl; + private final AccountCache accountCache; + private final GroupBackend groupBackend; + private final Boolean disableReverseDnsLookup; + private final Provider<SocketAddress> remotePeerProvider; + + @Inject + RequestFactory( + AuthConfig authConfig, + Realm realm, + @AnonymousCowardName String anonymousCowardName, + @CanonicalWebUrl Provider<String> canonicalUrl, + AccountCache accountCache, + GroupBackend groupBackend, + @DisableReverseDnsLookup Boolean disableReverseDnsLookup, + @RemotePeer Provider<SocketAddress> remotePeerProvider) { + this.authConfig = authConfig; + this.realm = realm; + this.anonymousCowardName = anonymousCowardName; + this.canonicalUrl = canonicalUrl; + this.accountCache = accountCache; + this.groupBackend = groupBackend; + this.disableReverseDnsLookup = disableReverseDnsLookup; + this.remotePeerProvider = remotePeerProvider; + } + + public IdentifiedUser create(Account.Id id) { + return new IdentifiedUser( + authConfig, + realm, + anonymousCowardName, + canonicalUrl, + accountCache, + groupBackend, + disableReverseDnsLookup, + remotePeerProvider, + id, + null); + } + + public IdentifiedUser runAs(Account.Id id, CurrentUser caller) { + return new IdentifiedUser( + authConfig, + realm, + anonymousCowardName, + canonicalUrl, + accountCache, + groupBackend, + disableReverseDnsLookup, + remotePeerProvider, + id, + caller); + } + } + + private static final GroupMembership registeredGroups = + new ListGroupMembership( + ImmutableSet.of(SystemGroupBackend.ANONYMOUS_USERS, SystemGroupBackend.REGISTERED_USERS)); + + private final Provider<String> canonicalUrl; + private final AccountCache accountCache; + private final AuthConfig authConfig; + private final Realm realm; + private final GroupBackend groupBackend; + private final String anonymousCowardName; + private final Boolean disableReverseDnsLookup; + private final Set<String> validEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER); + private final CurrentUser realUser; // Must be final since cached properties depend on it. + + private final Provider<SocketAddress> remotePeerProvider; + private final Account.Id accountId; + + private AccountState state; + private boolean loadedAllEmails; + private Set<String> invalidEmails; + private GroupMembership effectiveGroups; + private Map<PropertyKey<Object>, Object> properties; + + private IdentifiedUser( + AuthConfig authConfig, + Realm realm, + String anonymousCowardName, + Provider<String> canonicalUrl, + AccountCache accountCache, + GroupBackend groupBackend, + Boolean disableReverseDnsLookup, + @Nullable Provider<SocketAddress> remotePeerProvider, + AccountState state, + @Nullable CurrentUser realUser) { + this( + authConfig, + realm, + anonymousCowardName, + canonicalUrl, + accountCache, + groupBackend, + disableReverseDnsLookup, + remotePeerProvider, + state.getAccount().getId(), + realUser); + this.state = state; + } + + private IdentifiedUser( + AuthConfig authConfig, + Realm realm, + String anonymousCowardName, + Provider<String> canonicalUrl, + AccountCache accountCache, + GroupBackend groupBackend, + Boolean disableReverseDnsLookup, + @Nullable Provider<SocketAddress> remotePeerProvider, + Account.Id id, + @Nullable CurrentUser realUser) { + this.canonicalUrl = canonicalUrl; + this.accountCache = accountCache; + this.groupBackend = groupBackend; + this.authConfig = authConfig; + this.realm = realm; + this.anonymousCowardName = anonymousCowardName; + this.disableReverseDnsLookup = disableReverseDnsLookup; + this.remotePeerProvider = remotePeerProvider; + this.accountId = id; + this.realUser = realUser != null ? realUser : this; + } + + @Override + public CurrentUser getRealUser() { + return realUser; + } + + @Override + public boolean isImpersonating() { + if (realUser == this) { + return false; + } + if (realUser.isIdentifiedUser()) { + if (realUser.getAccountId().equals(getAccountId())) { + // Impersonating another copy of this user is allowed. + return false; + } + } + return true; + } + + /** + * Returns the account state of the identified user. + * + * @return the account state of the identified user, an empty account state if the account is + * missing + */ + public AccountState state() { + if (state == null) { + // TODO(ekempin): + // Ideally we would only create IdentifiedUser instances for existing accounts. To ensure + // this we could load the account state eagerly on the creation of IdentifiedUser and fail is + // the account is missing. In most cases, e.g. when creating an IdentifiedUser for a request + // context, we really want to fail early if the account is missing. However there are some + // usages where an IdentifiedUser may be instantiated for a missing account. We may go + // through all of them and ensure that they never try to create an IdentifiedUser for a + // missing account or make this explicit by adding a createEvenIfMissing method to + // IdentifiedUser.GenericFactory. However since this is a lot of effort we stick with calling + // AccountCache#getEvenIfMissing(Account.Id) for now. + // Alternatively we could be could also return an Optional<AccountState> from the state() + // method and let callers handle the missing account case explicitly. But this would be a lot + // of work too. + state = accountCache.getEvenIfMissing(getAccountId()); + } + return state; + } + + @Override + public IdentifiedUser asIdentifiedUser() { + return this; + } + + @Override + public Account.Id getAccountId() { + return accountId; + } + + /** + * @return the user's user name; null if one has not been selected/assigned or if the user name is + * empty. + */ + @Override + public Optional<String> getUserName() { + return state().getUserName(); + } + + /** @return unique name of the user for logging, never {@code null} */ + @Override + public String getLoggableName() { + return getUserName() + .orElseGet( + () -> firstNonNull(getAccount().getPreferredEmail(), "a/" + getAccountId().get())); + } + + /** + * Returns the account of the identified user. + * + * @return the account of the identified user, an empty account if the account is missing + */ + public Account getAccount() { + return state().getAccount(); + } + + public boolean hasEmailAddress(String email) { + if (validEmails.contains(email)) { + return true; + } else if (invalidEmails != null && invalidEmails.contains(email)) { + return false; + } else if (realm.hasEmailAddress(this, email)) { + validEmails.add(email); + return true; + } else if (invalidEmails == null) { + invalidEmails = Sets.newTreeSet(String.CASE_INSENSITIVE_ORDER); + } + invalidEmails.add(email); + return false; + } + + public ImmutableSet<String> getEmailAddresses() { + if (!loadedAllEmails) { + validEmails.addAll(realm.getEmailAddresses(this)); + loadedAllEmails = true; + } + return ImmutableSet.copyOf(validEmails); + } + + public String getName() { + return getAccount().getName(); + } + + public String getNameEmail() { + return getAccount().getNameEmail(anonymousCowardName); + } + + @Override + public GroupMembership getEffectiveGroups() { + if (effectiveGroups == null) { + if (authConfig.isIdentityTrustable(state().getExternalIds())) { + effectiveGroups = groupBackend.membershipsOf(this); + logger.atFinest().log( + "Known groups of %s: %s", getLoggableName(), lazy(effectiveGroups::getKnownGroups)); + } else { + effectiveGroups = registeredGroups; + logger.atFinest().log( + "%s has a non-trusted identity, falling back to %s as known groups", + getLoggableName(), lazy(registeredGroups::getKnownGroups)); + } + } + return effectiveGroups; + } + + @Override + public Object getCacheKey() { + return getAccountId(); + } + + public PersonIdent newRefLogIdent() { + return newRefLogIdent(new Date(), TimeZone.getDefault()); + } + + public PersonIdent newRefLogIdent(Date when, TimeZone tz) { + final Account ua = getAccount(); + + String name = ua.getFullName(); + if (name == null || name.isEmpty()) { + name = ua.getPreferredEmail(); + } + if (name == null || name.isEmpty()) { + name = anonymousCowardName; + } + + String user = getUserName().orElse("") + "|account-" + ua.getId().toString(); + return new PersonIdent(name, user + "@" + guessHost(), when, tz); + } + + public PersonIdent newCommitterIdent(Date when, TimeZone tz) { + final Account ua = getAccount(); + String name = ua.getFullName(); + String email = ua.getPreferredEmail(); + + if (email == null || email.isEmpty()) { + // No preferred email is configured. Use a generic identity so we + // don't leak an address the user may have given us, but doesn't + // necessarily want to publish through Git records. + // + String user = getUserName().orElseGet(() -> "account-" + ua.getId().toString()); + + String host; + if (canonicalUrl.get() != null) { + try { + host = new URL(canonicalUrl.get()).getHost(); + } catch (MalformedURLException e) { + host = SystemReader.getInstance().getHostname(); + } + } else { + host = SystemReader.getInstance().getHostname(); + } + + email = user + "@" + host; + } + + if (name == null || name.isEmpty()) { + final int at = email.indexOf('@'); + if (0 < at) { + name = email.substring(0, at); + } else { + name = anonymousCowardName; + } + } + + return new PersonIdent(name, email, when, tz); + } + + @Override + public String toString() { + return "IdentifiedUser[account " + getAccountId() + "]"; + } + + /** Check if user is the IdentifiedUser */ + @Override + public boolean isIdentifiedUser() { + return true; + } + + @Override + public synchronized <T> Optional<T> get(PropertyKey<T> key) { + if (properties != null) { + @SuppressWarnings("unchecked") + T value = (T) properties.get(key); + return Optional.ofNullable(value); + } + return Optional.empty(); + } + + /** + * Store a property for later retrieval. + * + * @param key unique property key. + * @param value value to store; or {@code null} to clear the value. + */ + @Override + public synchronized <T> void put(PropertyKey<T> key, @Nullable T value) { + if (properties == null) { + if (value == null) { + return; + } + properties = new HashMap<>(); + } + + @SuppressWarnings("unchecked") + PropertyKey<Object> k = (PropertyKey<Object>) key; + if (value != null) { + properties.put(k, value); + } else { + properties.remove(k); + } + } + + /** + * Returns a materialized copy of the user with all dependencies. + * + * <p>Invoke all providers and factories of dependent objects and store the references to a copy + * of the current identified user. + * + * @return copy of the identified user + */ + public IdentifiedUser materializedCopy() { + Provider<SocketAddress> remotePeer; + try { + remotePeer = Providers.of(remotePeerProvider.get()); + } catch (OutOfScopeException | ProvisionException e) { + remotePeer = + new Provider<SocketAddress>() { + @Override + public SocketAddress get() { + throw e; + } + }; + } + return new IdentifiedUser( + authConfig, + realm, + anonymousCowardName, + Providers.of(canonicalUrl.get()), + accountCache, + groupBackend, + disableReverseDnsLookup, + remotePeer, + state, + realUser); + } + + @Override + public boolean hasSameAccountId(CurrentUser other) { + return getAccountId().get() == other.getAccountId().get(); + } + + private String guessHost() { + String host = null; + SocketAddress remotePeer = null; + try { + remotePeer = remotePeerProvider.get(); + } catch (OutOfScopeException | ProvisionException e) { + // Leave null. + } + if (remotePeer instanceof InetSocketAddress) { + InetSocketAddress sa = (InetSocketAddress) remotePeer; + InetAddress in = sa.getAddress(); + host = in != null ? getHost(in) : sa.getHostName(); + } + if (Strings.isNullOrEmpty(host)) { + return "unknown"; + } + return host; + } + + private String getHost(InetAddress in) { + if (Boolean.FALSE.equals(disableReverseDnsLookup)) { + return in.getCanonicalHostName(); + } + return in.getHostAddress(); + } +} |