Merge changes If23916d2,I70110e99 into stable-3.2
* changes:
Schema: Show only a single log for inexistent commits
Schema: Refactor lambda in buildFields to a separate function
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 257bf6f..5f9b369 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5389,7 +5389,13 @@
login as the 'Gerrit Code Review' user, required for the link:cmd-suexec.html[suexec]
command.
-The format is one Base-64 encoded public key per line.
+The format is one Base-64 encoded public key per line with optional comment, e.g.:
+----
+# Comments allowed at start of line
+AAAAC3...51R== john@example.net
+# Another comment
+AAAAB5...21S== jane@example.net
+----
=== Configurable Parameters
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 8ab3d62..c8c2dff 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -391,6 +391,10 @@
same link:cmd-stream-events.html#events[events] that are also streamed
by the link:cmd-stream-events.html[gerrit stream-events] command.
+* `com.google.gerrit.extensions.events.AccountActivationListener`:
++
+User account got activated or deactivated
+
* `com.google.gerrit.extensions.events.LifecycleListener`:
+
Plugin start and stop
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 75ad9c2..b91adbb 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -444,6 +444,23 @@
[NOTE]
Never rebase commits that are already part of a central branch.
+[[move]]
+== Move a Change
+
+Changes can be link:rest-api-changes.html#move-change[moved] to a desired
+destination branch in the same project. This is useful in cases where
+development activity switches from one branch to another and there is a
+need to move open changes on the inactive branch to the new active one.
+Another useful case is to move changes from a newer branch back to an older
+bugfix branch where an issue first appeared.
+
+Users can move a change only if they have link:access-control.html#category_abandon[
+abandon permission] on the change and link:access-control.html#category_push[
+push permission] on the destination branch.
+
+The move operation will not update the change's parent and users will have
+to link:#rebase[rebase] the change. Also, merge commits cannot be moved.
+
[[abandon]]
[[restore]]
== Abandon/Restore a Change
@@ -561,7 +578,19 @@
----
$ git push origin HEAD:refs/for/master%ready
----
-Alternatively, click *Start Review* from the Change screen.
+There are two options for marking the change ready for review from the Change
+screen:
+
+1. Click *Start Review* (the primary action *Reply* is renamed when in WIP
+state).
++
+This will open the reply-modal and allow you to add reviewers and/or CC
+before you start review.
+
+2. Click button *Mark As Active*.
++
+This will only change the state from WIP to ready, without opening the
+reply-modal.
Change owners, project owners, site administrators and members of a group that
was granted "Toggle Work In Progress state" permission can mark changes as
@@ -615,8 +644,7 @@
exposing secret details.
[[private-changes-pitfalls]]
-Pitfalls
-===
+=== Pitfalls
If private changes are used, be aware of the following pitfalls:
diff --git a/java/com/google/gerrit/acceptance/EventRecorder.java b/java/com/google/gerrit/acceptance/EventRecorder.java
index 563c2ef..1618573 100644
--- a/java/com/google/gerrit/acceptance/EventRecorder.java
+++ b/java/com/google/gerrit/acceptance/EventRecorder.java
@@ -70,9 +70,9 @@
@Override
public void onEvent(Event e) {
if (e instanceof ReviewerDeletedEvent) {
- recordedEvents.put(ReviewerDeletedEvent.TYPE, (ReviewerDeletedEvent) e);
+ recordedEvents.put(ReviewerDeletedEvent.TYPE, e);
} else if (e instanceof ChangeDeletedEvent) {
- recordedEvents.put(ChangeDeletedEvent.TYPE, (ChangeDeletedEvent) e);
+ recordedEvents.put(ChangeDeletedEvent.TYPE, e);
} else if (e instanceof RefEvent) {
RefEvent event = (RefEvent) e;
String key =
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index 5376d23..cfe7964 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -18,6 +18,7 @@
import com.google.gerrit.extensions.config.CapabilityDefinition;
import com.google.gerrit.extensions.config.DownloadScheme;
import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
+import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.events.CommentAddedListener;
@@ -73,6 +74,7 @@
private final DynamicSet<GroupBackend> groupBackends;
private final DynamicSet<AccountActivationValidationListener>
accountActivationValidationListeners;
+ private final DynamicSet<AccountActivationListener> accountActivationListeners;
private final DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
private final DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners;
private final DynamicMap<CapabilityDefinition> capabilityDefinitions;
@@ -101,6 +103,7 @@
DynamicSet<RevisionCreatedListener> revisionCreatedListeners,
DynamicSet<GroupBackend> groupBackends,
DynamicSet<AccountActivationValidationListener> accountActivationValidationListeners,
+ DynamicSet<AccountActivationListener> accountActivationListeners,
DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners,
DynamicSet<WorkInProgressStateChangedListener> workInProgressStateChangedListeners,
DynamicMap<CapabilityDefinition> capabilityDefinitions,
@@ -126,6 +129,7 @@
this.revisionCreatedListeners = revisionCreatedListeners;
this.groupBackends = groupBackends;
this.accountActivationValidationListeners = accountActivationValidationListeners;
+ this.accountActivationListeners = accountActivationListeners;
this.onSubmitValidationListeners = onSubmitValidationListeners;
this.workInProgressStateChangedListeners = workInProgressStateChangedListeners;
this.capabilityDefinitions = capabilityDefinitions;
@@ -229,6 +233,10 @@
return add(accountActivationValidationListeners, accountActivationValidationListener);
}
+ public Registration add(AccountActivationListener accountDeactivatedListener) {
+ return add(accountActivationListeners, accountDeactivatedListener);
+ }
+
public Registration add(OnSubmitValidationListener onSubmitValidationListener) {
return add(onSubmitValidationListeners, onSubmitValidationListener);
}
diff --git a/java/com/google/gerrit/extensions/events/AccountActivationListener.java b/java/com/google/gerrit/extensions/events/AccountActivationListener.java
new file mode 100644
index 0000000..b45533b
--- /dev/null
+++ b/java/com/google/gerrit/extensions/events/AccountActivationListener.java
@@ -0,0 +1,42 @@
+// 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.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Notified whenever an account got activated or deactivated.
+ *
+ * <p>This listener is called only after an account got (de)activated and hence cannot cancel the
+ * (de)activation. See {@link
+ * com.google.gerrit.server.validators.AccountActivationValidationListener} for a listener that can
+ * cancel a (de)activation.
+ */
+@ExtensionPoint
+public interface AccountActivationListener {
+ /**
+ * Invoked after an account got activated
+ *
+ * @param id of the account
+ */
+ default void onAccountActivated(int id) {}
+
+ /**
+ * Invoked after an account got deactivated
+ *
+ * @param id of the account
+ */
+ default void onAccountDeactivated(int id) {}
+}
diff --git a/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index 7f878aa..5c4830c 100644
--- a/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -16,6 +16,7 @@
import static java.util.concurrent.TimeUnit.HOURS;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
@@ -27,6 +28,7 @@
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AuthResult;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.config.AuthConfig;
@@ -40,7 +42,7 @@
@RequestScoped
public abstract class CacheBasedWebSession implements WebSession {
- private static final String ACCOUNT_COOKIE = "GerritAccount";
+ @VisibleForTesting public static final String ACCOUNT_COOKIE = "GerritAccount";
protected static final long MAX_AGE_MINUTES = HOURS.toMinutes(12);
private final HttpServletRequest request;
@@ -50,6 +52,7 @@
private final Provider<AnonymousUser> anonymousProvider;
private final IdentifiedUser.RequestFactory identified;
private final EnumSet<AccessPath> okPaths = EnumSet.of(AccessPath.UNKNOWN);
+ private final AccountCache byIdCache;
private Cookie outCookie;
private Key key;
@@ -62,13 +65,15 @@
WebSessionManager manager,
AuthConfig authConfig,
Provider<AnonymousUser> anonymousProvider,
- IdentifiedUser.RequestFactory identified) {
+ IdentifiedUser.RequestFactory identified,
+ AccountCache byIdCache) {
this.request = request;
this.response = response;
this.manager = manager;
this.authConfig = authConfig;
this.anonymousProvider = anonymousProvider;
this.identified = identified;
+ this.byIdCache = byIdCache;
if (request.getRequestURI() == null || !GitSmartHttpTools.isGitClient(request)) {
String cookie = readCookie(request);
@@ -85,6 +90,10 @@
authFromQueryParameter(token);
}
}
+ if (val != null && !checkAccountStatus(val.getAccountId())) {
+ val = null;
+ okPaths.clear();
+ }
if (val != null && val.needsCookieRefresh()) {
// Session is more than half old; update cache entry with new expiration date.
val = manager.createVal(key, val);
@@ -177,6 +186,11 @@
manager.destroy(key);
}
+ if (!checkAccountStatus(id)) {
+ val = null;
+ return;
+ }
+
key = manager.createKey(id);
val = manager.createVal(key, id, rememberMe, identity, null, null);
saveCookie();
@@ -207,6 +221,10 @@
return val != null ? val.getSessionId() : null;
}
+ private boolean checkAccountStatus(Account.Id id) {
+ return byIdCache.get(id).filter(as -> as.account().isActive()).isPresent();
+ }
+
private void saveCookie() {
if (response == null) {
return;
diff --git a/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java b/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
index caced27..830d8d6 100644
--- a/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
+++ b/java/com/google/gerrit/httpd/H2CacheBasedWebSession.java
@@ -20,6 +20,7 @@
import com.google.gerrit.httpd.WebSessionManager.Val;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser.RequestFactory;
+import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
@@ -59,8 +60,15 @@
@Named(WebSessionManager.CACHE_NAME) Cache<String, Val> cache,
AuthConfig authConfig,
Provider<AnonymousUser> anonymousProvider,
- RequestFactory identified) {
+ RequestFactory identified,
+ AccountCache byIdCache) {
super(
- request, response, managerFactory.create(cache), authConfig, anonymousProvider, identified);
+ request,
+ response,
+ managerFactory.create(cache),
+ authConfig,
+ anonymousProvider,
+ identified,
+ byIdCache);
}
}
diff --git a/java/com/google/gerrit/server/account/SetInactiveFlag.java b/java/com/google/gerrit/server/account/SetInactiveFlag.java
index 32ed694..4b68198 100644
--- a/java/com/google/gerrit/server/account/SetInactiveFlag.java
+++ b/java/com/google/gerrit/server/account/SetInactiveFlag.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.account;
import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
@@ -38,13 +39,16 @@
private final PluginSetContext<AccountActivationValidationListener>
accountActivationValidationListeners;
private final Provider<AccountsUpdate> accountsUpdateProvider;
+ private final PluginSetContext<AccountActivationListener> accountActivationListeners;
@Inject
SetInactiveFlag(
PluginSetContext<AccountActivationValidationListener> accountActivationValidationListeners,
- @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
+ @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider,
+ PluginSetContext<AccountActivationListener> accountActivationListeners) {
this.accountActivationValidationListeners = accountActivationValidationListeners;
this.accountsUpdateProvider = accountsUpdateProvider;
+ this.accountActivationListeners = accountActivationListeners;
}
public Response<?> deactivate(Account.Id accountId)
@@ -77,6 +81,12 @@
if (alreadyInactive.get()) {
throw new ResourceConflictException("account not active");
}
+
+ // At this point the account got set inactive and no errors occurred
+
+ int id = accountId.get();
+ accountActivationListeners.runEach(l -> l.onAccountDeactivated(id));
+
return Response.none();
}
@@ -107,6 +117,16 @@
if (exception.get().isPresent()) {
throw exception.get().get();
}
- return alreadyActive.get() ? Response.ok() : Response.created();
+
+ Response<String> res;
+ if (alreadyActive.get()) {
+ res = Response.ok();
+ } else {
+ res = Response.created();
+
+ int id = accountId.get();
+ accountActivationListeners.runEach(l -> l.onAccountActivated(id));
+ }
+ return res;
}
}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index e447f2b..cf592bf 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -31,6 +31,7 @@
import com.google.gerrit.extensions.config.ExternalIncludedIn;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.config.PluginProjectPermissionDefinition;
+import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.events.AccountIndexedListener;
import com.google.gerrit.extensions.events.AgreementSignupListener;
import com.google.gerrit.extensions.events.AssigneeChangedListener;
@@ -343,6 +344,7 @@
DynamicSet.setOf(binder(), PostReceiveHook.class);
DynamicSet.setOf(binder(), PreUploadHook.class);
DynamicSet.setOf(binder(), PostUploadHook.class);
+ DynamicSet.setOf(binder(), AccountActivationListener.class);
DynamicSet.setOf(binder(), AccountIndexedListener.class);
DynamicSet.setOf(binder(), ChangeIndexedListener.class);
DynamicSet.setOf(binder(), GroupIndexedListener.class);
diff --git a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
index 54625a6..54915fb 100644
--- a/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
+++ b/java/com/google/gerrit/server/rules/IgnoreSelfApprovalRule.java
@@ -26,9 +26,8 @@
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.annotations.Exports;
-import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.inject.Inject;
+import com.google.inject.AbstractModule;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.Collection;
@@ -46,7 +45,7 @@
private static final String E_UNABLE_TO_FETCH_LABELS =
"Unable to fetch labels and approvals for the change";
- public static class Module extends FactoryModule {
+ public static class Module extends AbstractModule {
@Override
public void configure() {
bind(SubmitRule.class)
@@ -55,9 +54,6 @@
}
}
- @Inject
- IgnoreSelfApprovalRule() {}
-
@Override
public Optional<SubmitRecord> evaluate(ChangeData cd) {
List<LabelType> labelTypes;
diff --git a/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java b/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
index bc52308..9fdc9e6 100644
--- a/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
+++ b/java/com/google/gerrit/server/validators/AccountActivationValidationListener.java
@@ -26,6 +26,9 @@
/**
* Called when an account should be activated to allow validation of the account activation.
*
+ * <p>See {@link com.google.gerrit.extensions.events.AccountActivationListener} for a listener
+ * that's run after the account got activated.
+ *
* @param account the account that should be activated
* @throws ValidationException if validation fails
*/
@@ -34,6 +37,9 @@
/**
* Called when an account should be deactivated to allow validation of the account deactivation.
*
+ * <p>See {@link com.google.gerrit.extensions.events.AccountActivationListener} for a listener
+ * that's run after the account got deactivated.
+ *
* @param account the account that should be deactivated
* @throws ValidationException if validation fails
*/
diff --git a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 916775d..6997d96 100644
--- a/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -18,6 +18,7 @@
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
+import com.google.common.base.Splitter;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.FileUtil;
@@ -38,6 +39,7 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.sshd.common.SshException;
@@ -197,9 +199,15 @@
continue;
}
+ List<String> parts = Splitter.on(' ').splitToList(line);
+ if (parts.size() > 2) {
+ throw new IllegalArgumentException(
+ "Invalid peer key file format, only <key [comment]> lines supported");
+ }
try {
byte[] bin =
- BaseEncoding.base64().decode(new String(line.getBytes(ISO_8859_1), ISO_8859_1));
+ BaseEncoding.base64()
+ .decode(new String(parts.get(0).getBytes(ISO_8859_1), ISO_8859_1));
keys.add(new ByteArrayBuffer(bin).getRawPublicKey());
} catch (RuntimeException | SshException e) {
logBadKey(path, line, e);
diff --git a/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
new file mode 100644
index 0000000..1086626
--- /dev/null
+++ b/java/com/google/gerrit/sshd/InactiveAccountDisconnector.java
@@ -0,0 +1,60 @@
+// 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.sshd;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.extensions.events.AccountActivationListener;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.sshd.BaseCommand.Failure;
+import com.google.inject.Inject;
+import java.io.IOException;
+
+/** Closes open SSH connections upon account deactivation. */
+public class InactiveAccountDisconnector implements AccountActivationListener {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final SshDaemon sshDaemon;
+
+ @Inject
+ InactiveAccountDisconnector(SshDaemon sshDaemon) {
+ this.sshDaemon = sshDaemon;
+ }
+
+ @Override
+ public void onAccountDeactivated(int id) {
+ try {
+ SshUtil.forEachSshSession(
+ sshDaemon,
+ (sshId, sshSession, abstractSession, ioSession) -> {
+ CurrentUser sessionUser = sshSession.getUser();
+ if (sessionUser.isIdentifiedUser() && sessionUser.getAccountId().get() == id) {
+ logger.atInfo().log(
+ "Disconnecting SSH session %s because user %s(%d) got deactivated",
+ abstractSession, sessionUser.getLoggableName(), id);
+ try {
+ abstractSession.disconnect(-1, "user deactivated");
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log(
+ "Failure while deactivating session %s", abstractSession);
+ }
+ }
+ });
+ } catch (Failure e) {
+ // Ssh Daemon no longer running. Since we're only disconnecting connections anyways, this is
+ // most likely ok, so we log only at info level.
+ logger.atInfo().withCause(e).log("Failure while disconnecting deactivated account %d", id);
+ }
+ }
+}
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index e4aa14c..9301f8a 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -19,6 +19,7 @@
import com.google.common.base.CharMatcher;
import com.google.common.base.Splitter;
+import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.lifecycle.LifecycleModule;
@@ -103,6 +104,9 @@
DynamicItem.itemOf(binder(), SshCreateCommandInterceptor.class);
DynamicSet.setOf(binder(), SshExecuteCommandInterceptor.class);
+ DynamicSet.bind(binder(), AccountActivationListener.class)
+ .to(InactiveAccountDisconnector.class);
+
listener().toInstance(registerInParentInjectors());
listener().to(SshLog.class);
listener().to(SshDaemon.class);
diff --git a/java/com/google/gerrit/sshd/SshUtil.java b/java/com/google/gerrit/sshd/SshUtil.java
index eac9737..abbd81d 100644
--- a/java/com/google/gerrit/sshd/SshUtil.java
+++ b/java/com/google/gerrit/sshd/SshUtil.java
@@ -19,6 +19,7 @@
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountSshKey;
+import com.google.gerrit.sshd.BaseCommand.Failure;
import com.google.gerrit.sshd.SshScope.Context;
import java.io.BufferedReader;
import java.io.IOException;
@@ -30,7 +31,10 @@
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import org.apache.sshd.common.SshException;
+import org.apache.sshd.common.io.IoAcceptor;
+import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
+import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.server.session.ServerSession;
@@ -152,4 +156,30 @@
final Account.Id account) {
return userFactory.create(sd.getRemoteAddress(), account);
}
+
+ public static void forEachSshSession(SshDaemon sshDaemon, SessionConsumer consumer)
+ throws Failure {
+ IoAcceptor ioAcceptor = sshDaemon.getIoAcceptor();
+ if (ioAcceptor == null) {
+ throw new Failure(1, "fatal: sshd no longer running");
+ }
+ ioAcceptor
+ .getManagedSessions()
+ .forEach(
+ (id, ioSession) -> {
+ AbstractSession abstractSession = AbstractSession.getSession(ioSession, true);
+ if (abstractSession != null) {
+ SshSession sshSession = abstractSession.getAttribute(SshSession.KEY);
+ if (sshSession != null) {
+ consumer.accept(id, sshSession, abstractSession, ioSession);
+ }
+ }
+ });
+ }
+
+ @FunctionalInterface
+ public interface SessionConsumer {
+ public void accept(
+ Long id, SshSession sshSession, AbstractSession abstractSession, IoSession ioSession);
+ }
}
diff --git a/java/com/google/gerrit/sshd/commands/CloseConnection.java b/java/com/google/gerrit/sshd/commands/CloseConnection.java
index 60a878a..093f647 100644
--- a/java/com/google/gerrit/sshd/commands/CloseConnection.java
+++ b/java/com/google/gerrit/sshd/commands/CloseConnection.java
@@ -23,15 +23,12 @@
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.sshd.SshDaemon;
-import com.google.gerrit.sshd.SshSession;
+import com.google.gerrit.sshd.SshUtil;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.sshd.common.future.CloseFuture;
-import org.apache.sshd.common.io.IoAcceptor;
-import org.apache.sshd.common.io.IoSession;
-import org.apache.sshd.common.session.helpers.AbstractSession;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -60,36 +57,26 @@
@Override
protected void run() throws Failure {
- IoAcceptor acceptor = sshDaemon.getIoAcceptor();
- if (acceptor == null) {
- throw new Failure(1, "fatal: sshd no longer running");
- }
- for (String sessionId : sessionIds) {
- boolean connectionFound = false;
- int id = (int) Long.parseLong(sessionId, 16);
- for (IoSession io : acceptor.getManagedSessions().values()) {
- AbstractSession serverSession = AbstractSession.getSession(io, true);
- SshSession sshSession =
- serverSession != null ? serverSession.getAttribute(SshSession.KEY) : null;
- if (sshSession != null && sshSession.getSessionId() == id) {
- connectionFound = true;
- stdout.println("closing connection " + sessionId + "...");
- CloseFuture future = io.close(true);
- if (wait) {
- try {
- future.await();
- stdout.println("closed connection " + sessionId);
- } catch (IOException e) {
- logger.atWarning().log(
- "Wait for connection to close interrupted: %s", e.getMessage());
+ SshUtil.forEachSshSession(
+ sshDaemon,
+ (k, sshSession, abstractSession, ioSession) -> {
+ String sessionId = String.format("%08x", sshSession.getSessionId());
+ if (sessionIds.remove(sessionId)) {
+ stdout.println("closing connection " + sessionId + "...");
+ CloseFuture future = ioSession.close(true);
+ if (wait) {
+ try {
+ future.await();
+ stdout.println("closed connection " + sessionId);
+ } catch (IOException e) {
+ logger.atWarning().log(
+ "Wait for connection to close interrupted: %s", e.getMessage());
+ }
}
}
- break;
- }
- }
- if (!connectionFound) {
- stderr.print("close connection " + sessionId + ": no such connection\n");
- }
+ });
+ for (String sessionId : sessionIds) {
+ stderr.print("close connection " + sessionId + ": no such connection\n");
}
}
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index d5dd241..f9ba8a2 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -45,6 +45,10 @@
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
import com.github.rholder.retry.StopStrategies;
import com.google.common.collect.FluentIterable;
@@ -84,6 +88,7 @@
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.accounts.AccountInput;
import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
@@ -104,6 +109,7 @@
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.common.SshKeyInfo;
+import com.google.gerrit.extensions.events.AccountActivationListener;
import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
@@ -115,6 +121,7 @@
import com.google.gerrit.gpg.Fingerprint;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.testing.TestKey;
+import com.google.gerrit.httpd.CacheBasedWebSession;
import com.google.gerrit.mail.Address;
import com.google.gerrit.server.ExceptionHook;
import com.google.gerrit.server.ServerInitiated;
@@ -159,6 +166,14 @@
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
@@ -183,6 +198,7 @@
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.junit.After;
+import org.junit.Before;
import org.junit.Test;
public class AccountIT extends AbstractDaemonTest {
@@ -221,6 +237,9 @@
@Inject protected GroupOperations groupOperations;
+ private BasicCookieStore httpCookieStore;
+ private CloseableHttpClient httpclient;
+
@After
public void clearPublicKeyStore() throws Exception {
try (Repository repo = repoManager.openRepository(allUsers)) {
@@ -247,6 +266,16 @@
}
}
+ @Before
+ public void createHttpClient() {
+ httpCookieStore = new BasicCookieStore();
+ httpclient =
+ HttpClientBuilder.create()
+ .disableRedirectHandling()
+ .setDefaultCookieStore(httpCookieStore)
+ .build();
+ }
+
protected void assertLabelPermission(
Project.NameKey project,
GroupReference groupReference,
@@ -577,13 +606,50 @@
}
@Test
+ @GerritConfig(name = "auth.type", value = "DEVELOPMENT_BECOME_ANY_ACCOUNT")
+ public void activeUserGetSessionCookieOnLogin() throws Exception {
+ Integer accountId = accountIdApi().get()._accountId;
+ assertThat(accountIdApi().getActive()).isTrue();
+
+ webLogin(accountId);
+ assertThat(getCookiesNames()).contains(CacheBasedWebSession.ACCOUNT_COOKIE);
+ }
+
+ @Test
+ @GerritConfig(name = "auth.type", value = "DEVELOPMENT_BECOME_ANY_ACCOUNT")
+ public void inactiveUserDoesNotGetCookieOnLogin() throws Exception {
+ Integer accountId = accountIdApi().get()._accountId;
+ accountIdApi().setActive(false);
+ assertThat(accountIdApi().getActive()).isFalse();
+
+ webLogin(accountId);
+ assertThat(getCookiesNames()).isEmpty();
+ }
+
+ @Test
+ @GerritConfig(name = "auth.type", value = "DEVELOPMENT_BECOME_ANY_ACCOUNT")
+ public void userDeactivatedAfterLoginDoesNotGetCookie() throws Exception {
+ Integer accountId = accountIdApi().get()._accountId;
+ assertThat(accountIdApi().getActive()).isTrue();
+
+ webLogin(accountId);
+ assertThat(getCookiesNames()).contains(CacheBasedWebSession.ACCOUNT_COOKIE);
+ httpGetAndAssertStatus("accounts/self/detail", HttpServletResponse.SC_OK);
+
+ accountIdApi().setActive(false);
+ assertThat(accountIdApi().getActive()).isFalse();
+
+ httpGetAndAssertStatus("accounts/self/detail", HttpServletResponse.SC_FORBIDDEN);
+ }
+
+ @Test
public void validateAccountActivation() throws Exception {
Account.Id activatableAccountId =
accountOperations.newAccount().inactive().preferredEmail("foo@activatable.com").create();
Account.Id deactivatableAccountId =
accountOperations.newAccount().preferredEmail("foo@deactivatable.com").create();
- AccountActivationValidationListener listener =
+ AccountActivationValidationListener validationListener =
new AccountActivationValidationListener() {
@Override
public void validateActivation(AccountState account) throws ValidationException {
@@ -601,7 +667,11 @@
}
}
};
- try (Registration registration = extensionRegistry.newRegistration().add(listener)) {
+
+ AccountActivationListener listener = mock(AccountActivationListener.class);
+
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(validationListener).add(listener)) {
/* Test account that can be activated, but not deactivated */
// Deactivate account that is already inactive
ResourceConflictException thrown =
@@ -610,14 +680,18 @@
() -> gApi.accounts().id(activatableAccountId.get()).setActive(false));
assertThat(thrown).hasMessageThat().isEqualTo("account not active");
assertThat(accountOperations.account(activatableAccountId).get().active()).isFalse();
+ verifyZeroInteractions(listener);
// Activate account that can be activated
gApi.accounts().id(activatableAccountId.get()).setActive(true);
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+ verify(listener).onAccountActivated(activatableAccountId.get());
+ verifyNoMoreInteractions(listener);
// Activate account that is already active
gApi.accounts().id(activatableAccountId.get()).setActive(true);
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+ verifyZeroInteractions(listener);
// Try deactivating account that cannot be deactivated
thrown =
@@ -626,15 +700,19 @@
() -> gApi.accounts().id(activatableAccountId.get()).setActive(false));
assertThat(thrown).hasMessageThat().isEqualTo("not allowed to deactive account");
assertThat(accountOperations.account(activatableAccountId).get().active()).isTrue();
+ verifyZeroInteractions(listener);
/* Test account that can be deactivated, but not activated */
// Activate account that is already inactive
gApi.accounts().id(deactivatableAccountId.get()).setActive(true);
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isTrue();
+ verifyZeroInteractions(listener);
// Deactivate account that can be deactivated
gApi.accounts().id(deactivatableAccountId.get()).setActive(false);
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
+ verify(listener).onAccountDeactivated(deactivatableAccountId.get());
+ verifyNoMoreInteractions(listener);
// Deactivate account that is already inactive
thrown =
@@ -643,6 +721,7 @@
() -> gApi.accounts().id(deactivatableAccountId.get()).setActive(false));
assertThat(thrown).hasMessageThat().isEqualTo("account not active");
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
+ verifyZeroInteractions(listener);
// Try activating account that cannot be activated
thrown =
@@ -651,6 +730,7 @@
() -> gApi.accounts().id(deactivatableAccountId.get()).setActive(true));
assertThat(thrown).hasMessageThat().isEqualTo("not allowed to active account");
assertThat(accountOperations.account(deactivatableAccountId).get().active()).isFalse();
+ verifyZeroInteractions(listener);
}
}
@@ -2981,6 +3061,30 @@
assertThat(Iterables.getOnlyElement(accounts)).isEqualTo(expectedAccount.id());
}
+ private AccountApi accountIdApi() throws RestApiException {
+ return gApi.accounts().id(user.id().get());
+ }
+
+ private Set<String> getCookiesNames() {
+ Set<String> cookieNames =
+ httpCookieStore.getCookies().stream()
+ .map(cookie -> cookie.getName())
+ .collect(Collectors.toSet());
+ return cookieNames;
+ }
+
+ private void webLogin(Integer accountId) throws IOException, ClientProtocolException {
+ httpGetAndAssertStatus(
+ "login?account_id=" + accountId, HttpServletResponse.SC_MOVED_TEMPORARILY);
+ }
+
+ private void httpGetAndAssertStatus(String urlPath, int expectedHttpStatus)
+ throws ClientProtocolException, IOException {
+ HttpGet httpGet = new HttpGet(canonicalWebUrl.get() + urlPath);
+ HttpResponse loginResponse = httpclient.execute(httpGet);
+ assertThat(loginResponse.getStatusLine().getStatusCode()).isEqualTo(expectedHttpStatus);
+ }
+
private static class RefUpdateCounter implements GitReferenceUpdatedListener {
private final AtomicLongMap<String> countsByProjectRefs = AtomicLongMap.create();
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
new file mode 100644
index 0000000..80eff96
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountListenersIT.java
@@ -0,0 +1,212 @@
+// 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.acceptance.api.accounts;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
+import com.google.gerrit.extensions.events.AccountActivationListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.validators.AccountActivationValidationListener;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests the wiring of a real plugin's account listeners
+ *
+ * <p>This test really puts focus on the wiring of the account listeners. Tests for the inner
+ * workings of account activation/deactivation can be found in {@link AccountIT}.
+ */
+@TestPlugin(
+ name = "account-listener-it-plugin",
+ sysModule = "com.google.gerrit.acceptance.api.accounts.AccountListenersIT$Module")
+public class AccountListenersIT extends LightweightPluginDaemonTest {
+ @Inject private AccountOperations accountOperations;
+
+ public static class Module extends AbstractModule {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), AccountActivationValidationListener.class).to(Validator.class);
+ DynamicSet.bind(binder(), AccountActivationListener.class).to(Listener.class);
+ }
+ }
+
+ Validator validator;
+ Listener listener;
+
+ @Before
+ public void setUp() {
+ validator = plugin.getSysInjector().getInstance(Validator.class);
+
+ listener = plugin.getSysInjector().getInstance(Listener.class);
+ }
+
+ @Test
+ public void testActivation() throws RestApiException {
+ int id = accountOperations.newAccount().inactive().create().get();
+
+ gApi.accounts().id(id).setActive(true);
+
+ validator.assertActivationValidation(id);
+ listener.assertActivated(id);
+ assertNoMoreEvents();
+ assertThat(gApi.accounts().id(id).getActive()).isTrue();
+ }
+
+ @Test
+ public void testActivationProhibited() throws RestApiException {
+ int id = accountOperations.newAccount().inactive().create().get();
+
+ validator.failActivationValidations();
+
+ assertThrows(
+ ResourceConflictException.class,
+ () -> {
+ gApi.accounts().id(id).setActive(true);
+ });
+
+ validator.assertActivationValidation(id);
+ // No call to activation listener as validation failed
+ assertNoMoreEvents();
+ assertThat(gApi.accounts().id(id).getActive()).isFalse();
+ }
+
+ @Test
+ public void testDeactivation() throws RestApiException {
+ int id = accountOperations.newAccount().active().create().get();
+
+ gApi.accounts().id(id).setActive(false);
+
+ validator.assertDeactivationValidation(id);
+ listener.assertDeactivated(id);
+ assertNoMoreEvents();
+ assertThat(gApi.accounts().id(id).getActive()).isFalse();
+ }
+
+ @Test
+ public void testDeactivationProhibited() throws RestApiException {
+ int id = accountOperations.newAccount().active().create().get();
+
+ validator.failDeactivationValidations();
+
+ assertThrows(
+ ResourceConflictException.class,
+ () -> {
+ gApi.accounts().id(id).setActive(false);
+ });
+
+ validator.assertDeactivationValidation(id);
+ // No call to activation listener as validation failed
+ assertNoMoreEvents();
+ assertThat(gApi.accounts().id(id).getActive()).isTrue();
+ }
+
+ private void assertNoMoreEvents() {
+ validator.assertNoMoreEvents();
+ listener.assertNoMoreEvents();
+ }
+
+ @Singleton
+ public static class Validator implements AccountActivationValidationListener {
+ private Integer lastIdActivationValidation;
+ private Integer lastIdDeactivationValidation;
+ private boolean failActivationValidations;
+ private boolean failDeactivationValidations;
+
+ @Override
+ public void validateActivation(AccountState account) throws ValidationException {
+ assertThat(lastIdActivationValidation).isNull();
+ lastIdActivationValidation = account.account().id().get();
+ if (failActivationValidations) {
+ throw new ValidationException("testing validation failure");
+ }
+ }
+
+ @Override
+ public void validateDeactivation(AccountState account) throws ValidationException {
+ assertThat(lastIdDeactivationValidation).isNull();
+ lastIdDeactivationValidation = account.account().id().get();
+ if (failDeactivationValidations) {
+ throw new ValidationException("testing validation failure");
+ }
+ }
+
+ public void failActivationValidations() {
+ failActivationValidations = true;
+ }
+
+ public void failDeactivationValidations() {
+ failDeactivationValidations = true;
+ }
+
+ private void assertNoMoreEvents() {
+ assertThat(lastIdActivationValidation).isNull();
+ assertThat(lastIdDeactivationValidation).isNull();
+ }
+
+ private void assertActivationValidation(int id) {
+ assertThat(lastIdActivationValidation).isEqualTo(id);
+ lastIdActivationValidation = null;
+ }
+
+ private void assertDeactivationValidation(int id) {
+ assertThat(lastIdDeactivationValidation).isEqualTo(id);
+ lastIdDeactivationValidation = null;
+ }
+ }
+
+ @Singleton
+ public static class Listener implements AccountActivationListener {
+ private Integer lastIdActivated;
+ private Integer lastIdDeactivated;
+
+ @Override
+ public void onAccountActivated(int id) {
+ assertThat(lastIdActivated).isNull();
+ lastIdActivated = id;
+ }
+
+ @Override
+ public void onAccountDeactivated(int id) {
+ assertThat(lastIdDeactivated).isNull();
+ lastIdDeactivated = id;
+ }
+
+ private void assertNoMoreEvents() {
+ assertThat(lastIdActivated).isNull();
+ assertThat(lastIdDeactivated).isNull();
+ }
+
+ private void assertDeactivated(int id) {
+ assertThat(lastIdDeactivated).isEqualTo(id);
+ lastIdDeactivated = null;
+ }
+
+ private void assertActivated(int id) {
+ assertThat(lastIdActivated).isEqualTo(id);
+ lastIdActivated = null;
+ }
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
index 7d97934..11ca391 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AgreementsIT.java
@@ -25,7 +25,6 @@
import com.google.gerrit.acceptance.UseClockStep;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
-import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.common.data.ContributorAgreement;
@@ -64,7 +63,6 @@
private ContributorAgreement caNoAutoVerify;
@Inject private GroupOperations groupOperations;
@Inject private RequestScopeOperations requestScopeOperations;
- @Inject private ProjectOperations projectOperations;
protected void setUseContributorAgreements(InheritableBoolean value) throws Exception {
try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
index 0e23603..0514e03 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/AccessIT.java
@@ -149,11 +149,8 @@
@Test
public void grantRevertPermissionDoesntDeleteAdminsPreferences() throws Exception {
- String refsHeads = "refs/heads/*";
GroupReference registeredUsers = systemGroupBackend.getGroup(REGISTERED_USERS);
GroupReference otherGroup = systemGroupBackend.getGroup(ANONYMOUS_USERS);
- String groupId = "global:Registered-Users";
- String otherGroupId = "global:Anonymous-Users";
try (Repository repo = repoManager.openRepository(newProjectName)) {
MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, newProjectName, repo);
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
index 5421d8c..bbe7b81 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshCommandsIT.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
@@ -29,6 +30,9 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Spliterator;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
import org.junit.Test;
@NoHttpd
@@ -165,6 +169,38 @@
assertThat(commands).containsExactlyElementsIn(SLAVE_COMMANDS.get("gerrit")).inOrder();
}
+ @Test
+ @Sandboxed
+ public void showConnections() throws Exception {
+ Spliterator<String> connectionsOutput =
+ getOutputLines(adminSshSession.exec("gerrit show-connections"));
+
+ assertThat(findConnectionsInOutput(connectionsOutput, "user")).hasSize(1);
+ }
+
+ @Test
+ @Sandboxed
+ public void cloeConnections() throws Exception {
+ List<String> connectionsOutput =
+ findConnectionsInOutput(
+ getOutputLines(adminSshSession.exec("gerrit show-connections")), "user");
+ String connectionId =
+ Splitter.on(" ")
+ .trimResults()
+ .omitEmptyStrings()
+ .split(connectionsOutput.get(0))
+ .iterator()
+ .next();
+
+ String closeConnectionOutput = adminSshSession.exec("gerrit close-connection " + connectionId);
+ assertThat(closeConnectionOutput).contains(connectionId);
+
+ assertThat(
+ findConnectionsInOutput(
+ getOutputLines(adminSshSession.exec("gerrit show-connections")), "user"))
+ .isEmpty();
+ }
+
private List<String> parseCommandsFromGerritHelpText(String helpText) {
List<String> commands = new ArrayList<>();
@@ -197,4 +233,16 @@
return commands;
}
+
+ private List<String> findConnectionsInOutput(Spliterator<String> connectionsOutput, String user) {
+ List<String> connections =
+ StreamSupport.stream(connectionsOutput, false)
+ .filter(s -> s.contains("localhost") && s.contains(user))
+ .collect(Collectors.toList());
+ return connections;
+ }
+
+ private Spliterator<String> getOutputLines(String output) throws Exception {
+ return Splitter.on("\n").trimResults().omitEmptyStrings().split(output).spliterator();
+ }
}
diff --git a/javatests/com/google/gerrit/integration/ssh/BUILD b/javatests/com/google/gerrit/integration/ssh/BUILD
new file mode 100644
index 0000000..dc8e68c
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/ssh/BUILD
@@ -0,0 +1,7 @@
+load("//javatests/com/google/gerrit/acceptance:tests.bzl", "acceptance_tests")
+
+acceptance_tests(
+ srcs = ["PeerKeysAuthIT.java"],
+ group = "peer-keys-auth",
+ labels = ["ssh"],
+)
diff --git a/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java b/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
new file mode 100644
index 0000000..a219cc2
--- /dev/null
+++ b/javatests/com/google/gerrit/integration/ssh/PeerKeysAuthIT.java
@@ -0,0 +1,104 @@
+// 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.integration.ssh;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.testing.GerritJUnit.assertThrows;
+import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress;
+import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.acceptance.StandaloneSiteTest;
+import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.PeerDaemonUser;
+import com.google.inject.Inject;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.file.Files;
+import org.junit.Test;
+
+@NoHttpd
+@UseSsh
+public class PeerKeysAuthIT extends StandaloneSiteTest {
+ private static final String[] SSH_KEYGEN_CMD =
+ new String[] {"ssh-keygen", "-t", "rsa", "-q", "-P", "", "-f", "id_rsa"};
+ private static final String[] SSH_COMMAND =
+ new String[] {
+ "ssh",
+ "-o",
+ "UserKnownHostsFile=/dev/null",
+ "-o",
+ "StrictHostKeyChecking=no",
+ "-o",
+ "IdentitiesOnly=yes",
+ "-i"
+ };
+
+ @Inject private @TestSshServerAddress InetSocketAddress sshAddress;
+
+ @Test
+ public void test() throws Exception {
+ try (ServerContext ctx = startServer()) {
+ ctx.getInjector().injectMembers(this);
+ // Generate private/public key for user
+ execute(ImmutableList.<String>builder().add(SSH_KEYGEN_CMD).build());
+
+ String[] parts =
+ new String(Files.readAllBytes(sitePaths.data_dir.resolve("id_rsa.pub")), UTF_8)
+ .split(" ");
+
+ // Loose algorithm at index 0, verify the format: "key comment"
+ Files.write(
+ sitePaths.peer_keys, String.format("%s %s", parts[1], parts[2]).getBytes(ISO_8859_1));
+ assertContent(execGerritVersionCommand());
+
+ // Only preserve the key material: no algorithm and no comment
+ Files.write(sitePaths.peer_keys, parts[1].getBytes(ISO_8859_1));
+ assertContent(execGerritVersionCommand());
+
+ // Wipe out the content of the peer keys file
+ Files.delete(sitePaths.peer_keys);
+ assertThrows(IOException.class, () -> execGerritVersionCommand());
+ }
+ }
+
+ private String execGerritVersionCommand() throws Exception {
+ return execute(
+ ImmutableList.<String>builder()
+ .add(SSH_COMMAND)
+ .add(sitePaths.data_dir.resolve("id_rsa").toString())
+ .add("-p " + sshAddress.getPort())
+ .add(PeerDaemonUser.USER_NAME + "@" + sshAddress.getHostName())
+ .add("suexec")
+ .add("--as")
+ .add("admin")
+ .add("--")
+ .add("gerrit")
+ .add("version")
+ .build());
+ }
+
+ private String execute(ImmutableList<String> cmd) throws Exception {
+ return execute(cmd, sitePaths.data_dir.toFile(), ImmutableMap.of());
+ }
+
+ private static void assertContent(String result) {
+ assertThat(result).contains("gerrit version " + Version.getVersion());
+ }
+}
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
index f79dba3..bd262ec 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -76,7 +76,9 @@
}
_computeGroups(includedIn, filterText) {
- if (!includedIn) { return []; }
+ if (!includedIn || filterText === undefined) {
+ return [];
+ }
const filter = item => !filterText.length ||
item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1;
@@ -107,12 +109,6 @@
_computeLoadingClass(loaded) {
return loaded ? 'loading loaded' : 'loading';
}
-
- _onFilterChanged() {
- this.debounce('filter-change', () => {
- this._filterText = this.$.filterInput.bindValue;
- }, 100);
- }
}
customElements.define(GrIncludedInDialog.is, GrIncludedInDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
index f5948e8..2faac52 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
@@ -73,12 +73,15 @@
>Close</gr-button
>
</span>
- <iron-input placeholder="Filter" on-bind-value-changed="_onFilterChanged">
+ <iron-input
+ id="filterInput"
+ placeholder="Filter"
+ bind-value="{{_filterText}}"
+ >
<input
- id="filterInput"
is="iron-input"
placeholder="Filter"
- on-bind-value-changed="_onFilterChanged"
+ bind-value="{{_filterText}}"
/>
</iron-input>
</header>
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
index fe6119e..5d5b1fc 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
@@ -81,5 +81,20 @@
{title: 'Tags', items: ['v2.0', 'v2.1']},
]);
});
+
+ test('_computeGroups with .bindValue', done => {
+ element.$.filterInput.bindValue = 'stable-3.2';
+ const includedIn = {branches: [], tags: []};
+ includedIn.branches.push('master', 'stable-3.2');
+
+ setTimeout(() => {
+ const filterText = element._filterText;
+ assert.deepEqual(element._computeGroups(includedIn, filterText), [
+ {title: 'Branches', items: ['stable-3.2']},
+ ]);
+
+ done();
+ });
+ });
});
</script>
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 61b12f1..d49e700 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -40,7 +40,10 @@
java_binary(
name = "%s__non_stamped" % name,
- deploy_manifest_lines = manifest_entries + ["Gerrit-ApiType: plugin"],
+ deploy_manifest_lines = manifest_entries + [
+ "Gerrit-ApiType: plugin",
+ "Gerrit-ApiVersion: " + GERRIT_VERSION,
+ ],
main_class = "Dummy",
runtime_deps = [
":%s__plugin" % name,
@@ -64,7 +67,7 @@
"GEN_VERSION=$$(cat bazel-out/stable-status.txt | grep -w STABLE_BUILD_%s_LABEL | cut -d ' ' -f 2)" % dir_name.upper(),
"cd $$TMP",
"unzip -q $$ROOT/$<",
- "echo \"Implementation-Version: $$GEN_VERSION\nGerrit-ApiVersion: " + GERRIT_VERSION + "\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
+ "echo \"Implementation-Version: $$GEN_VERSION\n$$(cat META-INF/MANIFEST.MF)\" > META-INF/MANIFEST.MF",
"find . -exec touch '{}' ';'",
"zip -Xqr $$ROOT/$@ .",
]),
diff --git a/tools/maven/gerrit-acceptance-framework_pom.xml b/tools/maven/gerrit-acceptance-framework_pom.xml
index 5042e8b..446ace2 100644
--- a/tools/maven/gerrit-acceptance-framework_pom.xml
+++ b/tools/maven/gerrit-acceptance-framework_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-acceptance-framework</artifactId>
- <version>3.2.1-SNAPSHOT</version>
+ <version>3.2.2-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Acceptance Test Framework</name>
<description>Framework for Gerrit's acceptance tests</description>
diff --git a/tools/maven/gerrit-extension-api_pom.xml b/tools/maven/gerrit-extension-api_pom.xml
index ef8b039..78dcc57 100644
--- a/tools/maven/gerrit-extension-api_pom.xml
+++ b/tools/maven/gerrit-extension-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-extension-api</artifactId>
- <version>3.2.1-SNAPSHOT</version>
+ <version>3.2.2-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Extension API</name>
<description>API for Gerrit Extensions</description>
diff --git a/tools/maven/gerrit-plugin-api_pom.xml b/tools/maven/gerrit-plugin-api_pom.xml
index e7eef6c..5972ff7 100644
--- a/tools/maven/gerrit-plugin-api_pom.xml
+++ b/tools/maven/gerrit-plugin-api_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-plugin-api</artifactId>
- <version>3.2.1-SNAPSHOT</version>
+ <version>3.2.2-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Gerrit Code Review - Plugin API</name>
<description>API for Gerrit Plugins</description>
diff --git a/tools/maven/gerrit-war_pom.xml b/tools/maven/gerrit-war_pom.xml
index 345d829..45dea3d 100644
--- a/tools/maven/gerrit-war_pom.xml
+++ b/tools/maven/gerrit-war_pom.xml
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.gerrit</groupId>
<artifactId>gerrit-war</artifactId>
- <version>3.2.1-SNAPSHOT</version>
+ <version>3.2.2-SNAPSHOT</version>
<packaging>war</packaging>
<name>Gerrit Code Review - WAR</name>
<description>Gerrit WAR</description>
diff --git a/version.bzl b/version.bzl
index fa690dd..49b851e 100644
--- a/version.bzl
+++ b/version.bzl
@@ -2,4 +2,4 @@
# Used by :api_install and :api_deploy targets
# when talking to the destination repository.
#
-GERRIT_VERSION = "3.2.1-SNAPSHOT"
+GERRIT_VERSION = "3.2.2-SNAPSHOT"