Add REST endpoint to get account external IDs

Add a new endpoint /accounts/{id}/external.ids which will return a
list of the external IDs for the given account. It is only allowed
to get this list for a user's own account.

To determine whether or not the user can delete an external ID, we
need to know if that ID was used to log into the current session.

The WebSession is not available outside of gerrit-httpd, so pass the
external ID through a property on the current user.

Change-Id: Iab7ef2ffa06c3a8d9f22472308051b4b73668f82
This commit is contained in:
Changcheng Xiao
2017-01-10 13:03:53 +01:00
parent 6c5fa05cdd
commit 0748e7441d
8 changed files with 222 additions and 0 deletions

View File

@@ -1675,6 +1675,39 @@ can contain a list of link:#project-watch-info[ProjectWatchInfo] entities.
HTTP/1.1 204 No Content
----
[[get-account-external-ids]]
=== Get Account External IDs
--
'GET /accounts/link:#account-id[\{account-id\}]/external.ids'
--
Retrieves the external ids of a user account.
.Request
----
GET /a/accounts/self/external.ids HTTP/1.0
----
As result the external ids of the user are returned as a list of
link:#account-external-id-info[AccountExternalIdInfo] entities.
.Response
----
HTTP/1.1 200 OK
Content-Disposition: attachment
Content-Type: application/json; charset=UTF-8
)]}'
[
{
"identity": "username:john",
"email": "john.doe@example.com",
"trusted": true,
"_can_delete": false
}
]
----
[[default-star-endpoints]]
== Default Star Endpoints
@@ -2099,6 +2132,20 @@ for an account.
If not set or if set to an empty string, the account name is deleted.
|=============================
[[account-external-id-info]]
=== AccountExternalIdInfo
The `AccountExternalIdInfo` entity contains information for an external id of
an account.
[options="header",cols="1,^1,5"]
|============================
|Field Name ||Description
|`identity` ||The account external id.
|`email` |optional|The email address for the external id.
|`trusted` ||True if the external id is trusted.
|`_can_delete` ||True if the external id can be deleted.
|============================
[[capability-info]]
=== CapabilityInfo
The `CapabilityInfo` entity contains information about the global

View File

@@ -38,6 +38,7 @@ import com.google.common.io.BaseEncoding;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AccountCreator;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.UseSsh;
import com.google.gerrit.common.data.Permission;
@@ -46,6 +47,7 @@ import com.google.gerrit.extensions.api.changes.AddReviewerInput;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.StarsInput;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.GpgKeyInfo;
@@ -70,6 +72,7 @@ import com.google.gerrit.server.project.RefPattern;
import com.google.gerrit.server.util.MagicBranch;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.gerrit.testutil.FakeEmailSender.Message;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -98,6 +101,7 @@ import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class AccountIT extends AbstractDaemonTest {
@ConfigSuite.Default
@@ -743,6 +747,33 @@ public class AccountIT extends AbstractDaemonTest {
gApi.accounts().id(admin.username).index();
}
@Test
public void getAccountExternalIds() throws Exception {
List<AccountExternalIdInfo> expectedIdInfoList = getExternalIds(user)
.stream().map(AccountIT::toInfo).sorted().collect(Collectors.toList());
RestResponse response = userRestSession.get("/accounts/self/external.ids");
response.assertOK();
List<AccountExternalIdInfo> externalIdInfoList =
newGson().fromJson(response.getReader(),
new TypeToken<List<AccountExternalIdInfo>>() {}.getType());
// 'canDelete' field will be all false. It will be better if we can find
// a way to test it. But it looks a little difficult.
externalIdInfoList.stream().sorted();
assertThat(expectedIdInfoList)
.containsExactlyElementsIn(expectedIdInfoList);
}
private static AccountExternalIdInfo toInfo(AccountExternalId id) {
AccountExternalIdInfo info = new AccountExternalIdInfo();
info.identity = id.getExternalId();
info.emailAddress = id.getEmailAddress();
info.trusted = id.isTrusted();
info.canDelete = id.canDelete();
return info;
}
private void assertSequenceNumbers(List<SshKeyInfo> sshKeys) {
int seq = 1;
for (SshKeyInfo key : sshKeys) {

View File

@@ -19,6 +19,7 @@ import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AgreementInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
@@ -79,6 +80,8 @@ public interface AccountApi {
void index() throws RestApiException;
List<AccountExternalIdInfo> getExternalIds() throws RestApiException;
/**
* A default implementation which allows source compatibility
* when adding new methods to the interface.
@@ -225,5 +228,10 @@ public interface AccountApi {
public void index() {
throw new NotImplementedException();
}
@Override
public List<AccountExternalIdInfo> getExternalIds() {
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,33 @@
// 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.extensions.common;
import com.google.common.collect.ComparisonChain;
public class AccountExternalIdInfo
implements Comparable<AccountExternalIdInfo> {
public String identity;
public String emailAddress;
public Boolean trusted;
public Boolean canDelete;
@Override
public int compareTo(AccountExternalIdInfo a) {
return ComparisonChain.start()
.compare(a.identity, identity)
.compare(a.emailAddress, emailAddress)
.result();
}
}

View File

@@ -90,6 +90,7 @@ import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
@@ -1077,6 +1078,9 @@ public class RestApiServlet extends HttpServlet {
CurrentUser user = globals.currentUser.get();
if (isRead(req)) {
user.setAccessPath(AccessPath.REST_API);
CurrentUser.PropertyKey<AccountExternalId.Key> k =
CurrentUser.PropertyKey.create();
user.put(k, globals.webSession.get().getLastLoginExternalId());
} else if (user instanceof AnonymousUser) {
throw new AuthException("Authentication required");
} else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {

View File

@@ -0,0 +1,87 @@
// 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.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.client.AccountExternalId;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Singleton
public class GetExternalIds implements RestReadView<AccountResource> {
private final ExternalIdCache externalIdCache;
private final Provider<CurrentUser> self;
private final AuthConfig authConfig;
@Inject
GetExternalIds(ExternalIdCache externalIdCache,
Provider<CurrentUser> self,
AuthConfig authConfig) {
this.externalIdCache = externalIdCache;
this.self = self;
this.authConfig = authConfig;
}
@Override
public List<AccountExternalIdInfo> apply(AccountResource resource)
throws RestApiException {
if (self.get() != resource.getUser()) {
throw new AuthException("not allowed to get external IDs");
}
Collection<AccountExternalId> ids = externalIdCache.byAccount(
resource.getUser().getAccountId());
if (ids.isEmpty()) {
return ImmutableList.of();
}
List<AccountExternalIdInfo> result =
Lists.newArrayListWithCapacity(ids.size());
for (AccountExternalId id : ids) {
AccountExternalIdInfo info = new AccountExternalIdInfo();
info.identity = id.getExternalId();
info.emailAddress = id.getEmailAddress();
info.trusted = authConfig.isIdentityTrustable(
Collections.singleton(id));
// The identity can be deleted only if its not the one used to
// establish this web session, and if only if an identity was
// actually used to establish this web session.
if (id.isScheme(SCHEME_USERNAME)) {
info.canDelete = false;
} else {
CurrentUser.PropertyKey<AccountExternalId.Key> k =
CurrentUser.PropertyKey.create();
AccountExternalId.Key last = resource.getUser().get(k);
info.canDelete = (last != null) && (!last.get().equals(info.identity));
}
result.add(info);
}
return result;
}
}

View File

@@ -95,6 +95,8 @@ public class Module extends RestApiModule {
get(STAR_KIND).to(Stars.Get.class);
post(STAR_KIND).to(Stars.Post.class);
get(ACCOUNT_KIND, "external.ids").to(GetExternalIds.class);
factory(CreateAccount.Factory.class);
factory(CreateEmail.Factory.class);
}

View File

@@ -26,6 +26,7 @@ import com.google.gerrit.extensions.client.DiffPreferencesInfo;
import com.google.gerrit.extensions.client.EditPreferencesInfo;
import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.common.AccountExternalIdInfo;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.AgreementInfo;
import com.google.gerrit.extensions.common.AgreementInput;
@@ -49,6 +50,7 @@ import com.google.gerrit.server.account.GetAgreements;
import com.google.gerrit.server.account.GetAvatar;
import com.google.gerrit.server.account.GetDiffPreferences;
import com.google.gerrit.server.account.GetEditPreferences;
import com.google.gerrit.server.account.GetExternalIds;
import com.google.gerrit.server.account.GetPreferences;
import com.google.gerrit.server.account.GetSshKeys;
import com.google.gerrit.server.account.GetWatchedProjects;
@@ -110,6 +112,7 @@ public class AccountApiImpl implements AccountApi {
private final PutActive putActive;
private final DeleteActive deleteActive;
private final Index index;
private final GetExternalIds getExternalIds;
@Inject
AccountApiImpl(AccountLoader.Factory ailf,
@@ -141,6 +144,7 @@ public class AccountApiImpl implements AccountApi {
PutActive putActive,
DeleteActive deleteActive,
Index index,
GetExternalIds getExternalIds,
@Assisted AccountResource account) {
this.account = account;
this.accountLoaderFactory = ailf;
@@ -172,6 +176,7 @@ public class AccountApiImpl implements AccountApi {
this.putActive = putActive;
this.deleteActive = deleteActive;
this.index = index;
this.getExternalIds = getExternalIds;
}
@Override
@@ -447,4 +452,9 @@ public class AccountApiImpl implements AccountApi {
throw new RestApiException("Cannot index account", e);
}
}
@Override
public List<AccountExternalIdInfo> getExternalIds() throws RestApiException {
return getExternalIds.apply(account);
}
}