Add extension point that allows plugins to provide account state metadata
The new extension point allows us at Google to include important account
metadata such as plain text GAIA IDs, whether the account has read
access on host level or whether the account uses a trusted device.
If this metadata is available investigating account issues become
easier.
Bug: Google b/330836100
Release-Notes: Added extension point that allows plugins to provide account state metadata
Change-Id: If799d5a61eacf30d48f4e67e918274b200e0b0f2
Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 7a55867..3a19caf 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2916,6 +2916,28 @@
}
----
+[[account-state-provider]]
+== Account State Provider
+
+Gerrit provides an extension point that enables plugins to supply additional
+data for account states which are returned from the
+link:rest-api-accounts.html#get-state[Get Account State] REST endpoint (see
+`metadata` field in
+link:rest-api-accounts.html#account-state-info[AccountStateInfo]).
+
+[source, java]
+----
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.AccountStateProvider;
+
+public class MyPlugin implements AccountStateProvider {
+ public ImmutableList<AccountMetadataInfo> getMetadata(Account.Id accountId) {
+ // Implement your logic here
+ }
+}
+----
+
[[ssh-command-creation-interception]]
== SSH Command Creation Interception
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index 00eced8..50dd72d 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -691,7 +691,8 @@
"trusted": true,
"can_delete": true
}
- ]
+ ],
+ "metadata": {}
}
----
@@ -2333,6 +2334,19 @@
the groups to which the user should be added.
|============================
+[[account-metadata-info]]
+=== AccountMetadataInfo
+The `AccountMetadataInfo` entity contains account metadata.
+
+[options="header",cols="1,^2,4"]
+|==========================
+|Field Name ||Description
+|`name` ||The metadata name. Not guaranteed to be unique, e.g. for one
+account multiple metadata entries with the same name may be returned.
+|`value` |optional|The metadata value.
+|`description`|optional|A description of the metadata.
+|==========================
+
[[account-name-input]]
=== AccountNameInput
The `AccountNameInput` entity contains information for setting a name
@@ -2364,6 +2378,10 @@
|`external_ids`|
The external IDs of the account as a list of
link:#account-external-id-info[AccountExternalIdInfo] entities.
+|`metadata` |
+Optional account metadata as a list of
+link:account-metadata-info[AccountMetadataInfo] entities. If and which metadata
+is provided depends on the Gerrit setup.
|==========================
[[account-status-input]]
diff --git a/java/com/google/gerrit/acceptance/ExtensionRegistry.java b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
index a35e427..11d40fc 100644
--- a/java/com/google/gerrit/acceptance/ExtensionRegistry.java
+++ b/java/com/google/gerrit/acceptance/ExtensionRegistry.java
@@ -44,6 +44,7 @@
import com.google.gerrit.extensions.webui.PatchSetWebLink;
import com.google.gerrit.extensions.webui.ResolveConflictsWebLink;
import com.google.gerrit.server.ExceptionHook;
+import com.google.gerrit.server.account.AccountStateProvider;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.change.FilterIncludedIn;
import com.google.gerrit.server.change.ReviewerSuggestion;
@@ -106,6 +107,7 @@
private final DynamicSet<OnPostReview> onPostReviews;
private final DynamicSet<ReviewerAddedListener> reviewerAddedListeners;
private final DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners;
+ private final DynamicSet<AccountStateProvider> accountStateProviders;
private final DynamicSet<AttentionSetListener> attentionSetListeners;
private final DynamicMap<ChangeHasOperandFactory> hasOperands;
@@ -153,6 +155,7 @@
DynamicSet<ReviewerDeletedListener> reviewerDeletedListeners,
DynamicMap<ChangeHasOperandFactory> hasOperands,
DynamicMap<ChangeIsOperandFactory> isOperands,
+ DynamicSet<AccountStateProvider> accountStateProviders,
DynamicSet<AttentionSetListener> attentionSetListeners,
DynamicMap<ReviewerSuggestion> reviewerSuggestions) {
this.accountIndexedListeners = accountIndexedListeners;
@@ -194,6 +197,7 @@
this.reviewerDeletedListeners = reviewerDeletedListeners;
this.hasOperands = hasOperands;
this.isOperands = isOperands;
+ this.accountStateProviders = accountStateProviders;
this.attentionSetListeners = attentionSetListeners;
this.reviewerSuggestions = reviewerSuggestions;
}
@@ -373,6 +377,11 @@
}
@CanIgnoreReturnValue
+ public Registration add(AccountStateProvider accountStateProvider) {
+ return add(accountStateProviders, accountStateProvider);
+ }
+
+ @CanIgnoreReturnValue
public Registration add(AttentionSetListener attentionSetListener) {
return add(attentionSetListeners, attentionSetListener);
}
diff --git a/java/com/google/gerrit/extensions/common/AccountMetadataInfo.java b/java/com/google/gerrit/extensions/common/AccountMetadataInfo.java
new file mode 100644
index 0000000..83cc37d
--- /dev/null
+++ b/java/com/google/gerrit/extensions/common/AccountMetadataInfo.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2024 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.base.MoreObjects;
+import com.google.gerrit.common.Nullable;
+import java.util.Objects;
+
+/**
+ * Account metadata populated by plugins, see {code
+ * com.google.gerrit.server.account.AccountStateProvider}.
+ */
+public class AccountMetadataInfo {
+ /**
+ * The metadata name.
+ *
+ * <p>Not guaranteed to be unique, e.g. for one account multiple metadata entries with the same
+ * name may be returned.
+ */
+ public String name;
+
+ /** The metadata value. May be unset. */
+ @Nullable public String value;
+
+ /** A description of the metadata. May be unset. */
+ @Nullable public String description;
+
+ @Override
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("name", name)
+ .add("value", value)
+ .add("description", description)
+ .toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, value, description);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof AccountMetadataInfo) {
+ AccountMetadataInfo metadata = (AccountMetadataInfo) o;
+ return Objects.equals(name, metadata.name)
+ && Objects.equals(value, metadata.value)
+ && Objects.equals(description, metadata.description);
+ }
+ return false;
+ }
+}
diff --git a/java/com/google/gerrit/extensions/common/AccountStateInfo.java b/java/com/google/gerrit/extensions/common/AccountStateInfo.java
index 6789edb..f441145 100644
--- a/java/com/google/gerrit/extensions/common/AccountStateInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountStateInfo.java
@@ -34,4 +34,7 @@
/** The external IDs of the account. */
public List<AccountExternalIdInfo> externalIds;
+
+ /** Account metadata populated by plugins. */
+ public List<AccountMetadataInfo> metadata;
}
diff --git a/java/com/google/gerrit/server/account/AccountStateProvider.java b/java/com/google/gerrit/server/account/AccountStateProvider.java
new file mode 100644
index 0000000..8dca07e
--- /dev/null
+++ b/java/com/google/gerrit/server/account/AccountStateProvider.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2024 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 com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.common.AccountMetadataInfo;
+
+/**
+ * Extension point to retrieve account state that should be included in the response of the {@link
+ * com.google.gerrit.server.restapi.account.GetState} REST endpoint.
+ */
+@ExtensionPoint
+public interface AccountStateProvider {
+ /**
+ * Returns metadata to populate {@link
+ * com.google.gerrit.extensions.common.AccountStateInfo#metadata}.
+ */
+ public ImmutableList<AccountMetadataInfo> getMetadata(Account.Id accountId);
+}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index c4ecc80..469e9ed 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -97,6 +97,7 @@
import com.google.gerrit.server.account.AccountExternalIdCreator;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountModule;
+import com.google.gerrit.server.account.AccountStateProvider;
import com.google.gerrit.server.account.AccountTagProvider;
import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityCollection;
@@ -464,6 +465,7 @@
DynamicSet.setOf(binder(), MailSoyTemplateProvider.class);
DynamicSet.setOf(binder(), OnPostReview.class);
DynamicMap.mapOf(binder(), AccountTagProvider.class);
+ DynamicSet.setOf(binder(), AccountStateProvider.class);
DynamicSet.setOf(binder(), AttentionSetListener.class);
DynamicMap.mapOf(binder(), MailFilter.class);
diff --git a/java/com/google/gerrit/server/restapi/account/GetState.java b/java/com/google/gerrit/server/restapi/account/GetState.java
index e8e48bd..d8684c1 100644
--- a/java/com/google/gerrit/server/restapi/account/GetState.java
+++ b/java/com/google/gerrit/server/restapi/account/GetState.java
@@ -14,6 +14,11 @@
package com.google.gerrit.server.restapi.account;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.extensions.common.AccountMetadataInfo;
import com.google.gerrit.extensions.common.AccountStateInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.Response;
@@ -21,11 +26,15 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AccountStateProvider;
import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
/**
* REST endpoint to retrieve the superset of all information related to an account. This information
@@ -41,6 +50,7 @@
private final GetDetail getDetail;
private final GetGroups getGroups;
private final GetExternalIds getExternalIds;
+ private final PluginSetContext<AccountStateProvider> accountStateProviders;
@Inject
GetState(
@@ -48,12 +58,14 @@
Provider<GetCapabilities> getCapabilities,
GetDetail getDetail,
GetGroups getGroups,
- GetExternalIds getExternalIds) {
+ GetExternalIds getExternalIds,
+ PluginSetContext<AccountStateProvider> accountStateProviders) {
this.self = self;
this.getCapabilities = getCapabilities;
this.getDetail = getDetail;
this.getGroups = getGroups;
this.getExternalIds = getExternalIds;
+ this.accountStateProviders = accountStateProviders;
}
@Override
@@ -72,6 +84,18 @@
accountState.capabilities = getCapabilities.get().apply(rsrc).value();
accountState.groups = getGroups.apply(rsrc).value();
accountState.externalIds = getExternalIds.apply(rsrc).value();
+ accountState.metadata = getMetadata(rsrc.getUser().getAccountId());
return Response.ok(accountState);
}
+
+ private ImmutableList<AccountMetadataInfo> getMetadata(Account.Id accountId) {
+ ArrayList<AccountMetadataInfo> metadataList = new ArrayList<>();
+ accountStateProviders.runEach(
+ accountStateProvider -> metadataList.addAll(accountStateProvider.getMetadata(accountId)));
+ return metadataList.stream()
+ .sorted(
+ Comparator.comparing((AccountMetadataInfo metadata) -> metadata.name)
+ .thenComparing((AccountMetadataInfo metadata) -> metadata.value))
+ .collect(toImmutableList());
+ }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 1bff157..51c97cd 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -114,6 +114,7 @@
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.common.AccountDetailInfo;
import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.AccountMetadataInfo;
import com.google.gerrit.extensions.common.AccountStateInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommentInfo;
@@ -143,6 +144,7 @@
import com.google.gerrit.server.account.AccountLimits;
import com.google.gerrit.server.account.AccountProperties;
import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AccountStateProvider;
import com.google.gerrit.server.account.AccountsUpdate;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.account.GroupMembership;
@@ -3474,35 +3476,50 @@
String groupName = "SomeGroup";
groupOperations.newGroup().name(groupName).addMember(foo.id()).create();
- requestScopeOperations.setApiUser(foo.id());
- AccountStateInfo state = gApi.accounts().id(foo.id().get()).state();
+ TestAccountStateProvider testAccountStateProvider = new TestAccountStateProvider();
+ AccountMetadataInfo metadata1 =
+ testAccountStateProvider.addMetadata("employee_id", "123456", null);
+ AccountMetadataInfo metadata2 = testAccountStateProvider.addMetadata("role", null, "role name");
+ AccountMetadataInfo metadata3 =
+ testAccountStateProvider.addMetadata("team", "Bar", "team name");
+ AccountMetadataInfo metadata4 =
+ testAccountStateProvider.addMetadata("team", "Foo", "team name");
+ try (Registration registration =
+ extensionRegistry.newRegistration().add(testAccountStateProvider)) {
+ requestScopeOperations.setApiUser(foo.id());
+ AccountStateInfo state = gApi.accounts().id(foo.id().get()).state();
- AccountDetailInfo detail = state.account;
- assertThat(detail._accountId).isEqualTo(foo.id().get());
- assertThat(detail.name).isEqualTo(name);
- if (server.isUsernameSupported()) {
- assertThat(detail.username).isEqualTo(username);
+ AccountDetailInfo detail = state.account;
+ assertThat(detail._accountId).isEqualTo(foo.id().get());
+ assertThat(detail.name).isEqualTo(name);
+ if (server.isUsernameSupported()) {
+ assertThat(detail.username).isEqualTo(username);
+ }
+ assertThat(detail.email).isEqualTo(email);
+ assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
+ assertThat(detail.status).isEqualTo(status);
+ assertThat(detail.registeredOn.getTime())
+ .isEqualTo(getAccount(foo.id()).registeredOn().toEpochMilli());
+ assertThat(detail.inactive).isNull();
+ assertThat(detail._moreAccounts).isNull();
+
+ AccountLimits limits = limitsFactory.create(genericUserFactory.create(foo.id()));
+ GetCapabilities.Range queryLimitRange =
+ new GetCapabilities.Range(limits.getRange("queryLimit"));
+ assertThat(state.capabilities)
+ .containsExactly("emailReviewers", true, "queryLimit", queryLimitRange);
+
+ assertThat(state.groups)
+ .comparingElementsUsing(getGroupToNameCorrespondence())
+ .containsAtLeast("Anonymous Users", "Registered Users", groupName);
+
+ assertThat(state.externalIds.stream().map(e -> e.identity).collect(toImmutableSet()))
+ .containsExactly("mailto:" + email, "username:" + username, "mailto:" + secondaryEmail);
+
+ assertThat(state.metadata)
+ .containsExactly(metadata1, metadata2, metadata3, metadata4)
+ .inOrder();
}
- assertThat(detail.email).isEqualTo(email);
- assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
- assertThat(detail.status).isEqualTo(status);
- assertThat(detail.registeredOn.getTime())
- .isEqualTo(getAccount(foo.id()).registeredOn().toEpochMilli());
- assertThat(detail.inactive).isNull();
- assertThat(detail._moreAccounts).isNull();
-
- AccountLimits limits = limitsFactory.create(genericUserFactory.create(foo.id()));
- GetCapabilities.Range queryLimitRange =
- new GetCapabilities.Range(limits.getRange("queryLimit"));
- assertThat(state.capabilities)
- .containsExactly("emailReviewers", true, "queryLimit", queryLimitRange);
-
- assertThat(state.groups)
- .comparingElementsUsing(getGroupToNameCorrespondence())
- .containsAtLeast("Anonymous Users", "Registered Users", groupName);
-
- assertThat(state.externalIds.stream().map(e -> e.identity).collect(toImmutableSet()))
- .containsExactly("mailto:" + email, "username:" + username, "mailto:" + secondaryEmail);
}
@Test
@@ -3892,4 +3909,23 @@
return true;
}
}
+
+ public static class TestAccountStateProvider implements AccountStateProvider {
+ private ArrayList<AccountMetadataInfo> metadataList = new ArrayList<>();
+
+ public AccountMetadataInfo addMetadata(
+ String name, @Nullable String value, @Nullable String description) {
+ AccountMetadataInfo metadata = new AccountMetadataInfo();
+ metadata.name = name;
+ metadata.value = value;
+ metadata.description = description;
+ metadataList.add(metadata);
+ return metadata;
+ }
+
+ @Override
+ public ImmutableList<AccountMetadataInfo> getMetadata(Account.Id accountId) {
+ return ImmutableList.copyOf(metadataList);
+ }
+ }
}