CurrentUser: Factor out PropertyMap and make it immutable
CurrentUser has functionality for extensions and plugins to contribute state - that is properties unknown to Gerrit core - and retrieve them in a type-safe fashion. This is used to add the last external ID key that the user used to log into Gerrit from WebSession. It's also used on googlesource.com to add an internal representation of the login information in a similar fashion. CurrentUser and it's subclasses serve two purposes: 1) They are instantiated as per-request state scoped in Guice In this case, CurrentUser represents the requestor either as IdentifiedUser or AnonymousUser.. 2) They can be instantiated for any user to be used in internal APIs and for checking permissions of that absent user. We want to restrict the option to add per-request state to (1) and even there ensure that no per-request state is ever added to AnonymousUser or future subclasses of CurrentUser. This is relevant because we need to prevent that login info, such as a session is added to a potentially shared AnonnymousUser instance OR to objects constructed outside Guice's request scope initializer. At the same time, we want to allow scoped properties not only on IdentifiedUser, but also on SingleGroupUser. The best option to to all that is to restrict property creation to the instantiation of CurrentUser and disallow any dynamic mutations, which is what this commit does. While at it, we remove the IS_ADMIN property because it's superflous optimization. That bit is already cached inside WithUser. Change-Id: I547893c852668b0970455ec5e4591807a78448e8
This commit is contained in:
@@ -28,6 +28,7 @@ import com.google.gerrit.server.AccessPath;
|
||||
import com.google.gerrit.server.AnonymousUser;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.IdentifiedUser;
|
||||
import com.google.gerrit.server.PropertyMap;
|
||||
import com.google.gerrit.server.account.AccountCache;
|
||||
import com.google.gerrit.server.account.AuthResult;
|
||||
import com.google.gerrit.server.account.externalids.ExternalId;
|
||||
@@ -160,16 +161,12 @@ public abstract class CacheBasedWebSession implements WebSession {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExternalId.Key getLastLoginExternalId() {
|
||||
return val != null ? val.getExternalId() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CurrentUser getUser() {
|
||||
if (user == null) {
|
||||
if (isSignedIn()) {
|
||||
user = identified.create(val.getAccountId());
|
||||
|
||||
user = identified.create(val.getAccountId(), getUserProperties(val));
|
||||
} else {
|
||||
user = anonymousProvider.get();
|
||||
}
|
||||
@@ -177,6 +174,15 @@ public abstract class CacheBasedWebSession implements WebSession {
|
||||
return user;
|
||||
}
|
||||
|
||||
private static PropertyMap getUserProperties(@Nullable WebSessionManager.Val val) {
|
||||
if (val == null || val.getExternalId() == null) {
|
||||
return PropertyMap.EMPTY;
|
||||
}
|
||||
return PropertyMap.builder()
|
||||
.put(CurrentUser.LAST_LOGIN_EXTERNAL_ID_PROPERTY_KEY, val.getExternalId())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void login(AuthResult res, boolean rememberMe) {
|
||||
Account.Id id = res.getAccountId();
|
||||
@@ -194,7 +200,7 @@ public abstract class CacheBasedWebSession implements WebSession {
|
||||
key = manager.createKey(id);
|
||||
val = manager.createVal(key, id, rememberMe, identity, null, null);
|
||||
saveCookie();
|
||||
user = identified.create(val.getAccountId());
|
||||
user = identified.create(val.getAccountId(), getUserProperties(val));
|
||||
}
|
||||
|
||||
/** Set the user account for this current request only. */
|
||||
|
||||
@@ -19,7 +19,6 @@ import com.google.gerrit.entities.Account;
|
||||
import com.google.gerrit.server.AccessPath;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.account.AuthResult;
|
||||
import com.google.gerrit.server.account.externalids.ExternalId;
|
||||
|
||||
public interface WebSession {
|
||||
boolean isSignedIn();
|
||||
@@ -29,8 +28,6 @@ public interface WebSession {
|
||||
|
||||
boolean isValidXGerritAuth(String keyIn);
|
||||
|
||||
ExternalId.Key getLastLoginExternalId();
|
||||
|
||||
CurrentUser getUser();
|
||||
|
||||
void login(AuthResult res, boolean rememberMe);
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.FilterConfig;
|
||||
@@ -124,8 +125,8 @@ class HttpAuthFilter implements Filter {
|
||||
}
|
||||
|
||||
private static boolean correctUser(String user, WebSession session) {
|
||||
ExternalId.Key id = session.getLastLoginExternalId();
|
||||
return id != null && id.equals(ExternalId.Key.create(SCHEME_GERRIT, user));
|
||||
Optional<ExternalId.Key> id = session.getUser().getLastLoginExternalIdKey();
|
||||
return id.map(i -> i.equals(ExternalId.Key.create(SCHEME_GERRIT, user))).orElse(false);
|
||||
}
|
||||
|
||||
String getRemoteUser(HttpServletRequest req) {
|
||||
|
||||
@@ -1639,9 +1639,6 @@ public class RestApiServlet extends HttpServlet {
|
||||
"Invalid authentication method. In order to authenticate, "
|
||||
+ "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
|
||||
}
|
||||
if (user.isIdentifiedUser()) {
|
||||
user.setLastLoginExternalIdKey(globals.webSession.get().getLastLoginExternalId());
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getParameterNames(HttpServletRequest req) {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
package com.google.gerrit.server;
|
||||
|
||||
import com.google.gerrit.common.Nullable;
|
||||
import com.google.gerrit.entities.Account;
|
||||
import com.google.gerrit.server.account.GroupMembership;
|
||||
import com.google.gerrit.server.account.externalids.ExternalId;
|
||||
@@ -31,17 +30,19 @@ import java.util.function.Consumer;
|
||||
* @see IdentifiedUser
|
||||
*/
|
||||
public abstract class CurrentUser {
|
||||
/** Unique key for plugin/extension specific data on a CurrentUser. */
|
||||
public static final class PropertyKey<T> {
|
||||
public static <T> PropertyKey<T> create() {
|
||||
return new PropertyKey<>();
|
||||
}
|
||||
public static final PropertyMap.Key<ExternalId.Key> LAST_LOGIN_EXTERNAL_ID_PROPERTY_KEY =
|
||||
PropertyMap.key();
|
||||
|
||||
private PropertyKey() {}
|
||||
private final PropertyMap properties;
|
||||
private AccessPath accessPath = AccessPath.UNKNOWN;
|
||||
|
||||
protected CurrentUser() {
|
||||
this.properties = PropertyMap.EMPTY;
|
||||
}
|
||||
|
||||
private AccessPath accessPath = AccessPath.UNKNOWN;
|
||||
private PropertyKey<ExternalId.Key> lastLoginExternalIdPropertyKey = PropertyKey.create();
|
||||
protected CurrentUser(PropertyMap properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/** How this user is accessing the Gerrit Code Review application. */
|
||||
public final AccessPath getAccessPath() {
|
||||
@@ -127,29 +128,18 @@ public abstract class CurrentUser {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a previously stored property.
|
||||
* Lookup a stored property.
|
||||
*
|
||||
* @param key unique property key.
|
||||
* @return previously stored value, or {@code Optional#empty()}.
|
||||
* @param key unique property key. This key has to be the same instance that was used to store the
|
||||
* value when constructing the {@link PropertyMap}
|
||||
* @return stored value, or {@code Optional#empty()}.
|
||||
*/
|
||||
public <T> Optional<T> get(PropertyKey<T> key) {
|
||||
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.
|
||||
*/
|
||||
public <T> void put(PropertyKey<T> key, @Nullable T value) {}
|
||||
|
||||
public void setLastLoginExternalIdKey(ExternalId.Key externalIdKey) {
|
||||
put(lastLoginExternalIdPropertyKey, externalIdKey);
|
||||
public <T> Optional<T> get(PropertyMap.Key<T> key) {
|
||||
return properties.get(key);
|
||||
}
|
||||
|
||||
public Optional<ExternalId.Key> getLastLoginExternalIdKey() {
|
||||
return get(lastLoginExternalIdPropertyKey);
|
||||
return get(LAST_LOGIN_EXTERNAL_ID_PROPERTY_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,8 +46,6 @@ 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;
|
||||
@@ -121,7 +119,8 @@ public class IdentifiedUser extends CurrentUser {
|
||||
enableReverseDnsLookup,
|
||||
Providers.of(remotePeer),
|
||||
id,
|
||||
caller);
|
||||
caller,
|
||||
PropertyMap.EMPTY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +162,10 @@ public class IdentifiedUser extends CurrentUser {
|
||||
}
|
||||
|
||||
public IdentifiedUser create(Account.Id id) {
|
||||
return create(id, PropertyMap.EMPTY);
|
||||
}
|
||||
|
||||
public <T> IdentifiedUser create(Account.Id id, PropertyMap properties) {
|
||||
return new IdentifiedUser(
|
||||
authConfig,
|
||||
realm,
|
||||
@@ -173,7 +176,8 @@ public class IdentifiedUser extends CurrentUser {
|
||||
enableReverseDnsLookup,
|
||||
remotePeerProvider,
|
||||
id,
|
||||
null);
|
||||
null,
|
||||
properties);
|
||||
}
|
||||
|
||||
public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
|
||||
@@ -187,7 +191,8 @@ public class IdentifiedUser extends CurrentUser {
|
||||
enableReverseDnsLookup,
|
||||
remotePeerProvider,
|
||||
id,
|
||||
caller);
|
||||
caller,
|
||||
PropertyMap.EMPTY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +217,6 @@ public class IdentifiedUser extends CurrentUser {
|
||||
private boolean loadedAllEmails;
|
||||
private Set<String> invalidEmails;
|
||||
private GroupMembership effectiveGroups;
|
||||
private Map<PropertyKey<Object>, Object> properties;
|
||||
|
||||
private IdentifiedUser(
|
||||
AuthConfig authConfig,
|
||||
@@ -235,7 +239,8 @@ public class IdentifiedUser extends CurrentUser {
|
||||
enableReverseDnsLookup,
|
||||
remotePeerProvider,
|
||||
state.account().id(),
|
||||
realUser);
|
||||
realUser,
|
||||
PropertyMap.EMPTY);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@@ -249,7 +254,9 @@ public class IdentifiedUser extends CurrentUser {
|
||||
Boolean enableReverseDnsLookup,
|
||||
@Nullable Provider<SocketAddress> remotePeerProvider,
|
||||
Account.Id id,
|
||||
@Nullable CurrentUser realUser) {
|
||||
@Nullable CurrentUser realUser,
|
||||
PropertyMap properties) {
|
||||
super(properties);
|
||||
this.canonicalUrl = canonicalUrl;
|
||||
this.accountCache = accountCache;
|
||||
this.groupBackend = groupBackend;
|
||||
@@ -458,40 +465,6 @@ public class IdentifiedUser extends CurrentUser {
|
||||
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.
|
||||
*
|
||||
|
||||
84
java/com/google/gerrit/server/PropertyMap.java
Normal file
84
java/com/google/gerrit/server/PropertyMap.java
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (C) 2020 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 com.google.common.collect.ImmutableMap;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Immutable map that holds a collection of random objects allowing for a type-safe retrieval.
|
||||
*
|
||||
* <p>Intended to be used in {@link CurrentUser} when the object is constructed during login and
|
||||
* holds per-request state. This functionality allows plugins/extensions to contribute specific data
|
||||
* to {@link CurrentUser} that is unknown to Gerrit core.
|
||||
*/
|
||||
public class PropertyMap {
|
||||
/** Empty instance to be referenced once per JVM. */
|
||||
public static final PropertyMap EMPTY = builder().build();
|
||||
|
||||
/**
|
||||
* Typed key for {@link PropertyMap}. This class intentionally does not implement {@link
|
||||
* Object#equals(Object)} and {@link Object#hashCode()} so that the same instance has to be used
|
||||
* to retrieve a stored value.
|
||||
*
|
||||
* <p>We require the exact same key instance because {@link PropertyMap} is implemented in a
|
||||
* type-safe fashion by using Java generics to guarantee the return type. The generic type can't
|
||||
* be recovered at runtime, so there is no way to just use the type's full name as key - we'd have
|
||||
* to pass additional arguments. At the same time, this is in-line with how we'd want callers to
|
||||
* use {@link PropertyMap}: Instantiate a static, per-JVM key that is reused when setting and
|
||||
* getting values.
|
||||
*/
|
||||
public static class Key<T> {}
|
||||
|
||||
public static <T> Key<T> key() {
|
||||
return new Key<>();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private ImmutableMap.Builder<Object, Object> mutableMap;
|
||||
|
||||
private Builder() {
|
||||
this.mutableMap = ImmutableMap.builder();
|
||||
}
|
||||
|
||||
/** Adds the provided {@code value} to the {@link PropertyMap} that is being built. */
|
||||
public <T> Builder put(Key<T> key, T value) {
|
||||
mutableMap.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Builds and returns an immutable {@link PropertyMap}. */
|
||||
public PropertyMap build() {
|
||||
return new PropertyMap(mutableMap.build());
|
||||
}
|
||||
}
|
||||
|
||||
private final ImmutableMap<Object, Object> map;
|
||||
|
||||
private PropertyMap(ImmutableMap<Object, Object> map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
/** Returns a new {@link Builder} instance. */
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/** Returns the requested value wrapped as {@link Optional}. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> get(Key<T> key) {
|
||||
return Optional.ofNullable((T) map.get(key));
|
||||
}
|
||||
}
|
||||
@@ -41,15 +41,12 @@ import com.google.inject.Provider;
|
||||
import com.google.inject.Singleton;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Singleton
|
||||
public class DefaultPermissionBackend extends PermissionBackend {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final CurrentUser.PropertyKey<Boolean> IS_ADMIN = CurrentUser.PropertyKey.create();
|
||||
|
||||
private final Provider<CurrentUser> currentUser;
|
||||
private final ProjectCache projectCache;
|
||||
private final ProjectControl.Factory projectControlFactory;
|
||||
@@ -197,21 +194,13 @@ public class DefaultPermissionBackend extends PermissionBackend {
|
||||
}
|
||||
|
||||
private Boolean computeAdmin() {
|
||||
Optional<Boolean> r = user.get(IS_ADMIN);
|
||||
if (r.isPresent()) {
|
||||
return r.get();
|
||||
}
|
||||
|
||||
boolean isAdmin;
|
||||
if (user.isImpersonating()) {
|
||||
isAdmin = false;
|
||||
} else if (user instanceof PeerDaemonUser) {
|
||||
isAdmin = true;
|
||||
} else {
|
||||
isAdmin = allow(capabilities().administrateServer);
|
||||
return false;
|
||||
}
|
||||
user.put(IS_ADMIN, isAdmin);
|
||||
return isAdmin;
|
||||
if (user instanceof PeerDaemonUser) {
|
||||
return true;
|
||||
}
|
||||
return allow(capabilities().administrateServer);
|
||||
}
|
||||
|
||||
private boolean canEmailReviewers() {
|
||||
|
||||
@@ -16,7 +16,6 @@ package com.google.gerrit.acceptance.testsuite.request;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
import static com.google.common.truth.Truth8.assertThat;
|
||||
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -29,7 +28,6 @@ import com.google.gerrit.entities.Account;
|
||||
import com.google.gerrit.extensions.common.ChangeInput;
|
||||
import com.google.gerrit.server.AnonymousUser;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.CurrentUser.PropertyKey;
|
||||
import com.google.gerrit.server.notedb.Sequences;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
@@ -74,19 +72,6 @@ public class RequestScopeOperationsImplTest extends AbstractDaemonTest {
|
||||
checkCurrentUser(admin.id());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetCurrentApiUserClearsCachedState() throws Exception {
|
||||
requestScopeOperations.setApiUser(user.id());
|
||||
PropertyKey<String> key = PropertyKey.create();
|
||||
atrScope.get().getUser().put(key, "foo");
|
||||
assertThat(atrScope.get().getUser().get(key)).hasValue("foo");
|
||||
|
||||
AcceptanceTestRequestScope.Context oldCtx = requestScopeOperations.resetCurrentApiUser();
|
||||
checkCurrentUser(user.id());
|
||||
assertThat(atrScope.get().getUser().get(key)).isEmpty();
|
||||
assertThat(oldCtx.getUser().get(key)).hasValue("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setApiUserAnonymousSetsAnonymousUser() throws Exception {
|
||||
fastCheckCurrentUser(admin.id());
|
||||
|
||||
Reference in New Issue
Block a user