Account REST API: Add a new endpoint to delete external IDs

The REST endpoint is added as:
'POST /accounts/{account-id}/external.ids:delete'
The target external ids must be provided in the request body.

Change-Id: Ieef322d6bddc1f3659536081b3f3514474b7162a
This commit is contained in:
Changcheng Xiao
2017-01-16 11:38:03 +01:00
parent de5dd7a2ef
commit 402c249b4a
6 changed files with 211 additions and 0 deletions

View File

@@ -1707,6 +1707,32 @@ link:#account-external-id-info[AccountExternalIdInfo] entities.
] ]
---- ----
[[delete-account-external-ids]]
=== Delete Account External IDs
--
'POST /accounts/link:#account-id[\{account-id\}]/external.ids:delete'
--
Delete a list of external ids for a user account. The target external ids must
be provided as a list in the request body.
Only external ids belonging to the caller may be deleted.
.Request
----
POST /a/accounts/self/external.ids:delete HTTP/1.0
Content-Type: application/json;charset=UTF-8
{
"mailto:john.doe@example.com"
}
----
.Response
----
HTTP/1.1 204 No Content
----
[[default-star-endpoints]] [[default-star-endpoints]]
== Default Star Endpoints == Default Star Endpoints

View File

@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.acceptance.AbstractDaemonTest; import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.RestResponse; import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.Sandboxed;
import com.google.gerrit.extensions.common.AccountExternalIdInfo; import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.reviewdb.client.AccountExternalId; import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
@@ -29,6 +30,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@Sandboxed
public class ExternalIdIT extends AbstractDaemonTest { public class ExternalIdIT extends AbstractDaemonTest {
@Test @Test
public void getExternalIDs() throws Exception { public void getExternalIDs() throws Exception {
@@ -54,6 +56,59 @@ public class ExternalIdIT extends AbstractDaemonTest {
assertThat(results).containsExactlyElementsIn(expectedIdInfos); assertThat(results).containsExactlyElementsIn(expectedIdInfos);
} }
@Test
public void deleteExternalIDs() throws Exception {
setApiUser(user);
List<AccountExternalIdInfo> externalIds =
gApi.accounts().self().getExternalIds();
List<String> toDelete = new ArrayList<>();
List<AccountExternalIdInfo> expectedIds = new ArrayList<>();
for (AccountExternalIdInfo id : externalIds) {
if (id.canDelete != null && id.canDelete) {
toDelete.add(id.identity);
continue;
}
expectedIds.add(id);
}
assertThat(toDelete).hasSize(1);
RestResponse response = userRestSession.post(
"/accounts/self/external.ids:delete", toDelete);
response.assertNoContent();
List<AccountExternalIdInfo> results =
gApi.accounts().self().getExternalIds();
// The external ID in WebSession will not be set for tests, resulting that
// "mailto:user@example.com" can be deleted while "username:user" can't.
assertThat(results).hasSize(1);
assertThat(results).containsExactlyElementsIn(expectedIds);
}
@Test
public void deleteExternalIDs_Conflict() throws Exception {
List<String> toDelete = new ArrayList<>();
String externalIdStr = "username:" + user.username;
toDelete.add(externalIdStr);
RestResponse response = userRestSession.post(
"/accounts/self/external.ids:delete", toDelete);
response.assertConflict();
assertThat(response.getEntityContent()).isEqualTo(
String.format("External id %s cannot be deleted", externalIdStr));
}
@Test
public void deleteExternalIDs_UnprocessableEntity() throws Exception {
List<String> toDelete = new ArrayList<>();
String externalIdStr = "mailto:user@domain.com";
toDelete.add(externalIdStr);
RestResponse response = userRestSession.post(
"/accounts/self/external.ids:delete", toDelete);
response.assertUnprocessableEntity();
assertThat(response.getEntityContent()).isEqualTo(
String.format("External id %s does not exist", externalIdStr));
}
private static AccountExternalIdInfo toInfo(AccountExternalId id) { private static AccountExternalIdInfo toInfo(AccountExternalId id) {
AccountExternalIdInfo info = new AccountExternalIdInfo(); AccountExternalIdInfo info = new AccountExternalIdInfo();
info.identity = id.getExternalId(); info.identity = id.getExternalId();

View File

@@ -81,6 +81,7 @@ public interface AccountApi {
void index() throws RestApiException; void index() throws RestApiException;
List<AccountExternalIdInfo> getExternalIds() throws RestApiException; List<AccountExternalIdInfo> getExternalIds() throws RestApiException;
void deleteExternalIds(List<String> externalIds) throws RestApiException;
/** /**
* A default implementation which allows source compatibility * A default implementation which allows source compatibility
@@ -233,5 +234,10 @@ public interface AccountApi {
public List<AccountExternalIdInfo> getExternalIds() { public List<AccountExternalIdInfo> getExternalIds() {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@Override
public void deleteExternalIds(List<String> externalIds) {
throw new NotImplementedException();
}
} }
} }

View File

@@ -0,0 +1,109 @@
// Copyright (C) 2017 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.account;
import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class DeleteExternalIds implements
RestModifyView<AccountResource, List<String>> {
private final AccountByEmailCache accountByEmailCache;
private final AccountCache accountCache;
private final ExternalIdCache externalIdCache;
private final Provider<CurrentUser> self;
private final Provider<ReviewDb> dbProvider;
@Inject
DeleteExternalIds(
AccountByEmailCache accountByEmailCache,
AccountCache accountCache,
ExternalIdCache externalIdCache,
Provider<CurrentUser> self,
Provider<ReviewDb> dbProvider) {
this.accountByEmailCache = accountByEmailCache;
this.accountCache = accountCache;
this.externalIdCache = externalIdCache;
this.self = self;
this.dbProvider = dbProvider;
}
@Override
public Response<?> apply(AccountResource resource, List<String> externalIds)
throws RestApiException, IOException, OrmException {
if (self.get() != resource.getUser()) {
throw new AuthException("not allowed to delete external IDs");
}
if (externalIds == null || externalIds.size() == 0) {
throw new BadRequestException("external IDs are required");
}
Account.Id accountId = resource.getUser().getAccountId();
Map<AccountExternalId.Key, AccountExternalId> externalIdMap =
externalIdCache.byAccount(resource.getUser().getAccountId())
.stream().collect(Collectors.toMap(i -> i.getKey(), i -> i));
List<AccountExternalId> toDelete = new ArrayList<>();
AccountExternalId.Key last = resource.getUser().getLastLoginExternalIdKey();
for (String externalIdStr : externalIds) {
AccountExternalId id = externalIdMap.get(
new AccountExternalId.Key(externalIdStr));
if (id == null) {
throw new UnprocessableEntityException(String.format(
"External id %s does not exist", externalIdStr));
}
if ((!id.isScheme(SCHEME_USERNAME))
&& ((last == null) || (!last.get().equals(id.getExternalId())))) {
toDelete.add(id);
} else {
throw new ResourceConflictException(String.format(
"External id %s cannot be deleted", externalIdStr));
}
}
if (!toDelete.isEmpty()) {
dbProvider.get().accountExternalIds().delete(toDelete);
externalIdCache.onRemove(toDelete);
accountCache.evict(accountId);
for (AccountExternalId e : toDelete) {
accountByEmailCache.evict(e.getEmailAddress());
}
}
return Response.none();
}
}

View File

@@ -96,6 +96,7 @@ public class Module extends RestApiModule {
post(STAR_KIND).to(Stars.Post.class); post(STAR_KIND).to(Stars.Post.class);
get(ACCOUNT_KIND, "external.ids").to(GetExternalIds.class); get(ACCOUNT_KIND, "external.ids").to(GetExternalIds.class);
post(ACCOUNT_KIND, "external.ids:delete").to(DeleteExternalIds.class);
factory(CreateAccount.Factory.class); factory(CreateAccount.Factory.class);
factory(CreateEmail.Factory.class); factory(CreateEmail.Factory.class);

View File

@@ -43,6 +43,7 @@ import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AddSshKey; import com.google.gerrit.server.account.AddSshKey;
import com.google.gerrit.server.account.CreateEmail; import com.google.gerrit.server.account.CreateEmail;
import com.google.gerrit.server.account.DeleteActive; import com.google.gerrit.server.account.DeleteActive;
import com.google.gerrit.server.account.DeleteExternalIds;
import com.google.gerrit.server.account.DeleteSshKey; import com.google.gerrit.server.account.DeleteSshKey;
import com.google.gerrit.server.account.DeleteWatchedProjects; import com.google.gerrit.server.account.DeleteWatchedProjects;
import com.google.gerrit.server.account.GetActive; import com.google.gerrit.server.account.GetActive;
@@ -113,6 +114,7 @@ public class AccountApiImpl implements AccountApi {
private final DeleteActive deleteActive; private final DeleteActive deleteActive;
private final Index index; private final Index index;
private final GetExternalIds getExternalIds; private final GetExternalIds getExternalIds;
private final DeleteExternalIds deleteExternalIds;
@Inject @Inject
AccountApiImpl(AccountLoader.Factory ailf, AccountApiImpl(AccountLoader.Factory ailf,
@@ -145,6 +147,7 @@ public class AccountApiImpl implements AccountApi {
DeleteActive deleteActive, DeleteActive deleteActive,
Index index, Index index,
GetExternalIds getExternalIds, GetExternalIds getExternalIds,
DeleteExternalIds deleteExternalIds,
@Assisted AccountResource account) { @Assisted AccountResource account) {
this.account = account; this.account = account;
this.accountLoaderFactory = ailf; this.accountLoaderFactory = ailf;
@@ -177,6 +180,7 @@ public class AccountApiImpl implements AccountApi {
this.deleteActive = deleteActive; this.deleteActive = deleteActive;
this.index = index; this.index = index;
this.getExternalIds = getExternalIds; this.getExternalIds = getExternalIds;
this.deleteExternalIds = deleteExternalIds;
} }
@Override @Override
@@ -457,4 +461,14 @@ public class AccountApiImpl implements AccountApi {
public List<AccountExternalIdInfo> getExternalIds() throws RestApiException { public List<AccountExternalIdInfo> getExternalIds() throws RestApiException {
return getExternalIds.apply(account); return getExternalIds.apply(account);
} }
@Override
public void deleteExternalIds(List<String> externalIds)
throws RestApiException {
try {
deleteExternalIds.apply(account, externalIds);
} catch (IOException | OrmException e) {
throw new RestApiException("Cannot delete external IDs", e);
}
}
} }