Merge branch 'stable-3.0'
* stable-3.0:
Remove unused JSON_PREFIX constant
Fix adding additional ssh-keys via UI
Allow additional SSH-key formats
Switch required bazel version to 0.29.1
Upgrade bazlets to latest stable-3.0
Upgrade bazlets to latest stable-2.16
Upgrade bazlets to latest stable-2.15
Bump required bazel version to 1.0.0rc2
Upgrade bazlets to latest stable-2.14
Remove unwanted char from user details screen
Fix visibility of service users for non-admin owners
Fix setting a new owner from UI
Add validation for SSH-key before account creation
Apply GJF-1.7 codestyle
Upgrade bazlets to latest stable-2.15
Upgrade bazlets to latest stable-2.14
Add PolyGerrit UI for creating a service user
Upgrade bazlets to latest stable-3.0 to build with 3.0.2 API
Modify the detail-screen to allow editing the service user
Upgrade bazlets to latest stable-2.16 to build with 2.16.11.1 API
Upgrade bazlets to latest stable-2.15 to build with 2.15.16 API
Add PolyGerrit UI for showing service user details
Add PolyGerrit UI to list service users
Add eslint config
Upgrade bazlets to latest stable-2.15 to build with 2.15.15 API
Change-Id: I6fb19f26391e9a779dc39b59b92b1dd9d0855307
diff --git a/.bazelversion b/.bazelversion
new file mode 100644
index 0000000..25939d3
--- /dev/null
+++ b/.bazelversion
@@ -0,0 +1 @@
+0.29.1
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..3a9c4b9
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,79 @@
+{
+ "extends": ["eslint:recommended", "google"],
+ "parserOptions": {
+ "ecmaVersion": 8
+ },
+ "env": {
+ "browser": true,
+ "es6": true
+ },
+ "globals": {
+ "__dirname": false,
+ "app": false,
+ "page": false,
+ "Polymer": false,
+ "process": false,
+ "require": false,
+ "Gerrit": false,
+ "Promise": false,
+ "assert": false,
+ "test": false,
+ "flushAsynchronousOperations": false
+ },
+ "rules": {
+ "arrow-parens": ["error", "as-needed"],
+ "block-spacing": ["error", "always"],
+ "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
+ "camelcase": "off",
+ "comma-dangle": ["error", "always-multiline"],
+ "eol-last": "off",
+ "indent": "off",
+ "indent-legacy": ["error", 2, {
+ "MemberExpression": 2,
+ "FunctionDeclaration": {"body": 1, "parameters": 2},
+ "FunctionExpression": {"body": 1, "parameters": 2},
+ "CallExpression": {"arguments": 2},
+ "ArrayExpression": 1,
+ "ObjectExpression": 1,
+ "SwitchCase": 1
+ }],
+ "keyword-spacing": ["error", { "after": true, "before": true }],
+ "max-len": [
+ "error",
+ 80,
+ 2,
+ {"ignoreComments": true}
+ ],
+ "new-cap": ["error", { "capIsNewExceptions": ["Polymer"] }],
+ "no-console": "off",
+ "no-restricted-syntax": [
+ "error",
+ {
+ "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='test'][property.name='only']",
+ "message": "Remove test.only."
+ },
+ {
+ "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='suite'][property.name='only']",
+ "message": "Remove suite.only."
+ }
+ ],
+ "no-undef": "off",
+ "no-useless-escape": "off",
+ "no-var": "error",
+ "object-shorthand": ["error", "always"],
+ "prefer-arrow-callback": "error",
+ "prefer-const": "error",
+ "prefer-spread": "error",
+ "quote-props": ["error", "consistent-as-needed"],
+ "require-jsdoc": "off",
+ "semi": [2, "always"],
+ "template-curly-spacing": "error",
+ "valid-jsdoc": "off"
+ },
+ "plugins": [
+ "html"
+ ],
+ "settings": {
+ "html/report-bad-indent": "error"
+ }
+ }
diff --git a/BUILD b/BUILD
index 06e79d5..fcad0e5 100644
--- a/BUILD
+++ b/BUILD
@@ -1,4 +1,5 @@
-load("//tools/bzl:plugin.bzl", "gerrit_plugin")
+load("//tools/bzl:junit.bzl", "junit_tests")
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS", "PLUGIN_TEST_DEPS", "gerrit_plugin")
gerrit_plugin(
name = "serviceuser",
@@ -6,7 +7,20 @@
manifest_entries = [
"Gerrit-PluginName: serviceuser",
"Gerrit-Module: com.googlesource.gerrit.plugins.serviceuser.Module",
+ "Gerrit-HttpModule: com.googlesource.gerrit.plugins.serviceuser.HttpModule",
"Gerrit-SshModule: com.googlesource.gerrit.plugins.serviceuser.SshModule",
],
- resources = glob(["src/main/**/*"]),
+ resources = glob(["src/main/resources/**/*"]),
+)
+
+junit_tests(
+ name = "serviceuser_tests",
+ testonly = 1,
+ srcs = glob([
+ "src/test/java/**/*Test.java",
+ ]),
+ tags = ["serviceuser"],
+ deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [
+ ":serviceuser__plugin",
+ ],
)
diff --git a/WORKSPACE b/WORKSPACE
index e5e8101..44bb695 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
load("//:bazlets.bzl", "load_bazlets")
load_bazlets(
- commit = "3c0a1bd325152af99640655831aef109e460bbe7",
+ commit = "0ca51936ca46049cddd34e971a595d3baafe731b",
#local_path = "/home/<user>/projects/bazlets",
)
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java
index 3a94269..80ddf1b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java
@@ -122,8 +122,7 @@
@Override
public Response<ServiceUserInfo> apply(
ConfigResource parentResource, IdString id, CreateServiceUser.Input input)
- throws RestApiException, IOException, ConfigInvalidException,
- PermissionBackendException {
+ throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
CurrentUser user = userProvider.get();
if (user == null || !user.isIdentifiedUser()) {
throw new AuthException("authentication required");
@@ -136,10 +135,15 @@
if (input.username != null && !username.equals(input.username)) {
throw new BadRequestException("username must match URL");
}
+
if (Strings.isNullOrEmpty(input.sshKey)) {
throw new BadRequestException("sshKey not set");
}
+ if (!SshKeyValidator.validateFormat(input.sshKey)) {
+ throw new BadRequestException("sshKey invalid.");
+ }
+
if (blockedNames.contains(username.toLowerCase())) {
throw new BadRequestException(
"The username '" + username + "' is not allowed as name for service users.");
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserCommand.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserCommand.java
index 3f18c8f..3f27975 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserCommand.java
@@ -49,8 +49,7 @@
@Override
protected void run()
- throws IOException, UnloggedFailure, ConfigInvalidException,
- PermissionBackendException {
+ throws IOException, UnloggedFailure, ConfigInvalidException, PermissionBackendException {
CreateServiceUser.Input input = new CreateServiceUser.Input();
input.sshKey = readSshKey();
input.username = username;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserNotes.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserNotes.java
index e3181e6..63cb875 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserNotes.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserNotes.java
@@ -82,8 +82,7 @@
}
void createNotes(String branch, ObjectId oldObjectId, ObjectId newObjectId)
- throws IOException, ConfigInvalidException, PermissionBackendException,
- RestApiException {
+ throws IOException, ConfigInvalidException, PermissionBackendException, RestApiException {
if (ObjectId.zeroId().equals(newObjectId)) {
return;
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java
index 8f76f34..9ce83d3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java
@@ -38,8 +38,8 @@
@Override
public Response<?> apply(ServiceUserResource.SshKey rsrc, Input input)
- throws AuthException, RepositoryNotFoundException, IOException,
- ConfigInvalidException, PermissionBackendException {
+ throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException,
+ PermissionBackendException {
return deleteSshKey
.get()
.apply(new AccountResource.SshKey(rsrc.getUser(), rsrc.getSshKey()), input);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
index e955f43..85da7b0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
@@ -14,6 +14,7 @@
package com.googlesource.gerrit.plugins.serviceuser;
+import com.google.common.base.Strings;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.RestReadView;
@@ -61,6 +62,8 @@
public ConfigInfo apply(ConfigResource rsrc) throws PermissionBackendException {
PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName);
ConfigInfo info = new ConfigInfo();
+ info.info = Strings.emptyToNull(cfg.getString("infoMessage"));
+ info.onSuccess = Strings.emptyToNull(cfg.getString("onSuccessMessage"));
info.allowEmail = toBoolean(cfg.getBoolean("allowEmail", false));
info.allowHttpPassword = toBoolean(cfg.getBoolean("allowHttpPassword", false));
info.allowOwner = toBoolean(cfg.getBoolean("allowOwner", false));
@@ -92,6 +95,8 @@
}
public class ConfigInfo {
+ public String info;
+ public String onSuccess;
public Boolean allowEmail;
public Boolean allowHttpPassword;
public Boolean allowOwner;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetSshKeys.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetSshKeys.java
index 4ee551b..60dfc15 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetSshKeys.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetSshKeys.java
@@ -36,8 +36,7 @@
@Override
public List<SshKeyInfo> apply(ServiceUserResource rsrc)
- throws AuthException, RepositoryNotFoundException, IOException,
- ConfigInvalidException {
+ throws AuthException, RepositoryNotFoundException, IOException, ConfigInvalidException {
return getSshKeys.get().apply(rsrc.getUser());
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/HttpModule.java
new file mode 100644
index 0000000..a99987b
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/HttpModule.java
@@ -0,0 +1,28 @@
+// 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.googlesource.gerrit.plugins.serviceuser;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
+import com.google.inject.AbstractModule;
+
+public class HttpModule extends AbstractModule {
+ @Override
+ protected void configure() {
+ DynamicSet.bind(binder(), WebUiPlugin.class)
+ .toInstance(new JavaScriptPlugin("gr-serviceuser.html"));
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java
index 8d42071..0b93721 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java
@@ -67,8 +67,7 @@
@Override
public Map<String, ServiceUserInfo> apply(ConfigResource rscr)
- throws IOException, RestApiException, PermissionBackendException,
- ConfigInvalidException {
+ throws IOException, RestApiException, PermissionBackendException, ConfigInvalidException {
ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
CurrentUser user = userProvider.get();
if (user == null || !user.isIdentifiedUser()) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
index ca6f8f0..56d14e1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/Module.java
@@ -24,7 +24,6 @@
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.RestApiModule;
-import com.google.gerrit.extensions.webui.TopMenu;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.inject.AbstractModule;
import com.google.inject.assistedinject.FactoryModuleBuilder;
@@ -71,5 +70,6 @@
delete(SERVICE_USER_KIND, "owner").to(PutOwner.class);
}
});
+ install(new HttpModule());
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
index 8b45918..09a559e 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutConfig.java
@@ -40,6 +40,8 @@
@Singleton
class PutConfig implements RestModifyView<ConfigResource, Input> {
public static class Input {
+ public String info;
+ public String onSuccess;
public Boolean allowEmail;
public Boolean allowHttpPassword;
public Boolean allowOwner;
@@ -71,6 +73,12 @@
throws IOException, ConfigInvalidException, UnprocessableEntityException {
FileBasedConfig cfg = new FileBasedConfig(sitePaths.gerrit_config.toFile(), FS.DETECTED);
cfg.load();
+ if (input.info != null) {
+ cfg.setString("plugin", pluginName, "infoMessage", Strings.emptyToNull(input.info));
+ }
+ if (input.onSuccess != null) {
+ cfg.setString("plugin", pluginName, "onSuccessMessage", Strings.emptyToNull(input.onSuccess));
+ }
if (input.allowEmail != null) {
setBoolean(cfg, "allowEmail", input.allowEmail);
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java
index 53666bc..986b2bb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java
@@ -72,8 +72,8 @@
@Override
public Response<?> apply(ServiceUserResource rsrc, Input input)
- throws ConfigInvalidException, EmailException, IOException,
- PermissionBackendException, RestApiException {
+ throws ConfigInvalidException, EmailException, IOException, PermissionBackendException,
+ RestApiException {
Boolean emailAllowed = getConfig.get().apply(new ConfigResource()).allowEmail;
if ((emailAllowed == null || !emailAllowed)) {
permissionBackend.user(self.get()).check(ADMINISTRATE_SERVER);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java
index 7cd1c0d..066898f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java
@@ -78,8 +78,8 @@
@Override
public ServiceUserResource parse(ConfigResource parent, IdString id)
- throws ResourceNotFoundException, AuthException, IOException,
- PermissionBackendException, ConfigInvalidException {
+ throws ResourceNotFoundException, AuthException, IOException, PermissionBackendException,
+ ConfigInvalidException {
ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
IdentifiedUser serviceUser = accounts.get().parse(TopLevelResource.INSTANCE, id).getUser();
if (serviceUser == null
@@ -91,7 +91,8 @@
throw new AuthException("Authentication required");
}
if (!permissionBackend.user(user).testOrFalse(ADMINISTRATE_SERVER)) {
- String owner = storage.get().getString(USER, id.get(), KEY_OWNER);
+ String username = serviceUser.getUserName().get();
+ String owner = storage.get().getString(USER, username, KEY_OWNER);
if (owner != null) {
GroupDescription.Basic group =
groups.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(owner)).getGroup();
@@ -100,7 +101,7 @@
}
} else if (!((IdentifiedUser) user)
.getAccountId()
- .equals(new Account.Id(storage.get().getInt(USER, id.get(), KEY_CREATOR_ID, -1)))) {
+ .equals(new Account.Id(storage.get().getInt(USER, username, KEY_CREATOR_ID, -1)))) {
throw new ResourceNotFoundException(id);
}
}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshKeyValidator.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshKeyValidator.java
new file mode 100644
index 0000000..9a95db1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshKeyValidator.java
@@ -0,0 +1,51 @@
+// 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.googlesource.gerrit.plugins.serviceuser;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+class SshKeyValidator {
+
+ private static final String OPENSSH_KEY_PREFIXES[] = {
+ "ssh-ed25519", "ssh-rsa", "ssh-dss", "ecdsa-sha2-"
+ };
+ private static final Pattern RFC_KEY_FORMAT_PATTERN =
+ Pattern.compile(
+ "(?s)^-{4,5}\\s?BEGIN.* PUBLIC KEY\\s?-{4,5}.+-{4,5}\\s?END.* PUBLIC KEY\\s?-{4,5}$");
+
+ static boolean validateFormat(String sshKey) {
+ if (validateRfcFormat(sshKey)) {
+ return true;
+ }
+
+ return validateOpenSshFormat(sshKey);
+ }
+
+ private static boolean validateOpenSshFormat(String sshKey) {
+ for (String prefix : OPENSSH_KEY_PREFIXES) {
+ if (sshKey.startsWith(prefix)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean validateRfcFormat(String sshKey) {
+ Matcher matcher = RFC_KEY_FORMAT_PATTERN.matcher(sshKey);
+ return matcher.find();
+ }
+}
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 0793bb6..f52421e 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -22,6 +22,16 @@
should be automatically added. Multiple groups can be specified by
having multiple `plugin.@PLUGIN@.group` entries.
+<a id="infoMessage">
+`plugin.@PLUGIN@.infoMessage`
+: HTML formatted message that should be displayed on the service user
+ creation screen.
+
+<a id="onSuccessMessage">
+`plugin.@PLUGIN@.onSuccessMessage`
+: Message that should be displayed after a service user was
+ successfully created.
+
<a id="allowEmail">
`plugin.@PLUGIN@.allowEmail`
: Whether it is allowed for service user owners to set email
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index 8497fcd..3999e60 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -704,6 +704,10 @@
The `ConfigInfo` entity contains the configuration of the @PLUGIN@
plugin.
+* _info_: HTML formatted message that should be displayed on the
+ service user creation screen.
+* _on\_success_: Message that should be displayed after a service user
+ was successfully created.
* _allow\_email_: Whether it is allowed to provide an email address for
a service user (not set if `false`).
* _allow\_http\_password_: Whether it is allowed to generate an HTTP
@@ -727,6 +731,10 @@
The `ConfigInput` entity contains updates for the configuration of the
@PLUGIN@ plugin.
+* _info_: HTML formatted message that should be displayed on the
+ service user creation screen.
+* _on\_success_: Message that should be displayed after a service user
+ was successfully created.
* _allow\_email_: Whether it is allowed to provide an email address for
a service user (not set if `false`).
* _allow\_http\_password_: Whether it is allowed to generate an HTTP
diff --git a/src/main/resources/static/gr-serviceuser-create.html b/src/main/resources/static/gr-serviceuser-create.html
new file mode 100644
index 0000000..b6b71fe
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-create.html
@@ -0,0 +1,78 @@
+<!--
+@license
+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.
+-->
+
+<dom-module id="gr-serviceuser-create">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="gr-subpage-styles"></style>
+ <style include="gr-form-styles"></style>
+ <style>
+ main {
+ margin: 2em auto;
+ max-width: 50em;
+ }
+ </style>
+ <main class="gr-form-styles read-only">
+ <div class="topHeader">
+ <h2>Create Service User</h2>
+ </div>
+ <fieldset id="infoMessage"
+ hidden$="[[!_infoMessageEnabled]]">
+ </fieldset>
+ <fieldset>
+ <section>
+ <span class="title">Username</span>
+ <span class="value">
+ <input id="serviceUserNameInput"
+ bind-value="{{_newUsername}}"
+ is="iron-input"
+ type="text"
+ on-keyup="_validateData">
+ </span>
+ </section>
+ <section hidden$="[[!_emailEnabled]]">
+ <span class="title">Email</span>
+ <span class="value">
+ <input id="serviceUserEmailInput"
+ bind-value="{{_newEmail}}"
+ is="iron-input"
+ type="text"
+ on-keyup="_validateData">
+ </span>
+ </section>
+ </fieldset>
+ <fieldset>
+ <section>
+ <span class="title">Public SSH key</span>
+ <span class="value">
+ <iron-autogrow-textarea id="newKey"
+ bind-value="{{_newKey}}"
+ placeholder="New SSH Key"
+ on-keyup="_validateData">
+ </iron-autogrow-textarea>
+ </span>
+ </section>
+ </fieldset>
+ <gr-button id="createButton"
+ on-tap="_handleCreateServiceUser"
+ disabled="[[!_enableButton]]">
+ Create
+ </gr-button>
+ </main>
+ </template>
+ <script src="gr-serviceuser-create.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-create.js b/src/main/resources/static/gr-serviceuser-create.js
new file mode 100644
index 0000000..bc04de5
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-create.js
@@ -0,0 +1,142 @@
+/**
+ * @license
+ * Copyright (C) 2016 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.
+ */
+
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-serviceuser-create',
+ _legacyUndefinedCheck: true,
+
+ properties: {
+ _infoMessageEnabled: {
+ type: Boolean,
+ value: false,
+ },
+ _infoMessage: String,
+ _successMessageEnabled: {
+ type: Boolean,
+ value: false,
+ },
+ _successMessage: String,
+ _newUsername: String,
+ _emailEnabled: {
+ type: Boolean,
+ value: false,
+ },
+ _newEmail: String,
+ _newKey: String,
+ _dataValid: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdding: {
+ type: Boolean,
+ value: false,
+ },
+ _enableButton: {
+ type: Boolean,
+ value: false,
+ },
+ },
+
+ attached() {
+ this._getConfig();
+ },
+
+ _getConfig() {
+ return this.plugin.restApi('/config/server/serviceuser~config/').get('')
+ .then(config => {
+ if (!config) {
+ return;
+ }
+
+ if (config.info && config.info != '') {
+ this._infoMessageEnabled = true;
+ this._infoMessage = config.info;
+ this.$.infoMessage.innerHTML = this._infoMessage;
+ }
+
+ if (config.on_success && config.on_success != '') {
+ this._successMessageEnabled = true;
+ this._successMessage = config.on_success;
+ }
+
+ this._emailEnabled = config.allow_email;
+ });
+ },
+
+ _validateData() {
+ this._dataValid = this._validateName(this._newUsername)
+ && this._validateEmail(this._newEmail)
+ && this._validateKey(this._newKey);
+ this._computeButtonEnabled();
+ },
+
+ _validateName(username) {
+ if (username && username.trim().length > 0) {
+ return true;
+ }
+
+ return false;
+ },
+
+ _validateEmail(email) {
+ if (!email || email.trim().length == 0 || email.includes('@')) {
+ return true;
+ }
+
+ return false;
+ },
+
+ _validateKey(key) {
+ if (!key || !key.trim()) {
+ return false;
+ }
+
+ return true;
+ },
+
+ _computeButtonEnabled() {
+ this._enableButton = this._dataValid && !this._isAdding;
+ },
+
+ _handleCreateServiceUser() {
+ this._isAdding = true;
+ this._computeButtonEnabled();
+ const body = {
+ ssh_key: this._newKey.trim(),
+ email: this._newEmail ? this._newEmail.trim() : null,
+ };
+ return this.plugin.restApi('/config/server/serviceuser~serviceusers/')
+ .post(this._newUsername, body)
+ .then(response => {
+ if (this._successMessage) {
+ this.fire('show-alert', {message: this._successMessage});
+ }
+ page.show(
+ this.plugin.screenUrl()
+ + '/user/'
+ + response._account_id);
+ }).catch(response => {
+ this.fire('show-error', {message: response});
+ this._isAdding = false;
+ this._computeButtonEnabled();
+ });
+ },
+ });
+})();
diff --git a/src/main/resources/static/gr-serviceuser-detail.html b/src/main/resources/static/gr-serviceuser-detail.html
new file mode 100644
index 0000000..8dedf23
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-detail.html
@@ -0,0 +1,151 @@
+<!--
+@license
+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.
+-->
+
+<link rel="import" href="./gr-serviceuser-ssh-panel.html">
+<link rel="import" href="./gr-serviceuser-http-password.html">
+
+<dom-module id="gr-serviceuser-detail">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="gr-subpage-styles"></style>
+ <style include="gr-form-styles"></style>
+ <style>
+ div.serviceuser-detail {
+ margin: 2em auto;
+ max-width: 50em;
+ }
+
+ h1#Title {
+ margin-bottom: 1em;
+ }
+
+ p#ownerChangeWarning {
+ margin-top: 1em;
+ margin-bottom: 1em;
+ }
+
+ span#gr_serviceuser_activity {
+ border-radius: 1em;
+ width: 10em;
+ padding: 0.3em;
+ font-weight: bold;
+ text-align: center;
+ }
+
+ span.Active {
+ background-color: #9fcc6b;
+ }
+
+ span.Inactive {
+ background-color: #f7a1ad;
+ }
+ </style>
+ <div class="serviceuser-detail">
+ <main class="gr-form-styles read-only">
+ <div id="loading"
+ class$="[[_computeLoadingClass(_loading)]]">
+ Loading...
+ </div>
+ <div id="loadedContent"
+ class$="[[_computeLoadingClass(_loading)]]">
+ <h1 id="Title">Service User "[[_serviceUser.name]]"</h1>
+ <div id="form">
+ <fieldset>
+ <fieldset>
+ <h2 id="accountState">Account State</h2>
+ <section>
+ <span class="title">Current State</span>
+ <span id="gr_serviceuser_activity"
+ class$="value [[_active(_serviceUser)]]">
+ [[_active(_serviceUser)]]
+ </span>
+ </section>
+ <gr-button id="statusToggleButton" on-tap="_toggleStatus" disabled="[[_loading]]">
+ [[_statusButtonText]]</gr-button>
+ </fieldset>
+ <fieldset>
+ <h2 id="userDataHeader">User Data</h2>
+ <section>
+ <span class="title">Username</span>
+ <span class="value">[[_serviceUser.username]]</span>
+ </section>
+ <section>
+ <span class="title">Full Name</span>
+ <span class="value">
+ <input id="serviceUserFullNameInput" bind-value="{{_newFullName}}" is="iron-input" type="text"
+ disabled="[[_changingPrefs]]" placeholder$="[[_serviceUser.name]]"
+ on-keyup="_computePrefsChanged">
+ </span>
+ </section>
+ <section>
+ <span class="title">Email Address</span>
+ <input id="serviceUserEmailInput" bind-value="{{_newEmail}}" is="iron-input" type="text"
+ disabled="[[_changingPrefs]]" placeholder="[[_serviceUser.email]]" on-keyup="_computePrefsChanged"
+ hidden$="[[!_allowEmail]]">
+ <span class="value" hidden$="[[_allowEmail]]">[[_serviceUser.email]]</span>
+ </section>
+ <section>
+ <span class="title">Owner Group</span>
+ <gr-autocomplete id="serviceUserOwnerInput" text="{{_getOwnerGroup(_serviceUser)}}"
+ value="{{_newOwner}}" query="[[_query]]" disabled="[[_changingPrefs]]"
+ on-commit="_computePrefsChanged" on-keyup="_computePrefsChanged" hidden$="[[!_allowOwner]]">
+ [[_getOwnerGroup(_serviceUser)]]
+ </gr-autocomplete>
+ <span class="value" hidden$="[[_allowOwner]]">[[_getOwnerGroup(_serviceUser)]]</span>
+ </section>
+ <p id="ownerChangeWarning" class="style-scope gr-settings-view" hidden$="[[!_newOwner]]">
+ [[_ownerChangeWarning]]
+ </p>
+ <gr-button id="savePrefs" on-tap="_handleSavePreferences" disabled="[[!_prefsChanged]]">
+ Save changes
+ </gr-button>
+ </fieldset>
+ <fieldset>
+ <h2 id="creationHeader">Creation</h2>
+ <section>
+ <span class="title">Created By</span>
+ <span class="value">[[_getCreator(_serviceUser)]]</span>
+ </section>
+ <section>
+ <span class="title">Created At</span>
+ <span class="value">[[_serviceUser.created_at]]</span>
+ </section>
+ </fieldset>
+ <fieldset>
+ <fieldset>
+ <h2 id="credentialsHeader">Credentials</h2>
+ </fieldset>
+ <fieldset hidden$="[[!_allowHttpPassword]]">
+ <h3 id="HTTPCredentials">HTTP Credentials</h3>
+ <fieldset>
+ <gr-serviceuser-http-password id="httpPass">
+ </gr-http-password>
+ </fieldset>
+ </fieldset>
+ <fieldset>
+ <h3 id="SSHKeys">SSH keys</h3>
+ <gr-serviceuser-ssh-panel id="sshEditor"></gr-serviceuser-ssh-panel>
+ </fieldset>
+ </fieldset>
+ </fieldset>
+ </div>
+ </div>
+ </main>
+ </div>
+ </template>
+ <script src="gr-serviceuser-detail.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-detail.js b/src/main/resources/static/gr-serviceuser-detail.js
new file mode 100644
index 0000000..c2ea3e5
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-detail.js
@@ -0,0 +1,343 @@
+// 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.
+
+(function() {
+ 'use strict';
+
+ const NOT_FOUND_MESSAGE = 'Not Found';
+
+ Polymer({
+ is: 'gr-serviceuser-detail',
+ _legacyUndefinedCheck: true,
+
+ properties: {
+ _restApi: Object,
+ _serviceUserId: String,
+ _serviceUser: Object,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ _statusButtonText: {
+ type: String,
+ value: 'Activate',
+ },
+ _prefsChanged: {
+ type: Boolean,
+ value: false,
+ },
+ _changingPrefs: {
+ type: Boolean,
+ value: false,
+ },
+ _isAdmin: {
+ type: Boolean,
+ value: false,
+ },
+ _allowEmail: {
+ type: Boolean,
+ value: false,
+ },
+ _allowOwner: {
+ type: Boolean,
+ value: false,
+ },
+ _allowHttpPassword: {
+ type: Boolean,
+ value: false,
+ },
+ _newFullName: String,
+ _newEmail: String,
+ _availableOwners: Array,
+ _newOwner: String,
+ _ownerChangeWarning: String,
+ _query: {
+ type: Function,
+ value() {
+ return this._getGroupSuggestions.bind(this);
+ },
+ },
+ },
+
+ behaviors: [
+ Gerrit.ListViewBehavior,
+ ],
+
+ attached() {
+ this._extractUserId();
+ this._loadServiceUser();
+ },
+
+ _loadServiceUser() {
+ if (!this._serviceUserId) { return; }
+
+ const promises = [];
+
+ promises.push(this._getPluginConfig());
+ promises.push(this._getServiceUser());
+
+ Promise.all(promises).then(() => {
+ this.$.sshEditor.loadData(this._restApi, this._serviceUser);
+ this.$.httpPass.loadData(this._restApi, this._serviceUser);
+
+ this.fire('title-change', {title: this._serviceUser.name});
+ this._computeStatusButtonText();
+ this._loading = false;
+ });
+ },
+
+ _computeLoadingClass(loading) {
+ return loading ? 'loading' : '';
+ },
+
+ _extractUserId() {
+ this._serviceUserId = this.baseURI.split('/').pop();
+ },
+
+ _getPermissions() {
+ return this.plugin.restApi('/accounts/self/capabilities/').get('')
+ .then(capabilities => {
+ this._isAdmin = capabilities && capabilities.administrateServer;
+ });
+ },
+
+ _getPluginConfig() {
+ return Promise.resolve(this._getPermissions()).then(() => {
+ this.plugin.restApi('/config/server/serviceuser~config/').get('')
+ .then(config => {
+ if (!config) {
+ return;
+ }
+ this._allowEmail = config.allow_email || this._isAdmin;
+ this._allowOwner = config.allow_owner || this._isAdmin;
+ this._allowHttpPassword = config.allow_http_password
+ || this._isAdmin;
+ });
+ });
+ },
+
+ _getServiceUser() {
+ this._restApi = this.plugin.restApi(
+ '/config/server/serviceuser~serviceusers/');
+ return this._restApi.get(this._serviceUserId)
+ .then(serviceUser => {
+ if (!serviceUser) {
+ this._serviceUser = {};
+ return;
+ }
+ this._serviceUser = serviceUser;
+ });
+ },
+
+ _active(serviceUser) {
+ if (!serviceUser) {
+ return NOT_FOUND_MESSAGE;
+ }
+
+ return serviceUser.inactive === true ? 'Inactive' : 'Active';
+ },
+
+ _computeStatusButtonText() {
+ if (!this._serviceUser) {
+ return;
+ }
+
+ this._statusButtonText = this._serviceUser.inactive === true
+ ? 'Activate'
+ : 'Deactivate';
+ },
+
+ _toggleStatus() {
+ if (this._serviceUser.inactive === true) {
+ this._restApi.put(`${this._serviceUser._account_id}/active`)
+ .then(() => {
+ this._loadServiceUser();
+ });
+ } else {
+ this._restApi.delete(`${this._serviceUser._account_id}/active`)
+ .then(() => {
+ this._loadServiceUser();
+ });
+ }
+ },
+
+ _getCreator(serviceUser) {
+ if (!serviceUser || !serviceUser.created_by) {
+ return NOT_FOUND_MESSAGE;
+ }
+
+ if (serviceUser.created_by.username != undefined) {
+ return serviceUser.created_by.username;
+ }
+
+ if (serviceUser.created_by._account_id != -1) {
+ return serviceUser.created_by._account_id;
+ }
+
+ return NOT_FOUND_MESSAGE;
+ },
+
+ _getOwnerGroup(serviceUser) {
+ return serviceUser && serviceUser.owner
+ ? serviceUser.owner.name
+ : NOT_FOUND_MESSAGE;
+ },
+
+ _isEmailValid(email) {
+ if (!email) {
+ return false;
+ }
+ return email.includes('@');
+ },
+
+ _getGroupSuggestions(input) {
+ let query;
+ if (!input || input === this._getOwnerGroup(this._serviceUser)) {
+ query = '';
+ } else {
+ query = `?suggest=${input}`;
+ }
+
+ return this.plugin.restApi('/a/groups/').get(query)
+ .then(response => {
+ const groups = [];
+ for (const key in response) {
+ if (!response.hasOwnProperty(key)) { continue; }
+ groups.push({
+ name: key,
+ value: decodeURIComponent(response[key].id),
+ });
+ }
+ this._availableOwners = groups;
+ return groups;
+ });
+ },
+
+ _isOwnerValid(owner) {
+ if (!owner) {
+ return false;
+ }
+
+ return this._getOwnerName(owner);
+ },
+
+ _isNewOwner() {
+ return this._getOwnerName(this._newOwner)
+ === this._getOwnerGroup(this._serviceUser);
+ },
+
+ _getOwnerName(id) {
+ return this._availableOwners.find(o => { return o.value === id; }).name;
+ },
+
+ _computeOwnerWarning() {
+ let message = 'If ';
+ message += this._getOwnerGroup(this._serviceUser) != NOT_FOUND_MESSAGE
+ ? 'the owner group is changed' : 'an owner group is set';
+ message += ' only members of the ';
+ message += this._getOwnerGroup(this._serviceUser) != NOT_FOUND_MESSAGE
+ ? 'new ' : '';
+ message += 'owner group can see and administrate the service user.';
+ message += this._getOwnerGroup(this._serviceUser) != NOT_FOUND_MESSAGE
+ ? '' : ' The creator of the service user can no'
+ + ' longer see and administrate the service user if she/he'
+ + ' is not member of the owner group.';
+ this._ownerChangeWarning = message;
+ },
+
+ _computePrefsChanged() {
+ if (this.loading || this._changingPrefs) {
+ return;
+ }
+
+ if (!this._newOwner && !this._newEmail && !this._newFullName) {
+ this._prefsChanged = false;
+ return;
+ }
+
+ if (this._newEmail && !this._isEmailValid(this._newEmail)) {
+ this._prefsChanged = false;
+ return;
+ }
+
+ if (this._newOwner
+ && (this._isNewOwner() || !this._isOwnerValid(this._newOwner))) {
+ this._prefsChanged = false;
+ return;
+ }
+
+ if (this._newOwner) {
+ this._computeOwnerWarning();
+ }
+
+ this._prefsChanged = true;
+ },
+
+ _applyNewFullName() {
+ return this._restApi
+ .put(`${this._serviceUser._account_id}/name`,
+ {name: this._newFullName})
+ .then(() => {
+ this.$.serviceUserFullNameInput.value = '';
+ });
+ },
+
+ _applyNewEmail(email) {
+ if (!this._isEmailValid(email)) {
+ return;
+ }
+ return this._restApi
+ .put(`${this._serviceUser._account_id}/email`, {email})
+ .then(() => {
+ this.$.serviceUserEmailInput.value = '';
+ });
+ },
+
+ _applyNewOwner(owner) {
+ if (this._isNewOwner() || !this._isOwnerValid(this._newOwner)) {
+ return;
+ }
+ return this._restApi
+ .put(`${this._serviceUser._account_id}/owner`, {group: owner})
+ .then(() => {
+ this.$.serviceUserOwnerInput.text = this._getOwnerGroup(
+ this._serviceUser);
+ });
+ },
+
+ _handleSavePreferences() {
+ const promises = [];
+ this._changingPrefs = true;
+
+ if (this._newFullName) {
+ promises.push(this._applyNewFullName());
+ }
+
+ if (this._newEmail) {
+ promises.push(this._applyNewEmail(this._newEmail));
+ }
+
+ if (this._newOwner) {
+ promises.push(this._applyNewOwner(this._newOwner));
+ }
+
+ Promise.all(promises).then(() => {
+ this._changingPrefs = false;
+ this._prefsChanged = false;
+ this._ownerChangeWarning = '';
+ this._loadServiceUser();
+ });
+ },
+ });
+})();
diff --git a/src/main/resources/static/gr-serviceuser-http-password.html b/src/main/resources/static/gr-serviceuser-http-password.html
new file mode 100644
index 0000000..f2f5b3e
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-http-password.html
@@ -0,0 +1,81 @@
+<!--
+@license
+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.
+-->
+
+<dom-module id="gr-serviceuser-http-password">
+ <template>
+ <style include="shared-styles">
+ .password {
+ font-family: var(--monospace-font-family);
+ }
+
+ #generatedPasswordOverlay {
+ padding: 2em;
+ width: 50em;
+ }
+
+ #generatedPasswordDisplay {
+ margin: 1em 0;
+ }
+
+ #generatedPasswordDisplay .value {
+ font-family: var(--monospace-font-family);
+ }
+
+ #passwordWarning {
+ font-style: italic;
+ text-align: center;
+ }
+
+ .closeButton {
+ bottom: 2em;
+ position: absolute;
+ right: 2em;
+ }
+ </style>
+ <style include="gr-form-styles"></style>
+ <div class="gr-form-styles">
+ <div>
+ <section>
+ <span class="title">Username</span>
+ <span class="value">[[_serviceUser.username]]</span>
+ </section>
+ <gr-button id="generateButton"
+ on-tap="_handleGenerateTap">Generate new password</gr-button>
+ <gr-button id="deleteButton"
+ on-tap="_handleDelete">Delete password</gr-button>
+ </div>
+ </div>
+ <gr-overlay id="generatedPasswordOverlay"
+ on-iron-overlay-closed="_generatedPasswordOverlayClosed"
+ with-backdrop>
+ <div class="gr-form-styles">
+ <section id="generatedPasswordDisplay">
+ <span class="title">New Password:</span>
+ <span class="value">[[_generatedPassword]]</span>
+ </section>
+ <section id="passwordWarning">
+ This password will not be displayed again.<br>
+ If you lose it, you will need to generate a new one.
+ </section>
+ <gr-button link
+ class="closeButton"
+ on-tap="_closeOverlay">Close</gr-button>
+ </div>
+ </gr-overlay>
+ </template>
+ <script src="gr-serviceuser-http-password.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-http-password.js b/src/main/resources/static/gr-serviceuser-http-password.js
new file mode 100644
index 0000000..87e1543
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-http-password.js
@@ -0,0 +1,60 @@
+/**
+ * @license
+ * 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.
+ */
+
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-serviceuser-http-password',
+ _legacyUndefinedCheck: true,
+
+ properties: {
+ _restApi: Object,
+ _serviceUser: Object,
+ _generatedPassword: String,
+ _passwordUrl: String,
+ },
+
+ loadData(restApi, serviceUser) {
+ this._restApi = restApi;
+ this._serviceUser = serviceUser;
+ },
+
+ _handleGenerateTap() {
+ this._generatedPassword = 'Generating...';
+ this.$.generatedPasswordOverlay.open();
+ this._restApi
+ .put(`${this._serviceUser._account_id}/password.http`,
+ {generate: true})
+ .then(newPassword => {
+ this._generatedPassword = newPassword;
+ });
+ },
+
+ _closeOverlay() {
+ this.$.generatedPasswordOverlay.close();
+ },
+
+ _generatedPasswordOverlayClosed() {
+ this._generatedPassword = '';
+ },
+
+ _handleDelete() {
+ this._restApi.delete(`${this._serviceUser._account_id}/password.http`);
+ },
+ });
+})();
diff --git a/src/main/resources/static/gr-serviceuser-list.html b/src/main/resources/static/gr-serviceuser-list.html
new file mode 100644
index 0000000..052e9f4
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-list.html
@@ -0,0 +1,84 @@
+<!--
+@license
+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.
+-->
+
+<dom-module id="gr-serviceuser-list">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="gr-table-styles"></style>
+ <style>
+ .topHeader {
+ padding: 8px;
+ }
+
+ #topContainer {
+ align-items: center;
+ display: flex;
+ height: 3rem;
+ justify-content: space-between;
+ margin: 0 1em;
+ }
+ </style>
+ <div class="topHeader">
+ <h2>Service Users</h2>
+ </div>
+ <div id="topContainer">
+ <div></div>
+ <div id="createNewContainer"
+ class$="[[_computeCreateClass(createNew)]]">
+ <gr-button primary
+ link
+ id="createNew"
+ on-tap="_createNewServiceUser">
+ Create New
+ </gr-button>
+ </div>
+ </div>
+ <table id="list"
+ class="genericList">
+ <tr class="headerRow">
+ <th class="name topHeader">Username</th>
+ <th class="fullName topHeader">Full Name</th>
+ <th class="email topHeader">Email</th>
+ <th class="owner topHeader">Owner</th>
+ <th class="createdBy topHeader">Created By</th>
+ <th class="createdAt topHeader">Created At</th>
+ <th class="accountState topHeader">Account State</th>
+ </tr>
+ <tr id="loading"
+ class$="loadingMsg [[computeLoadingClass(_loading)]]">
+ <td>Loading...</td>
+ </tr>
+ <tbody class$="[[computeLoadingClass(_loading)]]">
+ <template is="dom-repeat"
+ items="[[_serviceUsers]]">
+ <tr class="table">
+ <td class="name">
+ <a href$="[[_computeServiceUserUrl(item._account_id)]]">[[item.username]]</a>
+ </td>
+ <td class="fullName">[[item.name]]</td>
+ <td class="email">[[item.email]]</td>
+ <td class="owner">[[_getOwnerGroup(item)]]</td>
+ <td class="createdBy">[[_getCreator(item)]]</td>
+ <td class="createdAt">[[item.created_at]]</td>
+ <td class="accountState">[[_active(item)]]</td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ </template>
+ <script src="gr-serviceuser-list.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-list.js b/src/main/resources/static/gr-serviceuser-list.js
new file mode 100644
index 0000000..8f35dd4
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-list.js
@@ -0,0 +1,95 @@
+// 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.
+
+(function() {
+ 'use strict';
+
+ const NOT_FOUND_MESSAGE = 'Not Found';
+
+ Polymer({
+ is: 'gr-serviceuser-list',
+ _legacyUndefinedCheck: true,
+
+ properties: {
+ _serviceUsers: Array,
+ _loading: {
+ type: Boolean,
+ value: true,
+ },
+ },
+
+ behaviors: [
+ Gerrit.ListViewBehavior,
+ ],
+
+ attached() {
+ this.fire('title-change', {title: 'Service Users'});
+ this._getServiceUsers();
+ },
+
+ _getServiceUsers() {
+ return this.plugin.restApi('/config/server/serviceuser~serviceusers/')
+ .get('')
+ .then(serviceUsers => {
+ if (!serviceUsers) {
+ this._serviceUsers = [];
+ return;
+ }
+ this._serviceUsers = Object.keys(serviceUsers)
+ .map(key => {
+ const serviceUser = serviceUsers[key];
+ serviceUser.username = key;
+ return serviceUser;
+ });
+ this._loading = false;
+ });
+ },
+
+ _active(item) {
+ if (!item) {
+ return NOT_FOUND_MESSAGE;
+ }
+
+ return item.inactive === true ? 'Inactive' : 'Active';
+ },
+
+ _getCreator(item) {
+ if (!item || !item.created_by) {
+ return NOT_FOUND_MESSAGE;
+ }
+
+ if (item.created_by.username != undefined) {
+ return item.created_by.username;
+ }
+
+ if (item.created_by._account_id != -1) {
+ return item.created_by._account_id;
+ }
+
+ return NOT_FOUND_MESSAGE;
+ },
+
+ _getOwnerGroup(item) {
+ return item && item.owner ? item.owner.name : NOT_FOUND_MESSAGE;
+ },
+
+ _computeServiceUserUrl(id) {
+ return `${this.plugin.screenUrl()}/user/${id}`;
+ },
+
+ _createNewServiceUser() {
+ page.show(this.plugin.screenUrl() + '/create');
+ },
+ });
+})();
diff --git a/src/main/resources/static/gr-serviceuser-ssh-panel.html b/src/main/resources/static/gr-serviceuser-ssh-panel.html
new file mode 100644
index 0000000..b7ed896
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-ssh-panel.html
@@ -0,0 +1,132 @@
+<!--
+@license
+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.
+-->
+
+<dom-module id="gr-serviceuser-ssh-panel">
+ <template>
+ <style include="shared-styles"></style>
+ <style include="gr-form-styles">
+ .statusHeader {
+ width: 4em;
+ }
+
+ .keyHeader {
+ width: 7.5em;
+ }
+
+ #viewKeyOverlay {
+ padding: 2em;
+ width: 50em;
+ }
+
+ .publicKey {
+ font-family: var(--monospace-font-family);
+ overflow-x: scroll;
+ overflow-wrap: break-word;
+ width: 30em;
+ }
+
+ .closeButton {
+ bottom: 2em;
+ position: absolute;
+ right: 2em;
+ }
+
+ #existing {
+ margin-bottom: 1em;
+ }
+
+ #existing .commentColumn {
+ min-width: 27em;
+ width: auto;
+ }
+ </style>
+ <div class="gr-form-styles">
+ <fieldset id="existing">
+ <table>
+ <thead>
+ <tr>
+ <th class="commentColumn">Comment</th>
+ <th class="statusHeader">Status</th>
+ <th class="keyHeader">Public key</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template is="dom-repeat"
+ items="[[_keys]]"
+ as="key">
+ <tr>
+ <td class="commentColumn">[[key.comment]]</td>
+ <td>[[_getStatusLabel(key.valid)]]</td>
+ <td>
+ <gr-button link
+ on-tap="_showKey"
+ data-index$="[[index]]"
+ link>Click to View</gr-button>
+ </td>
+ <td>
+ <gr-button link
+ data-index$="[[index]]"
+ on-tap="_handleDeleteKey">Delete</gr-button>
+ </td>
+ </tr>
+ </template>
+ </tbody>
+ </table>
+ <gr-overlay id="viewKeyOverlay"
+ with-backdrop>
+ <fieldset>
+ <section>
+ <span class="title">Algorithm</span>
+ <span class="value">[[_keyToView.algorithm]]</span>
+ </section>
+ <section>
+ <span class="title">Public key</span>
+ <span class="value publicKey">[[_keyToView.encoded_key]]</span>
+ </section>
+ <section>
+ <span class="title">Comment</span>
+ <span class="value">[[_keyToView.comment]]</span>
+ </section>
+ </fieldset>
+ <gr-button class="closeButton"
+ on-tap="_closeOverlay">Close</gr-button>
+ </gr-overlay>
+ </fieldset>
+ <fieldset>
+ <section>
+ <span class="title">New SSH key</span>
+ <span class="value">
+ <iron-autogrow-textarea id="newKey"
+ autocomplete="on"
+ bind-value="{{_newKey}}"
+ placeholder="New SSH Key">
+ </iron-autogrow-textarea>
+ </span>
+ </section>
+ <gr-button id="addButton"
+ link
+ disabled$="[[_computeAddButtonDisabled(_newKey)]]"
+ on-tap="_handleAddKey">
+ Add new SSH key
+ </gr-button>
+ </fieldset>
+ </div>
+ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+ </template>
+ <script src="gr-serviceuser-ssh-panel.js"></script>
+</dom-module>
diff --git a/src/main/resources/static/gr-serviceuser-ssh-panel.js b/src/main/resources/static/gr-serviceuser-ssh-panel.js
new file mode 100644
index 0000000..e1e8dd0
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-ssh-panel.js
@@ -0,0 +1,101 @@
+/**
+ * @license
+ * Copyright (C) 2016 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.
+ */
+
+(function() {
+ 'use strict';
+
+ Polymer({
+ is: 'gr-serviceuser-ssh-panel',
+ _legacyUndefinedCheck: true,
+
+ properties: {
+ _restApi: Object,
+ _serviceUser: Object,
+ _keys: Array,
+ /** @type {?} */
+ _keyToView: Object,
+ _newKey: {
+ type: String,
+ value: '',
+ },
+ _keysToRemove: {
+ type: Array,
+ value() { return []; },
+ },
+ },
+
+ loadData(restApi, serviceUser) {
+ this._restApi = restApi;
+ this._serviceUser = serviceUser;
+ return this._restApi.get(`${this._serviceUser._account_id}/sshkeys`)
+ .then(keys => {
+ if (!keys) {
+ this._keys = [];
+ return;
+ }
+ this._keys = keys;
+ });
+ },
+
+ _getStatusLabel(isValid) {
+ return isValid ? 'Valid' : 'Invalid';
+ },
+
+ _showKey(e) {
+ const el = Polymer.dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this._keyToView = this._keys[index];
+ this.$.viewKeyOverlay.open();
+ },
+
+ _closeOverlay() {
+ this.$.viewKeyOverlay.close();
+ },
+
+ _handleDeleteKey(e) {
+ const el = Polymer.dom(e).localTarget;
+ const index = parseInt(el.getAttribute('data-index'), 10);
+ this.push('_keysToRemove', this._keys[index]);
+
+ const promises = this._keysToRemove.map(key => {
+ this._restApi.delete(`${this._serviceUser._account_id}/sshkeys/${key.seq}`);
+ });
+
+ return Promise.all(promises).then(() => {
+ this.splice('_keys', index, 1);
+ this._keysToRemove = [];
+ });
+ },
+
+ _handleAddKey() {
+ this.$.addButton.disabled = true;
+ this.$.newKey.disabled = true;
+ return this._restApi.post(`${this._serviceUser._account_id}/sshkeys`,
+ this._newKey.trim(), null, 'plain/text')
+ .then(key => {
+ this.push('_keys', key);
+ }).catch(() => {
+ this.$.addButton.disabled = false;
+ this.$.newKey.disabled = false;
+ });
+ },
+
+ _computeAddButtonDisabled(newKey) {
+ return !newKey.length;
+ },
+ });
+})();
diff --git a/src/main/resources/static/gr-serviceuser.html b/src/main/resources/static/gr-serviceuser.html
new file mode 100644
index 0000000..92714a2
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser.html
@@ -0,0 +1,46 @@
+<!--
+@license
+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.
+-->
+
+
+<link rel="import"
+ href="./gr-serviceuser-list.html">
+<link rel="import"
+ href="./gr-serviceuser-detail.html">
+<link rel="import"
+ href="./gr-serviceuser-create.html">
+
+<dom-module id="gr-serviceuser">
+ <script>
+ Gerrit.install(plugin => {
+ plugin.restApi('/accounts/self/capabilities/').get('')
+ .then(capabilities => {
+ if (capabilities
+ && (capabilities.administrateServer
+ || capabilities['serviceuser-createServiceUser'])) {
+ plugin.screen('list', 'gr-serviceuser-list');
+ plugin.screen('user', 'gr-serviceuser-detail');
+ plugin.screen('create', 'gr-serviceuser-create');
+ }
+ plugin.admin()
+ .addMenuLink(
+ 'Service Users',
+ '/x/serviceuser/list',
+ 'serviceuser-createServiceUser');
+ });
+ });
+ </script>
+</dom-module>
diff --git a/src/test/java/com/googlesource/gerrit/plugins/serviceuser/SshKeyValidatorTest.java b/src/test/java/com/googlesource/gerrit/plugins/serviceuser/SshKeyValidatorTest.java
new file mode 100644
index 0000000..619ab98
--- /dev/null
+++ b/src/test/java/com/googlesource/gerrit/plugins/serviceuser/SshKeyValidatorTest.java
@@ -0,0 +1,60 @@
+// 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.googlesource.gerrit.plugins.serviceuser;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class SshKeyValidatorTest {
+
+ private final String[] VALID_PUBLIC_KEYS = {
+ "---- BEGIN SSH2 PUBLIC KEY ----\n"
+ + " Comment: comment\n"
+ + " AAAAB3NzaC1\n"
+ + " ---- END SSH2 PUBLIC KEY ----",
+ "---- BEGIN PUBLIC KEY ----\n"
+ + " Comment: comment\n"
+ + " AAAAB3NzaC1\n"
+ + " ---- END PUBLIC KEY ----",
+ "-----BEGIN RSA PUBLIC KEY-----\nMIIBC\n-----END RSA PUBLIC KEY-----",
+ "ssh-rsa AAAAB3NzaC1",
+ "ssh-dss AAAAB3NzaC1",
+ "ssh-ed25519 AAAAB3NzaC1",
+ "ecdsa-sha2-nistp256 AAAAB3NzaC1"
+ };
+
+ private final String[] INVALID_PUBLIC_KEYS = {
+ "---- BEGIN SSH2 PUBLIC KEY ----\n Comment: comment\n AAAAB3NzaC1\n",
+ "-----BEGIN PRIVATE KEY-----\nMIIBC\n-----END PRIVATE KEY-----",
+ "AAAAB3NzaC1\n ---- END SSH2 PUBLIC KEY ----",
+ "",
+ "invalid key"
+ };
+
+ @Test
+ public void testValidateSshKeyFormat_Valid() {
+ for (String keyToTest : VALID_PUBLIC_KEYS) {
+ assertThat(SshKeyValidator.validateFormat(keyToTest)).isTrue();
+ }
+ }
+
+ @Test
+ public void testValidateSshKeyFormat_Invalid() {
+ for (String keyToTest : INVALID_PUBLIC_KEYS) {
+ assertThat(SshKeyValidator.validateFormat(keyToTest)).isFalse();
+ }
+ }
+}
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..240c448
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,5 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+ _junit_tests = "junit_tests",
+)
+junit_tests = _junit_tests
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 89a1643..4d2dbdd 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -2,7 +2,9 @@
"@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
_gerrit_plugin = "gerrit_plugin",
_plugin_deps = "PLUGIN_DEPS",
+ _plugin_test_deps = "PLUGIN_TEST_DEPS",
)
gerrit_plugin = _gerrit_plugin
PLUGIN_DEPS = _plugin_deps
+PLUGIN_TEST_DEPS = _plugin_test_deps