Merge branch 'stable-2.16' into stable-3.0

* stable-2.16:
  Upgrade bazlets to latest stable-2.16 to build with 2.16.19 API

Change-Id: Idfa92ce9ecd549ea03d065c2f56a80a46910d45f
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 f4c8d63..fcad0e5 100644
--- a/BUILD
+++ b/BUILD
@@ -1,14 +1,26 @@
-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",
     srcs = glob(["src/main/java/**/*.java"]),
-    gwt_module = "com.googlesource.gerrit.plugins.serviceuser.CreateServiceUserForm",
     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 c5a4925..1e57d3f 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "22b242638bd36017790d94ffcc18a57f223bda2f",
+    commit = "97eef86854b0fb68b9287831f3075b6f6218ff41",
     #local_path = "/home/<user>/projects/bazlets",
 )
 
@@ -24,10 +24,3 @@
 
 # Load release Plugin API
 gerrit_api()
-
-load(
-    "@com_googlesource_gerrit_bazlets//:gerrit_gwt.bzl",
-    "gerrit_gwt",
-)
-
-gerrit_gwt()
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/AddSshKey.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/AddSshKey.java
index 64a024d..c5ccadd 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/AddSshKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/AddSshKey.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -38,7 +37,7 @@
 
   @Override
   public Response<SshKeyInfo> apply(ServiceUserResource rsrc, SshKeyInput input)
-      throws AuthException, BadRequestException, OrmException, IOException, ConfigInvalidException {
+      throws AuthException, BadRequestException, IOException, ConfigInvalidException {
     return addSshKey.get().apply(rsrc.getUser(), input);
   }
 }
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 5b3ae51..80ddf1b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUser.java
@@ -42,7 +42,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.account.CreateAccount;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -123,8 +122,7 @@
   @Override
   public Response<ServiceUserInfo> apply(
       ConfigResource parentResource, IdString id, CreateServiceUser.Input input)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException,
-          PermissionBackendException {
+      throws RestApiException, IOException, ConfigInvalidException, PermissionBackendException {
     CurrentUser user = userProvider.get();
     if (user == null || !user.isIdentifiedUser()) {
       throw new AuthException("authentication required");
@@ -137,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 bebd520..3f27975 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserCommand.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserCommand.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -50,8 +49,7 @@
 
   @Override
   protected void run()
-      throws OrmException, 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/CreateServiceUserForm.gwt.xml b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml
deleted file mode 100644
index 9d921cb..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserForm.gwt.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
- Copyright (C) 2013 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.
--->
-<module rename-to="serviceuser">
-  <!-- Inherit the core Web Toolkit stuff.                        -->
-  <inherits name="com.google.gwt.user.User"/>
-  <!-- Other module inherits                                      -->
-  <inherits name="com.google.gerrit.Plugin"/>
-  <inherits name="com.google.gwt.http.HTTP"/>
-  <inherits name="com.google.gwt.json.JSON"/>
-  <inherits name='com.google.gwtexpui.clippy.Clippy'/>
-  <inherits name='com.google.gwtexpui.globalkey.GlobalKey'/>
-  <!-- Using GWT built-in themes adds a number of static          -->
-  <!-- resources to the plugin. No theme inherits lines were      -->
-  <!-- added in order to make this plugin as simple as possible   -->
-  <!-- Specify the app entry point class.                         -->
-  <entry-point class="com.googlesource.gerrit.plugins.serviceuser.client.ServiceUserPlugin"/>
-  <stylesheet src="serviceuser.css"/>
-</module>
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 c0e4c20..63cb875 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserNotes.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/CreateServiceUserNotes.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import com.googlesource.gerrit.plugins.serviceuser.GetServiceUser.ServiceUserInfo;
@@ -83,8 +82,7 @@
   }
 
   void createNotes(String branch, ObjectId oldObjectId, ObjectId newObjectId)
-      throws IOException, OrmException, ConfigInvalidException, PermissionBackendException,
-          RestApiException {
+      throws IOException, ConfigInvalidException, PermissionBackendException, RestApiException {
     if (ObjectId.zeroId().equals(newObjectId)) {
       return;
     }
@@ -156,13 +154,13 @@
   }
 
   private ObjectId createNoteContent(String branch, ServiceUserInfo serviceUser)
-      throws IOException, OrmException, MethodNotAllowedException, PermissionBackendException {
+      throws IOException, MethodNotAllowedException, PermissionBackendException {
     return getInserter()
         .insert(Constants.OBJ_BLOB, createServiceUserNote(branch, serviceUser).getBytes(UTF_8));
   }
 
   private String createServiceUserNote(String branch, ServiceUserInfo serviceUser)
-      throws OrmException, MethodNotAllowedException, PermissionBackendException {
+      throws MethodNotAllowedException, PermissionBackendException {
     HeaderFormatter fmt = new HeaderFormatter(gerritServerIdent.getTimeZone(), anonymousCowardName);
     fmt.appendDate();
     fmt.append("Project", project.get());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteActive.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteActive.java
index 5393e03..ec5d063 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteActive.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteActive.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -36,7 +35,7 @@
 
   @Override
   public Response<?> apply(ServiceUserResource rsrc, Input input)
-      throws RestApiException, OrmException, IOException, ConfigInvalidException {
+      throws RestApiException, IOException, ConfigInvalidException {
     return deleteActive.get().apply(rsrc, input);
   }
 }
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 9092e74..9ce83d3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/DeleteSshKey.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -39,8 +38,8 @@
 
   @Override
   public Response<?> apply(ServiceUserResource.SshKey rsrc, Input input)
-      throws OrmException, 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 28d7be3..85da7b0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetConfig.java
@@ -27,7 +27,6 @@
 import com.google.gerrit.server.group.InternalGroupDescription;
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.restapi.group.GroupJson;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.util.Arrays;
@@ -60,7 +59,7 @@
   }
 
   @Override
-  public ConfigInfo apply(ConfigResource rsrc) throws OrmException, PermissionBackendException {
+  public ConfigInfo apply(ConfigResource rsrc) throws PermissionBackendException {
     PluginConfig cfg = cfgFactory.getFromGerritConfig(pluginName);
     ConfigInfo info = new ConfigInfo();
     info.info = Strings.emptyToNull(cfg.getString("infoMessage"));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetOwner.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetOwner.java
index 47783f5..31c2eb6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetOwner.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetOwner.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.group.GroupJson;
 import com.google.gerrit.server.restapi.group.GroupsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -55,7 +54,7 @@
 
   @Override
   public Response<GroupInfo> apply(ServiceUserResource rsrc)
-      throws RestApiException, OrmException, PermissionBackendException {
+      throws RestApiException, PermissionBackendException {
     ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
     String owner = storage.get().getString(USER, rsrc.getUser().getUserName().get(), KEY_OWNER);
     if (owner != null) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
index a4ad837..3a2460f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetServiceUser.java
@@ -32,7 +32,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.account.GetAccount;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -62,7 +61,7 @@
 
   @Override
   public ServiceUserInfo apply(ServiceUserResource rsrc)
-      throws RestApiException, OrmException, PermissionBackendException {
+      throws RestApiException, PermissionBackendException {
     ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
     String username = rsrc.getUser().getUserName().get();
     Config db = storage.get();
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 92a9430..60dfc15 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetSshKeys.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/GetSshKeys.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.extensions.common.SshKeyInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -37,8 +36,7 @@
 
   @Override
   public List<SshKeyInfo> apply(ServiceUserResource rsrc)
-      throws AuthException, OrmException, 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
index 80d86e2..a99987b 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/HttpModule.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2013 The Android Open Source Project
+// 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.
@@ -15,14 +15,14 @@
 package com.googlesource.gerrit.plugins.serviceuser;
 
 import com.google.gerrit.extensions.registration.DynamicSet;
-import com.google.gerrit.extensions.webui.GwtPlugin;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
-import com.google.inject.servlet.ServletModule;
+import com.google.inject.AbstractModule;
 
-class HttpModule extends ServletModule {
-
+public class HttpModule extends AbstractModule {
   @Override
-  protected void configureServlets() {
-    DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new GwtPlugin("serviceuser"));
+  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 cf6ea40..0b93721 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ListServiceUsers.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectLevelConfig;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -68,8 +67,7 @@
 
   @Override
   public Map<String, ServiceUserInfo> apply(ConfigResource rscr)
-      throws OrmException, 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()) {
@@ -84,7 +82,12 @@
         ServiceUserInfo info;
         try {
           ServiceUserResource serviceUserResource =
-              serviceUsers.get().parse(new ConfigResource(), IdString.fromDecoded(username));
+              serviceUsers
+                  .get()
+                  .parse(
+                      new ConfigResource(),
+                      IdString.fromDecoded(
+                          String.valueOf(account.get().getAccount().getId().get())));
           info = getServiceUser.get().apply(serviceUserResource);
           info.username = null;
           accounts.put(username, info);
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 9ceba80..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;
@@ -36,7 +35,6 @@
     bind(CapabilityDefinition.class)
         .annotatedWith(Exports.named(CreateServiceUserCapability.ID))
         .to(CreateServiceUserCapability.class);
-    DynamicSet.bind(binder(), TopMenu.class).to(ServiceUserMenu.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(RefUpdateListener.class);
     DynamicSet.bind(binder(), CommitValidationListener.class).to(ValidateServiceUserCommits.class);
     install(new FactoryModuleBuilder().build(CreateServiceUserNotes.Factory.class));
@@ -72,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/PutActive.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutActive.java
index 1282935..f4b0365 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutActive.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutActive.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -36,7 +35,7 @@
 
   @Override
   public Response<String> apply(ServiceUserResource rsrc, Input input)
-      throws OrmException, IOException, ConfigInvalidException, RestApiException {
+      throws IOException, ConfigInvalidException, RestApiException {
     return putActive.get().apply(rsrc, input);
   }
 }
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 4a5c6f6..986b2bb 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutEmail.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.exceptions.EmailException;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.restapi.DefaultInput;
 import com.google.gerrit.extensions.restapi.IdString;
@@ -31,7 +31,6 @@
 import com.google.gerrit.server.restapi.account.CreateEmail;
 import com.google.gerrit.server.restapi.account.DeleteEmail;
 import com.google.gerrit.server.restapi.account.PutPreferred;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -73,8 +72,8 @@
 
   @Override
   public Response<?> apply(ServiceUserResource rsrc, Input input)
-      throws OrmException, 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/PutHttpPassword.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutHttpPassword.java
index 61b5841..8d0110a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutHttpPassword.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutHttpPassword.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.server.config.ConfigResource;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -61,7 +60,7 @@
   @Override
   public Response<String> apply(ServiceUserResource rsrc, Input input)
       throws AuthException, ResourceConflictException, ConfigInvalidException,
-          ResourceNotFoundException, OrmException, IOException, PermissionBackendException {
+          ResourceNotFoundException, IOException, PermissionBackendException {
     if (input == null) {
       input = new Input();
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutName.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutName.java
index 4bcdd6a..8b10d59 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutName.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutName.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -37,7 +36,7 @@
 
   @Override
   public Response<String> apply(ServiceUserResource rsrc, NameInput input)
-      throws MethodNotAllowedException, ResourceNotFoundException, OrmException, IOException,
+      throws MethodNotAllowedException, ResourceNotFoundException, IOException,
           ConfigInvalidException {
     return putName.get().apply(rsrc.getUser(), input);
   }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
index 37bbf17..f444380 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/PutOwner.java
@@ -40,7 +40,6 @@
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.group.GroupJson;
 import com.google.gerrit.server.restapi.group.GroupsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -87,7 +86,7 @@
 
   @Override
   public Response<GroupInfo> apply(ServiceUserResource rsrc, Input input)
-      throws RestApiException, IOException, OrmException, PermissionBackendException {
+      throws RestApiException, IOException, PermissionBackendException {
     ProjectLevelConfig storage = projectCache.getAllProjects().getConfig(pluginName + ".db");
     Boolean ownerAllowed = getConfig.get().apply(new ConfigResource()).allowOwner;
     if ((ownerAllowed == null || !ownerAllowed)) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RefUpdateListener.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RefUpdateListener.java
index 69512c5..c4090ac 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RefUpdateListener.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/RefUpdateListener.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -109,7 +108,6 @@
           ObjectId.fromString(e.getNewObjectId()));
       crn.commitNotes();
     } catch (IOException
-        | OrmException
         | ConfigInvalidException
         | PermissionBackendException
         | RestApiException x) {
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 729282c..066898f 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserCollection.java
@@ -38,7 +38,6 @@
 import com.google.gerrit.server.project.ProjectLevelConfig;
 import com.google.gerrit.server.restapi.account.AccountsCollection;
 import com.google.gerrit.server.restapi.group.GroupsCollection;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -79,8 +78,8 @@
 
   @Override
   public ServiceUserResource parse(ConfigResource parent, IdString id)
-      throws ResourceNotFoundException, AuthException, IOException, OrmException,
-          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
@@ -92,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();
@@ -101,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/ServiceUserMenu.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserMenu.java
deleted file mode 100644
index df2cf8c..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserMenu.java
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright (C) 2013 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.gerrit.server.permissions.GlobalPermission.ADMINISTRATE_SERVER;
-
-import com.google.common.collect.Lists;
-import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.extensions.api.access.PluginPermission;
-import com.google.gerrit.extensions.client.MenuItem;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.extensions.webui.TopMenu;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.ConfigResource;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.IOException;
-import java.util.List;
-import org.eclipse.jgit.errors.ConfigInvalidException;
-
-class ServiceUserMenu implements TopMenu {
-  private final String pluginName;
-  private final Provider<CurrentUser> userProvider;
-  private final List<MenuEntry> menuEntries;
-  private final Provider<ListServiceUsers> listServiceUsers;
-  private final PermissionBackend permissionBackend;
-
-  @Inject
-  ServiceUserMenu(
-      @PluginName String pluginName,
-      Provider<CurrentUser> userProvider,
-      Provider<ListServiceUsers> listServiceUsers,
-      PermissionBackend permissionBackend)
-      throws IOException, PermissionBackendException, ConfigInvalidException, RestApiException {
-    this.pluginName = pluginName;
-    this.userProvider = userProvider;
-    this.listServiceUsers = listServiceUsers;
-    menuEntries = Lists.newArrayList();
-    this.permissionBackend = permissionBackend;
-
-    List<MenuItem> peopleItems = Lists.newArrayListWithExpectedSize(2);
-    if (canCreateServiceUser()) {
-      peopleItems.add(new MenuItem("Create Service User", "#/x/" + pluginName + "/create", ""));
-    }
-    if (canCreateServiceUser() || hasServiceUser()) {
-      peopleItems.add(new MenuItem("List Service Users", "#/x/" + pluginName + "/list", ""));
-    }
-    if (!peopleItems.isEmpty()) {
-      menuEntries.add(new MenuEntry("People", peopleItems));
-    }
-  }
-
-  private boolean canCreateServiceUser() {
-    if (userProvider.get().isIdentifiedUser()) {
-      IdentifiedUser user = userProvider.get().asIdentifiedUser();
-      return permissionBackend
-              .user(user)
-              .testOrFalse(new PluginPermission(pluginName, CreateServiceUserCapability.ID))
-          || permissionBackend.user(user).testOrFalse(ADMINISTRATE_SERVER);
-    }
-    return false;
-  }
-
-  private boolean hasServiceUser()
-      throws PermissionBackendException, IOException, ConfigInvalidException, RestApiException {
-    try {
-      return !listServiceUsers.get().apply(new ConfigResource()).isEmpty();
-    } catch (AuthException | OrmException e) {
-      return false;
-    }
-  }
-
-  @Override
-  public List<MenuEntry> getEntries() {
-    return menuEntries;
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserResolver.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserResolver.java
index 686b6ba..e2f16a6 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserResolver.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ServiceUserResolver.java
@@ -21,11 +21,11 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupMembership;
@@ -35,8 +35,6 @@
 import com.google.gerrit.server.restapi.group.ListMembers;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -56,7 +54,6 @@
   private final IdentifiedUser.GenericFactory genericUserFactory;
   private final Provider<GetServiceUser> getServiceUser;
   private final Provider<ListMembers> listMembers;
-  private final SchemaFactory<ReviewDb> schema;
   private final ThreadLocalRequestContext tl;
   private final AccountCache accountCache;
   private final GroupControl.Factory groupControlFactory;
@@ -68,7 +65,6 @@
       IdentifiedUser.GenericFactory genericUserFactory,
       Provider<GetServiceUser> getServiceUser,
       Provider<ListMembers> listMembers,
-      SchemaFactory<ReviewDb> schema,
       ThreadLocalRequestContext tl,
       AccountCache accountCache,
       GroupControl.Factory groupControlFactory,
@@ -77,7 +73,6 @@
     this.genericUserFactory = genericUserFactory;
     this.getServiceUser = getServiceUser;
     this.listMembers = listMembers;
-    this.schema = schema;
     this.tl = tl;
     this.accountCache = accountCache;
     this.groupControlFactory = groupControlFactory;
@@ -85,103 +80,86 @@
   }
 
   ServiceUserInfo getAsServiceUser(PersonIdent committerIdent)
-      throws ConfigInvalidException, IOException, OrmException, PermissionBackendException,
-          RestApiException {
+      throws ConfigInvalidException, IOException, PermissionBackendException, RestApiException {
     StringBuilder committer = new StringBuilder();
     committer.append(committerIdent.getName());
     committer.append(" <");
     committer.append(committerIdent.getEmailAddress());
     committer.append("> ");
 
-    Account account = resolver.find(committer.toString());
-    if (account == null) {
-      return null;
-    }
     try {
+      Account account = resolver.resolve(committer.toString()).asUnique().getAccount();
       return getServiceUser
           .get()
           .apply(new ServiceUserResource(genericUserFactory.create(account.getId())));
-    } catch (ResourceNotFoundException e) {
+    } catch (ResourceNotFoundException | UnresolvableAccountException e) {
       return null;
     }
   }
 
   List<AccountInfo> listOwners(ServiceUserInfo serviceUser)
-      throws OrmException, MethodNotAllowedException, PermissionBackendException {
+      throws MethodNotAllowedException, PermissionBackendException {
     if (serviceUser.owner == null) {
       return Collections.emptyList();
     }
 
-    try (ReviewDb db = schema.open()) {
-      RequestContext context =
-          new RequestContext() {
-            @Override
-            public CurrentUser getUser() {
-              return new CurrentUser() {
+    RequestContext context =
+        new RequestContext() {
+          @Override
+          public CurrentUser getUser() {
+            return new CurrentUser() {
 
-                @Override
-                public GroupMembership getEffectiveGroups() {
-                  return new GroupMembership() {
-                    @Override
-                    public Set<AccountGroup.UUID> intersection(
-                        Iterable<AccountGroup.UUID> groupIds) {
-                      return null;
-                    }
+              @Override
+              public GroupMembership getEffectiveGroups() {
+                return new GroupMembership() {
+                  @Override
+                  public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupIds) {
+                    return null;
+                  }
 
-                    @Override
-                    public Set<AccountGroup.UUID> getKnownGroups() {
-                      return null;
-                    }
+                  @Override
+                  public Set<AccountGroup.UUID> getKnownGroups() {
+                    return null;
+                  }
 
-                    @Override
-                    public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds) {
-                      return true;
-                    }
+                  @Override
+                  public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds) {
+                    return true;
+                  }
 
-                    @Override
-                    public boolean contains(AccountGroup.UUID groupId) {
-                      return true;
-                    }
-                  };
-                }
+                  @Override
+                  public boolean contains(AccountGroup.UUID groupId) {
+                    return true;
+                  }
+                };
+              }
 
-                @Override
-                public Object getCacheKey() {
-                  return null;
-                }
-              };
-            }
-
-            @Override
-            public Provider<ReviewDb> getReviewDbProvider() {
-              return new Provider<ReviewDb>() {
-                @Override
-                public ReviewDb get() {
-                  return db;
-                }
-              };
-            }
-          };
-      RequestContext old = tl.setContext(context);
-      try {
-        GroupDescription.Basic group = groupResolver.parseId(serviceUser.owner.id);
-        GroupControl ctl = groupControlFactory.controlFor(group);
-        ListMembers lm = listMembers.get();
-        GroupResource rsrc = new GroupResource(ctl);
-        lm.setRecursive(true);
-        List<AccountInfo> owners = new ArrayList<>();
-        for (AccountInfo a : lm.apply(rsrc)) {
-          owners.add(a);
-        }
-        return owners;
-      } finally {
-        tl.setContext(old);
+              @Override
+              public Object getCacheKey() {
+                return null;
+              }
+            };
+          }
+        };
+    RequestContext old = tl.setContext(context);
+    try {
+      GroupDescription.Basic group = groupResolver.parseId(serviceUser.owner.id);
+      GroupControl ctl = groupControlFactory.controlFor(group);
+      ListMembers lm = listMembers.get();
+      GroupResource rsrc = new GroupResource(ctl);
+      lm.setRecursive(true);
+      List<AccountInfo> owners = new ArrayList<>();
+      for (AccountInfo a : lm.apply(rsrc)) {
+        owners.add(a);
       }
+      return owners;
+    } finally {
+      tl.setContext(old);
     }
   }
 
   List<AccountInfo> listActiveOwners(ServiceUserInfo serviceUser)
-      throws OrmException, MethodNotAllowedException, PermissionBackendException {
+      throws MethodNotAllowedException, PermissionBackendException {
     List<AccountInfo> activeOwners = new ArrayList<>();
     for (AccountInfo owner : listOwners(serviceUser)) {
       Optional<AccountState> accountState = accountCache.get(new Account.Id(owner._accountId));
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/java/com/googlesource/gerrit/plugins/serviceuser/SshKeys.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshKeys.java
index 7858b12..70833ef 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshKeys.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/SshKeys.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -49,7 +48,7 @@
 
   @Override
   public ServiceUserResource.SshKey parse(ServiceUserResource parent, IdString id)
-      throws ResourceNotFoundException, OrmException, IOException, ConfigInvalidException {
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
     return new ServiceUserResource.SshKey(sshKeys.get().parse(parent.getUser(), id));
   }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ValidateServiceUserCommits.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ValidateServiceUserCommits.java
index ebf072b..d8c2176 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ValidateServiceUserCommits.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/ValidateServiceUserCommits.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
 import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.serviceuser.GetServiceUser.ServiceUserInfo;
@@ -75,7 +74,6 @@
         }
       }
     } catch (IOException
-        | OrmException
         | ConfigInvalidException
         | PermissionBackendException
         | RestApiException e) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/AccountCapabilities.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/AccountCapabilities.java
deleted file mode 100644
index a6f7f84..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/AccountCapabilities.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gerrit.plugin.client.rpc.RestApi;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-
-/** Capabilities the caller has from {@code /accounts/self/capabilities}. */
-public class AccountCapabilities extends JavaScriptObject {
-  public static void all(AsyncCallback<AccountCapabilities> cb, String... filter) {
-    new RestApi("/accounts/self/capabilities").addParameter("q", filter).get(cb);
-  }
-
-  protected AccountCapabilities() {}
-
-  public final native boolean canPerform(String name) /*-{ return this[name] ? true : false; }-*/;
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/AccountInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/AccountInfo.java
deleted file mode 100644
index ffc1953..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/AccountInfo.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class AccountInfo extends JavaScriptObject {
-  public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
-
-  public final native String name() /*-{ return this.name; }-*/;
-
-  public final native String username() /*-{ return this.username; }-*/;
-
-  public final native String email() /*-{ return this.email; }-*/;
-
-  protected AccountInfo() {}
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ConfigInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ConfigInfo.java
deleted file mode 100644
index 6291319..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ConfigInfo.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.JsArrayString;
-import java.util.List;
-
-public class ConfigInfo extends JavaScriptObject {
-  final native String getInfoMessage() /*-{ return this.info }-*/;
-
-  final native String getOnSuccessMessage() /*-{ return this.on_success }-*/;
-
-  final native boolean getAllowEmail() /*-{ return this.allow_email ? true : false; }-*/;
-
-  final native boolean
-      getAllowHttpPassword() /*-{ return this.allow_http_password ? true : false; }-*/;
-
-  final native boolean getAllowOwner() /*-{ return this.allow_owner ? true : false; }-*/;
-
-  final native boolean getCreateNotes() /*-{ return this.create_notes ? true : false; }-*/;
-
-  final native boolean
-      getCreateNotesAsync() /*-{ return this.create_notes_async ? true : false; }-*/;
-
-  final native JsArrayString getBlockedNames() /*-{ return this.blocked_names; }-*/;
-
-  final native NativeMap<GroupInfo> getGroups() /*-{ return this.groups; }-*/;
-
-  final native void setInfoMessage(String s) /*-{ this.info = s; }-*/;
-
-  final native void setOnSuccessMessage(String s) /*-{ this.on_success = s; }-*/;
-
-  final native void setAllowEmail(boolean s) /*-{ this.allow_email = s; }-*/;
-
-  final native void setAllowHttpPassword(boolean s) /*-{ this.allow_http_password = s; }-*/;
-
-  final native void setAllowOwner(boolean s) /*-{ this.allow_owner = s; }-*/;
-
-  final native void setCreateNotes(boolean s) /*-{ this.create_notes = s; }-*/;
-
-  final native void setCreateNotesAsync(boolean s) /*-{ this.create_notes_async = s; }-*/;
-
-  final void setBlockedNames(List<String> blockedNames) {
-    initBlockedNames();
-    for (String n : blockedNames) {
-      addBlockedName(n);
-    }
-  }
-
-  final native void initBlockedNames() /*-{ this.blocked_names = []; }-*/;
-
-  final native void addBlockedName(String n) /*-{ this.blocked_names.push(n); }-*/;
-
-  final void setGroups(List<String> groups) {
-    initGroups();
-    for (String g : groups) {
-      addGroup(g);
-    }
-  }
-
-  final native void initGroups() /*-{ this.groups = []; }-*/;
-
-  final native void addGroup(String g) /*-{ this.groups.push(g); }-*/;
-
-  static ConfigInfo create() {
-    ConfigInfo g = (ConfigInfo) createObject();
-    return g;
-  }
-
-  protected ConfigInfo() {}
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java
deleted file mode 100644
index c57eb5f..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/CreateServiceUserScreen.java
+++ /dev/null
@@ -1,259 +0,0 @@
-// Copyright (C) 2013 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.client;
-
-import com.google.gerrit.plugin.client.Plugin;
-import com.google.gerrit.plugin.client.rpc.RestApi;
-import com.google.gerrit.plugin.client.screen.Screen;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.DialogBox;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.TextArea;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwt.user.client.ui.VerticalPanel;
-
-public class CreateServiceUserScreen extends VerticalPanel {
-  static class Factory implements Screen.EntryPoint {
-    @Override
-    public void onLoad(Screen screen) {
-      screen.setPageTitle("Create Service User");
-      screen.show(new CreateServiceUserScreen());
-    }
-  }
-
-  private TextBox usernameTxt;
-  private TextBox emailTxt;
-  private TextArea sshKeyTxt;
-  private String onSuccessMessage;
-
-  CreateServiceUserScreen() {
-    setStyleName("serviceuser-panel");
-
-    Panel usernamePanel = new VerticalPanel();
-    usernamePanel.add(new Label("Username:"));
-    usernameTxt =
-        new TextBox() {
-          @Override
-          public void onBrowserEvent(Event event) {
-            super.onBrowserEvent(event);
-            if (event.getTypeInt() == Event.ONPASTE) {
-              Scheduler.get()
-                  .scheduleDeferred(
-                      new ScheduledCommand() {
-                        @Override
-                        public void execute() {
-                          if (getValue().trim().length() != 0) {
-                            setEnabled(true);
-                          }
-                        }
-                      });
-            }
-          }
-        };
-    usernameTxt.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(final KeyPressEvent event) {
-            event.stopPropagation();
-          }
-        });
-    usernameTxt.sinkEvents(Event.ONPASTE);
-    usernameTxt.setVisibleLength(40);
-    usernamePanel.add(usernameTxt);
-    add(usernamePanel);
-
-    Panel sshKeyPanel = new VerticalPanel();
-    sshKeyPanel.add(new Label("Public SSH Key:"));
-    sshKeyPanel.add(new SshKeyHelpPanel());
-    sshKeyTxt = new TextArea();
-    sshKeyTxt.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(final KeyPressEvent event) {
-            event.stopPropagation();
-          }
-        });
-    sshKeyTxt.setVisibleLines(12);
-    sshKeyTxt.setCharacterWidth(80);
-    sshKeyTxt.getElement().setPropertyBoolean("spellcheck", false);
-    sshKeyPanel.add(sshKeyTxt);
-    add(sshKeyPanel);
-
-    HorizontalPanel buttons = new HorizontalPanel();
-    add(buttons);
-
-    final Button createButton = new Button("Create");
-    createButton.addStyleName("serviceuser-createButton");
-    createButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            doCreate();
-          }
-        });
-    buttons.add(createButton);
-    createButton.setEnabled(false);
-    new OnEditEnabler(createButton, usernameTxt);
-
-    usernameTxt.setFocus(true);
-    createButton.setEnabled(false);
-
-    new RestApi("config")
-        .id("server")
-        .view(Plugin.get().getPluginName(), "config")
-        .get(
-            new AsyncCallback<ConfigInfo>() {
-              @Override
-              public void onSuccess(ConfigInfo info) {
-                onSuccessMessage = info.getOnSuccessMessage();
-
-                String infoMessage = info.getInfoMessage();
-                if (infoMessage != null && !"".equals(infoMessage)) {
-                  insert(new HTML(infoMessage), 0);
-                }
-
-                if (info.getAllowEmail()) {
-                  Panel emailPanel = new VerticalPanel();
-                  emailPanel.add(new Label("Email:"));
-                  emailTxt =
-                      new TextBox() {
-                        @Override
-                        public void onBrowserEvent(Event event) {
-                          super.onBrowserEvent(event);
-                          if (event.getTypeInt() == Event.ONPASTE) {
-                            Scheduler.get()
-                                .scheduleDeferred(
-                                    new ScheduledCommand() {
-                                      @Override
-                                      public void execute() {
-                                        if (getValue().trim().length() != 0) {
-                                          setEnabled(true);
-                                        }
-                                      }
-                                    });
-                          }
-                        }
-                      };
-                  emailTxt.addKeyPressHandler(
-                      new KeyPressHandler() {
-                        @Override
-                        public void onKeyPress(final KeyPressEvent event) {
-                          event.stopPropagation();
-                        }
-                      });
-                  emailTxt.sinkEvents(Event.ONPASTE);
-                  emailTxt.setVisibleLength(40);
-                  emailPanel.add(emailTxt);
-                  insert(emailPanel, 2);
-                }
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                // never invoked
-              }
-            });
-  }
-
-  private void doCreate() {
-    final String username = usernameTxt.getValue().trim();
-    String sshKey = sshKeyTxt.getText();
-    if (sshKey != null) {
-      sshKey = sshKey.trim();
-    }
-
-    ServiceUserInput in = ServiceUserInput.create();
-    in.ssh_key(sshKey);
-    if (emailTxt != null) {
-      in.email(emailTxt.getValue().trim());
-    }
-    new RestApi("config")
-        .id("server")
-        .view("serviceuser", "serviceusers")
-        .id(username)
-        .post(
-            in,
-            new AsyncCallback<JavaScriptObject>() {
-
-              @Override
-              public void onSuccess(JavaScriptObject result) {
-                clearForm();
-                Plugin.get().go("/x/" + Plugin.get().getName() + "/user/" + username);
-
-                final DialogBox successDialog = new DialogBox();
-                successDialog.setText("Service User Created");
-                successDialog.setAnimationEnabled(true);
-
-                Panel p = new VerticalPanel();
-                p.setStyleName("serviceuser-panel");
-                p.add(new Label("The service user '" + username + "' was created."));
-                Button okButton = new Button("OK");
-                okButton.addClickHandler(
-                    new ClickHandler() {
-                      @Override
-                      public void onClick(ClickEvent event) {
-                        successDialog.hide();
-                      }
-                    });
-
-                if (onSuccessMessage != null && !"".equals(onSuccessMessage)) {
-                  p.add(new HTML(onSuccessMessage));
-                }
-
-                p.add(okButton);
-                successDialog.add(p);
-
-                successDialog.center();
-                successDialog.show();
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {}
-            });
-  }
-
-  private void clearForm() {
-    usernameTxt.setValue("");
-    if (emailTxt != null) {
-      emailTxt.setValue("");
-    }
-    sshKeyTxt.setValue("");
-  }
-
-  private static class ServiceUserInput extends JavaScriptObject {
-    final native void ssh_key(String s) /*-{ this.ssh_key = s; }-*/;
-
-    final native void email(String e) /*-{ this.email = e; }-*/;
-
-    static ServiceUserInput create() {
-      ServiceUserInput g = (ServiceUserInput) createObject();
-      return g;
-    }
-
-    protected ServiceUserInput() {}
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/EditableValue.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/EditableValue.java
deleted file mode 100644
index 0ab741f..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/EditableValue.java
+++ /dev/null
@@ -1,169 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-public abstract class EditableValue extends FlowPanel {
-  private final Widget labelWidget;
-  private final Image edit;
-  private final NpTextBox input;
-  private final Button save;
-  private final Button cancel;
-  private Image warning;
-
-  EditableValue(String serviceUser, String value) {
-    this(serviceUser, value, null);
-  }
-
-  EditableValue(final String serviceUser, String value, String href) {
-    if (href != null) {
-      labelWidget = new Anchor(value, href);
-    } else {
-      labelWidget = new InlineLabel(value);
-    }
-    edit = new Image(ServiceUserPlugin.RESOURCES.edit());
-    edit.addStyleName("serviceuser-editButton");
-    edit.setTitle("Edit");
-
-    input = new NpTextBox();
-    input.setVisibleLength(25);
-    input.setValue(value);
-    input.setVisible(false);
-    save = new Button();
-    save.setText("Save");
-    save.setVisible(false);
-    save.setEnabled(false);
-    cancel = new Button();
-    cancel.setText("Cancel");
-    cancel.setVisible(false);
-
-    OnEditEnabler e = new OnEditEnabler(save);
-    e.listenTo(input);
-
-    edit.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            labelWidget.setVisible(false);
-            edit.setVisible(false);
-            input.setVisible(true);
-            input.setFocus(true);
-            save.setVisible(true);
-            if (warning != null) {
-              warning.setVisible(true);
-            }
-            cancel.setVisible(true);
-          }
-        });
-    save.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            save.setEnabled(false);
-            save(serviceUser, input.getValue().trim());
-          }
-        });
-    input.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              save.setEnabled(false);
-              save(serviceUser, input.getValue().trim());
-            } else if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ESCAPE) {
-              cancel();
-            }
-          }
-        });
-    cancel.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            cancel();
-          }
-        });
-
-    add(labelWidget);
-    add(edit);
-    add(input);
-    add(save);
-    add(cancel);
-  }
-
-  private void cancel() {
-    labelWidget.setVisible(true);
-    edit.setVisible(true);
-    input.setVisible(false);
-    if (labelWidget instanceof Label) {
-      input.setValue(((Label) labelWidget).getText());
-    } else {
-      input.setValue(((Anchor) labelWidget).getText());
-    }
-    save.setVisible(false);
-    save.setEnabled(false);
-    if (warning != null) {
-      warning.setVisible(false);
-    }
-    cancel.setVisible(false);
-  }
-
-  public void setWarning(String msg) {
-    if (warning == null) {
-      warning = new Image(ServiceUserPlugin.RESOURCES.warning());
-      insert(warning, getWidgetIndex(save));
-      warning.setVisible(save.isVisible());
-    }
-    warning.setTitle(msg);
-  }
-
-  protected void updateValue(String newValue) {
-    if (labelWidget instanceof Label) {
-      ((Label) labelWidget).setText(newValue);
-    } else {
-      ((Anchor) labelWidget).setText(newValue);
-    }
-    labelWidget.setVisible(true);
-    edit.setVisible(true);
-    input.setVisible(false);
-    input.setValue(newValue);
-    save.setVisible(false);
-    if (warning != null) {
-      warning.setVisible(false);
-    }
-    save.setEnabled(false);
-    cancel.setVisible(false);
-  }
-
-  protected void updateHref(String newHref) {
-    if (labelWidget instanceof Anchor) {
-      ((Anchor) labelWidget).setHref(newHref);
-    }
-  }
-
-  protected abstract void save(String serviceUser, final String newValue);
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/GroupInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/GroupInfo.java
deleted file mode 100644
index b398092..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/GroupInfo.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class GroupInfo extends JavaScriptObject {
-  public final native String name() /*-{ return this.name; }-*/;
-
-  public final native String url() /*-{ return this.url; }-*/;
-
-  protected GroupInfo() {}
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/HttpPasswordInput.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/HttpPasswordInput.java
deleted file mode 100644
index 5549597..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/HttpPasswordInput.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) 2015 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.client;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class HttpPasswordInput extends JavaScriptObject {
-  final native void generate(boolean g) /*-{ if (g) this.generate = g; }-*/;
-
-  public static HttpPasswordInput create() {
-    return createObject().cast();
-  }
-
-  protected HttpPasswordInput() {}
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/OnEditEnabler.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/OnEditEnabler.java
deleted file mode 100644
index 815962c..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/OnEditEnabler.java
+++ /dev/null
@@ -1,188 +0,0 @@
-// Copyright (C) 2010 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.client;
-
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-import com.google.gwt.event.dom.client.KeyDownEvent;
-import com.google.gwt.event.dom.client.KeyDownHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.dom.client.MouseUpEvent;
-import com.google.gwt.event.dom.client.MouseUpHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FocusWidget;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.TextBoxBase;
-import com.google.gwt.user.client.ui.ValueBoxBase;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Enables a FocusWidget (e.g. a Button) if an edit is detected from any registered input widget.
- */
-public class OnEditEnabler
-    implements KeyPressHandler,
-        KeyDownHandler,
-        MouseUpHandler,
-        ChangeHandler,
-        ValueChangeHandler<Object> {
-
-  private final FocusWidget widget;
-  private Map<TextBoxBase, String> strings = new HashMap<>();
-  private String originalValue;
-
-  // The first parameter to the constructors must be the FocusWidget to enable,
-  // subsequent parameters are widgets to listenTo.
-
-  public OnEditEnabler(final FocusWidget w, final TextBoxBase tb) {
-    this(w);
-    originalValue = tb.getValue().trim();
-    listenTo(tb);
-  }
-
-  public OnEditEnabler(final FocusWidget w, final ListBox lb) {
-    this(w);
-    listenTo(lb);
-  }
-
-  public OnEditEnabler(final FocusWidget w, final CheckBox cb) {
-    this(w);
-    listenTo(cb);
-  }
-
-  public OnEditEnabler(final FocusWidget w) {
-    widget = w;
-  }
-
-  // Register input widgets to be listened to
-
-  public void listenTo(final TextBoxBase tb) {
-    strings.put(tb, tb.getText().trim());
-    tb.addKeyPressHandler(this);
-
-    // Is there another way to capture middle button X11 pastes in browsers
-    // which do not yet support ONPASTE events (Firefox)?
-    tb.addMouseUpHandler(this);
-
-    // Resetting the "original text" on focus ensures that we are
-    // up to date with non-user updates of the text (calls to
-    // setText()...) and also up to date with user changes which
-    // occured after enabling "widget".
-    tb.addFocusHandler(
-        new FocusHandler() {
-          @Override
-          public void onFocus(FocusEvent event) {
-            strings.put(tb, tb.getText().trim());
-          }
-        });
-
-    // CTRL-V Pastes in Chrome seem only detectable via BrowserEvents or
-    // KeyDownEvents, the latter is better.
-    tb.addKeyDownHandler(this);
-  }
-
-  public void listenTo(final ListBox lb) {
-    lb.addChangeHandler(this);
-  }
-
-  @SuppressWarnings({"unchecked", "rawtypes"})
-  public void listenTo(final CheckBox cb) {
-    cb.addValueChangeHandler((ValueChangeHandler) this);
-  }
-
-  // Handlers
-
-  @Override
-  public void onKeyPress(final KeyPressEvent e) {
-    on(e);
-  }
-
-  @Override
-  public void onKeyDown(final KeyDownEvent e) {
-    on(e);
-  }
-
-  @Override
-  public void onMouseUp(final MouseUpEvent e) {
-    on(e);
-  }
-
-  @Override
-  public void onChange(final ChangeEvent e) {
-    on(e);
-  }
-
-  @SuppressWarnings("rawtypes")
-  @Override
-  public void onValueChange(final ValueChangeEvent e) {
-    on(e);
-  }
-
-  private void on(final GwtEvent<?> e) {
-    if (widget.isEnabled()
-        || !(e.getSource() instanceof FocusWidget)
-        || !((FocusWidget) e.getSource()).isEnabled()) {
-      if (e.getSource() instanceof ValueBoxBase) {
-        final TextBoxBase box = ((TextBoxBase) e.getSource());
-        Scheduler.get()
-            .scheduleDeferred(
-                new ScheduledCommand() {
-                  @Override
-                  public void execute() {
-                    if (box.getValue().trim().equals(originalValue)) {
-                      widget.setEnabled(false);
-                    }
-                  }
-                });
-      }
-      return;
-    }
-
-    if (e.getSource() instanceof TextBoxBase) {
-      onTextBoxBase((TextBoxBase) e.getSource());
-    } else {
-      // For many widgets, we can assume that a change is an edit. If
-      // a widget does not work that way, it should be special cased
-      // above.
-      widget.setEnabled(true);
-    }
-  }
-
-  private void onTextBoxBase(final TextBoxBase tb) {
-    // The text appears to not get updated until the handlers complete.
-    Scheduler.get()
-        .scheduleDeferred(
-            new ScheduledCommand() {
-              @Override
-              public void execute() {
-                String orig = strings.get(tb);
-                if (orig == null) {
-                  orig = "";
-                }
-                if (!orig.equals(tb.getText().trim())) {
-                  widget.setEnabled(true);
-                }
-              }
-            });
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserInfo.java
index 96c543d..47723f1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserInfo.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserInfo.java
@@ -14,34 +14,28 @@
 
 package com.googlesource.gerrit.plugins.serviceuser.client;
 
-import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.GroupInfo;
 
-public class ServiceUserInfo extends JavaScriptObject {
+public class ServiceUserInfo {
   public final String getDisplayName() {
-    if (created_by().username() != null) {
-      return created_by().username();
+    if (created_by.username != null) {
+      return created_by.username;
     }
-    if (created_by()._account_id() != -1) {
-      return Integer.toString(created_by()._account_id());
+    if (created_by._accountId != -1) {
+      return Integer.toString(created_by._accountId);
     }
     return "N/A";
   }
 
-  public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
-
-  public final native String name() /*-{ return this.name; }-*/;
-
-  public final native String username() /*-{ return this.username; }-*/;
-
-  public final native String email() /*-{ return this.email; }-*/;
-
-  public final native AccountInfo created_by() /*-{ return this.created_by; }-*/;
-
-  public final native String created_at() /*-{ return this.created_at; }-*/;
-
-  public final native boolean active() /*-{ return this.inactive ? false : true; }-*/;
-
-  public final native GroupInfo owner() /*-{ return this.owner; }-*/;
+  public int _account_id;
+  public String name;
+  public String username;
+  public String email;
+  public AccountInfo created_by;
+  public String created_at;
+  public boolean active;
+  public GroupInfo owner;
 
   protected ServiceUserInfo() {}
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserListScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserListScreen.java
deleted file mode 100644
index 5c30bea..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserListScreen.java
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gerrit.client.rpc.NativeMap;
-import com.google.gerrit.plugin.client.Plugin;
-import com.google.gerrit.plugin.client.rpc.RestApi;
-import com.google.gerrit.plugin.client.screen.Screen;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.InlineHyperlink;
-import com.google.gwt.user.client.ui.VerticalPanel;
-
-public class ServiceUserListScreen extends VerticalPanel {
-  static class Factory implements Screen.EntryPoint {
-    @Override
-    public void onLoad(Screen screen) {
-      screen.setPageTitle("Service Users");
-      screen.show(new ServiceUserListScreen());
-    }
-  }
-
-  ServiceUserListScreen() {
-    setStyleName("serviceuser-panel");
-
-    new RestApi("config")
-        .id("server")
-        .view(Plugin.get().getPluginName(), "serviceusers")
-        .get(
-            NativeMap.copyKeysIntoChildren(
-                "username",
-                new AsyncCallback<NativeMap<ServiceUserInfo>>() {
-                  @Override
-                  public void onSuccess(NativeMap<ServiceUserInfo> info) {
-                    display(info);
-                  }
-
-                  @Override
-                  public void onFailure(Throwable caught) {
-                    // never invoked
-                  }
-                }));
-  }
-
-  private void display(NativeMap<ServiceUserInfo> info) {
-    int columns = 7;
-    FlexTable t = new FlexTable();
-    t.setStyleName("serviceuser-serviceUserTable");
-    FlexCellFormatter fmt = t.getFlexCellFormatter();
-    for (int c = 0; c < columns; c++) {
-      fmt.addStyleName(0, c, "dataHeader");
-      fmt.addStyleName(0, c, "topMostCell");
-    }
-    fmt.addStyleName(0, 0, "leftMostCell");
-
-    t.setText(0, 0, "Username");
-    t.setText(0, 1, "Full Name");
-    t.setText(0, 2, "Email");
-    t.setText(0, 3, "Owner");
-    t.setText(0, 4, "Created By");
-    t.setText(0, 5, "Created At");
-    t.setText(0, 6, "Account State");
-
-    int row = 1;
-    for (String username : info.keySet()) {
-      ServiceUserInfo a = info.get(username);
-
-      for (int c = 0; c < columns; c++) {
-        fmt.addStyleName(row, c, "dataCell");
-        fmt.addStyleName(row, 0, "leftMostCell");
-      }
-
-      t.setWidget(
-          row,
-          0,
-          new InlineHyperlink(username, "/x/" + Plugin.get().getName() + "/user/" + username));
-      t.setText(row, 1, a.name());
-      t.setText(row, 2, a.email());
-
-      if (a.owner() != null) {
-        if (a.owner().url() != null) {
-          t.setWidget(row, 3, new Anchor(a.owner().name(), a.owner().url()));
-        } else {
-          t.setText(row, 3, a.owner().name());
-        }
-      }
-
-      t.setText(row, 4, a.getDisplayName());
-      t.setText(row, 5, a.created_at());
-      t.setText(row, 6, !a.active() ? "Inactive" : "");
-      row++;
-    }
-
-    add(t);
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserPlugin.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserPlugin.java
deleted file mode 100644
index 393963d..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserPlugin.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gerrit.client.Resources;
-import com.google.gerrit.plugin.client.Plugin;
-import com.google.gerrit.plugin.client.PluginEntryPoint;
-import com.google.gwt.core.client.GWT;
-
-public class ServiceUserPlugin extends PluginEntryPoint {
-  public static final Resources RESOURCES = GWT.create(Resources.class);
-
-  @Override
-  public void onPluginLoad() {
-    Plugin.get().screen("create", new CreateServiceUserScreen.Factory());
-    Plugin.get().screen("settings", new ServiceUserSettingsScreen.Factory());
-    Plugin.get().screen("list", new ServiceUserListScreen.Factory());
-    Plugin.get().screenRegex("user/(.*)", new ServiceUserScreen.Factory());
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java
deleted file mode 100644
index 78cb65e..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserScreen.java
+++ /dev/null
@@ -1,371 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gerrit.client.rpc.NativeString;
-import com.google.gerrit.plugin.client.Plugin;
-import com.google.gerrit.plugin.client.rpc.NoContent;
-import com.google.gerrit.plugin.client.rpc.RestApi;
-import com.google.gerrit.plugin.client.screen.Screen;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.ToggleButton;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-
-public class ServiceUserScreen extends VerticalPanel {
-  static class Factory implements Screen.EntryPoint {
-    @Override
-    public void onLoad(Screen screen) {
-      screen.setPageTitle("Service User " + screen.getToken(1));
-      screen.show(new ServiceUserScreen(screen.getToken(1)));
-    }
-  }
-
-  ServiceUserScreen(final String serviceUser) {
-    setStyleName("serviceuser-panel");
-
-    new RestApi("config")
-        .id("server")
-        .view(Plugin.get().getPluginName(), "serviceusers")
-        .id(serviceUser)
-        .get(
-            new AsyncCallback<ServiceUserInfo>() {
-              @Override
-              public void onSuccess(final ServiceUserInfo serviceUserInfo) {
-                new RestApi("config")
-                    .id("server")
-                    .view(Plugin.get().getPluginName(), "config")
-                    .get(
-                        new AsyncCallback<ConfigInfo>() {
-                          @Override
-                          public void onSuccess(final ConfigInfo configInfo) {
-                            AccountCapabilities.all(
-                                new AsyncCallback<AccountCapabilities>() {
-                                  @Override
-                                  public void onSuccess(AccountCapabilities ac) {
-                                    boolean isAdmin = ac.canPerform("administrateServer");
-                                    display(
-                                        serviceUserInfo,
-                                        configInfo.getAllowEmail() || isAdmin,
-                                        configInfo.getAllowOwner() || isAdmin,
-                                        configInfo.getAllowHttpPassword() || isAdmin);
-                                  }
-
-                                  @Override
-                                  public void onFailure(Throwable caught) {
-                                    // never invoked
-                                  }
-                                },
-                                "administrateServer");
-                          }
-
-                          @Override
-                          public void onFailure(Throwable caught) {
-                            // never invoked
-                          }
-                        });
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                // never invoked
-              }
-            });
-  }
-
-  private void display(
-      ServiceUserInfo info, boolean allowEmail, boolean allowOwner, boolean allowHttpPassword) {
-    MyTable t = new MyTable();
-    t.setStyleName("serviceuser-serviceUserInfoTable");
-    t.addRow("Account State", createActiveToggle(info));
-    t.addRow("Username", info.username());
-    t.addRow(
-        "Full Name",
-        new EditableValue(info.username(), info.name()) {
-          @Override
-          protected void save(String serviceUser, final String newValue) {
-            new RestApi("config")
-                .id("server")
-                .view(Plugin.get().getPluginName(), "serviceusers")
-                .id(serviceUser)
-                .view("name")
-                .put(
-                    newValue,
-                    new AsyncCallback<NativeString>() {
-                      @Override
-                      public void onSuccess(NativeString result) {
-                        updateValue(newValue);
-                      }
-
-                      @Override
-                      public void onFailure(Throwable caught) {
-                        // never invoked
-                      }
-                    });
-          }
-        });
-    if (allowEmail) {
-      t.addRow(
-          "Email Address",
-          new EditableValue(info.username(), info.email()) {
-            @Override
-            protected void save(String serviceUser, final String newValue) {
-              new RestApi("config")
-                  .id("server")
-                  .view(Plugin.get().getPluginName(), "serviceusers")
-                  .id(serviceUser)
-                  .view("email")
-                  .put(
-                      newValue,
-                      new AsyncCallback<NativeString>() {
-                        @Override
-                        public void onSuccess(NativeString result) {
-                          updateValue(newValue);
-                        }
-
-                        @Override
-                        public void onFailure(Throwable caught) {
-                          // never invoked
-                        }
-                      });
-            }
-          });
-    } else {
-      t.addRow("Email Address", info.email());
-    }
-    t.addRow("HTTP Password", createHttpPasswordWidget(info.username(), allowHttpPassword));
-    t.addRow("Owner Group", createOwnerWidget(info, allowOwner));
-    t.addRow("Created By", info.getDisplayName());
-    t.addRow("Created At", info.created_at());
-    add(t);
-
-    add(new SshPanel(info.username()));
-  }
-
-  private ToggleButton createActiveToggle(final ServiceUserInfo info) {
-    final ToggleButton activeToggle = new ToggleButton();
-    activeToggle.setStyleName("serviceuser-toggleButton");
-    activeToggle.setVisible(false);
-    activeToggle.setValue(true);
-    activeToggle.setText("Active");
-    activeToggle.setValue(false);
-    activeToggle.setText("Inactive");
-    activeToggle.setValue(info.active());
-    activeToggle.setVisible(true);
-
-    activeToggle.addValueChangeHandler(
-        new ValueChangeHandler<Boolean>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<Boolean> event) {
-            if (event.getValue()) {
-              new RestApi("config")
-                  .id("server")
-                  .view(Plugin.get().getPluginName(), "serviceusers")
-                  .id(info.username())
-                  .view("active")
-                  .put(
-                      new AsyncCallback<NoContent>() {
-                        @Override
-                        public void onSuccess(NoContent result) {}
-
-                        @Override
-                        public void onFailure(Throwable caught) {
-                          // never invoked
-                        }
-                      });
-            } else {
-              new RestApi("config")
-                  .id("server")
-                  .view(Plugin.get().getPluginName(), "serviceusers")
-                  .id(info.username())
-                  .view("active")
-                  .delete(
-                      new AsyncCallback<NoContent>() {
-                        @Override
-                        public void onSuccess(NoContent result) {}
-
-                        @Override
-                        public void onFailure(Throwable caught) {
-                          // never invoked
-                        }
-                      });
-            }
-          }
-        });
-
-    return activeToggle;
-  }
-
-  private Widget createHttpPasswordWidget(final String serviceUser, boolean allowHttpPassword) {
-    if (allowHttpPassword) {
-      HorizontalPanel p = new HorizontalPanel();
-      final CopyableLabel label = new CopyableLabel("");
-      label.setVisible(false);
-      p.add(label);
-
-      // The redNot icon is only used as temporary measure until gerrit core
-      // provides a better icon that symbolizes "clear".
-      final Image delete = new Image(ServiceUserPlugin.RESOURCES.redNot());
-      delete.addStyleName("serviceuser-deleteButton");
-      delete.setTitle("Clear HTTP password");
-      delete.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              new RestApi("config")
-                  .id("server")
-                  .view(Plugin.get().getPluginName(), "serviceusers")
-                  .id(serviceUser)
-                  .view("password.http")
-                  .delete(
-                      new AsyncCallback<NoContent>() {
-                        @Override
-                        public void onSuccess(NoContent noContent) {
-                          label.setText("");
-                          label.setVisible(false);
-                          delete.setVisible(false);
-                        }
-
-                        @Override
-                        public void onFailure(Throwable caught) {
-                          // never invoked
-                        }
-                      });
-            }
-          });
-      delete.setVisible(true);
-      p.add(delete);
-
-      Image generate = new Image(ServiceUserPlugin.RESOURCES.gear());
-      generate.addStyleName("serviceuser-generateButton");
-      generate.setTitle("Generate new HTTP password");
-      generate.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(ClickEvent event) {
-              HttpPasswordInput in = HttpPasswordInput.create();
-              in.generate(true);
-              new RestApi("config")
-                  .id("server")
-                  .view(Plugin.get().getPluginName(), "serviceusers")
-                  .id(serviceUser)
-                  .view("password.http")
-                  .put(
-                      in,
-                      new AsyncCallback<NativeString>() {
-                        @Override
-                        public void onSuccess(NativeString newPassword) {
-                          label.setText(newPassword.asString());
-                          label.setVisible(true);
-                          delete.setVisible(true);
-                        }
-
-                        @Override
-                        public void onFailure(Throwable caught) {
-                          // never invoked
-                        }
-                      });
-            }
-          });
-      p.add(generate);
-      return p;
-    }
-    return new CopyableLabel("");
-  }
-
-  private Widget createOwnerWidget(ServiceUserInfo info, boolean allowOwner) {
-    if (allowOwner) {
-      EditableValue ownerWidget =
-          new EditableValue(
-              info.username(),
-              info.owner() != null ? info.owner().name() : "",
-              info.owner() != null ? info.owner().url() : null) {
-            @Override
-            protected void save(String serviceUser, final String newValue) {
-              new RestApi("config")
-                  .id("server")
-                  .view(Plugin.get().getPluginName(), "serviceusers")
-                  .id(serviceUser)
-                  .view("owner")
-                  .put(
-                      newValue,
-                      new AsyncCallback<GroupInfo>() {
-                        @Override
-                        public void onSuccess(GroupInfo result) {
-                          updateValue(result != null ? result.name() : "");
-                          updateHref(result != null ? result.url() : "");
-                          Plugin.get().refresh();
-                        }
-
-                        @Override
-                        public void onFailure(Throwable caught) {
-                          // never invoked
-                        }
-                      });
-            }
-          };
-      StringBuilder ownerWarning = new StringBuilder();
-      ownerWarning.append("If ");
-      ownerWarning.append(
-          info.owner() != null ? "the owner group is changed" : "an owner group is set");
-      ownerWarning.append(" only members of the ");
-      ownerWarning.append(info.owner() != null ? "new " : "");
-      ownerWarning.append("owner group can see and administrate" + " the service user.");
-      if (info.owner() != null) {
-        ownerWarning.append(
-            " If the owner group is removed only the"
-                + " creator of the service user can see and administrate"
-                + " the service user.");
-      } else {
-        ownerWarning.append(
-            " 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.");
-      }
-      ownerWidget.setWarning(ownerWarning.toString());
-      return ownerWidget;
-    }
-    if (info.owner() != null && info.owner().url() != null) {
-      return new Anchor(info.owner().name(), info.owner().url());
-    }
-    return new Label(info.owner() != null ? info.owner().name() : "");
-  }
-
-  private static class MyTable extends FlexTable {
-    private static int row = 0;
-
-    private void addRow(String label, String value) {
-      setWidget(row, 0, new Label(label + ":"));
-      setWidget(row, 1, new Label(value));
-      row++;
-    }
-
-    private void addRow(String label, Widget w) {
-      setWidget(row, 0, new Label(label + ":"));
-      setWidget(row, 1, w);
-      row++;
-    }
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserSettingsScreen.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserSettingsScreen.java
deleted file mode 100644
index 1755cda..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/ServiceUserSettingsScreen.java
+++ /dev/null
@@ -1,268 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gerrit.plugin.client.Plugin;
-import com.google.gerrit.plugin.client.rpc.RestApi;
-import com.google.gerrit.plugin.client.screen.Screen;
-import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.TextArea;
-import com.google.gwt.user.client.ui.VerticalPanel;
-
-public class ServiceUserSettingsScreen extends VerticalPanel {
-  static class Factory implements Screen.EntryPoint {
-    @Override
-    public void onLoad(Screen screen) {
-      screen.setPageTitle("Service User Administration");
-      screen.show(new ServiceUserSettingsScreen());
-    }
-  }
-
-  private TextArea infoMsgTxt;
-  private TextArea onSuccessMsgTxt;
-  private CheckBox allowEmailCheckBox;
-  private CheckBox allowHttpPasswordCheckBox;
-  private CheckBox allowOwnerCheckBox;
-  private CheckBox createNotesCheckBox;
-  private CheckBox createNotesAsyncCheckBox;
-  private StringListPanel blockedUsernamesPanel;
-  private StringListPanel groupsPanel;
-  private Button saveButton;
-
-  ServiceUserSettingsScreen() {
-    setStyleName("serviceuser-panel");
-
-    new RestApi("config")
-        .id("server")
-        .view(Plugin.get().getPluginName(), "config")
-        .get(
-            new AsyncCallback<ConfigInfo>() {
-              @Override
-              public void onSuccess(ConfigInfo info) {
-                display(info);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                // never invoked
-              }
-            });
-  }
-
-  private void display(ConfigInfo info) {
-    Panel infoMsgPanel = new VerticalPanel();
-    Panel infoMsgTitelPanel = new HorizontalPanel();
-    infoMsgTitelPanel.add(new Label("Info Message"));
-    Image infoMsgInfo = new Image(ServiceUserPlugin.RESOURCES.info());
-    infoMsgInfo.setTitle(
-        "HTML formatted message that should be"
-            + " displayed on the service user creation screen.");
-    infoMsgTitelPanel.add(infoMsgInfo);
-    infoMsgTitelPanel.add(new Label(":"));
-    infoMsgPanel.add(infoMsgTitelPanel);
-    infoMsgTxt = new TextArea();
-    infoMsgTxt.setValue(info.getInfoMessage());
-    infoMsgTxt.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(final KeyPressEvent event) {
-            event.stopPropagation();
-          }
-        });
-    infoMsgTxt.setVisibleLines(12);
-    infoMsgTxt.setCharacterWidth(80);
-    infoMsgTxt.getElement().setPropertyBoolean("spellcheck", false);
-    infoMsgPanel.add(infoMsgTxt);
-    add(infoMsgPanel);
-
-    Panel onSuccessMsgPanel = new VerticalPanel();
-    Panel onSuccessMsgTitelPanel = new HorizontalPanel();
-    onSuccessMsgTitelPanel.add(new Label("On Success Message"));
-    Image onSuccessMsgInfo = new Image(ServiceUserPlugin.RESOURCES.info());
-    onSuccessMsgInfo.setTitle(
-        "HTML formatted message that should be"
-            + " displayed after a service user was successfully created.");
-    onSuccessMsgTitelPanel.add(onSuccessMsgInfo);
-    onSuccessMsgTitelPanel.add(new Label(":"));
-    onSuccessMsgPanel.add(onSuccessMsgTitelPanel);
-    onSuccessMsgTxt = new TextArea();
-    onSuccessMsgTxt.setValue(info.getOnSuccessMessage());
-    onSuccessMsgTxt.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(final KeyPressEvent event) {
-            event.stopPropagation();
-          }
-        });
-    onSuccessMsgTxt.setVisibleLines(12);
-    onSuccessMsgTxt.setCharacterWidth(80);
-    onSuccessMsgTxt.getElement().setPropertyBoolean("spellcheck", false);
-    onSuccessMsgPanel.add(onSuccessMsgTxt);
-    add(onSuccessMsgPanel);
-
-    Panel allowEmailPanel = new HorizontalPanel();
-    allowEmailCheckBox = new CheckBox("Allow Email Address");
-    allowEmailCheckBox.setValue(info.getAllowEmail());
-    allowEmailPanel.add(allowEmailCheckBox);
-    Image allowEmailInfo = new Image(ServiceUserPlugin.RESOURCES.info());
-    allowEmailInfo.setTitle(
-        "Whether it is allowed to provide an email address "
-            + "for a service user. E.g. having an email address allows a service user "
-            + "to push commits and tags.");
-    allowEmailPanel.add(allowEmailInfo);
-    add(allowEmailPanel);
-
-    Panel allowHttpPasswordPanel = new HorizontalPanel();
-    allowHttpPasswordCheckBox = new CheckBox("Allow HTTP password");
-    allowHttpPasswordCheckBox.setValue(info.getAllowHttpPassword());
-    allowHttpPasswordPanel.add(allowHttpPasswordCheckBox);
-    Image allowHttpPasswordInfo = new Image(ServiceUserPlugin.RESOURCES.info());
-    allowHttpPasswordInfo.setTitle(
-        "Whether it is allowed to generate an HTTP password "
-            + "for a service user. E.g. having an HTTP password allows a service user "
-            + "to use the Gerrit REST API.");
-    allowHttpPasswordPanel.add(allowHttpPasswordInfo);
-    add(allowHttpPasswordPanel);
-
-    Panel allowOwnerPanel = new HorizontalPanel();
-    allowOwnerCheckBox = new CheckBox("Allow Owner Group");
-    allowOwnerCheckBox.setValue(info.getAllowOwner());
-    allowOwnerPanel.add(allowOwnerCheckBox);
-    Image allowOwnerInfo = new Image(ServiceUserPlugin.RESOURCES.info());
-    allowOwnerInfo.setTitle("Whether it is allowed to set an owner group " + "for a service user.");
-    allowOwnerPanel.add(allowOwnerInfo);
-    add(allowOwnerPanel);
-
-    Panel createNotesPanel = new HorizontalPanel();
-    createNotesCheckBox = new CheckBox("Create Git Notes");
-    createNotesCheckBox.setValue(info.getCreateNotes());
-    createNotesPanel.add(createNotesCheckBox);
-    Image createNotesInfo = new Image(ServiceUserPlugin.RESOURCES.info());
-    createNotesInfo.setTitle(
-        "Whether commits of a service user should be "
-            + "annotated by a Git note that contains information about the current "
-            + "owners of the service user. This allows to find a real person that "
-            + "is responsible for this commit. To get such a Git note for each commit "
-            + "of a service user the 'Forge Committer' access right must be blocked "
-            + "for service users.");
-    createNotesPanel.add(createNotesInfo);
-    add(createNotesPanel);
-
-    Panel createNotesAsyncPanel = new HorizontalPanel();
-    createNotesAsyncCheckBox = new CheckBox("Create Git Notes Asynchronously");
-    createNotesAsyncCheckBox.setValue(info.getCreateNotesAsync());
-    createNotesAsyncCheckBox.setEnabled(info.getCreateNotes());
-    createNotesAsyncPanel.add(createNotesAsyncCheckBox);
-    Image createNotesAsyncInfo = new Image(ServiceUserPlugin.RESOURCES.info());
-    createNotesAsyncInfo.setTitle(
-        "Whether the Git notes on commits that are "
-            + "pushed by a service user should be created asynchronously.");
-    createNotesAsyncPanel.add(createNotesAsyncInfo);
-    add(createNotesAsyncPanel);
-
-    createNotesCheckBox.addValueChangeHandler(
-        new ValueChangeHandler<Boolean>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<Boolean> event) {
-            createNotesAsyncCheckBox.setEnabled(event.getValue());
-          }
-        });
-
-    saveButton = new Button("Save");
-    saveButton.addStyleName("serviceuser-saveButton");
-    saveButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            doSave();
-          }
-        });
-
-    blockedUsernamesPanel =
-        new StringListPanel("Blocked Usernames", "Username", info.getBlockedNames(), saveButton);
-    blockedUsernamesPanel.setInfo(
-        "List of usernames which are "
-            + "forbidden to be used as name for a service user. "
-            + "The blocked usernames are case insensitive.");
-    add(blockedUsernamesPanel);
-
-    groupsPanel =
-        new StringListPanel("Groups", "Group Name", info.getGroups().keySet(), saveButton);
-    groupsPanel.setInfo(
-        "Names of groups to which newly created " + "service users should be added automatically.");
-    add(groupsPanel);
-
-    HorizontalPanel buttons = new HorizontalPanel();
-    add(buttons);
-
-    buttons.add(saveButton);
-    saveButton.setEnabled(false);
-    OnEditEnabler onEditEnabler = new OnEditEnabler(saveButton, infoMsgTxt);
-    onEditEnabler.listenTo(onSuccessMsgTxt);
-    onEditEnabler.listenTo(allowEmailCheckBox);
-    onEditEnabler.listenTo(allowHttpPasswordCheckBox);
-    onEditEnabler.listenTo(allowOwnerCheckBox);
-    onEditEnabler.listenTo(createNotesCheckBox);
-    onEditEnabler.listenTo(createNotesAsyncCheckBox);
-
-    infoMsgTxt.setFocus(true);
-    saveButton.setEnabled(false);
-  }
-
-  private void doSave() {
-    ConfigInfo in = ConfigInfo.create();
-    in.setInfoMessage(infoMsgTxt.getValue());
-    in.setOnSuccessMessage(onSuccessMsgTxt.getValue());
-    in.setAllowEmail(allowEmailCheckBox.getValue());
-    in.setAllowHttpPassword(allowHttpPasswordCheckBox.getValue());
-    in.setAllowOwner(allowOwnerCheckBox.getValue());
-    in.setCreateNotes(createNotesCheckBox.getValue());
-    if (createNotesAsyncCheckBox.isEnabled()) {
-      in.setCreateNotesAsync(createNotesAsyncCheckBox.getValue());
-    }
-    in.setBlockedNames(blockedUsernamesPanel.getValues());
-    in.setGroups(groupsPanel.getValues());
-    new RestApi("config")
-        .id("server")
-        .view(Plugin.get().getPluginName(), "config")
-        .put(
-            in,
-            new AsyncCallback<JavaScriptObject>() {
-
-              @Override
-              public void onSuccess(JavaScriptObject result) {
-                saveButton.setEnabled(false);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                // never invoked
-              }
-            });
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java
deleted file mode 100644
index 9845b23..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyHelpPanel.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gwt.user.client.ui.DisclosurePanel;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTML;
-
-public class SshKeyHelpPanel extends FlowPanel {
-
-  SshKeyHelpPanel() {
-    DisclosurePanel dp = new DisclosurePanel("How to generate an SSH Key");
-    StringBuilder b = new StringBuilder();
-    b.append("<ol>")
-        .append("<li>From the Terminal or Git Bash, run <em>ssh-keygen</em></li>")
-        .append("<li>")
-        .append(
-            "Enter a path for the key, e.g. <em>id_rsa</em>. If you are generating the key<br />")
-        .append("on your local system take care to not overwrite your own SSH key.")
-        .append("</li>")
-        .append("<li>")
-        .append("Enter a passphrase only if the service where you intend to use this<br />")
-        .append("service user is able to deal with passphrases, otherwise leave it blank.<br />")
-        .append("Remember this passphrase, as you will need it to unlock the key.")
-        .append("</li>")
-        .append("<li>")
-        .append(
-            "Open <em>id_rsa.pub</em> and copy &amp; paste the contents into the box below.<br />")
-        .append("Note that <em>id_rsa.pub</em> is your public key and can be shared,<br />")
-        .append("while <em>id_rsa</em> is your private key and should be kept secret.")
-        .append("</li>")
-        .append("</ol>");
-    dp.add(new HTML(b.toString()));
-    add(dp);
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java
deleted file mode 100644
index 52a7044..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshKeyInfo.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-public class SshKeyInfo extends JavaScriptObject {
-  public final native int seq() /*-{ return this.seq || 0; }-*/;
-
-  public final native String sshPublicKey() /*-{ return this.ssh_public_key; }-*/;
-
-  public final native String encodedKey() /*-{ return this.encoded_key; }-*/;
-
-  public final native String algorithm() /*-{ return this.algorithm; }-*/;
-
-  public final native String comment() /*-{ return this.comment; }-*/;
-
-  public final native boolean isValid() /*-{ return this['valid'] ? true : false; }-*/;
-
-  protected SshKeyInfo() {}
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java
deleted file mode 100644
index 10acc65..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/SshPanel.java
+++ /dev/null
@@ -1,382 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gerrit.plugin.client.Plugin;
-import com.google.gerrit.plugin.client.rpc.NoContent;
-import com.google.gerrit.plugin.client.rpc.RestApi;
-import com.google.gwt.core.client.JsArray;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HasHorizontalAlignment;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.clippy.client.CopyableLabel;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-
-class SshPanel extends Composite {
-  private final String serviceUser;
-
-  private SshKeyTable keys;
-
-  private Button showAddKeyBlock;
-  private Panel addKeyBlock;
-  private Button closeAddKeyBlock;
-  private Button clearNew;
-  private Button addNew;
-  private NpTextArea addTxt;
-  private Button deleteKey;
-
-  private Panel serverKeys;
-
-  private int loadCount;
-
-  SshPanel(String serviceUser) {
-    this.serviceUser = serviceUser;
-
-    FlowPanel body = new FlowPanel();
-
-    showAddKeyBlock = new Button("Add Key ...");
-    showAddKeyBlock.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            showAddKeyBlock(true);
-          }
-        });
-
-    keys = new SshKeyTable();
-    body.add(keys);
-    {
-      final FlowPanel fp = new FlowPanel();
-      deleteKey = new Button("Delete");
-      deleteKey.setEnabled(false);
-      deleteKey.addClickHandler(
-          new ClickHandler() {
-            @Override
-            public void onClick(final ClickEvent event) {
-              keys.deleteChecked();
-            }
-          });
-      fp.add(deleteKey);
-      fp.add(showAddKeyBlock);
-      body.add(fp);
-    }
-
-    addKeyBlock = new VerticalPanel();
-    addKeyBlock.setVisible(false);
-    addKeyBlock.setStyleName("serviceuser-addSshKeyPanel");
-    addKeyBlock.add(new Label("Add SSH Public Key"));
-    addKeyBlock.add(new SshKeyHelpPanel());
-
-    addTxt = new NpTextArea();
-    addTxt.setVisibleLines(12);
-    addTxt.setCharacterWidth(80);
-    addTxt.setSpellCheck(false);
-    addKeyBlock.add(addTxt);
-
-    final HorizontalPanel buttons = new HorizontalPanel();
-    addKeyBlock.add(buttons);
-
-    clearNew = new Button("Clear");
-    clearNew.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            addTxt.setText("");
-            addTxt.setFocus(true);
-          }
-        });
-    buttons.add(clearNew);
-
-    addNew = new Button("Add");
-    addNew.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            doAddNew();
-          }
-        });
-    buttons.add(addNew);
-
-    closeAddKeyBlock = new Button("Close");
-    closeAddKeyBlock.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            showAddKeyBlock(false);
-          }
-        });
-    buttons.add(closeAddKeyBlock);
-    buttons.setCellWidth(closeAddKeyBlock, "100%");
-    buttons.setCellHorizontalAlignment(closeAddKeyBlock, HasHorizontalAlignment.ALIGN_RIGHT);
-
-    body.add(addKeyBlock);
-
-    serverKeys = new FlowPanel();
-    body.add(serverKeys);
-
-    initWidget(body);
-  }
-
-  void setKeyTableVisible(final boolean on) {
-    keys.setVisible(on);
-    deleteKey.setVisible(on);
-    closeAddKeyBlock.setVisible(on);
-  }
-
-  void doAddNew() {
-    final String txt = addTxt.getText();
-    if (txt != null && txt.length() > 0) {
-      new RestApi("config")
-          .id("server")
-          .view(Plugin.get().getPluginName(), "serviceusers")
-          .id(serviceUser)
-          .view("sshkeys")
-          .post(
-              txt,
-              new AsyncCallback<SshKeyInfo>() {
-                @Override
-                public void onSuccess(SshKeyInfo k) {
-                  addTxt.setText("");
-                  keys.addOneKey(k);
-                  if (!keys.isVisible()) {
-                    showAddKeyBlock(false);
-                    setKeyTableVisible(true);
-                    keys.updateDeleteButton();
-                  }
-                }
-
-                @Override
-                public void onFailure(final Throwable caught) {
-                  // never invoked
-                }
-              });
-    }
-  }
-
-  @Override
-  protected void onLoad() {
-    super.onLoad();
-    refreshSshKeys();
-  }
-
-  private void refreshSshKeys() {
-    new RestApi("config")
-        .id("server")
-        .view(Plugin.get().getPluginName(), "serviceusers")
-        .id(serviceUser)
-        .view("sshkeys")
-        .get(
-            new AsyncCallback<JsArray<SshKeyInfo>>() {
-              @Override
-              public void onSuccess(JsArray<SshKeyInfo> result) {
-                keys.display(Natives.asList(result));
-                if (result.length() == 0 && keys.isVisible()) {
-                  showAddKeyBlock(true);
-                }
-                if (++loadCount == 2) {
-                  display();
-                }
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                // never invoked
-              }
-            });
-  }
-
-  void display() {}
-
-  private void showAddKeyBlock(boolean show) {
-    showAddKeyBlock.setVisible(!show);
-    addKeyBlock.setVisible(show);
-  }
-
-  private class SshKeyTable extends FlexTable {
-    private final Map<Integer, SshKeyInfo> sshKeyInfos;
-    private ValueChangeHandler<Boolean> updateDeleteHandler;
-
-    SshKeyTable() {
-      this.sshKeyInfos = new HashMap<>();
-      setStyleName("serviceuser-sshKeyTable");
-      setWidth("");
-      setText(0, 2, "Status");
-      setText(0, 3, "Algorithm");
-      setText(0, 4, "Key");
-      setText(0, 5, "Comment");
-
-      FlexCellFormatter fmt = getFlexCellFormatter();
-      fmt.addStyleName(0, 1, "iconHeader");
-      fmt.addStyleName(0, 2, "dataHeader");
-      fmt.addStyleName(0, 3, "dataHeader");
-      fmt.addStyleName(0, 4, "dataHeader");
-      fmt.addStyleName(0, 5, "dataHeader");
-
-      fmt.addStyleName(0, 1, "topMostCell");
-      fmt.addStyleName(0, 2, "topMostCell");
-      fmt.addStyleName(0, 3, "topMostCell");
-      fmt.addStyleName(0, 4, "topMostCell");
-      fmt.addStyleName(0, 5, "topMostCell");
-
-      updateDeleteHandler =
-          new ValueChangeHandler<Boolean>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<Boolean> event) {
-              updateDeleteButton();
-            }
-          };
-    }
-
-    void deleteChecked() {
-      final HashSet<Integer> sequenceNumbers = new HashSet<>();
-      for (int row = 1; row < getRowCount(); row++) {
-        SshKeyInfo k = getRowItem(row);
-        if (k != null && ((CheckBox) getWidget(row, 1)).getValue()) {
-          sequenceNumbers.add(k.seq());
-        }
-      }
-      if (sequenceNumbers.isEmpty()) {
-        updateDeleteButton();
-      } else {
-        for (int seq : sequenceNumbers) {
-          new RestApi("config")
-              .id("server")
-              .view(Plugin.get().getPluginName(), "serviceusers")
-              .id(serviceUser)
-              .view("sshkeys")
-              .id(seq)
-              .delete(
-                  new AsyncCallback<NoContent>() {
-                    @Override
-                    public void onSuccess(NoContent result) {
-                      for (int row = 1; row < getRowCount(); ) {
-                        SshKeyInfo k = getRowItem(row);
-                        if (k != null && sequenceNumbers.contains(k.seq())) {
-                          removeRow(row);
-                        } else {
-                          row++;
-                        }
-                      }
-                      if (getRowCount() == 1) {
-                        display(Collections.<SshKeyInfo>emptyList());
-                      } else {
-                        updateDeleteButton();
-                      }
-                    }
-
-                    @Override
-                    public void onFailure(Throwable caught) {
-                      // never invoked
-                    }
-                  });
-        }
-      }
-    }
-
-    void display(List<SshKeyInfo> result) {
-      if (result.isEmpty()) {
-        setKeyTableVisible(false);
-        showAddKeyBlock(true);
-      } else {
-        while (1 < getRowCount()) removeRow(getRowCount() - 1);
-        for (SshKeyInfo k : result) {
-          addOneKey(k);
-        }
-        setKeyTableVisible(true);
-        deleteKey.setEnabled(false);
-      }
-    }
-
-    void addOneKey(SshKeyInfo k) {
-      FlexCellFormatter fmt = getFlexCellFormatter();
-      int row = getRowCount();
-      insertRow(row);
-      getCellFormatter().addStyleName(row, 0, "iconCell");
-      getCellFormatter().addStyleName(row, 0, "leftMostCell");
-
-      CheckBox sel = new CheckBox();
-      sel.addValueChangeHandler(updateDeleteHandler);
-
-      setWidget(row, 1, sel);
-      if (k.isValid()) {
-        setText(row, 2, "");
-        fmt.removeStyleName(row, 2, "serviceuser-sshKeyPanelInvalid");
-      } else {
-        setText(row, 2, "Invalid Key");
-        fmt.addStyleName(row, 2, "serviceuser-sshKeyPanelInvalid");
-      }
-      setText(row, 3, k.algorithm());
-
-      CopyableLabel keyLabel = new CopyableLabel(k.sshPublicKey());
-      keyLabel.setPreviewText(elide(k.encodedKey(), 40));
-      setWidget(row, 4, keyLabel);
-
-      setText(row, 5, k.comment());
-
-      fmt.addStyleName(row, 1, "iconCell");
-      fmt.addStyleName(row, 4, "serviceuser-sshKeyPanelEncodedKey");
-      for (int c = 2; c <= 5; c++) {
-        fmt.addStyleName(row, c, "dataCell");
-      }
-
-      setRowItem(row, k);
-    }
-
-    void updateDeleteButton() {
-      boolean on = false;
-      for (int row = 1; row < getRowCount(); row++) {
-        CheckBox sel = (CheckBox) getWidget(row, 1);
-        if (sel.getValue()) {
-          on = true;
-          break;
-        }
-      }
-      deleteKey.setEnabled(on);
-    }
-
-    private SshKeyInfo getRowItem(int row) {
-      return sshKeyInfos.get(row);
-    }
-
-    private void setRowItem(int row, SshKeyInfo sshKeyInfo) {
-      sshKeyInfos.put(row, sshKeyInfo);
-    }
-  }
-
-  static String elide(String s, int len) {
-    if (s == null || s.length() < len || len <= 10) {
-      return s;
-    }
-    return s.substring(0, len - 10) + "..." + s.substring(s.length() - 10);
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/StringListPanel.java b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/StringListPanel.java
deleted file mode 100644
index c2fff99..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/client/StringListPanel.java
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright (C) 2014 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.client;
-
-import com.google.gerrit.client.rpc.Natives;
-import com.google.gwt.core.client.JsArrayString;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlexTable;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FocusWidget;
-import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-
-public class StringListPanel extends FlowPanel {
-  private final NpTextBox input;
-  private final StringListTable t;
-  private final Button deleteButton;
-  private final HorizontalPanel titlePanel;
-  private Image info;
-
-  StringListPanel(String title, String fieldName, JsArrayString values, final FocusWidget w) {
-    this(title, fieldName, Natives.asList(values), w);
-  }
-
-  StringListPanel(String title, String fieldName, Collection<String> values, final FocusWidget w) {
-    titlePanel = new HorizontalPanel();
-    Label titleLabel = new Label(title);
-    titleLabel.setStyleName("serviceuser-smallHeading");
-    titlePanel.add(titleLabel);
-    add(titlePanel);
-    input = new NpTextBox();
-    input.addKeyPressHandler(
-        new KeyPressHandler() {
-          @Override
-          public void onKeyPress(KeyPressEvent event) {
-            if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-              w.setEnabled(true);
-              add();
-            }
-          }
-        });
-    HorizontalPanel p = new HorizontalPanel();
-    p.add(input);
-    Button addButton = new Button("Add");
-    addButton.setEnabled(false);
-    new OnEditEnabler(addButton, input);
-    addButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            w.setEnabled(true);
-            add();
-          }
-        });
-    p.add(addButton);
-    add(p);
-
-    t = new StringListTable(fieldName);
-    add(t);
-
-    deleteButton = new Button("Delete");
-    deleteButton.setEnabled(false);
-    add(deleteButton);
-    deleteButton.addClickHandler(
-        new ClickHandler() {
-          @Override
-          public void onClick(ClickEvent event) {
-            w.setEnabled(true);
-            t.deleteChecked();
-          }
-        });
-
-    t.display(values);
-  }
-
-  void setInfo(String msg) {
-    if (info == null) {
-      info = new Image(ServiceUserPlugin.RESOURCES.info());
-      titlePanel.add(info);
-    }
-    info.setTitle(msg);
-  }
-
-  List<String> getValues() {
-    return t.getValues();
-  }
-
-  private void add() {
-    String v = input.getValue().trim();
-    if (!v.isEmpty()) {
-      input.setValue("");
-      t.insert(v);
-    }
-  }
-
-  private class StringListTable extends FlexTable {
-    StringListTable(String name) {
-      setStyleName("serviceuser-stringListTable");
-      FlexCellFormatter fmt = getFlexCellFormatter();
-      fmt.addStyleName(0, 0, "iconHeader");
-      fmt.addStyleName(0, 0, "topMostCell");
-      fmt.addStyleName(0, 0, "leftMostCell");
-      fmt.addStyleName(0, 1, "dataHeader");
-      fmt.addStyleName(0, 1, "topMostCell");
-
-      setText(0, 1, name);
-    }
-
-    void display(Collection<String> values) {
-      int row = 1;
-      for (String v : values) {
-        populate(row, v);
-        row++;
-      }
-    }
-
-    List<String> getValues() {
-      List<String> values = new ArrayList<>();
-      for (int row = 1; row < getRowCount(); row++) {
-        values.add(getText(row, 1));
-      }
-      return values;
-    }
-
-    private void populate(int row, String value) {
-      FlexCellFormatter fmt = getFlexCellFormatter();
-      fmt.addStyleName(row, 0, "leftMostCell");
-      fmt.addStyleName(row, 0, "iconCell");
-      fmt.addStyleName(row, 1, "dataCell");
-
-      CheckBox checkBox = new CheckBox();
-      checkBox.addValueChangeHandler(
-          new ValueChangeHandler<Boolean>() {
-            @Override
-            public void onValueChange(ValueChangeEvent<Boolean> event) {
-              enableDelete();
-            }
-          });
-      setWidget(row, 0, checkBox);
-      setText(row, 1, value);
-    }
-
-    void insert(String v) {
-      int insertPos = getRowCount();
-      for (int row = 1; row < getRowCount(); row++) {
-        int compareResult = v.compareTo(getText(row, 1));
-        if (compareResult < 0) {
-          insertPos = row;
-          break;
-        } else if (compareResult == 0) {
-          return;
-        }
-      }
-      insertRow(insertPos);
-      populate(insertPos, v);
-    }
-
-    void enableDelete() {
-      for (int row = 1; row < getRowCount(); row++) {
-        if (((CheckBox) getWidget(row, 0)).getValue()) {
-          deleteButton.setEnabled(true);
-          return;
-        }
-      }
-      deleteButton.setEnabled(false);
-    }
-
-    void deleteChecked() {
-      deleteButton.setEnabled(false);
-      for (int row = 1; row < getRowCount(); row++) {
-        if (((CheckBox) getWidget(row, 0)).getValue()) {
-          removeRow(row--);
-        }
-      }
-    }
-  }
-}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css b/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
deleted file mode 100644
index c996fa0..0000000
--- a/src/main/java/com/googlesource/gerrit/plugins/serviceuser/public/serviceuser.css
+++ /dev/null
@@ -1,141 +0,0 @@
-.serviceuser-panel {
-  border-spacing: 0px 5px;
-}
-
-.serviceuser-createButton, .serviceuser-saveButton {
-  margin-left: 10px !important;
-}
-
-.serviceuser-serviceUserTable,
-.serviceuser-sshKeyTable,
-.serviceuser-stringListTable {
-  border-collapse: separate;
-  border-spacing: 0;
-}
-
-.serviceuser-serviceUserTable .leftMostCell,
-.serviceuser-sshKeyTable .leftMostCell,
-.serviceuser-stringListTable .leftMostCell {
-  border-left: 1px solid #EEE;
-}
-
-.serviceuser-serviceUserTable .topMostCell,
-.serviceuser-sshKeyTable .topMostCell,
-.serviceuser-stringListTable .topMostCell {
-  border-top: 1px solid #EEE;
-}
-
-.serviceuser-serviceUserTable .dataHeader,
-.serviceuser-sshKeyTable .dataHeader,
-.serviceuser-stringListTable .dataHeader {
-  border: 1px solid #FFF;
-  padding: 2px 6px 1px;
-  background-color: #EEE;
-  font-style: italic;
-  white-space: nowrap;
-  color: textColor;
-}
-
-.serviceuser-sshKeyTable .iconHeader,
-.serviceuser-stringListTable .iconHeader {
-  border-top: 1px solid #FFF;
-  border-bottom: 1px solid #FFF;
-  background-color: #EEE;
-}
-
-.serviceuser-serviceUserTable .dataCell,
-.serviceuser-sshKeyTable .dataCell,
-.serviceuser-stringListTable .dataCell {
-  padding-left: 5px;
-  padding-right: 5px;
-  border-right: 1px solid #EEE;
-  border-bottom: 1px solid #EEE;
-  vertical-align: middle;
-  height: 20px;
-}
-
-.serviceuser-sshKeyTable .iconCell,
-.serviceuser-stringListTable .iconCell {
-  width: 1px;
-  padding: 0px;
-  vertical-align: middle;
-  border-bottom: 1px solid #EEE;
-}
-
-.serviceuser-sshKeyPanelEncodedKey {
-  white-space: nowrap;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  font-family: mono-font;
-  font-size: small;
-}
-
-.serviceuser-sshKeyPanelInvalid {
-  white-space: nowrap;
-  color: red;
-  font-weight: bold;
-}
-
-.serviceuser-addSshKeyPanel {
-  margin-top: 10px;
-  background-color: #EEE;
-  padding: 5px 5px 5px 5px;
-}
-
-.serviceuser-serviceUserInfoTable {
-  margin-bottom: 10px;
-}
-
-.serviceuser-editButton,
-.serviceuser-deleteButton,
-.serviceuser-generateButton {
-  float: right;
-  cursor: pointer;
-  margin-left: 2px;
-}
-
-.serviceuser-toggleButton {
-  position: relative;
-  height: 19px;
-  width: 140px;
-  background: #FFF;
-  color: #000;
-  text-shadow: none;
-  border: 1px solid #EEE !important;
-}
-.serviceuser-toggleButton .html-face {
-  position: absolute;
-  top: 0;
-  width: 68px;
-  height: 17px;
-  line-height: 17px;
-  text-align: center;
-  border-width: 1px;
-}
-
-.serviceuser-toggleButton-up,
-.serviceuser-toggleButton-up-hovering,
-.serviceuser-toggleButton-up-disabled,
-.serviceuser-toggleButton-down,
-.serviceuser-toggleButton-down-hovering,
-.serviceuser-toggleButton-down-disabled {
-  padding: 0;
-  border: 0;
-}
-.serviceuser-toggleButton-up .html-face,
-.serviceuser-toggleButton-up-hovering .html-face {
-  left: 0;
-  background: #FCB;
-  border-style: outset;
-}
-.serviceuser-toggleButton-down .html-face,
-.serviceuser-toggleButton-down-hovering .html-face {
-  right: 0;
-  background: #BFC;
-  border-style: inset;
-}
-
-.serviceuser-smallHeading {
-  margin-top: 5px;
-  font-weight: bold;
-}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index e5ad4c7..17a4e29 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -6,8 +6,8 @@
 in Jenkins. A service user is not able to login into the Gerrit WebUI
 and it cannot push commits or tags.
 
-This plugin supports the creation of service users via [SSH](cmd-create.md),
-[REST](rest-api-config.md) and in the [WebUI](#webui).
+This plugin supports the creation of service users via [SSH](cmd-create.md) and
+[REST](rest-api-config.md).
 
 To create a service user a user must be a member of a group that is
 granted the 'Create Service User' capability (provided by this plugin)
@@ -20,12 +20,6 @@
 For each created service user the plugin stores some
 [properties](#properties).
 
-<a id="webui"></a>
-Create Service User in WebUI
-----------------------------
-In the `People` top menu there is a menu item `Create Service User`
-that opens a dialog for creating a service user.
-
 <a id="properties"></a>
 Service User Properties
 -----------------------
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index 3dd3a01..49709ef 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -19,6 +19,13 @@
   bazel-bin/plugins/@PLUGIN@/@PLUGIN@.jar
 ```
 
+To execute the tests run either one of:
+
+```
+  bazel test --test_tag_filters=@PLUGIN@ //...
+  bazel test plugins/@PLUGIN@:@PLUGIN@_tests
+```
+
 This project can be imported into the Eclipse IDE.
 Add the plugin name to the `CUSTOM_PLUGINS` set in
 Gerrit core in `tools/bzl/plugins.bzl`, and execute:
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index 1380b73..f52421e 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -29,8 +29,8 @@
 
 <a id="onSuccessMessage">
 `plugin.@PLUGIN@.onSuccessMessage`
-:	HTML formatted message that should be displayed after a service
-	user was successfully created.
+:	Message that should be displayed after a service user was
+	successfully created.
 
 <a id="allowEmail">
 `plugin.@PLUGIN@.allowEmail`
diff --git a/src/main/resources/Documentation/rest-api-config.md b/src/main/resources/Documentation/rest-api-config.md
index 302be07..3999e60 100644
--- a/src/main/resources/Documentation/rest-api-config.md
+++ b/src/main/resources/Documentation/rest-api-config.md
@@ -706,8 +706,8 @@
 
 * _info_: HTML formatted message that should be displayed on the
   service user creation screen.
-* _on\_success_: HTML formatted message that should be displayed after
-  a service user was successfully created.
+* _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
@@ -733,8 +733,8 @@
 
 * _info_: HTML formatted message that should be displayed on the
   service user creation screen.
-* _on\_success_: HTML formatted message that should be displayed after
-  a service user was successfully created.
+* _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..763c47d
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-create.html
@@ -0,0 +1,91 @@
+<!--
+@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-click="_handleCreateServiceUser"
+                 disabled="[[!_enableButton]]">
+        Create
+      </gr-button>
+      <gr-overlay id="successDialogOverlay" with-backdrop>
+        <gr-dialog id="successDialog"
+                   confirm-label="OK"
+                   cancel-label=""
+                   on-confirm="_forwardToDetails"
+                   confirm-on-enter>
+          <div slot="header">
+            Success
+          </div>
+          <div id="successMessage" slot="main">
+          </div>
+        </gr-dialog>
+      </gr-overlay>
+    </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..df5d9a6
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-create.js
@@ -0,0 +1,150 @@
+/**
+ * @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,
+      },
+      _accountId: String,
+    },
+
+    attached() {
+      this._getConfig();
+    },
+
+    _forwardToDetails() {
+      page.show(
+          this.plugin.screenUrl()
+          + '/user/'
+          + this._accountId);
+    },
+
+    _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.$.successMessage.innerHTML = this._successMessage;
+            }
+
+            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 => {
+            this._accountId = response._account_id;
+            if (this._successMessage) {
+              this.$.successDialogOverlay.open();
+            } else {
+              this._forwardToDetails();
+            }
+          }).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..28d1d93
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-detail.html
@@ -0,0 +1,163 @@
+<!--
+@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.value {
+        width: 50%;
+      }
+
+      input.wide {
+        width: 100%;
+      }
+
+      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-click="_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" class="wide" 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>
+                  <span class="value" hidden$="[[!_allowEmail]]">
+                    <input id="serviceUserEmailInput" class="wide" bind-value="{{_newEmail}}"
+                      is="iron-input" type="text" disabled="[[_changingPrefs]]"
+                      placeholder="[[_serviceUser.email]]" on-keyup="_computePrefsChanged">
+                  </span>
+                  <span class="value" hidden$="[[_allowEmail]]">[[_serviceUser.email]]</span>
+                </section>
+                <section>
+                  <span class="title">Owner Group</span>
+                  <span class="value" hidden$="[[!_allowOwner]]">
+                    <gr-autocomplete id="serviceUserOwnerInput" text="{{_getOwnerGroup(_serviceUser)}}"
+                      value="{{_newOwner}}" query="[[_query]]" disabled="[[_changingPrefs]]"
+                      on-commit="_computePrefsChanged" on-keyup="_computePrefsChanged">
+                        [[_getOwnerGroup(_serviceUser)]]
+                    </gr-autocomplete>
+                  </span>
+                  <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-click="_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..95aded2
--- /dev/null
+++ b/src/main/resources/static/gr-serviceuser-detail.js
@@ -0,0 +1,345 @@
+// 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;
+        this._newFullName = this._serviceUser.name;
+        this._newEmail = this._serviceUser.email;
+      });
+    },
+
+    _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..8cebbe5
--- /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-click="_handleGenerateTap">Generate new password</gr-button>
+        <gr-button id="deleteButton"
+                   on-click="_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-click="_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..e927d63
--- /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-click="_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..2906a10
--- /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-click="_showKey"
+                             data-index$="[[index]]"
+                             link>Click to View</gr-button>
+                </td>
+                <td>
+                  <gr-button link
+                             data-index$="[[index]]"
+                             on-click="_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-click="_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-click="_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 9568c84..4d2dbdd 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -1,10 +1,10 @@
 load(
     "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
     _gerrit_plugin = "gerrit_plugin",
-    _gwt_plugin_deps = "GWT_PLUGIN_DEPS",
     _plugin_deps = "PLUGIN_DEPS",
+    _plugin_test_deps = "PLUGIN_TEST_DEPS",
 )
 
 gerrit_plugin = _gerrit_plugin
-GWT_PLUGIN_DEPS = _gwt_plugin_deps
 PLUGIN_DEPS = _plugin_deps
+PLUGIN_TEST_DEPS = _plugin_test_deps
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
index 9b694c1..a38b222 100644
--- a/tools/eclipse/BUILD
+++ b/tools/eclipse/BUILD
@@ -1,16 +1,10 @@
 load("//tools/bzl:classpath.bzl", "classpath_collector")
-load(
-    "//tools/bzl:plugin.bzl",
-    "GWT_PLUGIN_DEPS",
-    "PLUGIN_DEPS",
-)
+load("//tools/bzl:plugin.bzl", "PLUGIN_DEPS")
 
 classpath_collector(
     name = "main_classpath_collect",
     testonly = 1,
-    deps = PLUGIN_DEPS + GWT_PLUGIN_DEPS + [
-        "//external:gwt-dev",
-        "//external:gwt-user",
+    deps = PLUGIN_DEPS + [
         "//:serviceuser__plugin",
     ],
 )