blob: e338543571882ecb8e24995ec47579137df78024 [file] [log] [blame]
// 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.plugins.codeowners.restapi;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.gerrit.entities.Account;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.plugins.codeowners.api.RenameEmailInput;
import com.google.gerrit.plugins.codeowners.api.RenameEmailResultInfo;
import com.google.gerrit.plugins.codeowners.backend.CodeOwner;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackend;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerBackendId;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerConfigFileUpdateScanner;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerReference;
import com.google.gerrit.plugins.codeowners.backend.CodeOwnerResolver;
import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.git.CommitUtil;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.permissions.RefPermission;
import com.google.gerrit.server.project.BranchResource;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Optional;
import org.eclipse.jgit.revwalk.RevCommit;
@Singleton
public class RenameEmail implements RestModifyView<BranchResource, RenameEmailInput> {
@VisibleForTesting
public static final String DEFAULT_COMMIT_MESSAGE = "Rename email in code owner config files";
private final Provider<CurrentUser> currentUser;
private final PermissionBackend permissionBackend;
private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration;
private final CodeOwnerResolver codeOwnerResolver;
private final CodeOwnerConfigFileUpdateScanner codeOwnerConfigFileUpdateScanner;
@Inject
public RenameEmail(
Provider<CurrentUser> currentUser,
PermissionBackend permissionBackend,
CodeOwnersPluginConfiguration codeOwnersPluginConfiguration,
CodeOwnerResolver codeOwnerResolver,
CodeOwnerConfigFileUpdateScanner codeOwnerConfigFileUpdateScanner) {
this.currentUser = currentUser;
this.permissionBackend = permissionBackend;
this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration;
this.codeOwnerResolver = codeOwnerResolver;
this.codeOwnerConfigFileUpdateScanner = codeOwnerConfigFileUpdateScanner;
}
@Override
public Response<RenameEmailResultInfo> apply(
BranchResource branchResource, RenameEmailInput input)
throws AuthException, BadRequestException, ResourceConflictException,
MethodNotAllowedException, UnprocessableEntityException, PermissionBackendException,
IOException {
if (!currentUser.get().isIdentifiedUser()) {
throw new AuthException("Authentication required");
}
// caller needs to be project owner or have direct push permissions for the branch
if (!permissionBackend
.currentUser()
.project(branchResource.getNameKey())
.test(ProjectPermission.WRITE_CONFIG)) {
permissionBackend
.currentUser()
.ref(branchResource.getBranchKey())
.check(RefPermission.UPDATE);
}
validateInput(input);
CodeOwnerBackend codeOwnerBackend =
codeOwnersPluginConfiguration
.getProjectConfig(branchResource.getNameKey())
.getBackend(branchResource.getBranchKey().branch());
Account.Id accountOwningOldEmail = resolveEmail(input.oldEmail);
Account.Id accountOwningNewEmail = resolveEmail(input.newEmail);
if (!accountOwningOldEmail.equals(accountOwningNewEmail)) {
throw new BadRequestException(
String.format(
"emails must belong to the same account"
+ " (old email %s is owned by account %d, new email %s is owned by account %d)",
input.oldEmail,
accountOwningOldEmail.get(),
input.newEmail,
accountOwningNewEmail.get()));
}
String inputMessage = Strings.nullToEmpty(input.message).trim();
String commitMessage = !inputMessage.isEmpty() ? inputMessage : DEFAULT_COMMIT_MESSAGE;
try {
Optional<RevCommit> commitId =
codeOwnerConfigFileUpdateScanner.update(
branchResource.getBranchKey(),
commitMessage,
(codeOwnerConfigFilePath, codeOwnerConfigFileContent) ->
renameEmailInCodeOwnerConfig(
codeOwnerBackend,
codeOwnerConfigFileContent,
input.oldEmail,
input.newEmail));
RenameEmailResultInfo result = new RenameEmailResultInfo();
if (commitId.isPresent()) {
result.commit = CommitUtil.toCommitInfo(commitId.get());
}
return Response.ok(result);
} catch (NotImplementedException e) {
throw new MethodNotAllowedException(
String.format(
"rename email not supported by %s backend",
CodeOwnerBackendId.getBackendId(codeOwnerBackend.getClass())),
e);
}
}
private void validateInput(RenameEmailInput input) throws BadRequestException {
if (input.oldEmail == null) {
throw new BadRequestException("old email is required");
}
if (input.newEmail == null) {
throw new BadRequestException("new email is required");
}
if (input.oldEmail.equals(input.newEmail)) {
throw new BadRequestException("old and new email must differ");
}
}
private Account.Id resolveEmail(String email) throws UnprocessableEntityException {
requireNonNull(email, "email");
Optional<CodeOwner> codeOwner = codeOwnerResolver.resolve(CodeOwnerReference.create(email));
if (!codeOwner.isPresent()) {
throw new UnprocessableEntityException(String.format("cannot resolve email %s", email));
}
return codeOwner.get().accountId();
}
/**
* Renames an email in the given code owner config.
*
* @param codeOwnerBackend the code owner backend that is being used
* @param codeOwnerConfigFileContent the content of the code owner config file
* @param oldEmail the old email that should be replaced by the new email
* @param newEmail the new email that should replace the old email
* @return the updated code owner config file content if an update was performed, {@link
* Optional#empty()} if no update was done
*/
private Optional<String> renameEmailInCodeOwnerConfig(
CodeOwnerBackend codeOwnerBackend,
String codeOwnerConfigFileContent,
String oldEmail,
String newEmail) {
requireNonNull(codeOwnerConfigFileContent, "codeOwnerConfigFileContent");
requireNonNull(oldEmail, "oldEmail");
requireNonNull(newEmail, "newEmail");
String updatedCodeOwnerConfigFileContent =
codeOwnerBackend.replaceEmail(codeOwnerConfigFileContent, oldEmail, newEmail);
if (codeOwnerConfigFileContent.equals(updatedCodeOwnerConfigFileContent)) {
return Optional.empty();
}
return Optional.of(updatedCodeOwnerConfigFileContent);
}
}