Merge branch 'stable-2.15' into stable-2.16

* stable-2.15:
  Elasticsearch: Exclude types from V7 which deprecates them
  Send an email notification when the HTTP password is deleted or changed
  Send email notification when SSH key or GPG key is removed
  Show progress on number of users migrated during schema migration 146

Additional changes done in this merge to adjust to stable-2.16:

- Replace slf4j with Flogger.
- Edit newly added soy templates to remove 'autoescape' and 'kind' parameters
  which are no longer needed and cause parsing errors.
- Move newly added email sender classes to correct package.

Change-Id: I01a804f10c7247d18a0958eb7e0f03cbcf7453c7
This commit is contained in:
David Pursehouse
2019-05-21 09:10:42 +09:00
20 changed files with 665 additions and 44 deletions

View File

@@ -66,6 +66,12 @@ The CommentFooter templates will determine the contents of the footer text that
will be appended to emails related to a user submitting comments on changes.
See `ChangeSubject.soy`, Comment and ChangeFooter.
=== DeleteKey.soy and DeleteKeyHtml.soy
DeleteKey templates will determine the contents of the email related to SSH or GPG keys
being deleted from a user account. This notification is not sent when the key is
administratively deleted from another user account.
=== DeleteVote.soy and DeleteVoteHtml.soy
The DeleteVote templates will determine the contents of the email related to
@@ -83,6 +89,11 @@ a user removing a reviewer (with a vote) from a change. It is a
The Footer templates will determine the contents of the footer text appended to
the end of all outgoing emails after the ChangeFooter and CommentFooter.
=== HttpPasswordUpdate.soy and HttpPasswordUpdateHtml.soy
HttpPasswordUpdate templates will determine the contents of the email related to adding,
changing or deleting the HTTP password on a user account.
=== Merged.soy and MergedHtml.soy
The Merged templates will determine the contents of the email related to a

View File

@@ -213,10 +213,15 @@ abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
}
protected String getMappingsFor(String type, MappingProperties properties) {
JsonObject mappingType = new JsonObject();
mappingType.add(type, gson.toJsonTree(properties));
JsonObject mappings = new JsonObject();
mappings.add(MAPPINGS, gson.toJsonTree(mappingType));
if (client.adapter().omitType()) {
mappings.add(MAPPINGS, gson.toJsonTree(properties));
} else {
JsonObject mappingType = new JsonObject();
mappingType.add(type, gson.toJsonTree(properties));
mappings.add(MAPPINGS, gson.toJsonTree(mappingType));
}
return gson.toJson(mappings);
}
@@ -298,11 +303,12 @@ abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
protected String getURI(String type, String request) throws UnsupportedEncodingException {
String encodedIndexName = URLEncoder.encode(indexName, UTF_8.toString());
if (SEARCH.equals(request) && client.adapter().omitTypeFromSearch()) {
if (SEARCH.equals(request) && client.adapter().omitType()) {
return encodedIndexName + "/" + request;
}
String encodedType = URLEncoder.encode(type, UTF_8.toString());
return encodedIndexName + "/" + encodedType + "/" + request;
String encodedTypeIfAny =
client.adapter().omitType() ? "" : "/" + URLEncoder.encode(type, UTF_8.toString());
return encodedIndexName + encodedTypeIfAny + "/" + request;
}
protected Response postRequest(String uri, Object payload) throws IOException {

View File

@@ -133,7 +133,7 @@ class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
BulkRequest bulk =
new IndexRequest(getId(cd), indexName, adapter.getType(insertIndex), adapter)
.add(new UpdateRequest<>(schema, cd));
if (!adapter.usePostV5Type()) {
if (adapter.deleteToReplace()) {
bulk.add(new DeleteRequest(cd.getId().toString(), indexName, deleteIndex, adapter));
}
@@ -152,17 +152,19 @@ class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
throws QueryParseException {
Set<Change.Status> statuses = ChangeIndexRewriter.getPossibleStatus(p);
List<String> indexes = Lists.newArrayListWithCapacity(2);
if (client.adapter().usePostV5Type()) {
if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()
|| !Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(ElasticQueryAdapter.POST_V5_TYPE);
}
} else {
if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
indexes.add(OPEN_CHANGES);
}
if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(CLOSED_CHANGES);
if (!client.adapter().omitType()) {
if (client.adapter().useV6Type()) {
if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()
|| !Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(ElasticQueryAdapter.V6_TYPE);
}
} else {
if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
indexes.add(OPEN_CHANGES);
}
if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
indexes.add(CLOSED_CHANGES);
}
}
}
@@ -187,16 +189,16 @@ class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
@Override
protected String getDeleteActions(Id c) {
if (client.adapter().usePostV5Type()) {
return delete(ElasticQueryAdapter.POST_V5_TYPE, c);
if (!client.adapter().useV5Type()) {
return delete(client.adapter().getType(), c);
}
return delete(OPEN_CHANGES, c) + delete(CLOSED_CHANGES, c);
}
@Override
protected String getMappings() {
if (client.adapter().usePostV5Type()) {
return getMappingsFor(ElasticQueryAdapter.POST_V5_TYPE, mapping.changes);
if (!client.adapter().useV5Type()) {
return getMappingsFor(client.adapter().getType(), mapping.changes);
}
return gson.toJson(ImmutableMap.of(MAPPINGS, mapping));
}

View File

@@ -17,11 +17,12 @@ package com.google.gerrit.elasticsearch;
import com.google.gson.JsonObject;
public class ElasticQueryAdapter {
static final String POST_V5_TYPE = "_doc";
static final String V6_TYPE = "_doc";
private final boolean ignoreUnmapped;
private final boolean usePostV5Type;
private final boolean omitTypeFromSearch;
private final boolean useV5Type;
private final boolean useV6Type;
private final boolean omitType;
private final String searchFilteringName;
private final String indicesExistParam;
@@ -34,8 +35,9 @@ public class ElasticQueryAdapter {
ElasticQueryAdapter(ElasticVersion version) {
this.ignoreUnmapped = false;
this.usePostV5Type = version.isV6OrLater();
this.omitTypeFromSearch = version.isV7OrLater();
this.useV5Type = !version.isV6OrLater();
this.useV6Type = version.isV6();
this.omitType = version.isV7OrLater();
this.versionDiscoveryUrl = version.isV6OrLater() ? "/%s*" : "/%s*/_aliases";
this.searchFilteringName = "_source";
this.indicesExistParam = "?allow_no_indices=false";
@@ -43,7 +45,7 @@ public class ElasticQueryAdapter {
this.stringFieldType = "text";
this.indexProperty = "true";
this.rawFieldsKey = "_source";
this.includeTypeNameParam = version.isV7OrLater() ? "?include_type_name=true" : "";
this.includeTypeNameParam = version.isV6() ? "?include_type_name=true" : "";
}
void setIgnoreUnmapped(JsonObject properties) {
@@ -53,7 +55,7 @@ public class ElasticQueryAdapter {
}
public void setType(JsonObject properties, String type) {
if (!usePostV5Type) {
if (useV5Type) {
properties.addProperty("_type", type);
}
}
@@ -82,16 +84,31 @@ public class ElasticQueryAdapter {
return rawFieldsKey;
}
boolean usePostV5Type() {
return usePostV5Type;
boolean deleteToReplace() {
return useV5Type;
}
boolean omitTypeFromSearch() {
return omitTypeFromSearch;
boolean useV5Type() {
return useV5Type;
}
boolean useV6Type() {
return useV6Type;
}
boolean omitType() {
return omitType;
}
String getType() {
return getType("");
}
String getType(String type) {
return usePostV5Type() ? POST_V5_TYPE : type;
if (useV6Type()) {
return V6_TYPE;
}
return useV5Type() ? type : "";
}
String getVersionDiscoveryUrl(String name) {

View File

@@ -58,6 +58,10 @@ public enum ElasticVersion {
return Joiner.on(", ").join(ElasticVersion.values());
}
public boolean isV6() {
return getMajor() == 6;
}
public boolean isV6OrLater() {
return isAtLeastVersion(6);
}
@@ -67,7 +71,11 @@ public enum ElasticVersion {
}
private boolean isAtLeastVersion(int v) {
return Integer.valueOf(version.split("\\.")[0]) >= v;
return getMajor() >= v;
}
private Integer getMajor() {
return Integer.valueOf(version.split("\\.")[0]);
}
@Override

View File

@@ -17,7 +17,10 @@ package com.google.gerrit.gpg.server;
import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
@@ -30,6 +33,7 @@ import com.google.gerrit.server.UserInitiated;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.mail.send.DeleteKeySender;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -43,22 +47,26 @@ import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
public class DeleteGpgKey implements RestModifyView<GpgKey, Input> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Provider<PersonIdent> serverIdent;
private final Provider<PublicKeyStore> storeProvider;
private final Provider<AccountsUpdate> accountsUpdateProvider;
private final ExternalIds externalIds;
private final DeleteKeySender.Factory deleteKeySenderFactory;
@Inject
DeleteGpgKey(
@GerritPersonIdent Provider<PersonIdent> serverIdent,
Provider<PublicKeyStore> storeProvider,
@UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
ExternalIds externalIds) {
ExternalIds externalIds,
DeleteKeySender.Factory deleteKeySenderFactory) {
this.serverIdent = serverIdent;
this.storeProvider = storeProvider;
this.accountsUpdateProvider = accountsUpdateProvider;
this.externalIds = externalIds;
this.deleteKeySenderFactory = deleteKeySenderFactory;
}
@Override
@@ -91,6 +99,15 @@ public class DeleteGpgKey implements RestModifyView<GpgKey, Input> {
switch (saveResult) {
case NO_CHANGE:
case FAST_FORWARD:
try {
deleteKeySenderFactory
.create(rsrc.getUser(), ImmutableList.of(PublicKeyStore.keyToString(key)))
.send();
} catch (EmailException e) {
logger.atSevere().withCause(e).log(
"Cannot send GPG key deletion message to %s",
rsrc.getUser().getAccount().getPreferredEmail());
}
break;
case FORCED:
case IO_FAILURE:

View File

@@ -18,6 +18,7 @@ import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
@@ -49,6 +50,7 @@ import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.mail.send.AddKeySender;
import com.google.gerrit.server.mail.send.DeleteKeySender;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -80,7 +82,8 @@ public class PostGpgKeys implements RestModifyView<AccountResource, GpgKeysInput
private final Provider<CurrentUser> self;
private final Provider<PublicKeyStore> storeProvider;
private final GerritPublicKeyChecker.Factory checkerFactory;
private final AddKeySender.Factory addKeyFactory;
private final AddKeySender.Factory addKeySenderFactory;
private final DeleteKeySender.Factory deleteKeySenderFactory;
private final Provider<InternalAccountQuery> accountQueryProvider;
private final ExternalIds externalIds;
private final Provider<AccountsUpdate> accountsUpdateProvider;
@@ -91,7 +94,8 @@ public class PostGpgKeys implements RestModifyView<AccountResource, GpgKeysInput
Provider<CurrentUser> self,
Provider<PublicKeyStore> storeProvider,
GerritPublicKeyChecker.Factory checkerFactory,
AddKeySender.Factory addKeyFactory,
AddKeySender.Factory addKeySenderFactory,
DeleteKeySender.Factory deleteKeySenderFactory,
Provider<InternalAccountQuery> accountQueryProvider,
ExternalIds externalIds,
@UserInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
@@ -99,7 +103,8 @@ public class PostGpgKeys implements RestModifyView<AccountResource, GpgKeysInput
this.self = self;
this.storeProvider = storeProvider;
this.checkerFactory = checkerFactory;
this.addKeyFactory = addKeyFactory;
this.addKeySenderFactory = addKeySenderFactory;
this.deleteKeySenderFactory = deleteKeySenderFactory;
this.accountQueryProvider = accountQueryProvider;
this.externalIds = externalIds;
this.accountsUpdateProvider = accountsUpdateProvider;
@@ -223,13 +228,24 @@ public class PostGpgKeys implements RestModifyView<AccountResource, GpgKeysInput
case FORCED:
if (!addedKeys.isEmpty()) {
try {
addKeyFactory.create(user, addedKeys).send();
addKeySenderFactory.create(user, addedKeys).send();
} catch (EmailException e) {
logger.atSevere().withCause(e).log(
"Cannot send GPG key added message to %s",
rsrc.getUser().getAccount().getPreferredEmail());
}
}
if (!toRemove.isEmpty()) {
try {
deleteKeySenderFactory
.create(user, toRemove.stream().map(Fingerprint::toString).collect(toList()))
.send();
} catch (EmailException e) {
logger.atSevere().withCause(e).log(
"Cannot send GPG key deleted message to %s",
user.getAccount().getPreferredEmail());
}
}
break;
case NO_CHANGE:
break;

View File

@@ -115,6 +115,8 @@ public class SitePathInitializer {
extractMailExample("CommentHtml.soy");
extractMailExample("CommentFooter.soy");
extractMailExample("CommentFooterHtml.soy");
extractMailExample("DeleteKey.soy");
extractMailExample("DeleteKeyHtml.soy");
extractMailExample("DeleteReviewer.soy");
extractMailExample("DeleteReviewerHtml.soy");
extractMailExample("DeleteVote.soy");
@@ -122,6 +124,8 @@ public class SitePathInitializer {
extractMailExample("Footer.soy");
extractMailExample("FooterHtml.soy");
extractMailExample("HeaderHtml.soy");
extractMailExample("HttpPasswordUpdate.soy");
extractMailExample("HttpPasswordUpdateHtml.soy");
extractMailExample("InboundEmailRejection.soy");
extractMailExample("InboundEmailRejectionHtml.soy");
extractMailExample("Merged.soy");

View File

@@ -20,8 +20,10 @@ import com.google.gerrit.server.mail.send.AddKeySender;
import com.google.gerrit.server.mail.send.AddReviewerSender;
import com.google.gerrit.server.mail.send.CommentSender;
import com.google.gerrit.server.mail.send.CreateChangeSender;
import com.google.gerrit.server.mail.send.DeleteKeySender;
import com.google.gerrit.server.mail.send.DeleteReviewerSender;
import com.google.gerrit.server.mail.send.DeleteVoteSender;
import com.google.gerrit.server.mail.send.HttpPasswordUpdateSender;
import com.google.gerrit.server.mail.send.MergedSender;
import com.google.gerrit.server.mail.send.RegisterNewEmailSender;
import com.google.gerrit.server.mail.send.ReplacePatchSetSender;
@@ -37,8 +39,10 @@ public class EmailModule extends FactoryModule {
factory(AddReviewerSender.Factory.class);
factory(CommentSender.Factory.class);
factory(CreateChangeSender.Factory.class);
factory(DeleteKeySender.Factory.class);
factory(DeleteReviewerSender.Factory.class);
factory(DeleteVoteSender.Factory.class);
factory(HttpPasswordUpdateSender.Factory.class);
factory(MergedSender.Factory.class);
factory(RegisterNewEmailSender.Factory.class);
factory(ReplacePatchSetSender.Factory.class);

View File

@@ -0,0 +1,150 @@
// Copyright (C) 2019 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.mail.send;
import com.google.common.base.Joiner;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.mail.Address;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountSshKey;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import java.util.Collections;
import java.util.List;
public class DeleteKeySender extends OutgoingEmail {
public interface Factory {
DeleteKeySender create(IdentifiedUser user, AccountSshKey sshKey);
DeleteKeySender create(IdentifiedUser user, List<String> gpgKeyFingerprints);
}
private final PermissionBackend permissionBackend;
private final IdentifiedUser callingUser;
private final IdentifiedUser user;
private final AccountSshKey sshKey;
private final List<String> gpgKeyFingerprints;
@AssistedInject
public DeleteKeySender(
EmailArguments ea,
PermissionBackend permissionBackend,
IdentifiedUser callingUser,
@Assisted IdentifiedUser user,
@Assisted AccountSshKey sshKey) {
super(ea, "deletekey");
this.permissionBackend = permissionBackend;
this.callingUser = callingUser;
this.user = user;
this.gpgKeyFingerprints = Collections.emptyList();
this.sshKey = sshKey;
}
@AssistedInject
public DeleteKeySender(
EmailArguments ea,
PermissionBackend permissionBackend,
IdentifiedUser callingUser,
@Assisted IdentifiedUser user,
@Assisted List<String> gpgKeyFingerprints) {
super(ea, "deletekey");
this.permissionBackend = permissionBackend;
this.callingUser = callingUser;
this.user = user;
this.gpgKeyFingerprints = gpgKeyFingerprints;
this.sshKey = null;
}
@Override
protected void init() throws EmailException {
super.init();
setHeader("Subject", String.format("[Gerrit Code Review] %s Keys Deleted", getKeyType()));
add(RecipientType.TO, new Address(getEmail()));
}
@Override
protected boolean shouldSendMessage() {
if (user.equals(callingUser)) {
// Send email if the user self-removed a key; this notification is necessary to alert
// the user if their account was compromised and a key was unexpectedly deleted.
return true;
}
try {
// Don't email if an administrator removed a key on behalf of the user.
permissionBackend.user(callingUser).check(GlobalPermission.ADMINISTRATE_SERVER);
return false;
} catch (AuthException | PermissionBackendException e) {
// Send email if a non-administrator modified the keys, e.g. by MODIFY_ACCOUNT.
return true;
}
}
@Override
protected void format() throws EmailException {
appendText(textTemplate("DeleteKey"));
if (useHtml()) {
appendHtml(soyHtmlTemplate("DeleteKeyHtml"));
}
}
public String getEmail() {
return user.getAccount().getPreferredEmail();
}
public String getUserNameEmail() {
return getUserNameEmailFor(user.getAccountId());
}
public String getKeyType() {
if (sshKey != null) {
return "SSH";
} else if (gpgKeyFingerprints != null) {
return "GPG";
}
throw new IllegalStateException("key type is not SSH or GPG");
}
public String getSshKey() {
return (sshKey != null) ? sshKey.sshPublicKey() + "\n" : null;
}
public String getGpgKeyFingerprints() {
if (!gpgKeyFingerprints.isEmpty()) {
return Joiner.on("\n").join(gpgKeyFingerprints);
}
return null;
}
@Override
protected void setupSoyContext() {
super.setupSoyContext();
soyContextEmailData.put("email", getEmail());
soyContextEmailData.put("gpgKeyFingerprints", getGpgKeyFingerprints());
soyContextEmailData.put("keyType", getKeyType());
soyContextEmailData.put("sshKey", getSshKey());
soyContextEmailData.put("userNameEmail", getUserNameEmail());
}
@Override
protected boolean supportsHtml() {
return true;
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (C) 2019 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.mail.send;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.mail.Address;
import com.google.gerrit.server.IdentifiedUser;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
public class HttpPasswordUpdateSender extends OutgoingEmail {
public interface Factory {
HttpPasswordUpdateSender create(IdentifiedUser user, String operation);
}
private final IdentifiedUser user;
private final String operation;
@AssistedInject
public HttpPasswordUpdateSender(
EmailArguments ea, @Assisted IdentifiedUser user, @Assisted String operation) {
super(ea, "HttpPasswordUpdate");
this.user = user;
this.operation = operation;
}
@Override
protected void init() throws EmailException {
super.init();
setHeader("Subject", "[Gerrit Code Review] HTTP password was " + operation);
add(RecipientType.TO, new Address(getEmail()));
}
@Override
protected boolean shouldSendMessage() {
// Always send an email if the HTTP password is updated.
return true;
}
@Override
protected void format() throws EmailException {
appendText(textTemplate("HttpPasswordUpdate"));
if (useHtml()) {
appendHtml(soyHtmlTemplate("HttpPasswordUpdateHtml"));
}
}
public String getEmail() {
return user.getAccount().getPreferredEmail();
}
public String getUserNameEmail() {
return getUserNameEmailFor(user.getAccountId());
}
@Override
protected void setupSoyContext() {
super.setupSoyContext();
soyContextEmailData.put("email", getEmail());
soyContextEmailData.put("userNameEmail", getUserNameEmail());
soyContextEmailData.put("operation", operation);
}
@Override
protected boolean supportsHtml() {
return true;
}
}

View File

@@ -47,6 +47,8 @@ public class MailSoyTofuProvider implements Provider<SoyTofu> {
"CommentHtml.soy",
"CommentFooter.soy",
"CommentFooterHtml.soy",
"DeleteKey.soy",
"DeleteKeyHtml.soy",
"DeleteReviewer.soy",
"DeleteReviewerHtml.soy",
"DeleteVote.soy",
@@ -56,6 +58,8 @@ public class MailSoyTofuProvider implements Provider<SoyTofu> {
"Footer.soy",
"FooterHtml.soy",
"HeaderHtml.soy",
"HttpPasswordUpdate.soy",
"HttpPasswordUpdateHtml.soy",
"Merged.soy",
"MergedHtml.soy",
"NewChange.soy",

View File

@@ -14,13 +14,17 @@
package com.google.gerrit.server.restapi.account;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.common.Input;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.VersionedAuthorizedKeys;
import com.google.gerrit.server.mail.send.DeleteKeySender;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -35,22 +39,26 @@ import org.eclipse.jgit.errors.RepositoryNotFoundException;
@Singleton
public class DeleteSshKey implements RestModifyView<AccountResource.SshKey, Input> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
private final VersionedAuthorizedKeys.Accessor authorizedKeys;
private final SshKeyCache sshKeyCache;
private final DeleteKeySender.Factory deleteKeySenderFactory;
@Inject
DeleteSshKey(
Provider<CurrentUser> self,
PermissionBackend permissionBackend,
VersionedAuthorizedKeys.Accessor authorizedKeys,
SshKeyCache sshKeyCache) {
SshKeyCache sshKeyCache,
DeleteKeySender.Factory deleteKeySenderFactory) {
this.self = self;
this.permissionBackend = permissionBackend;
this.authorizedKeys = authorizedKeys;
this.sshKeyCache = sshKeyCache;
this.deleteKeySenderFactory = deleteKeySenderFactory;
}
@Override
@@ -61,8 +69,15 @@ public class DeleteSshKey implements RestModifyView<AccountResource.SshKey, Inpu
permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
}
authorizedKeys.deleteKey(rsrc.getUser().getAccountId(), rsrc.getSshKey().seq());
rsrc.getUser().getUserName().ifPresent(sshKeyCache::evict);
IdentifiedUser user = rsrc.getUser();
authorizedKeys.deleteKey(user.getAccountId(), rsrc.getSshKey().seq());
try {
deleteKeySenderFactory.create(user, rsrc.getSshKey()).send();
} catch (EmailException e) {
logger.atSevere().withCause(e).log(
"Cannot send SSH key deletion message to %s", user.getAccount().getPreferredEmail());
}
user.getUserName().ifPresent(sshKeyCache::evict);
return Response.none();
}

View File

@@ -17,6 +17,8 @@ package com.google.gerrit.server.restapi.account;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.common.HttpPasswordInput;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -31,6 +33,7 @@ import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.mail.send.HttpPasswordUpdateSender;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -45,6 +48,8 @@ import org.apache.commons.codec.binary.Base64;
import org.eclipse.jgit.errors.ConfigInvalidException;
public class PutHttpPassword implements RestModifyView<AccountResource, HttpPasswordInput> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final int LEN = 31;
private static final SecureRandom rng;
@@ -60,17 +65,20 @@ public class PutHttpPassword implements RestModifyView<AccountResource, HttpPass
private final PermissionBackend permissionBackend;
private final ExternalIds externalIds;
private final Provider<AccountsUpdate> accountsUpdateProvider;
private final HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory;
@Inject
PutHttpPassword(
Provider<CurrentUser> self,
PermissionBackend permissionBackend,
ExternalIds externalIds,
@UserInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
@UserInitiated Provider<AccountsUpdate> accountsUpdateProvider,
HttpPasswordUpdateSender.Factory httpPasswordUpdateSenderFactory) {
this.self = self;
this.permissionBackend = permissionBackend;
this.externalIds = externalIds;
this.accountsUpdateProvider = accountsUpdateProvider;
this.httpPasswordUpdateSenderFactory = httpPasswordUpdateSenderFactory;
}
@Override
@@ -113,6 +121,15 @@ public class PutHttpPassword implements RestModifyView<AccountResource, HttpPass
ExternalId.createWithPassword(
extId.key(), extId.accountId(), extId.email(), newPassword)));
try {
httpPasswordUpdateSenderFactory
.create(user, newPassword == null ? "deleted" : "added or updated")
.send();
} catch (EmailException e) {
logger.atSevere().withCause(e).log(
"Cannot send HttpPassword update message to %s", user.getAccount().getPreferredEmail());
}
return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword);
}

View File

@@ -80,6 +80,7 @@ public class Schema_146 extends SchemaVersion {
ObjectInserter oi = repo.newObjectInserter()) {
ObjectId emptyTree = emptyTree(oi);
int i = 0;
for (Map.Entry<Account.Id, Timestamp> e : scanAccounts(db).entrySet()) {
String refName = RefNames.refsUsers(e.getKey());
Ref ref = repo.exactRef(refName);
@@ -88,7 +89,12 @@ public class Schema_146 extends SchemaVersion {
} else {
createUserBranch(repo, oi, emptyTree, e.getKey(), e.getValue());
}
i++;
if (i % 100 == 0) {
ui.message(String.format("... migrated %d users", i));
}
}
ui.message(String.format("Migrated all %d users to schema 146", i));
} catch (IOException e) {
throw new OrmException("Failed to rewrite user branches.", e);
}

View File

@@ -1970,9 +1970,12 @@ public class AccountIT extends AbstractDaemonTest {
addGpgKey(key.getPublicKeyArmored());
assertKeys(key);
sender.clear();
gApi.accounts().self().gpgKey(id).delete();
accountIndexedCounter.assertReindexOf(admin);
assertKeys();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("GPG keys have been deleted");
exception.expect(ResourceNotFoundException.class);
exception.expectMessage(id);
@@ -2073,6 +2076,7 @@ public class AccountIT extends AbstractDaemonTest {
assertThat(sender.getMessages().get(0).body()).contains("new SSH keys have been added");
// Delete second key
sender.clear();
gApi.accounts().self().deleteSshKey(2);
info = gApi.accounts().self().listSshKeys();
assertThat(info).hasSize(2);
@@ -2080,6 +2084,9 @@ public class AccountIT extends AbstractDaemonTest {
assertThat(info.get(1).seq).isEqualTo(3);
accountIndexedCounter.assertReindexOf(admin);
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("SSH keys have been deleted");
// Mark first key as invalid
assertThat(info.get(0).valid).isTrue();
authorizedKeys.markKeyInvalid(admin.id, 1);
@@ -2757,15 +2764,21 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void userCanGenerateNewHttpPassword() throws Exception {
sender.clear();
String newPassword = gApi.accounts().self().generateHttpPassword();
assertThat(newPassword).isNotNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
}
@Test
public void adminCanGenerateNewHttpPasswordForUser() throws Exception {
setApiUser(admin);
sender.clear();
String newPassword = gApi.accounts().id(user.username).generateHttpPassword();
assertThat(newPassword).isNotNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
}
@Test
@@ -2792,7 +2805,10 @@ public class AccountIT extends AbstractDaemonTest {
@Test
public void userCanRemoveHttpPassword() throws Exception {
setApiUser(user);
sender.clear();
assertThat(gApi.accounts().self().setHttpPassword(null)).isNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
}
@Test
@@ -2806,14 +2822,20 @@ public class AccountIT extends AbstractDaemonTest {
public void adminCanExplicitlySetHttpPasswordForUser() throws Exception {
setApiUser(admin);
String httpPassword = "new-password-for-user";
sender.clear();
assertThat(gApi.accounts().id(user.username).setHttpPassword(httpPassword))
.isEqualTo(httpPassword);
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was added or updated");
}
@Test
public void adminCanRemoveHttpPasswordForUser() throws Exception {
setApiUser(admin);
sender.clear();
assertThat(gApi.accounts().id(user.username).setHttpPassword(null)).isNull();
assertThat(sender.getMessages()).hasSize(1);
assertThat(sender.getMessages().get(0).body()).contains("HTTP password was deleted");
}
@Test

View File

@@ -0,0 +1,72 @@
/**
* Copyright (C) 2019 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.
*/
{namespace com.google.gerrit.server.mail.template}
/**
* The .DeleteKey template will determine the contents of the email related to
* deleting a SSH or GPG key.
* @param email
*/
{template .DeleteKey kind="text"}
One or more {$email.keyType} keys have been deleted on Gerrit Code Review at
{sp}{$email.gerritHost}:
{\n}
{\n}
{if $email.sshKey}
{$email.sshKey}
{elseif $email.gpgKeyFingerprints}
{$email.gpgKeyFingerprints}
{/if}
{\n}
{\n}
If this is not expected, please contact your Gerrit Administrators
immediately.
{\n}
{\n}
You can also manage your {$email.keyType} keys by visiting
{\n}
{if $email.sshKey}
{$email.gerritUrl}#/settings/ssh-keys
{elseif $email.gpgKey}
{$email.gerritUrl}#/settings/gpg-keys
{/if}
{\n}
{if $email.userNameEmail}
(while signed in as {$email.userNameEmail})
{else}
(while signed in as {$email.email})
{/if}
{\n}
{\n}
If clicking the link above does not work, copy and paste the URL in a new
browser window instead.
{\n}
{\n}
This is a send-only email address. Replies to this message will not be read
or answered.
{/template}

View File

@@ -0,0 +1,66 @@
/**
* Copyright (C) 2019 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.
*/
{namespace com.google.gerrit.server.mail.template}
/**
* @param email
*/
{template .DeleteKeyHtml}
<p>
One or more {$email.keyType} keys have been deleted on Gerrit Code Review
at {$email.gerritHost}:
</p>
{let $keyStyle kind="css"}
background: #f0f0f0;
border: 1px solid #ccc;
color: #555;
padding: 12px;
width: 400px;
{/let}
{if $email.sshKey}
<pre style="{$keyStyle}">{$email.sshKey}</pre>
{elseif $email.gpgKeyFingerprints}
<pre style="{$keyStyle}">{$email.gpgKeyFingerprints}</pre>
{/if}
<p>
If this is not expected, please contact your Gerrit Administrators
immediately.
</p>
<p>
You can also manage your {$email.keyType} keys by following{sp}
{if $email.sshKey}
<a href="{$email.gerritUrl}#/settings/ssh-keys">this link</a>
{elseif $email.gpgKeyFingerprints}
<a href="{$email.gerritUrl}#/settings/gpg-keys">this link</a>
{/if}
{sp}
{if $email.userNameEmail}
(while signed in as {$email.userNameEmail})
{else}
(while signed in as {$email.email})
{/if}.
</p>
<p>
This is a send-only email address. Replies to this message will not be read
or answered.
</p>
{/template}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (C) 2019 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.
*/
{namespace com.google.gerrit.server.mail.template}
/**
* The .HttpPasswordUpdate template will determine the contents of the email related to
* adding, changing or deleting the HTTP password.
* @param email
*/
{template .HttpPasswordUpdate kind="text"}
The HTTP password was {$email.operation} on Gerrit Code Review at
{sp}{$email.gerritHost}.
If this is not expected, please contact your Gerrit Administrators
immediately.
{\n}
{\n}
You can also manage your HTTP password by visiting
{\n}
{$email.gerritUrl}#/settings/http-password
{\n}
{if $email.userNameEmail}
(while signed in as {$email.userNameEmail})
{else}
(while signed in as {$email.email})
{/if}
{\n}
{\n}
If clicking the link above does not work, copy and paste the URL in a new
browser window instead.
{\n}
{\n}
This is a send-only email address. Replies to this message will not be read
or answered.
{/template}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (C) 2019 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.
*/
{namespace com.google.gerrit.server.mail.template}
/**
* @param email
*/
{template .HttpPasswordUpdateHtml}
<p>
The HTTP password was {$email.operation} on Gerrit Code Review
at {$email.gerritHost}.
</p>
<p>
If this is not expected, please contact your Gerrit Administrators
immediately.
</p>
<p>
You can also manage your HTTP password by following{sp}
<a href="{$email.gerritUrl}#/settings/http-password">this link</a>
{sp}
{if $email.userNameEmail}
(while signed in as {$email.userNameEmail})
{else}
(while signed in as {$email.email})
{/if}.
</p>
<p>
This is a send-only email address. Replies to this message will not be read
or answered.
</p>
{/template}