Merge "Only limit width of prose text in message list"
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 62f7fd2..eed2eb4 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -82,6 +82,9 @@
 link:cmd-set-head.html[gerrit set-head]::
 	Change the HEAD reference of a project.
 
+link:cmd-set-project.html[gerrit set-project]::
+	Change a project's settings.
+
 link:cmd-set-reviewers.html[gerrit set-reviewers]::
 	Add or remove reviewers on a change.
 
@@ -172,9 +175,6 @@
 link:cmd-set-members.html[gerrit set-members]::
 	Set group members.
 
-link:cmd-set-project.html[gerrit set-project]::
-	Change a project's settings.
-
 link:cmd-set-project-parent.html[gerrit set-project-parent]::
 	Change the project permissions are inherited from.
 
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 823424e..405b1d7 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -154,8 +154,8 @@
 To format Java source code, Gerrit uses the
 link:https://github.com/google/google-java-format[`google-java-format`]
 tool (version 1.3), and to format Bazel BUILD and WORKSPACE files the
-link:https://github.com/bazelbuild/buildifier[`buildifier`] tool. These
-tools automatically apply format according to the style guides; this
+link:https://github.com/bazelbuild/buildifier[`buildifier`] tool (version 0.4.5).
+These tools automatically apply format according to the style guides; this
 streamlines code review by reducing the need for time-consuming, tedious,
 and contentious discussions about trivial issues like whitespace.
 
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
index ade1287..42b1014 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -494,12 +494,22 @@
 
   protected TestRepository<InMemoryRepository> cloneProject(
       Project.NameKey p, TestAccount testAccount) throws Exception {
+    return GitUtil.cloneProject(p, registerRepoConnection(p, testAccount));
+  }
+
+  /**
+   * Register a repository connection over the test protocol.
+   *
+   * @return a URI string that can be used to connect to this repository for both fetch and push.
+   */
+  protected String registerRepoConnection(Project.NameKey p, TestAccount testAccount)
+      throws Exception {
     InProcessProtocol.Context ctx =
         new InProcessProtocol.Context(
             reviewDbProvider, identifiedUserFactory, testAccount.getId(), p);
     Repository repo = repoManager.openRepository(p);
     toClose.add(repo);
-    return GitUtil.cloneProject(p, inProcessProtocol.register(ctx, repo).toString());
+    return inProcessProtocol.register(ctx, repo).toString();
   }
 
   protected void afterTest() throws Exception {
diff --git a/gerrit-acceptance-tests/BUILD b/gerrit-acceptance-tests/BUILD
index 9fb5e67..c593783 100644
--- a/gerrit-acceptance-tests/BUILD
+++ b/gerrit-acceptance-tests/BUILD
@@ -3,8 +3,8 @@
 java_library(
     name = "lib",
     testonly = 1,
-    resources = RESOURCES,
     srcs = ["src/test/java/com/google/gerrit/acceptance/Dummy.java"],
+    resources = RESOURCES,
     visibility = ["//visibility:public"],
     exports = [
         "//gerrit-acceptance-framework:lib",
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 2e98e0c..ad8140e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
+import static com.google.gerrit.acceptance.GitUtil.deleteRef;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
 import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
@@ -44,6 +45,7 @@
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.acceptance.TestAccount;
 import com.google.gerrit.acceptance.UseSsh;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -99,6 +101,8 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PushCertificateIdent;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -683,6 +687,50 @@
   }
 
   @Test
+  @Sandboxed
+  public void cannotDeleteUserBranch() throws Exception {
+    grant(
+        Permission.DELETE,
+        allUsers,
+        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
+        true,
+        REGISTERED_USERS);
+
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    String userRef = RefNames.refsUsers(admin.id);
+    PushResult r = deleteRef(allUsersRepo, userRef);
+    RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef);
+    assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.REJECTED_OTHER_REASON);
+    assertThat(refUpdate.getMessage()).contains("Not allowed to delete user branch.");
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNotNull();
+    }
+  }
+
+  @Test
+  @Sandboxed
+  public void deleteUserBranchWithAccessDatabaseCapability() throws Exception {
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
+    grant(
+        Permission.DELETE,
+        allUsers,
+        RefNames.REFS_USERS + "${" + RefPattern.USERID_SHARDED + "}",
+        true,
+        REGISTERED_USERS);
+
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    String userRef = RefNames.refsUsers(admin.id);
+    PushResult r = deleteRef(allUsersRepo, userRef);
+    RemoteRefUpdate refUpdate = r.getRemoteUpdate(userRef);
+    assertThat(refUpdate.getStatus()).isEqualTo(RemoteRefUpdate.Status.OK);
+
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      assertThat(repo.exactRef(userRef)).isNull();
+    }
+  }
+
+  @Test
   public void addGpgKey() throws Exception {
     TestKey key = validKeyWithoutExpiration();
     String id = key.getKeyIdString();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
index 82eae1b..b221ec5 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ImpersonationIT.java
@@ -314,7 +314,7 @@
     in.label("Code-Review", 1);
 
     exception.expect(UnprocessableEntityException.class);
-    exception.expectMessage("on_behalf_of account " + user.id + " cannot see destination ref");
+    exception.expectMessage("on_behalf_of account " + user.id + " cannot see change");
     revision.review(in);
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index 4c9b64a..6fd0e77 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.common.data;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -115,6 +116,9 @@
 
   private static final List<String> NAMES_ALL;
   private static final List<String> NAMES_LC;
+  private static final String[] RANGE_NAMES = {
+    QUERY_LIMIT, BATCH_CHANGES_LIMIT,
+  };
 
   static {
     NAMES_ALL = new ArrayList<>();
@@ -158,7 +162,16 @@
 
   /** @return true if the capability should have a range attached. */
   public static boolean hasRange(String varName) {
-    return QUERY_LIMIT.equalsIgnoreCase(varName) || BATCH_CHANGES_LIMIT.equalsIgnoreCase(varName);
+    for (String n : RANGE_NAMES) {
+      if (n.equalsIgnoreCase(varName)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static List<String> getRangeNames() {
+    return Collections.unmodifiableList(Arrays.asList(RANGE_NAMES));
   }
 
   /** @return the valid range for the capability if it has one, otherwise null. */
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
index 4862a70..47850c4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RunAsFilter.java
@@ -19,11 +19,15 @@
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 
 import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.permissions.GlobalPermission;
+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;
@@ -57,6 +61,7 @@
   private final Provider<ReviewDb> db;
   private final boolean enabled;
   private final DynamicItem<WebSession> session;
+  private final PermissionBackend permissionBackend;
   private final AccountResolver accountResolver;
 
   @Inject
@@ -64,10 +69,12 @@
       Provider<ReviewDb> db,
       AuthConfig config,
       DynamicItem<WebSession> session,
+      PermissionBackend permissionBackend,
       AccountResolver accountResolver) {
     this.db = db;
     this.enabled = config.isRunAsEnabled();
     this.session = session;
+    this.permissionBackend = permissionBackend;
     this.accountResolver = accountResolver;
   }
 
@@ -85,12 +92,20 @@
       }
 
       CurrentUser self = session.get().getUser();
-      if (!self.getCapabilities().canRunAs()
+      try {
+        if (!self.isIdentifiedUser()) {
           // Always disallow for anonymous users, even if permitted by the ACL,
           // because that would be crazy.
-          || !self.isIdentifiedUser()) {
+          throw new AuthException("denied");
+        }
+        permissionBackend.user(self).check(GlobalPermission.RUN_AS);
+      } catch (AuthException e) {
         replyError(req, res, SC_FORBIDDEN, "not permitted to use " + RUN_AS, null);
         return;
+      } catch (PermissionBackendException e) {
+        log.warn("cannot check runAs", e);
+        replyError(req, res, SC_INTERNAL_SERVER_ERROR, RUN_AS + " unavailable", null);
+        return;
       }
 
       Account target;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 1f7848d..8ba0978 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
@@ -174,7 +175,11 @@
       AddReviewerInput input = new AddReviewerInput();
       input.reviewer = projectOwners;
       reviewersProvider.get().apply(rsrc, input);
-    } catch (IOException | OrmException | RestApiException | UpdateException e) {
+    } catch (IOException
+        | OrmException
+        | RestApiException
+        | UpdateException
+        | PermissionBackendException e) {
       // one of the owner groups is not visible to the user and this it why it
       // can't be added as reviewer
     }
@@ -193,7 +198,11 @@
         AddReviewerInput input = new AddReviewerInput();
         input.reviewer = r.getGroup().getUUID().get();
         reviewersProvider.get().apply(rsrc, input);
-      } catch (IOException | OrmException | RestApiException | UpdateException e) {
+      } catch (IOException
+          | OrmException
+          | RestApiException
+          | UpdateException
+          | PermissionBackendException e) {
         // ignore
       }
     }
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy
index 7b828b6..4d8c43b 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy
@@ -27,6 +27,8 @@
   <meta name="description" content="Gerrit Code Review">{\n}
   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
 
+  <script>window.CANONICAL_PATH = '{$canonicalPath}';</script>{\n}
+
   // SourceCodePro fonts are used in styles/fonts.css
   // @see https://github.com/w3c/preload/issues/32 regarding crossorigin
   <link rel="preload" href="{$staticResourcePath}/fonts/SourceCodePro-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
index 52e35c3..f0ae97e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/GetMetric.java
@@ -17,10 +17,14 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import org.kohsuke.args4j.Option;
 
 class GetMetric implements RestReadView<MetricResource> {
+  private final PermissionBackend permissionBackend;
   private final CurrentUser user;
   private final DropWizardMetricMaker metrics;
 
@@ -28,16 +32,16 @@
   boolean dataOnly;
 
   @Inject
-  GetMetric(CurrentUser user, DropWizardMetricMaker metrics) {
+  GetMetric(PermissionBackend permissionBackend, CurrentUser user, DropWizardMetricMaker metrics) {
+    this.permissionBackend = permissionBackend;
     this.user = user;
     this.metrics = metrics;
   }
 
   @Override
-  public MetricJson apply(MetricResource resource) throws AuthException {
-    if (!user.getCapabilities().canViewCaches()) {
-      throw new AuthException("restricted to viewCaches");
-    }
+  public MetricJson apply(MetricResource resource)
+      throws AuthException, PermissionBackendException {
+    permissionBackend.user(user).check(GlobalPermission.VIEW_CACHES);
     return new MetricJson(
         resource.getMetric(), metrics.getAnnotations(resource.getName()), dataOnly);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
index 891f4ac..59f6b97 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/ListMetrics.java
@@ -19,6 +19,9 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import java.util.ArrayList;
 import java.util.List;
@@ -28,6 +31,7 @@
 import org.kohsuke.args4j.Option;
 
 class ListMetrics implements RestReadView<ConfigResource> {
+  private final PermissionBackend permissionBackend;
   private final CurrentUser user;
   private final DropWizardMetricMaker metrics;
 
@@ -43,16 +47,17 @@
   List<String> query = new ArrayList<>();
 
   @Inject
-  ListMetrics(CurrentUser user, DropWizardMetricMaker metrics) {
+  ListMetrics(
+      PermissionBackend permissionBackend, CurrentUser user, DropWizardMetricMaker metrics) {
+    this.permissionBackend = permissionBackend;
     this.user = user;
     this.metrics = metrics;
   }
 
   @Override
-  public Map<String, MetricJson> apply(ConfigResource resource) throws AuthException {
-    if (!user.getCapabilities().canViewCaches()) {
-      throw new AuthException("restricted to viewCaches");
-    }
+  public Map<String, MetricJson> apply(ConfigResource resource)
+      throws AuthException, PermissionBackendException {
+    permissionBackend.user(user).check(GlobalPermission.VIEW_CACHES);
 
     SortedMap<String, MetricJson> out = new TreeMap<>();
     List<String> prefixes = new ArrayList<>(query.size());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
index 2686f1f..6abf17c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/metrics/dropwizard/MetricsCollection.java
@@ -23,6 +23,9 @@
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.ConfigResource;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -31,6 +34,7 @@
 class MetricsCollection implements ChildCollection<ConfigResource, MetricResource> {
   private final DynamicMap<RestView<MetricResource>> views;
   private final Provider<ListMetrics> list;
+  private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> user;
   private final DropWizardMetricMaker metrics;
 
@@ -38,10 +42,12 @@
   MetricsCollection(
       DynamicMap<RestView<MetricResource>> views,
       Provider<ListMetrics> list,
+      PermissionBackend permissionBackend,
       Provider<CurrentUser> user,
       DropWizardMetricMaker metrics) {
     this.views = views;
     this.list = list;
+    this.permissionBackend = permissionBackend;
     this.user = user;
     this.metrics = metrics;
   }
@@ -58,10 +64,8 @@
 
   @Override
   public MetricResource parse(ConfigResource parent, IdString id)
-      throws ResourceNotFoundException, AuthException {
-    if (!user.get().getCapabilities().canViewCaches()) {
-      throw new AuthException("restricted to viewCaches");
-    }
+      throws ResourceNotFoundException, AuthException, PermissionBackendException {
+    permissionBackend.user(user).check(GlobalPermission.VIEW_CACHES);
 
     Metric metric = metrics.getMetric(id.get());
     if (metric == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
index 9538121a4..23c59f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -166,6 +167,7 @@
     }
 
     private final ProjectCache projectCache;
+    private final PermissionBackend permissionBackend;
     private final GitRepositoryManager repositoryManager;
     private final PatchListCache patchListCache;
     private final PatchSetInfoFactory patchSetInfoFactory;
@@ -177,6 +179,7 @@
     @Inject
     Args(
         ProjectCache projectCache,
+        PermissionBackend permissionBackend,
         GitRepositoryManager repositoryManager,
         PatchListCache patchListCache,
         PatchSetInfoFactory patchSetInfoFactory,
@@ -184,6 +187,7 @@
         Provider<AnonymousUser> anonymousUser,
         @GerritServerConfig Config config) {
       this.projectCache = projectCache;
+      this.permissionBackend = permissionBackend;
       this.repositoryManager = repositoryManager;
       this.patchListCache = patchListCache;
       this.patchSetInfoFactory = patchSetInfoFactory;
@@ -213,6 +217,10 @@
       return projectCache;
     }
 
+    public PermissionBackend getPermissionBackend() {
+      return permissionBackend;
+    }
+
     public GitRepositoryManager getGitRepositoryManager() {
       return repositoryManager;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 98ec569..e35171b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -130,6 +131,15 @@
         }
       };
 
+  public static final StoredValue<PermissionBackend> PERMISSION_BACKEND =
+      new StoredValue<PermissionBackend>() {
+        @Override
+        protected PermissionBackend createValue(Prolog engine) {
+          PrologEnvironment env = (PrologEnvironment) engine.control;
+          return env.getArgs().getPermissionBackend();
+        }
+      };
+
   public static final StoredValue<AnonymousUser> ANONYMOUS_USER =
       new StoredValue<AnonymousUser>() {
         @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 12dee31..f678379 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -26,6 +26,8 @@
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -75,21 +77,6 @@
     return canAdministrateServer;
   }
 
-  /** @return true if the user can create an account for another user. */
-  public boolean canCreateAccount() {
-    return canPerform(GlobalCapability.CREATE_ACCOUNT) || canAdministrateServer();
-  }
-
-  /** @return true if the user can create a group. */
-  public boolean canCreateGroup() {
-    return canPerform(GlobalCapability.CREATE_GROUP) || canAdministrateServer();
-  }
-
-  /** @return true if the user can create a project. */
-  public boolean canCreateProject() {
-    return canPerform(GlobalCapability.CREATE_PROJECT) || canAdministrateServer();
-  }
-
   /** @return true if the user can email reviewers. */
   public boolean canEmailReviewers() {
     if (canEmailReviewers == null) {
@@ -100,11 +87,6 @@
     return canEmailReviewers;
   }
 
-  /** @return true if the user can kill any running task. */
-  public boolean canKillTask() {
-    return canPerform(GlobalCapability.KILL_TASK) || canMaintainServer();
-  }
-
   /** @return true if the user can modify an account for another user. */
   public boolean canModifyAccount() {
     return canPerform(GlobalCapability.MODIFY_ACCOUNT) || canAdministrateServer();
@@ -115,31 +97,11 @@
     return canPerform(GlobalCapability.VIEW_ALL_ACCOUNTS) || canAdministrateServer();
   }
 
-  /** @return true if the user can view the server caches. */
-  public boolean canViewCaches() {
-    return canPerform(GlobalCapability.VIEW_CACHES) || canMaintainServer();
-  }
-
-  /** @return true if the user can flush the server's caches. */
-  public boolean canFlushCaches() {
-    return canPerform(GlobalCapability.FLUSH_CACHES) || canMaintainServer();
-  }
-
   /** @return true if the user can perform basic server maintenance. */
   public boolean canMaintainServer() {
     return canPerform(GlobalCapability.MAINTAIN_SERVER) || canAdministrateServer();
   }
 
-  /** @return true if the user can view open connections. */
-  public boolean canViewConnections() {
-    return canPerform(GlobalCapability.VIEW_CONNECTIONS) || canAdministrateServer();
-  }
-
-  /** @return true if the user can view the installed plugins. */
-  public boolean canViewPlugins() {
-    return canPerform(GlobalCapability.VIEW_PLUGINS) || canAdministrateServer();
-  }
-
   /** @return true if the user can view the entire queue. */
   public boolean canViewQueue() {
     return canPerform(GlobalCapability.VIEW_QUEUE) || canMaintainServer();
@@ -147,22 +109,11 @@
 
   /** @return true if the user can access the database (with gsql). */
   public boolean canAccessDatabase() {
-    return canPerform(GlobalCapability.ACCESS_DATABASE);
-  }
-
-  /** @return true if the user can stream Gerrit events. */
-  public boolean canStreamEvents() {
-    return canPerform(GlobalCapability.STREAM_EVENTS) || canAdministrateServer();
-  }
-
-  /** @return true if the user can run the Git garbage collection. */
-  public boolean canRunGC() {
-    return canPerform(GlobalCapability.RUN_GC) || canMaintainServer();
-  }
-
-  /** @return true if the user can impersonate another user. */
-  public boolean canRunAs() {
-    return canPerform(GlobalCapability.RUN_AS);
+    try {
+      return doCanForDefaultPermissionBackend(GlobalPermission.ACCESS_DATABASE);
+    } catch (PermissionBackendException e) {
+      return false;
+    }
   }
 
   /** @return which priority queue the user's tasks should be submitted to. */
@@ -278,4 +229,42 @@
   private static boolean match(GroupMembership groups, PermissionRule rule) {
     return groups.contains(rule.getGroup().getUUID());
   }
+
+  /** Do not use unless inside DefaultPermissionBackend. */
+  public boolean doCanForDefaultPermissionBackend(GlobalPermission perm)
+      throws PermissionBackendException {
+    switch (perm) {
+      case ADMINISTRATE_SERVER:
+        return canAdministrateServer();
+      case EMAIL_REVIEWERS:
+        return canEmailReviewers();
+      case MAINTAIN_SERVER:
+        return canMaintainServer();
+      case MODIFY_ACCOUNT:
+        return canModifyAccount();
+      case VIEW_ALL_ACCOUNTS:
+        return canViewAllAccounts();
+      case VIEW_QUEUE:
+        return canViewQueue();
+
+      case FLUSH_CACHES:
+      case KILL_TASK:
+      case RUN_GC:
+      case VIEW_CACHES:
+        return canPerform(perm.permissionName()) || canMaintainServer();
+
+      case CREATE_ACCOUNT:
+      case CREATE_GROUP:
+      case CREATE_PROJECT:
+      case STREAM_EVENTS:
+      case VIEW_CONNECTIONS:
+      case VIEW_PLUGINS:
+        return canPerform(perm.permissionName()) || canAdministrateServer();
+
+      case ACCESS_DATABASE:
+      case RUN_AS:
+        return canPerform(perm.permissionName());
+    }
+    throw new PermissionBackendException(perm + " unsupported");
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
index 876807d..1268ef2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -14,23 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.gerrit.common.data.GlobalCapability.ACCESS_DATABASE;
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
-import static com.google.gerrit.common.data.GlobalCapability.EMAIL_REVIEWERS;
-import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
-import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
-import static com.google.gerrit.common.data.GlobalCapability.MAINTAIN_SERVER;
-import static com.google.gerrit.common.data.GlobalCapability.MODIFY_ACCOUNT;
 import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
-import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
-import static com.google.gerrit.common.data.GlobalCapability.STREAM_EVENTS;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_ALL_ACCOUNTS;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_PLUGINS;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
 
 import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -45,12 +29,15 @@
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.account.AccountResource.Capability;
 import com.google.gerrit.server.git.QueueProvider;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import java.util.EnumSet;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
@@ -67,34 +54,72 @@
 
   private Set<String> query;
 
+  private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> self;
   private final DynamicMap<CapabilityDefinition> pluginCapabilities;
 
   @Inject
-  GetCapabilities(Provider<CurrentUser> self, DynamicMap<CapabilityDefinition> pluginCapabilities) {
+  GetCapabilities(
+      PermissionBackend permissionBackend,
+      Provider<CurrentUser> self,
+      DynamicMap<CapabilityDefinition> pluginCapabilities) {
+    this.permissionBackend = permissionBackend;
     this.self = self;
     this.pluginCapabilities = pluginCapabilities;
   }
 
   @Override
-  public Object apply(AccountResource resource) throws AuthException {
-    if (self.get() != resource.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("restricted to administrator");
+  public Object apply(AccountResource rsrc) throws AuthException, PermissionBackendException {
+    PermissionBackend.WithUser perm = permissionBackend.user(self);
+    if (self.get() != rsrc.getUser()) {
+      perm.check(GlobalPermission.ADMINISTRATE_SERVER);
+      perm = permissionBackend.user(rsrc.getUser());
     }
 
-    CapabilityControl cc = resource.getUser().getCapabilities();
     Map<String, Object> have = new LinkedHashMap<>();
-    for (String name : GlobalCapability.getAllNames()) {
-      if (want(name)) {
-        if (GlobalCapability.hasRange(name)) {
-          if (cc.hasExplicitRange(name)) {
-            have.put(name, new Range(cc.getRange(name)));
-          }
-        } else if (!name.equals(PRIORITY) && cc.canPerform(name)) {
-          have.put(name, true);
+    for (GlobalPermission p : testGlobalPermissions(perm)) {
+      have.put(p.permissionName(), true);
+    }
+    addRanges(have, rsrc);
+    addPluginCapabilities(have, rsrc);
+    addPriority(have, rsrc);
+
+    return OutputFormat.JSON
+        .newGson()
+        .toJsonTree(have, new TypeToken<Map<String, Object>>() {}.getType());
+  }
+
+  private Set<GlobalPermission> testGlobalPermissions(PermissionBackend.WithUser perm)
+      throws PermissionBackendException {
+    EnumSet<GlobalPermission> toTest;
+    if (query != null) {
+      toTest = EnumSet.noneOf(GlobalPermission.class);
+      for (GlobalPermission p : GlobalPermission.values()) {
+        if (want(p.permissionName())) {
+          toTest.add(p);
         }
       }
+    } else {
+      toTest = EnumSet.allOf(GlobalPermission.class);
     }
+    return perm.test(toTest);
+  }
+
+  private boolean want(String name) {
+    return query == null || query.contains(name.toLowerCase());
+  }
+
+  private void addRanges(Map<String, Object> have, AccountResource rsrc) {
+    CapabilityControl cc = rsrc.getUser().getCapabilities();
+    for (String name : GlobalCapability.getRangeNames()) {
+      if (want(name) && cc.hasExplicitRange(name)) {
+        have.put(name, new Range(cc.getRange(name)));
+      }
+    }
+  }
+
+  private void addPluginCapabilities(Map<String, Object> have, AccountResource rsrc) {
+    CapabilityControl cc = rsrc.getUser().getCapabilities();
     for (String pluginName : pluginCapabilities.plugins()) {
       for (String capability : pluginCapabilities.byPlugin(pluginName).keySet()) {
         String name = String.format("%s-%s", pluginName, capability);
@@ -103,47 +128,14 @@
         }
       }
     }
+  }
 
-    have.put(ACCESS_DATABASE, cc.canAccessDatabase());
-    have.put(CREATE_ACCOUNT, cc.canCreateAccount());
-    have.put(CREATE_GROUP, cc.canCreateGroup());
-    have.put(CREATE_PROJECT, cc.canCreateProject());
-    have.put(EMAIL_REVIEWERS, cc.canEmailReviewers());
-    have.put(FLUSH_CACHES, cc.canFlushCaches());
-    have.put(KILL_TASK, cc.canKillTask());
-    have.put(MAINTAIN_SERVER, cc.canMaintainServer());
-    have.put(MODIFY_ACCOUNT, cc.canModifyAccount());
-    have.put(RUN_GC, cc.canRunGC());
-    have.put(STREAM_EVENTS, cc.canStreamEvents());
-    have.put(VIEW_ALL_ACCOUNTS, cc.canViewAllAccounts());
-    have.put(VIEW_CACHES, cc.canViewCaches());
-    have.put(VIEW_CONNECTIONS, cc.canViewConnections());
-    have.put(VIEW_PLUGINS, cc.canViewPlugins());
-    have.put(VIEW_QUEUE, cc.canViewQueue());
-
-    QueueProvider.QueueType queue = cc.getQueueType();
+  private void addPriority(Map<String, Object> have, AccountResource rsrc) {
+    QueueProvider.QueueType queue = rsrc.getUser().getCapabilities().getQueueType();
     if (queue != QueueProvider.QueueType.INTERACTIVE
         || (query != null && query.contains(PRIORITY))) {
       have.put(PRIORITY, queue);
     }
-
-    Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
-    while (itr.hasNext()) {
-      Map.Entry<String, Object> e = itr.next();
-      if (!want(e.getKey())) {
-        itr.remove();
-      } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
-        itr.remove();
-      }
-    }
-
-    return OutputFormat.JSON
-        .newGson()
-        .toJsonTree(have, new TypeToken<Map<String, Object>>() {}.getType());
-  }
-
-  private boolean want(String name) {
-    return query == null || query.contains(name.toLowerCase());
   }
 
   private static class Range {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
index 32c5345..4c525c2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutActive.java
@@ -28,7 +28,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 @RequiresCapability(GlobalCapability.MODIFY_ACCOUNT)
@@ -69,7 +68,6 @@
     if (a == null) {
       throw new ResourceNotFoundException("account not found");
     }
-    dbProvider.get().accounts().update(Collections.singleton(a));
     byIdCache.evict(a.getId());
     return alreadyActive.get() ? Response.ok("") : Response.created("");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
index ec60fb3..fb87e1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutPreferred.java
@@ -29,7 +29,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 @Singleton
@@ -79,7 +78,6 @@
     if (a == null) {
       throw new ResourceNotFoundException("account not found");
     }
-    dbProvider.get().accounts().update(Collections.singleton(a));
     byIdCache.evict(a.getId());
     return alreadyPreferred.get() ? Response.ok("") : Response.created("");
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 4698a80..d534c5a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -416,7 +416,7 @@
   public void addReviewer(AddReviewerInput in) throws RestApiException {
     try {
       postReviewers.apply(change, in);
-    } catch (OrmException | IOException | UpdateException e) {
+    } catch (OrmException | IOException | UpdateException | PermissionBackendException e) {
       throw new RestApiException("Cannot add change reviewer", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
index 2af7b90..d934f6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/RevisionApiImpl.java
@@ -214,7 +214,7 @@
   public void review(ReviewInput in) throws RestApiException {
     try {
       review.apply(revision, in);
-    } catch (OrmException | UpdateException | IOException e) {
+    } catch (OrmException | UpdateException | IOException | PermissionBackendException e) {
       throw new RestApiException("Cannot post review", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 190f59b..c86714a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -59,8 +59,6 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.extensions.api.changes.FixInput;
@@ -104,7 +102,6 @@
 import com.google.gerrit.server.api.accounts.AccountInfoComparator;
 import com.google.gerrit.server.api.accounts.GpgApiAdapter;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LabelNormalizer;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.index.change.ChangeIndexCollection;
@@ -112,6 +109,9 @@
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.SubmitRuleOptions;
@@ -186,9 +186,9 @@
   }
 
   private final Provider<ReviewDb> db;
-  private final LabelNormalizer labelNormalizer;
   private final Provider<CurrentUser> userProvider;
   private final AnonymousUser anonymous;
+  private final PermissionBackend permissionBackend;
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final MergeUtil.Factory mergeUtilFactory;
@@ -217,9 +217,9 @@
   @Inject
   ChangeJson(
       Provider<ReviewDb> db,
-      LabelNormalizer ln,
       Provider<CurrentUser> user,
       AnonymousUser au,
+      PermissionBackend permissionBackend,
       GitRepositoryManager repoManager,
       ProjectCache projectCache,
       MergeUtil.Factory mergeUtilFactory,
@@ -241,10 +241,10 @@
       ApprovalsUtil approvalsUtil,
       @Assisted Iterable<ListChangesOption> options) {
     this.db = db;
-    this.labelNormalizer = ln;
     this.userProvider = user;
     this.anonymous = au;
     this.changeDataFactory = cdf;
+    this.permissionBackend = permissionBackend;
     this.repoManager = repoManager;
     this.userFactory = uf;
     this.projectCache = projectCache;
@@ -316,6 +316,7 @@
         | GpgException
         | OrmException
         | IOException
+        | PermissionBackendException
         | RuntimeException e) {
       if (!has(CHECK)) {
         Throwables.throwIfInstanceOf(e, OrmException.class);
@@ -393,6 +394,7 @@
             | GpgException
             | OrmException
             | IOException
+            | PermissionBackendException
             | RuntimeException e) {
           if (has(CHECK)) {
             i = checkOnly(cd);
@@ -450,7 +452,8 @@
   }
 
   private ChangeInfo toChangeInfo(ChangeData cd, Optional<PatchSet.Id> limitToPsId)
-      throws PatchListNotAvailableException, GpgException, OrmException, IOException {
+      throws PatchListNotAvailableException, GpgException, OrmException, IOException,
+          PermissionBackendException {
     ChangeInfo out = new ChangeInfo();
     CurrentUser user = userProvider.get();
     ChangeControl ctl = cd.changeControl().forUser(user);
@@ -466,6 +469,7 @@
       }
     }
 
+    PermissionBackend.ForChange perm = permissionBackend.user(user).database(db).change(cd);
     Change in = cd.change();
     out.project = in.getProject().get();
     out.branch = in.getDest().getShortName();
@@ -514,16 +518,17 @@
       out.reviewed = cd.reviewedBy().contains(accountId) ? true : null;
     }
 
-    out.labels = labelsFor(ctl, cd, has(LABELS), has(DETAILED_LABELS));
+    out.labels = labelsFor(perm, ctl, cd, has(LABELS), has(DETAILED_LABELS));
     out.submitted = getSubmittedOn(cd);
 
     if (out.labels != null && has(DETAILED_LABELS)) {
       // If limited to specific patch sets but not the current patch set, don't
       // list permitted labels, since users can't vote on those patch sets.
-      if (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId())) {
+      if (user.isIdentifiedUser()
+          && (!limitToPsId.isPresent() || limitToPsId.get().equals(in.currentPatchSetId()))) {
         out.permittedLabels =
             cd.change().getStatus() != Change.Status.ABANDONED
-                ? permittedLabels(ctl, cd)
+                ? permittedLabels(perm, cd)
                 : ImmutableMap.of();
       }
 
@@ -603,7 +608,12 @@
   }
 
   private Map<String, LabelInfo> labelsFor(
-      ChangeControl ctl, ChangeData cd, boolean standard, boolean detailed) throws OrmException {
+      PermissionBackend.ForChange perm,
+      ChangeControl ctl,
+      ChangeData cd,
+      boolean standard,
+      boolean detailed)
+      throws OrmException, PermissionBackendException {
     if (!standard && !detailed) {
       return null;
     }
@@ -612,20 +622,24 @@
       return null;
     }
 
-    LabelTypes labelTypes = ctl.getLabelTypes();
+    LabelTypes labelTypes = cd.getLabelTypes();
     Map<String, LabelWithStatus> withStatus =
         cd.change().getStatus().isOpen()
-            ? labelsForOpenChange(ctl, cd, labelTypes, standard, detailed)
-            : labelsForClosedChange(ctl, cd, labelTypes, standard, detailed);
+            ? labelsForOpenChange(perm, cd, labelTypes, standard, detailed)
+            : labelsForClosedChange(perm, cd, labelTypes, standard, detailed);
     return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
   }
 
   private Map<String, LabelWithStatus> labelsForOpenChange(
-      ChangeControl ctl, ChangeData cd, LabelTypes labelTypes, boolean standard, boolean detailed)
-      throws OrmException {
+      PermissionBackend.ForChange perm,
+      ChangeData cd,
+      LabelTypes labelTypes,
+      boolean standard,
+      boolean detailed)
+      throws OrmException, PermissionBackendException {
     Map<String, LabelWithStatus> labels = initLabels(cd, labelTypes, standard);
     if (detailed) {
-      setAllApprovals(ctl, cd, labels);
+      setAllApprovals(perm, cd, labels);
     }
     for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
       LabelType type = labelTypes.byLabel(e.getKey());
@@ -712,8 +726,8 @@
   }
 
   private void setAllApprovals(
-      ChangeControl baseCtrl, ChangeData cd, Map<String, LabelWithStatus> labels)
-      throws OrmException {
+      PermissionBackend.ForChange basePerm, ChangeData cd, Map<String, LabelWithStatus> labels)
+      throws OrmException, PermissionBackendException {
     Change.Status status = cd.change().getStatus();
     checkState(status.isOpen(), "should not call setAllApprovals on %s change", status);
 
@@ -727,17 +741,17 @@
     }
 
     Table<Account.Id, String, PatchSetApproval> current =
-        HashBasedTable.create(allUsers.size(), baseCtrl.getLabelTypes().getLabelTypes().size());
+        HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size());
     for (PatchSetApproval psa : cd.currentApprovals()) {
       current.put(psa.getAccountId(), psa.getLabel(), psa);
     }
 
+    LabelTypes labelTypes = cd.getLabelTypes();
     for (Account.Id accountId : allUsers) {
-      IdentifiedUser user = userFactory.create(accountId);
-      ChangeControl ctl = baseCtrl.forUser(user);
-      Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(ctl, cd));
+      PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId));
+      Map<String, VotingRangeInfo> pvr = getPermittedVotingRanges(permittedLabels(perm, cd));
       for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
-        LabelType lt = ctl.getLabelTypes().byLabel(e.getKey());
+        LabelType lt = labelTypes.byLabel(e.getKey());
         if (lt == null) {
           // Ignore submit record for undefined label; likely the submit rule
           // author didn't intend for the label to show up in the table.
@@ -754,7 +768,7 @@
             // This may be a dummy approval that was inserted when the reviewer
             // was added. Explicitly check whether the user can vote on this
             // label.
-            value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
+            value = perm.test(new LabelPermission(lt)) ? 0 : null;
           }
           tag = psa.getTag();
           date = psa.getGranted();
@@ -765,7 +779,7 @@
           // Either the user cannot vote on this label, or they were added as a
           // reviewer but have not responded yet. Explicitly check whether the
           // user can vote on this label.
-          value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
+          value = perm.test(new LabelPermission(lt)) ? 0 : null;
         }
         addApproval(
             e.getValue().label(), approvalInfo(accountId, value, permittedVotingRange, tag, date));
@@ -813,12 +827,12 @@
   }
 
   private Map<String, LabelWithStatus> labelsForClosedChange(
-      ChangeControl baseCtrl,
+      PermissionBackend.ForChange basePerm,
       ChangeData cd,
       LabelTypes labelTypes,
       boolean standard,
       boolean detailed)
-      throws OrmException {
+      throws OrmException, PermissionBackendException {
     Set<Account.Id> allUsers = new HashSet<>();
     if (detailed) {
       // Users expect to see all reviewers on closed changes, even if they
@@ -886,8 +900,8 @@
       Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size());
       Map<String, VotingRangeInfo> pvr = Collections.emptyMap();
       if (detailed) {
-        ChangeControl ctl = baseCtrl.forUser(userFactory.create(accountId));
-        pvr = getPermittedVotingRanges(permittedLabels(ctl, cd));
+        PermissionBackend.ForChange perm = basePerm.user(userFactory.create(accountId));
+        pvr = getPermittedVotingRanges(permittedLabels(perm, cd));
         for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
           ApprovalInfo ai = approvalInfo(accountId, 0, null, null, null);
           byLabel.put(entry.getKey(), ai);
@@ -961,15 +975,25 @@
     }
   }
 
-  private Map<String, Collection<String>> permittedLabels(ChangeControl ctl, ChangeData cd)
-      throws OrmException {
-    if (ctl == null || !ctl.getUser().isIdentifiedUser()) {
-      return null;
+  private Map<String, Collection<String>> permittedLabels(
+      PermissionBackend.ForChange perm, ChangeData cd)
+      throws OrmException, PermissionBackendException {
+    boolean isMerged = cd.change().getStatus() == Change.Status.MERGED;
+    LabelTypes labelTypes = cd.getLabelTypes();
+    Map<String, LabelType> toCheck = new HashMap<>();
+    for (SubmitRecord rec : submitRecords(cd)) {
+      if (rec.labels != null) {
+        for (SubmitRecord.Label r : rec.labels) {
+          LabelType type = labelTypes.byLabel(r.label);
+          if (type != null && (!isMerged || type.allowPostSubmit())) {
+            toCheck.put(type.getName(), type);
+          }
+        }
+      }
     }
 
     Map<String, Short> labels = null;
-    boolean isMerged = ctl.getChange().getStatus() == Change.Status.MERGED;
-    LabelTypes labelTypes = ctl.getLabelTypes();
+    Set<LabelPermission.WithValue> can = perm.testLabels(toCheck.values());
     SetMultimap<String, String> permitted = LinkedHashMultimap.create();
     for (SubmitRecord rec : submitRecords(cd)) {
       if (rec.labels == null) {
@@ -980,12 +1004,12 @@
         if (type == null || (isMerged && !type.allowPostSubmit())) {
           continue;
         }
-        PermissionRange range = ctl.getRange(Permission.forLabel(r.label));
+
         for (LabelValue v : type.getValues()) {
-          boolean ok = range.contains(v.getValue());
+          boolean ok = can.contains(new LabelPermission.WithValue(type, v));
           if (isMerged) {
             if (labels == null) {
-              labels = currentLabels(ctl);
+              labels = currentLabels(perm, cd);
             }
             short prev = labels.getOrDefault(type.getName(), (short) 0);
             ok &= v.getValue() >= prev;
@@ -996,6 +1020,7 @@
         }
       }
     }
+
     List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
     for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) {
       if (isOnlyZero(e.getValue())) {
@@ -1008,11 +1033,14 @@
     return permitted.asMap();
   }
 
-  private Map<String, Short> currentLabels(ChangeControl ctl) throws OrmException {
+  private Map<String, Short> currentLabels(PermissionBackend.ForChange perm, ChangeData cd)
+      throws OrmException {
+    IdentifiedUser user = perm.user().asIdentifiedUser();
+    ChangeControl ctl = cd.changeControl().forUser(user);
     Map<String, Short> result = new HashMap<>();
     for (PatchSetApproval psa :
         approvalsUtil.byPatchSetUser(
-            db.get(), ctl, ctl.getChange().currentPatchSetId(), ctl.getUser().getAccountId())) {
+            db.get(), ctl, cd.change().currentPatchSetId(), user.getAccountId())) {
       result.put(psa.getLabel(), psa.getValue());
     }
     return result;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
index aa0b339..db9af1d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -31,7 +32,8 @@
   }
 
   @Override
-  public List<ReviewerInfo> apply(ReviewerResource rsrc) throws OrmException {
+  public List<ReviewerInfo> apply(ReviewerResource rsrc)
+      throws OrmException, PermissionBackendException {
     return json.format(rsrc);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
index 1dba58c..ba2a10b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -48,7 +49,8 @@
   }
 
   @Override
-  public List<ReviewerInfo> apply(ChangeResource rsrc) throws OrmException {
+  public List<ReviewerInfo> apply(ChangeResource rsrc)
+      throws OrmException, PermissionBackendException {
     Map<String, ReviewerResource> reviewers = new LinkedHashMap<>();
     ReviewDb db = dbProvider.get();
     for (Account.Id accountId : approvalsUtil.getReviewers(db, rsrc.getNotes()).all()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java
index 5aaee56..6d9dc79 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListRevisionReviewers.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -50,7 +51,7 @@
 
   @Override
   public List<ReviewerInfo> apply(RevisionResource rsrc)
-      throws OrmException, MethodNotAllowedException {
+      throws OrmException, MethodNotAllowedException, PermissionBackendException {
     if (!rsrc.isCurrent()) {
       throw new MethodNotAllowedException("Cannot list reviewers on non-current patch set");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 81ab39e..6365fd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -18,6 +18,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.REVIEWER;
+import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.groupingBy;
 import static java.util.stream.Collectors.joining;
@@ -39,8 +40,6 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
@@ -83,6 +82,7 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.PatchSetUtil;
@@ -95,6 +95,10 @@
 import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -188,12 +192,14 @@
 
   @Override
   public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input)
-      throws RestApiException, UpdateException, OrmException, IOException {
+      throws RestApiException, UpdateException, OrmException, IOException,
+          PermissionBackendException {
     return apply(revision, input, TimeUtil.nowTs());
   }
 
   public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Timestamp ts)
-      throws RestApiException, UpdateException, OrmException, IOException {
+      throws RestApiException, UpdateException, OrmException, IOException,
+          PermissionBackendException {
     // Respect timestamp, but truncate at change created-on time.
     ts = Ordering.natural().max(ts, revision.getChange().getCreatedOn());
     if (revision.getEdit().isPresent()) {
@@ -348,7 +354,8 @@
   }
 
   private RevisionResource onBehalfOf(RevisionResource rev, ReviewInput in)
-      throws BadRequestException, AuthException, UnprocessableEntityException, OrmException {
+      throws BadRequestException, AuthException, UnprocessableEntityException, OrmException,
+          PermissionBackendException {
     if (in.labels == null || in.labels.isEmpty()) {
       throw new AuthException(
           String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
@@ -360,11 +367,13 @@
       throw new AuthException("not allowed to modify other user's drafts");
     }
 
-    ChangeControl caller = rev.getControl();
+    CurrentUser caller = rev.getUser();
+    PermissionBackend.ForChange perm = rev.permissions().database(db);
+    LabelTypes labelTypes = rev.getControl().getLabelTypes();
     Iterator<Map.Entry<String, Short>> itr = in.labels.entrySet().iterator();
     while (itr.hasNext()) {
       Map.Entry<String, Short> ent = itr.next();
-      LabelType type = caller.getLabelTypes().byLabel(ent.getKey());
+      LabelType type = labelTypes.byLabel(ent.getKey());
       if (type == null && in.strictLabels) {
         throw new BadRequestException(
             String.format("label \"%s\" is not a configured label", ent.getKey()));
@@ -373,16 +382,15 @@
         continue;
       }
 
-      if (caller.getUser().isInternalUser()) {
-        continue;
-      }
-
-      PermissionRange r = caller.getRange(Permission.forLabelAs(type.getName()));
-      if (r == null || r.isEmpty() || !r.contains(ent.getValue())) {
-        throw new AuthException(
-            String.format(
-                "not permitted to modify label \"%s\" on behalf of \"%s\"",
-                ent.getKey(), in.onBehalfOf));
+      if (!caller.isInternalUser()) {
+        try {
+          perm.check(new LabelPermission.WithValue(ON_BEHALF_OF, type, ent.getValue()));
+        } catch (AuthException e) {
+          throw new AuthException(
+              String.format(
+                  "not permitted to modify label \"%s\" on behalf of \"%s\"",
+                  type.getName(), in.onBehalfOf));
+        }
       }
     }
     if (in.labels.isEmpty()) {
@@ -390,25 +398,26 @@
           String.format("label required to post review on behalf of \"%s\"", in.onBehalfOf));
     }
 
-    ChangeControl target =
-        caller.forUser(accounts.parseOnBehalfOf(caller.getUser(), in.onBehalfOf));
-    if (!target.getRefControl().isVisible()) {
+    IdentifiedUser reviewer = accounts.parseOnBehalfOf(caller, in.onBehalfOf);
+    try {
+      perm.user(reviewer).check(ChangePermission.READ);
+    } catch (AuthException e) {
       throw new UnprocessableEntityException(
-          String.format(
-              "on_behalf_of account %s cannot see destination ref",
-              target.getUser().getAccountId()));
+          String.format("on_behalf_of account %s cannot see change", reviewer.getAccountId()));
     }
-    return new RevisionResource(changes.parse(target), rev.getPatchSet());
+
+    ChangeControl ctl = rev.getControl().forUser(reviewer);
+    return new RevisionResource(changes.parse(ctl), rev.getPatchSet());
   }
 
-  private void checkLabels(RevisionResource revision, boolean strict, Map<String, Short> labels)
-      throws BadRequestException, AuthException {
-    ChangeControl ctl = revision.getControl();
+  private void checkLabels(RevisionResource rsrc, boolean strict, Map<String, Short> labels)
+      throws BadRequestException, AuthException, PermissionBackendException {
+    LabelTypes types = rsrc.getControl().getLabelTypes();
+    PermissionBackend.ForChange perm = rsrc.permissions();
     Iterator<Map.Entry<String, Short>> itr = labels.entrySet().iterator();
     while (itr.hasNext()) {
       Map.Entry<String, Short> ent = itr.next();
-
-      LabelType lt = revision.getControl().getLabelTypes().byLabel(ent.getKey());
+      LabelType lt = types.byLabel(ent.getKey());
       if (lt == null) {
         if (strict) {
           throw new BadRequestException(
@@ -433,18 +442,15 @@
         continue;
       }
 
-      String name = lt.getName();
-      PermissionRange range = ctl.getRange(Permission.forLabel(name));
-      if (range == null || !range.contains(ent.getValue())) {
+      short val = ent.getValue();
+      try {
+        perm.check(new LabelPermission.WithValue(lt, val));
+      } catch (AuthException e) {
         if (strict) {
           throw new AuthException(
-              String.format(
-                  "Applying label \"%s\": %d is restricted", ent.getKey(), ent.getValue()));
-        } else if (range == null || range.isEmpty()) {
-          ent.setValue((short) 0);
-        } else {
-          ent.setValue((short) range.squash(ent.getValue()));
+              String.format("Applying label \"%s\": %d is restricted", lt.getName(), val));
         }
+        ent.setValue(perm.squashThenCheck(lt, val));
       }
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
index b7fe986..cc759fd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -54,6 +54,8 @@
 import com.google.gerrit.server.mail.Address;
 import com.google.gerrit.server.mail.send.OutgoingEmailValidator;
 import com.google.gerrit.server.notedb.NotesMigration;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -79,6 +81,7 @@
 
   private final AccountsCollection accounts;
   private final ReviewerResource.Factory reviewerFactory;
+  private final PermissionBackend permissionBackend;
 
   private final GroupsCollection groupsCollection;
   private final GroupMembers.Factory groupMembersFactory;
@@ -98,6 +101,7 @@
   PostReviewers(
       AccountsCollection accounts,
       ReviewerResource.Factory reviewerFactory,
+      PermissionBackend permissionBackend,
       GroupsCollection groupsCollection,
       GroupMembers.Factory groupMembersFactory,
       AccountLoader.Factory accountLoaderFactory,
@@ -113,6 +117,7 @@
       PostReviewersOp.Factory postReviewersOpFactory) {
     this.accounts = accounts;
     this.reviewerFactory = reviewerFactory;
+    this.permissionBackend = permissionBackend;
     this.groupsCollection = groupsCollection;
     this.groupMembersFactory = groupMembersFactory;
     this.accountLoaderFactory = accountLoaderFactory;
@@ -130,7 +135,8 @@
 
   @Override
   public AddReviewerResult apply(ChangeResource rsrc, AddReviewerInput input)
-      throws IOException, OrmException, RestApiException, UpdateException {
+      throws IOException, OrmException, RestApiException, UpdateException,
+          PermissionBackendException {
     if (input.reviewer == null) {
       throw new BadRequestException("missing reviewer field");
     }
@@ -398,14 +404,17 @@
               rsrc, this.reviewers, this.reviewersByEmail, state, notify, accountsToNotify);
     }
 
-    void gatherResults() throws OrmException {
+    void gatherResults() throws OrmException, PermissionBackendException {
       // Generate result details and fill AccountLoader. This occurs outside
       // the Op because the accounts are in a different table.
       PostReviewersOp.Result opResult = op.getResult();
       if (migration.readChanges() && state == CC) {
         result.ccs = Lists.newArrayListWithCapacity(opResult.addedCCs().size());
         for (Account.Id accountId : opResult.addedCCs()) {
-          result.ccs.add(json.format(new ReviewerInfo(accountId.get()), reviewers.get(accountId)));
+          ChangeControl ctl = reviewers.get(accountId);
+          PermissionBackend.ForChange perm =
+              permissionBackend.user(ctl.getUser()).database(dbProvider).change(ctl.getNotes());
+          result.ccs.add(json.format(new ReviewerInfo(accountId.get()), perm, ctl));
         }
         accountLoaderFactory.create(true).fill(result.ccs);
         for (Address a : reviewersByEmail) {
@@ -415,11 +424,12 @@
         result.reviewers = Lists.newArrayListWithCapacity(opResult.addedReviewers().size());
         for (PatchSetApproval psa : opResult.addedReviewers()) {
           // New reviewers have value 0, don't bother normalizing.
+          ChangeControl ctl = reviewers.get(psa.getAccountId());
+          PermissionBackend.ForChange perm =
+              permissionBackend.user(ctl.getUser()).database(dbProvider).change(ctl.getNotes());
           result.reviewers.add(
               json.format(
-                  new ReviewerInfo(psa.getAccountId().get()),
-                  reviewers.get(psa.getAccountId()),
-                  ImmutableList.of(psa)));
+                  new ReviewerInfo(psa.getAccountId().get()), perm, ctl, ImmutableList.of(psa)));
         }
         accountLoaderFactory.create(true).fill(result.reviewers);
         for (Address a : reviewersByEmail) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
index ac7f15e..d6ef2a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -20,7 +20,6 @@
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.extensions.api.changes.ReviewerInfo;
@@ -30,6 +29,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -44,6 +46,7 @@
 @Singleton
 public class ReviewerJson {
   private final Provider<ReviewDb> db;
+  private final PermissionBackend permissionBackend;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
   private final AccountLoader.Factory accountLoaderFactory;
@@ -51,22 +54,29 @@
   @Inject
   ReviewerJson(
       Provider<ReviewDb> db,
+      PermissionBackend permissionBackend,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
       AccountLoader.Factory accountLoaderFactory) {
     this.db = db;
+    this.permissionBackend = permissionBackend;
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
     this.accountLoaderFactory = accountLoaderFactory;
   }
 
-  public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs) throws OrmException {
+  public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
+      throws OrmException, PermissionBackendException {
     List<ReviewerInfo> infos = Lists.newArrayListWithCapacity(rsrcs.size());
     AccountLoader loader = accountLoaderFactory.create(true);
     for (ReviewerResource rsrc : rsrcs) {
       ReviewerInfo info =
           format(
               new ReviewerInfo(rsrc.getReviewerUser().getAccountId().get()),
+              permissionBackend
+                  .user(rsrc.getReviewerUser())
+                  .database(db)
+                  .change(rsrc.getChangeResource().getNotes()),
               rsrc.getReviewerControl());
       loader.put(info);
       infos.add(info);
@@ -75,22 +85,29 @@
     return infos;
   }
 
-  public List<ReviewerInfo> format(ReviewerResource rsrc) throws OrmException {
+  public List<ReviewerInfo> format(ReviewerResource rsrc)
+      throws OrmException, PermissionBackendException {
     return format(ImmutableList.<ReviewerResource>of(rsrc));
   }
 
-  public ReviewerInfo format(ReviewerInfo out, ChangeControl ctl) throws OrmException {
+  public ReviewerInfo format(ReviewerInfo out, PermissionBackend.ForChange perm, ChangeControl ctl)
+      throws OrmException, PermissionBackendException {
     PatchSet.Id psId = ctl.getChange().currentPatchSetId();
     return format(
         out,
+        perm,
         ctl,
         approvalsUtil.byPatchSetUser(db.get(), ctl, psId, new Account.Id(out._accountId)));
   }
 
   public ReviewerInfo format(
-      ReviewerInfo out, ChangeControl ctl, Iterable<PatchSetApproval> approvals)
-      throws OrmException {
-    LabelTypes labelTypes = ctl.getLabelTypes();
+      ReviewerInfo out,
+      PermissionBackend.ForChange perm,
+      ChangeControl ctl,
+      Iterable<PatchSetApproval> approvals)
+      throws OrmException, PermissionBackendException {
+    ChangeData cd = changeDataFactory.create(db.get(), ctl);
+    LabelTypes labelTypes = cd.getLabelTypes();
 
     // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
     out.approvals = new TreeMap<>(labelTypes.nameComparator());
@@ -107,7 +124,6 @@
 
     // Add dummy approvals for all permitted labels for the user even if they
     // do not exist in the DB.
-    ChangeData cd = changeDataFactory.create(db.get(), ctl);
     PatchSet ps = cd.currentPatchSet();
     if (ps != null) {
       for (SubmitRecord rec :
@@ -117,8 +133,10 @@
         }
         for (SubmitRecord.Label label : rec.labels) {
           String name = label.label;
+          LabelType type = labelTypes.byLabel(name);
           if (!out.approvals.containsKey(name)
-              && !ctl.getRange(Permission.forLabel(name)).isEmpty()) {
+              && type != null
+              && perm.test(new LabelPermission(type))) {
             out.approvals.put(name, formatValue((short) 0));
           }
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
index f002f8d..1e88842 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CachesCollection.java
@@ -27,8 +27,10 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestView;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -40,6 +42,7 @@
 
   private final DynamicMap<RestView<CacheResource>> views;
   private final Provider<ListCaches> list;
+  private final PermissionBackend permissionBackend;
   private final Provider<CurrentUser> self;
   private final DynamicMap<Cache<?, ?>> cacheMap;
   private final PostCaches postCaches;
@@ -48,11 +51,13 @@
   CachesCollection(
       DynamicMap<RestView<CacheResource>> views,
       Provider<ListCaches> list,
+      PermissionBackend permissionBackend,
       Provider<CurrentUser> self,
       DynamicMap<Cache<?, ?>> cacheMap,
       PostCaches postCaches) {
     this.views = views;
     this.list = list;
+    this.permissionBackend = permissionBackend;
     this.self = self;
     this.cacheMap = cacheMap;
     this.postCaches = postCaches;
@@ -65,15 +70,8 @@
 
   @Override
   public CacheResource parse(ConfigResource parent, IdString id)
-      throws AuthException, ResourceNotFoundException {
-    CurrentUser user = self.get();
-    if (user instanceof AnonymousUser) {
-      throw new AuthException("Authentication required");
-    } else if (!user.isIdentifiedUser()) {
-      throw new ResourceNotFoundException();
-    } else if (!user.getCapabilities().canViewCaches()) {
-      throw new AuthException("not allowed to view caches");
-    }
+      throws AuthException, ResourceNotFoundException, PermissionBackendException {
+    permissionBackend.user(self).check(GlobalPermission.VIEW_CACHES);
 
     String cacheName = id.get();
     String pluginName = "gerrit";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
index 6a05d22..323f352 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -142,16 +142,6 @@
     return Result.create(unchanged, updated, deleted);
   }
 
-  /**
-   * @param ctl change control (for any user).
-   * @param lt label type.
-   * @param id account ID.
-   * @return whether the given account ID has any permissions to vote on this label for this change.
-   */
-  public boolean canVote(ChangeControl ctl, LabelType lt, Account.Id id) {
-    return !getRange(ctl, lt, id).isEmpty();
-  }
-
   private PatchSetApproval copy(PatchSetApproval src) {
     return new PatchSetApproval(src.getPatchSetId(), src);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
index 80792c3..0c4eb89 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/RefOperationValidators.java
@@ -14,10 +14,13 @@
 package com.google.gerrit.server.git.validators;
 
 import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.events.RefReceivedEvent;
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.inject.Inject;
@@ -42,15 +45,18 @@
         update.getExpectedOldObjectId(), update.getNewObjectId(), update.getName(), type);
   }
 
-  private final RefReceivedEvent event;
+  private final AllUsersName allUsersName;
   private final DynamicSet<RefOperationValidationListener> refOperationValidationListeners;
+  private final RefReceivedEvent event;
 
   @Inject
   RefOperationValidators(
+      AllUsersName allUsersName,
       DynamicSet<RefOperationValidationListener> refOperationValidationListeners,
       @Assisted Project project,
       @Assisted IdentifiedUser user,
       @Assisted ReceiveCommand cmd) {
+    this.allUsersName = allUsersName;
     this.refOperationValidationListeners = refOperationValidationListeners;
     event = new RefReceivedEvent();
     event.command = cmd;
@@ -59,11 +65,13 @@
   }
 
   public List<ValidationMessage> validateForRefOperation() throws RefOperationValidationException {
-
     List<ValidationMessage> messages = new ArrayList<>();
     boolean withException = false;
+    List<RefOperationValidationListener> listeners = new ArrayList<>();
+    listeners.add(new DisallowDeletionOfUserBranches(allUsersName));
+    refOperationValidationListeners.forEach(l -> listeners.add(l));
     try {
-      for (RefOperationValidationListener listener : refOperationValidationListeners) {
+      for (RefOperationValidationListener listener : listeners) {
         messages.addAll(listener.onRefOperation(event));
       }
     } catch (ValidationException e) {
@@ -95,4 +103,26 @@
       return input.isError();
     }
   }
+
+  private static class DisallowDeletionOfUserBranches implements RefOperationValidationListener {
+    private final AllUsersName allUsersName;
+
+    DisallowDeletionOfUserBranches(AllUsersName allUsersName) {
+      this.allUsersName = allUsersName;
+    }
+
+    @Override
+    public List<ValidationMessage> onRefOperation(RefReceivedEvent refEvent)
+        throws ValidationException {
+      if (refEvent.project.getNameKey().equals(allUsersName)
+          && (refEvent.command.getRefName().startsWith(RefNames.REFS_USERS)
+              && !refEvent.command.getRefName().equals(RefNames.REFS_USERS_DEFAULT))
+          && refEvent.command.getType().equals(ReceiveCommand.Type.DELETE)) {
+        if (!refEvent.user.getCapabilities().canAccessDatabase()) {
+          throw new ValidationException("Not allowed to delete user branch.");
+        }
+      }
+      return ImmutableList.of();
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java
index 47115af..4d48990 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/MergedSender.java
@@ -41,7 +41,7 @@
   public MergedSender(EmailArguments ea, @Assisted Project.NameKey project, @Assisted Change.Id id)
       throws OrmException {
     super(ea, "merged", newChangeData(ea, project, id));
-    labelTypes = changeData.changeControl().getLabelTypes();
+    labelTypes = changeData.getLabelTypes();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
index 4945879..24f5164 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/FailedPermissionBackend.java
@@ -164,5 +164,10 @@
         throws PermissionBackendException {
       throw new PermissionBackendException(message, cause);
     }
+
+    @Override
+    public CurrentUser user() {
+      throw new UnsupportedOperationException("FailedPermissionBackend is not scoped to user");
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
new file mode 100644
index 0000000..575a08b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/GlobalPermission.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.permissions;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import java.util.Locale;
+
+public enum GlobalPermission {
+  ACCESS_DATABASE(GlobalCapability.ACCESS_DATABASE),
+  ADMINISTRATE_SERVER(GlobalCapability.ADMINISTRATE_SERVER),
+  CREATE_ACCOUNT(GlobalCapability.CREATE_ACCOUNT),
+  CREATE_GROUP(GlobalCapability.CREATE_GROUP),
+  CREATE_PROJECT(GlobalCapability.CREATE_PROJECT),
+  EMAIL_REVIEWERS(GlobalCapability.EMAIL_REVIEWERS),
+  FLUSH_CACHES(GlobalCapability.FLUSH_CACHES),
+  KILL_TASK(GlobalCapability.KILL_TASK),
+  MAINTAIN_SERVER(GlobalCapability.MAINTAIN_SERVER),
+  MODIFY_ACCOUNT(GlobalCapability.MODIFY_ACCOUNT),
+  RUN_AS(GlobalCapability.RUN_AS),
+  RUN_GC(GlobalCapability.RUN_GC),
+  STREAM_EVENTS(GlobalCapability.STREAM_EVENTS),
+  VIEW_ALL_ACCOUNTS(GlobalCapability.VIEW_ALL_ACCOUNTS),
+  VIEW_CACHES(GlobalCapability.VIEW_CACHES),
+  VIEW_CONNECTIONS(GlobalCapability.VIEW_CONNECTIONS),
+  VIEW_PLUGINS(GlobalCapability.VIEW_PLUGINS),
+  VIEW_QUEUE(GlobalCapability.VIEW_QUEUE);
+
+  private final String name;
+
+  GlobalPermission(String name) {
+    this.name = name;
+  }
+
+  /** @return name used in {@code project.config} permissions. */
+  public String permissionName() {
+    return name;
+  }
+
+  public String describeForException() {
+    return toString().toLowerCase(Locale.US).replace('_', ' ');
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java
index 61f7330..747c997 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/LabelPermission.java
@@ -15,25 +15,69 @@
 package com.google.gerrit.server.permissions;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
+import static com.google.gerrit.server.permissions.LabelPermission.ForUser.SELF;
 
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.server.util.LabelVote;
 import java.util.Optional;
 
 /** Permission representing a label. */
 public class LabelPermission implements ChangePermissionOrLabel {
+  public enum ForUser {
+    SELF,
+    ON_BEHALF_OF;
+  }
+
+  private final ForUser forUser;
   private final String name;
 
   /**
    * Construct a reference to a label permission.
    *
+   * @param type type description of the label.
+   */
+  public LabelPermission(LabelType type) {
+    this(SELF, type);
+  }
+
+  /**
+   * Construct a reference to a label permission.
+   *
+   * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+   * @param type type description of the label.
+   */
+  public LabelPermission(ForUser forUser, LabelType type) {
+    this(forUser, type.getName());
+  }
+
+  /**
+   * Construct a reference to a label permission.
+   *
    * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
    */
   public LabelPermission(String name) {
+    this(SELF, name);
+  }
+
+  /**
+   * Construct a reference to a label permission.
+   *
+   * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+   * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+   */
+  public LabelPermission(ForUser forUser, String name) {
+    this.forUser = checkNotNull(forUser, "ForUser");
     this.name = LabelType.checkName(name);
   }
 
+  /** @return {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
+  public ForUser forUser() {
+    return forUser;
+  }
+
   /** @return name of the label, e.g. {@code "Code-Review"}. */
   public String label() {
     return name;
@@ -42,12 +86,21 @@
   /** @return name used in {@code project.config} permissions. */
   @Override
   public Optional<String> permissionName() {
-    return Optional.of(Permission.forLabel(label()));
+    switch (forUser) {
+      case SELF:
+        return Optional.of(Permission.forLabel(name));
+      case ON_BEHALF_OF:
+        return Optional.of(Permission.forLabelAs(name));
+    }
+    return Optional.empty();
   }
 
   @Override
   public String describeForException() {
-    return "label " + label();
+    if (forUser == ON_BEHALF_OF) {
+      return "labelAs " + name;
+    }
+    return "label " + name;
   }
 
   @Override
@@ -57,26 +110,87 @@
 
   @Override
   public boolean equals(Object other) {
-    return other instanceof LabelPermission && name.equals(((LabelPermission) other).name);
+    if (other instanceof LabelPermission) {
+      LabelPermission b = (LabelPermission) other;
+      return forUser == b.forUser && name.equals(b.name);
+    }
+    return false;
   }
 
   @Override
   public String toString() {
+    if (forUser == ON_BEHALF_OF) {
+      return "LabelAs[" + name + ']';
+    }
     return "Label[" + name + ']';
   }
 
   /** A {@link LabelPermission} at a specific value. */
   public static class WithValue implements ChangePermissionOrLabel {
+    private final ForUser forUser;
     private final LabelVote label;
 
     /**
      * Construct a reference to a label at a specific value.
      *
+     * @param type description of the label.
+     * @param value numeric score assigned to the label.
+     */
+    public WithValue(LabelType type, LabelValue value) {
+      this(SELF, type, value);
+    }
+
+    /**
+     * Construct a reference to a label at a specific value.
+     *
+     * @param type description of the label.
+     * @param value numeric score assigned to the label.
+     */
+    public WithValue(LabelType type, short value) {
+      this(SELF, type.getName(), value);
+    }
+
+    /**
+     * Construct a reference to a label at a specific value.
+     *
+     * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+     * @param type description of the label.
+     * @param value numeric score assigned to the label.
+     */
+    public WithValue(ForUser forUser, LabelType type, LabelValue value) {
+      this(forUser, type.getName(), value.getValue());
+    }
+
+    /**
+     * Construct a reference to a label at a specific value.
+     *
+     * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+     * @param type description of the label.
+     * @param value numeric score assigned to the label.
+     */
+    public WithValue(ForUser forUser, LabelType type, short value) {
+      this(forUser, type.getName(), value);
+    }
+
+    /**
+     * Construct a reference to a label at a specific value.
+     *
      * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
      * @param value numeric score assigned to the label.
      */
     public WithValue(String name, short value) {
-      this(LabelVote.create(name, value));
+      this(SELF, name, value);
+    }
+
+    /**
+     * Construct a reference to a label at a specific value.
+     *
+     * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+     * @param name name of the label, e.g. {@code "Code-Review"} or {@code "Verified"}.
+     * @param value numeric score assigned to the label.
+     */
+    public WithValue(ForUser forUser, String name, short value) {
+      this(forUser, LabelVote.create(name, value));
     }
 
     /**
@@ -85,9 +199,25 @@
      * @param label label name and vote.
      */
     public WithValue(LabelVote label) {
+      this(SELF, label);
+    }
+
+    /**
+     * Construct a reference to a label at a specific value.
+     *
+     * @param forUser {@code SELF} (default) or {@code ON_BEHALF_OF} for labelAs behavior.
+     * @param label label name and vote.
+     */
+    public WithValue(ForUser forUser, LabelVote label) {
+      this.forUser = checkNotNull(forUser, "ForUser");
       this.label = checkNotNull(label, "LabelVote");
     }
 
+    /** @return {@code SELF} or {@code ON_BEHALF_OF} (or labelAs). */
+    public ForUser forUser() {
+      return forUser;
+    }
+
     /** @return name of the label, e.g. {@code "Code-Review"}. */
     public String label() {
       return label.label();
@@ -101,11 +231,20 @@
     /** @return name used in {@code project.config} permissions. */
     @Override
     public Optional<String> permissionName() {
-      return Optional.of(Permission.forLabel(label()));
+      switch (forUser) {
+        case SELF:
+          return Optional.of(Permission.forLabel(label()));
+        case ON_BEHALF_OF:
+          return Optional.of(Permission.forLabelAs(label()));
+      }
+      return Optional.empty();
     }
 
     @Override
     public String describeForException() {
+      if (forUser == ON_BEHALF_OF) {
+        return "labelAs " + label.formatWithEquals();
+      }
       return "label " + label.formatWithEquals();
     }
 
@@ -116,11 +255,18 @@
 
     @Override
     public boolean equals(Object other) {
-      return other instanceof WithValue && label.equals(((WithValue) other).label);
+      if (other instanceof WithValue) {
+        WithValue b = (WithValue) other;
+        return forUser == b.forUser && label.equals(b.label);
+      }
+      return false;
     }
 
     @Override
     public String toString() {
+      if (forUser == ON_BEHALF_OF) {
+        return "LabelAs[" + label.format() + ']';
+      }
       return "Label[" + label.format() + ']';
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
index 9e5350b..b3a858c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/permissions/PermissionBackend.java
@@ -15,7 +15,9 @@
 package com.google.gerrit.server.permissions;
 
 import static com.google.common.base.Preconditions.checkNotNull;
+import static java.util.stream.Collectors.toSet;
 
+import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
@@ -34,10 +36,7 @@
 import org.slf4j.LoggerFactory;
 
 /**
- * Checks authorization to perform an action on project, ref, or change.
- *
- * <p>{@code PermissionBackend} should be a singleton for the server, acting as a factory for
- * lightweight request instances.
+ * Checks authorization to perform an action on a project, reference, or change.
  *
  * <p>{@code check} methods should be used during action handlers to verify the user is allowed to
  * exercise the specified permission. For convenience in implementation {@code check} methods throw
@@ -48,6 +47,13 @@
  * permission. This is suitable for configuring UI button state, but should not be relied upon to
  * guard handlers before making state changes.
  *
+ * <p>{@code PermissionBackend} is a singleton for the server, acting as a factory for lightweight
+ * request instances. Implementation classes may cache supporting data inside of {@link WithUser},
+ * {@link ForProject}, {@link ForRef}, and {@link ForChange} instances, in addition to storing
+ * within {@link CurrentUser} using a {@link com.google.gerrit.server.CurrentUser.PropertyKey}.
+ * {@link GlobalPermission} caching for {@link WithUser} may best cached inside {@link CurrentUser}
+ * as {@link WithUser} instances are frequently created.
+ *
  * <p>Example use:
  *
  * <pre>
@@ -126,6 +132,27 @@
     public ForChange change(ChangeNotes notes) {
       return ref(notes.getChange().getDest()).change(notes);
     }
+
+    /** Verify scoped user can {@code perm}, throwing if denied. */
+    public abstract void check(GlobalPermission perm)
+        throws AuthException, PermissionBackendException;
+
+    /** Filter {@code permSet} to permissions scoped user might be able to perform. */
+    public abstract Set<GlobalPermission> test(Collection<GlobalPermission> permSet)
+        throws PermissionBackendException;
+
+    public boolean test(GlobalPermission perm) throws PermissionBackendException {
+      return test(EnumSet.of(perm)).contains(perm);
+    }
+
+    public boolean testOrFalse(GlobalPermission perm) {
+      try {
+        return test(perm);
+      } catch (PermissionBackendException e) {
+        logger.warn("Cannot test " + perm + "; assuming false", e);
+        return false;
+      }
+    }
   }
 
   /** PermissionBackend scoped to a user and project. */
@@ -174,6 +201,9 @@
 
   /** PermissionBackend scoped to a user, project, reference and change. */
   public abstract static class ForChange extends AcceptsReviewDb<ForChange> {
+    /** @return user this instance is scoped to. */
+    public abstract CurrentUser user();
+
     /** @return new instance rescoped to same change, but different {@code user}. */
     public abstract ForChange user(CurrentUser user);
 
@@ -207,5 +237,97 @@
         return false;
       }
     }
+
+    /**
+     * Test which values of a label the user may be able to set.
+     *
+     * @param label definition of the label to test values of.
+     * @return set containing values the user may be able to use; may be empty if none.
+     * @throws PermissionBackendException if failure consulting backend configuration.
+     */
+    public Set<LabelPermission.WithValue> test(LabelType label) throws PermissionBackendException {
+      return test(valuesOf(checkNotNull(label, "LabelType")));
+    }
+
+    /**
+     * Test which values of a group of labels the user may be able to set.
+     *
+     * @param types definition of the labels to test values of.
+     * @return set containing values the user may be able to use; may be empty if none.
+     * @throws PermissionBackendException if failure consulting backend configuration.
+     */
+    public Set<LabelPermission.WithValue> testLabels(Collection<LabelType> types)
+        throws PermissionBackendException {
+      checkNotNull(types, "LabelType");
+      return test(types.stream().flatMap((t) -> valuesOf(t).stream()).collect(toSet()));
+    }
+
+    private static Set<LabelPermission.WithValue> valuesOf(LabelType label) {
+      return label
+          .getValues()
+          .stream()
+          .map((v) -> new LabelPermission.WithValue(label, v))
+          .collect(toSet());
+    }
+
+    /**
+     * Squash a label value to the nearest allowed value.
+     *
+     * <p>For multi-valued labels like Code-Review with values -2..+2 a user may try to use +2, but
+     * only have permission for the -1..+1 range. The caller should have already tried:
+     *
+     * <pre>
+     * check(new LabelPermission.WithValue("Code-Review", 2));
+     * </pre>
+     *
+     * and caught {@link AuthException}. {@code squashThenCheck} will use {@link #test(LabelType)}
+     * to determine potential values of Code-Review the user can use, and select the nearest value
+     * along the same sign, e.g. -1 for -2 and +1 for +2.
+     *
+     * @param label definition of the label to test values of.
+     * @param val previously denied value the user attempted.
+     * @return nearest allowed value, or {@code 0} if no value was allowed.
+     * @throws PermissionBackendException backend cannot run test or check.
+     */
+    public short squashThenCheck(LabelType label, short val) throws PermissionBackendException {
+      short s = squashByTest(label, val);
+      if (s == 0 || s == val) {
+        return 0;
+      }
+      try {
+        check(new LabelPermission.WithValue(label, s));
+        return s;
+      } catch (AuthException e) {
+        return 0;
+      }
+    }
+
+    /**
+     * Squash a label value to the nearest allowed value using only test methods.
+     *
+     * <p>Tests all possible values and selects the closet available to {@code val} while matching
+     * the sign of {@code val}. Unlike {@code #squashThenCheck(LabelType, short)} this method only
+     * uses {@code test} methods and should not be used in contexts like a review handler without
+     * checking the resulting score.
+     *
+     * @param label definition of the label to test values of.
+     * @param val previously denied value the user attempted.
+     * @return nearest likely allowed value, or {@code 0} if no value was identified.
+     * @throws PermissionBackendException backend cannot run test.
+     */
+    public short squashByTest(LabelType label, short val) throws PermissionBackendException {
+      return nearest(test(label), val);
+    }
+
+    private static short nearest(Iterable<LabelPermission.WithValue> possible, short wanted) {
+      short s = 0;
+      for (LabelPermission.WithValue v : possible) {
+        if ((wanted < 0 && v.value() < 0 && wanted <= v.value() && v.value() < s)
+            || (wanted > 0 && v.value() > 0 && wanted >= v.value() && v.value() > s)) {
+          s = v.value();
+        }
+      }
+      return s;
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index af89d94..3e5eba3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkState;
+import static com.google.gerrit.server.permissions.LabelPermission.ForUser.ON_BEHALF_OF;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -527,8 +528,13 @@
     }
 
     @Override
+    public CurrentUser user() {
+      return getUser();
+    }
+
+    @Override
     public ForChange user(CurrentUser user) {
-      return getUser().equals(user) ? this : forUser(user).asForChange(cd, db);
+      return user().equals(user) ? this : forUser(user).asForChange(cd, db);
     }
 
     @Override
@@ -603,7 +609,11 @@
     }
 
     private boolean can(LabelPermission.WithValue perm) {
-      return label(perm.permissionName().get()).contains(perm.value());
+      PermissionRange r = label(perm.permissionName().get());
+      if (perm.forUser() == ON_BEHALF_OF && r.isEmpty()) {
+        return false;
+      }
+      return r.contains(perm.value());
     }
 
     private PermissionRange label(String permission) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
index 51fe493..a8f9efa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DefaultPermissionBackend.java
@@ -16,13 +16,19 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.permissions.FailedPermissionBackend;
+import com.google.gerrit.server.permissions.GlobalPermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Set;
 
 @Singleton
 class DefaultPermissionBackend extends PermissionBackend {
@@ -57,5 +63,28 @@
         return FailedPermissionBackend.project("unavailable", e);
       }
     }
+
+    @Override
+    public void check(GlobalPermission perm) throws AuthException, PermissionBackendException {
+      if (!can(perm)) {
+        throw new AuthException(perm.describeForException() + " not permitted");
+      }
+    }
+
+    @Override
+    public Set<GlobalPermission> test(Collection<GlobalPermission> permSet)
+        throws PermissionBackendException {
+      EnumSet<GlobalPermission> ok = EnumSet.noneOf(GlobalPermission.class);
+      for (GlobalPermission perm : permSet) {
+        if (can(perm)) {
+          ok.add(perm);
+        }
+      }
+      return ok;
+    }
+
+    private boolean can(GlobalPermission perm) throws PermissionBackendException {
+      return user.getCapabilities().doCanForDefaultPermissionBackend(perm);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 1dbe5cd..64ef091 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -33,6 +33,7 @@
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -757,6 +758,10 @@
     return change;
   }
 
+  public LabelTypes getLabelTypes() {
+    return changeControl.getLabelTypes();
+  }
+
   public ChangeNotes notes() throws OrmException {
     if (notes == null) {
       if (!lazyLoad) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 6b66c41..1ae54ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -66,6 +66,7 @@
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
 import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ListChildProjects;
 import com.google.gerrit.server.project.ProjectCache;
@@ -182,6 +183,7 @@
     final AccountResolver accountResolver;
     final AllProjectsName allProjectsName;
     final AllUsersName allUsersName;
+    final PermissionBackend permissionBackend;
     final CapabilityControl.Factory capabilityControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
     final ChangeData.Factory changeDataFactory;
@@ -221,6 +223,7 @@
         DynamicMap<ChangeHasOperandFactory> hasOperands,
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
+        PermissionBackend permissionBackend,
         CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         ChangeNotes.Factory notesFactory,
@@ -253,6 +256,7 @@
           hasOperands,
           userFactory,
           self,
+          permissionBackend,
           capabilityControlFactory,
           changeControlGenericFactory,
           notesFactory,
@@ -287,6 +291,7 @@
         DynamicMap<ChangeHasOperandFactory> hasOperands,
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
+        PermissionBackend permissionBackend,
         CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         ChangeNotes.Factory notesFactory,
@@ -317,6 +322,7 @@
       this.opFactories = opFactories;
       this.userFactory = userFactory;
       this.self = self;
+      this.permissionBackend = permissionBackend;
       this.capabilityControlFactory = capabilityControlFactory;
       this.notesFactory = notesFactory;
       this.changeControlGenericFactory = changeControlGenericFactory;
@@ -353,6 +359,7 @@
           hasOperands,
           userFactory,
           Providers.of(otherUser),
+          permissionBackend,
           capabilityControlFactory,
           changeControlGenericFactory,
           notesFactory,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index bb251cb..a5814fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
@@ -24,8 +23,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.index.change.ChangeField;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.permissions.ChangePermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
@@ -33,7 +33,7 @@
 
 class EqualsLabelPredicate extends ChangeIndexPredicate {
   private final ProjectCache projectCache;
-  private final ChangeControl.GenericFactory ccFactory;
+  private final PermissionBackend permissionBackend;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<ReviewDb> dbProvider;
   private final String label;
@@ -43,7 +43,7 @@
 
   EqualsLabelPredicate(LabelPredicate.Args args, String label, int expVal, Account.Id account) {
     super(ChangeField.LABEL, ChangeField.formatLabel(label, expVal, account));
-    this.ccFactory = args.ccFactory;
+    this.permissionBackend = args.permissionBackend;
     this.projectCache = args.projectCache;
     this.userFactory = args.userFactory;
     this.dbProvider = args.dbProvider;
@@ -78,7 +78,7 @@
     for (PatchSetApproval p : object.currentApprovals()) {
       if (labelType.matches(p)) {
         hasVote = true;
-        if (match(c, p.getValue(), p.getAccountId(), labelType)) {
+        if (match(object, p.getValue(), p.getAccountId(), labelType)) {
           return true;
         }
       }
@@ -104,40 +104,28 @@
     return null;
   }
 
-  private boolean match(Change change, int value, Account.Id approver, LabelType type)
-      throws OrmException {
-    int psVal = value;
-    if (psVal == expVal) {
-      // Double check the value is still permitted for the user.
-      //
-      IdentifiedUser reviewer = userFactory.create(approver);
-      try {
-        ChangeControl cc = ccFactory.controlFor(dbProvider.get(), change, reviewer);
-        if (!cc.isVisible(dbProvider.get())) {
-          // The user can't see the change anymore.
-          //
-          return false;
-        }
-        psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal);
-      } catch (NoSuchChangeException e) {
-        // The project has disappeared.
-        //
-        return false;
-      }
-
-      if (account != null && !account.equals(approver)) {
-        return false;
-      }
-
-      if (group != null && !reviewer.getEffectiveGroups().contains(group)) {
-        return false;
-      }
-
-      if (psVal == expVal) {
-        return true;
-      }
+  private boolean match(ChangeData cd, short value, Account.Id approver, LabelType type) {
+    if (value != expVal) {
+      return false;
     }
-    return false;
+
+    if (account != null && !account.equals(approver)) {
+      return false;
+    }
+
+    IdentifiedUser reviewer = userFactory.create(approver);
+    if (group != null && !reviewer.getEffectiveGroups().contains(group)) {
+      return false;
+    }
+
+    // Double check the value is still permitted for the user.
+    try {
+      PermissionBackend.ForChange perm =
+          permissionBackend.user(reviewer).database(dbProvider).change(cd);
+      return perm.test(ChangePermission.READ) && expVal == perm.squashByTest(type, value);
+    } catch (PermissionBackendException e) {
+      return false;
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index 9fdbcef..3fe6a6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.OrPredicate;
@@ -36,6 +37,7 @@
 
   static class Args {
     final ProjectCache projectCache;
+    final PermissionBackend permissionBackend;
     final ChangeControl.GenericFactory ccFactory;
     final IdentifiedUser.GenericFactory userFactory;
     final Provider<ReviewDb> dbProvider;
@@ -45,6 +47,7 @@
 
     private Args(
         ProjectCache projectCache,
+        PermissionBackend permissionBackend,
         ChangeControl.GenericFactory ccFactory,
         IdentifiedUser.GenericFactory userFactory,
         Provider<ReviewDb> dbProvider,
@@ -52,6 +55,7 @@
         Set<Account.Id> accounts,
         AccountGroup.UUID group) {
       this.projectCache = projectCache;
+      this.permissionBackend = permissionBackend;
       this.ccFactory = ccFactory;
       this.userFactory = userFactory;
       this.dbProvider = dbProvider;
@@ -84,6 +88,7 @@
         predicates(
             new Args(
                 a.projectCache,
+                a.permissionBackend,
                 a.changeControlGenericFactory,
                 a.userFactory,
                 a.db,
diff --git a/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java b/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java
new file mode 100644
index 0000000..b2b9890
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED__check_user_label_3.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gerrit;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
+import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
+import com.googlecode.prolog_cafe.exceptions.PrologException;
+import com.googlecode.prolog_cafe.exceptions.SystemException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+import com.googlecode.prolog_cafe.lang.Predicate;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
+
+/**
+ * Checks user can set label to val.
+ *
+ * <pre>
+ *   '_check_user_label'(+Label, +CurrentUser, +Val)
+ * </pre>
+ */
+class PRED__check_user_label_3 extends Predicate.P3 {
+  PRED__check_user_label_3(Term a1, Term a2, Term a3, Operation n) {
+    arg1 = a1;
+    arg2 = a2;
+    arg3 = a3;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+    Term a2 = arg2.dereference();
+    Term a3 = arg3.dereference();
+
+    if (a1 instanceof VariableTerm) {
+      throw new PInstantiationException(this, 1);
+    }
+    if (!(a1 instanceof SymbolTerm)) {
+      throw new IllegalTypeException(this, 1, "atom", a1);
+    }
+    String label = a1.name();
+
+    if (a2 instanceof VariableTerm) {
+      throw new PInstantiationException(this, 2);
+    }
+    if (!(a2 instanceof JavaObjectTerm) || !a2.convertible(CurrentUser.class)) {
+      throw new IllegalTypeException(this, 2, "CurrentUser)", a2);
+    }
+    CurrentUser user = (CurrentUser) ((JavaObjectTerm) a2).object();
+
+    if (a3 instanceof VariableTerm) {
+      throw new PInstantiationException(this, 3);
+    }
+    if (!(a3 instanceof IntegerTerm)) {
+      throw new IllegalTypeException(this, 3, "integer", a3);
+    }
+    short val = (short) ((IntegerTerm) a3).intValue();
+
+    ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
+    LabelType type = cd.getLabelTypes().byLabel(label);
+    if (type == null) {
+      return engine.fail();
+    }
+
+    try {
+      StoredValues.PERMISSION_BACKEND
+          .get(engine)
+          .user(user)
+          .change(cd)
+          .check(new LabelPermission.WithValue(type, val));
+      return cont;
+    } catch (AuthException err) {
+      return engine.fail();
+    } catch (PermissionBackendException err) {
+      SystemException se = new SystemException(err.getMessage());
+      se.initCause(err);
+      throw se;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index 8b5a33d..5a3d656 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -38,7 +38,7 @@
     Term listHead = Prolog.Nil;
     try {
       ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
-      LabelTypes types = StoredValues.CHANGE_CONTROL.get(engine).getLabelTypes();
+      LabelTypes types = cd.getLabelTypes();
 
       for (PatchSetApproval a : cd.currentApprovals()) {
         LabelType t = types.byLabel(a.getLabelId());
diff --git a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
index d06664e..5c61007 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
@@ -14,14 +14,16 @@
 
 package gerrit;
 
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.permissions.LabelPermission;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.googlecode.prolog_cafe.exceptions.IllegalTypeException;
 import com.googlecode.prolog_cafe.exceptions.PInstantiationException;
 import com.googlecode.prolog_cafe.exceptions.PrologException;
+import com.googlecode.prolog_cafe.exceptions.SystemException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
@@ -30,12 +32,13 @@
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 import com.googlecode.prolog_cafe.lang.VariableTerm;
+import java.util.Set;
 
 /**
  * Resolves the valid range for a label on a CurrentUser.
  *
  * <pre>
- *   '$user_label_range'(+Label, +CurrentUser, -Min, -Max)
+ *   '_user_label_range'(+Label, +CurrentUser, -Min, -Max)
  * </pre>
  */
 class PRED__user_label_range_4 extends Predicate.P4 {
@@ -71,20 +74,33 @@
     }
     CurrentUser user = (CurrentUser) ((JavaObjectTerm) a2).object();
 
-    ChangeControl ctl = StoredValues.CHANGE_CONTROL.get(engine).forUser(user);
-    PermissionRange range = ctl.getRange(Permission.LABEL + label);
-    if (range == null) {
+    ChangeData cd = StoredValues.CHANGE_DATA.get(engine);
+    LabelType type = cd.getLabelTypes().byLabel(label);
+    if (type == null) {
       return engine.fail();
     }
 
-    IntegerTerm min = new IntegerTerm(range.getMin());
-    IntegerTerm max = new IntegerTerm(range.getMax());
+    Set<LabelPermission.WithValue> can;
+    try {
+      can = StoredValues.PERMISSION_BACKEND.get(engine).user(user).change(cd).test(type);
+    } catch (PermissionBackendException err) {
+      SystemException se = new SystemException(err.getMessage());
+      se.initCause(err);
+      throw se;
+    }
 
-    if (!a3.unify(min, engine.trail)) {
+    int min = 0;
+    int max = 0;
+    for (LabelPermission.WithValue v : can) {
+      min = Math.min(min, v.value());
+      max = Math.max(max, v.value());
+    }
+
+    if (!a3.unify(new IntegerTerm(min), engine.trail)) {
       return engine.fail();
     }
 
-    if (!a4.unify(max, engine.trail)) {
+    if (!a4.unify(new IntegerTerm(max), engine.trail)) {
       return engine.fail();
     }
 
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
index ea3fb17..33d63c4 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
@@ -51,7 +51,7 @@
   public Operation exec(Prolog engine) throws PrologException {
     engine.setB0();
     Term a1 = arg1.dereference();
-    List<LabelType> list = StoredValues.CHANGE_CONTROL.get(engine).getLabelTypes().getLabelTypes();
+    List<LabelType> list = StoredValues.CHANGE_DATA.get(engine).getLabelTypes().getLabelTypes();
     Term head = Prolog.Nil;
     for (int idx = list.size() - 1; 0 <= idx; idx--) {
       head = new ListTerm(export(list.get(idx)), head);
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 59c926f..4671e0d 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -92,6 +92,27 @@
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %%
+%% check_user_label/3:
+%%
+%%   Check Who can set Label to Val.
+%%
+check_user_label(Label, Who, Val) :-
+  hash_get(commit_labels, '$fast_range', true), !,
+  atom(Label),
+  assume_range_from_label(Label, Who, Min, Max),
+  Min @=< Val, Val @=< Max.
+check_user_label(Label, Who, Val) :-
+  Who = user(_), !,
+  atom(Label),
+  current_user(Who, User),
+  '_check_user_label'(Label, User, Val).
+check_user_label(Label, test_user(Name), Val) :-
+  clause(user:test_grant(Label, test_user(Name), range(Min, Max)), _),
+  Min @=< Val, Val @=< Max
+  .
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
 %% user_label_range/4:
 %%
 %%   Lookup the range allowed to be used.
@@ -319,8 +340,7 @@
 %%
 check_label_range_permission(Label, ExpValue, ok(Who)) :-
   commit_label(label(Label, ExpValue), Who),
-  user_label_range(Label, Who, Min, Max),
-  Min @=< ExpValue, ExpValue @=< Max
+  check_user_label(Label, Who, ExpValue)
   .
 %TODO Uncomment this clause when group suggesting is possible.
 %check_label_range_permission(Label, ExpValue, ask(Group)) :-
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index fa4a951..3dfcaec 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -17,8 +17,8 @@
 import static org.easymock.EasyMock.expect;
 
 import com.google.gerrit.common.data.LabelTypes;
-import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.AbstractModule;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
 import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
@@ -47,7 +47,8 @@
             cfg.setInt("rules", null, "reductionLimit", 1300);
             cfg.setInt("rules", null, "compileReductionLimit", (int) 1e6);
             bind(PrologEnvironment.Args.class)
-                .toInstance(new PrologEnvironment.Args(null, null, null, null, null, null, cfg));
+                .toInstance(
+                    new PrologEnvironment.Args(null, null, null, null, null, null, null, cfg));
           }
         });
   }
@@ -55,10 +56,10 @@
   @Override
   protected void setUpEnvironment(PrologEnvironment env) {
     LabelTypes labelTypes = new LabelTypes(Arrays.asList(Util.codeReview(), Util.verified()));
-    ChangeControl ctl = EasyMock.createMock(ChangeControl.class);
-    expect(ctl.getLabelTypes()).andStubReturn(labelTypes);
-    EasyMock.replay(ctl);
-    env.set(StoredValues.CHANGE_CONTROL, ctl);
+    ChangeData cd = EasyMock.createMock(ChangeData.class);
+    expect(cd.getLabelTypes()).andStubReturn(labelTypes);
+    EasyMock.replay(cd);
+    env.set(StoredValues.CHANGE_DATA, cd);
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
index 6fda100..3bbd335 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/FakeQueryBuilder.java
@@ -27,8 +27,8 @@
         new FakeQueryBuilder.Definition<>(FakeQueryBuilder.class),
         new ChangeQueryBuilder.Arguments(
             null, null, null, null, null, null, null, null, null, null, null, null, null, null,
-            null, null, null, null, null, null, null, indexes, null, null, null, null, null, null,
-            null, null, null));
+            null, null, null, null, null, null, null, null, indexes, null, null, null, null, null,
+            null, null, null, null));
   }
 
   @Operator
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 53a98eb..54371c1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -18,11 +18,15 @@
 
 import com.google.common.base.Throwables;
 import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -45,6 +49,7 @@
 public final class SuExec extends BaseCommand {
   private final SshScope sshScope;
   private final DispatchCommandProvider dispatcher;
+  private final PermissionBackend permissionBackend;
 
   private boolean enableRunAs;
   private CurrentUser caller;
@@ -67,6 +72,7 @@
   SuExec(
       final SshScope sshScope,
       @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
+      PermissionBackend permissionBackend,
       final CurrentUser caller,
       final SshSession session,
       final IdentifiedUser.GenericFactory userFactory,
@@ -74,6 +80,7 @@
       AuthConfig config) {
     this.sshScope = sshScope;
     this.dispatcher = dispatcher;
+    this.permissionBackend = permissionBackend;
     this.caller = caller;
     this.session = session;
     this.userFactory = userFactory;
@@ -115,8 +122,14 @@
       // OK.
     } else if (!enableRunAs) {
       throw die("suexec disabled by auth.enableRunAs = false");
-    } else if (!caller.getCapabilities().canRunAs()) {
-      throw die("suexec not permitted");
+    } else {
+      try {
+        permissionBackend.user(caller).check(GlobalPermission.RUN_AS);
+      } catch (AuthException e) {
+        throw die("suexec not permitted");
+      } catch (PermissionBackendException e) {
+        throw die("suexec not available: " + e);
+      }
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 42c7578..ef1cd81 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
 import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
@@ -29,8 +32,8 @@
 @RequiresCapability(GlobalCapability.ACCESS_DATABASE)
 @CommandMetaData(name = "gsql", description = "Administrative interface to active database")
 final class AdminQueryShell extends SshCommand {
+  @Inject private PermissionBackend permissionBackend;
   @Inject private QueryShell.Factory factory;
-
   @Inject private IdentifiedUser currentUser;
 
   @Option(name = "--format", usage = "Set output format")
@@ -42,9 +45,11 @@
   @Override
   protected void run() throws Failure {
     try {
-      checkPermission();
-    } catch (PermissionDeniedException err) {
+      permissionBackend.user(currentUser).check(GlobalPermission.ACCESS_DATABASE);
+    } catch (AuthException err) {
       throw die(err.getMessage());
+    } catch (PermissionBackendException e) {
+      throw new Failure(1, "unavailable", e);
     }
 
     QueryShell shell = factory.create(in, out);
@@ -55,22 +60,4 @@
       shell.run();
     }
   }
-
-  /**
-   * Assert that the current user is permitted to perform raw queries.
-   *
-   * <p>As the @RequireCapability guards at various entry points of internal commands implicitly add
-   * administrators (which we want to avoid), we also check permissions within QueryShell and grant
-   * access only to those who can access the database, regardless of whether they are administrators
-   * or not.
-   *
-   * @throws PermissionDeniedException
-   */
-  private void checkPermission() throws PermissionDeniedException {
-    if (!currentUser.getCapabilities().canAccessDatabase()) {
-      throw new PermissionDeniedException(
-          String.format(
-              "%s does not have \"Access Database\" capability.", currentUser.getUserName()));
-    }
-  }
 }
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
new file mode 100644
index 0000000..2ec8538
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
@@ -0,0 +1,37 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<script>
+(function(window) {
+  'use strict';
+
+  /** @polymerBehavior Gerrit.BaseUrlBehavior */
+  var BaseUrlBehavior = {
+    getBaseUrl: function() {
+      return window.CANONICAL_PATH || '';
+    },
+
+    computeGwtUrl: function(path) {
+      var base = this.getBaseUrl();
+      var clientPath = path.substring(base.length);
+      return base + '/?polygerrit=0#' + clientPath;
+    },
+  };
+
+  window.Gerrit = window.Gerrit || {};
+  window.Gerrit.BaseUrlBehavior = BaseUrlBehavior;
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
new file mode 100644
index 0000000..1e277bc
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior_test.html
@@ -0,0 +1,75 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>keyboard-shortcut-behavior</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<script>
+  window.CANONICAL_PATH = '/r';
+</script>
+<link rel="import" href="base-url-behavior.html">
+
+<test-fixture id="basic">
+  <template>
+    <test-element></test-element>
+  </template>
+</test-fixture>
+
+<test-fixture id="within-overlay">
+  <template>
+    <gr-overlay>
+      <test-element></test-element>
+    </gr-overlay>
+  </template>
+</test-fixture>
+
+<script>
+  suite('base-url-behavior tests', function() {
+    var element;
+    var overlay;
+
+    suiteSetup(function() {
+      // Define a Polymer element that uses this behavior.
+      Polymer({
+        is: 'test-element',
+        behaviors: [
+          Gerrit.BaseUrlBehavior,
+        ],
+      });
+    });
+
+    setup(function() {
+      element = fixture('basic');
+      overlay = fixture('within-overlay');
+    });
+
+    test('getBaseUrl', function() {
+      assert.deepEqual(element.getBaseUrl(), '/r');
+    });
+
+    test('computeGwtUrl', function() {
+      assert.deepEqual(
+        element.computeGwtUrl('/r/c/1/'),
+        '/r/?polygerrit=0#/c/1/'
+      );
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
similarity index 90%
rename from polygerrit-ui/app/behaviors/rest-client-behavior.html
rename to polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
index b7cf467..f71fe8f 100644
--- a/polygerrit-ui/app/behaviors/rest-client-behavior.html
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior.html
@@ -13,7 +13,8 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
-<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../../bower_components/polymer/polymer.html">
+<link rel="import" href="../base-url-behavior/base-url-behavior.html">
 <script>
 (function(window) {
   'use strict';
@@ -99,7 +100,7 @@
     },
 
     changeBaseURL: function(changeNum, patchNum) {
-      var v = '/changes/' + changeNum;
+      var v =  this.getBaseUrl() + '/changes/' + changeNum;
       if (patchNum) {
         v += '/revisions/' + patchNum;
       }
@@ -107,7 +108,7 @@
     },
 
     changePath: function(changeNum) {
-      return '/c/' + changeNum;
+      return this.getBaseUrl() + '/c/' + changeNum;
     },
 
     changeIsOpen: function(status) {
@@ -134,6 +135,9 @@
   };
 
   window.Gerrit = window.Gerrit || {};
-  window.Gerrit.RESTClientBehavior = RESTClientBehavior;
+  window.Gerrit.RESTClientBehavior = [
+    Gerrit.BaseUrlBehavior,
+    RESTClientBehavior
+  ];
 })(window);
 </script>
diff --git a/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
new file mode 100644
index 0000000..2b3e858
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/rest-client-behavior/rest-client-behavior_test.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>keyboard-shortcut-behavior</title>
+
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<script>
+  window.CANONICAL_PATH = '/r';
+</script>
+
+<link rel="import" href="../../bower_components/iron-test-helpers/iron-test-helpers.html">
+<link rel="import" href="../base-url-behavior/base-url-behavior.html">
+<link rel="import" href="rest-client-behavior.html">
+
+<test-fixture id="basic">
+  <template>
+    <test-element></test-element>
+  </template>
+</test-fixture>
+
+<test-fixture id="within-overlay">
+  <template>
+    <gr-overlay>
+      <test-element></test-element>
+    </gr-overlay>
+  </template>
+</test-fixture>
+
+<script>
+  suite('rest-client-behavior tests', function() {
+    var element;
+    var overlay;
+
+    suiteSetup(function() {
+      // Define a Polymer element that uses this behavior.
+      Polymer({
+        is: 'test-element',
+        behaviors: [
+          Gerrit.BaseUrlBehavior,
+          Gerrit.RESTClientBehavior,
+        ],
+      });
+    });
+
+    setup(function() {
+      element = fixture('basic');
+      overlay = fixture('within-overlay');
+    });
+
+    test('changeBaseURL', function() {
+      assert.deepEqual(
+        element.changeBaseURL('1', '1'),
+        '/r/changes/1/revisions/1'
+      );
+    });
+
+    test('changePath', function() {
+      assert.deepEqual(element.changePath('1'), '/r/c/1');
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
index 8e2b192..d50e0b3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.html
@@ -13,9 +13,10 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
index 280de86..566dfe0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -34,6 +34,7 @@
     },
 
     behaviors: [
+      Gerrit.BaseUrlBehavior,
       Gerrit.ChangeTableBehavior,
       Gerrit.RESTClientBehavior,
       Gerrit.URLEncodingBehavior,
@@ -41,7 +42,7 @@
 
     _computeChangeURL: function(changeNum) {
       if (!changeNum) { return ''; }
-      return '/c/' + changeNum + '/';
+      return this.getBaseUrl() + '/c/' + changeNum + '/';
     },
 
     _computeLabelTitle: function(change, labelName) {
@@ -101,7 +102,7 @@
     },
 
     _computeProjectURL: function(project) {
-      return '/q/status:open+project:' +
+      return this.getBaseUrl() + '/q/status:open+project:' +
           this.encodeURL(project, false);
     },
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
index c9a8d64..8a95fa8 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.html
@@ -14,10 +14,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../styles/gr-change-list-styles.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-change-list-item/gr-change-list-item.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index 05306ae..71ccb04 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -14,9 +14,9 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
 
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 5f11219..4ecd4b7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -14,9 +14,10 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html">
 <link rel="import" href="../../shared/gr-label/gr-label.html">
 <link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 6caed12..834069e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -47,6 +47,7 @@
     },
 
     behaviors: [
+      Gerrit.BaseUrlBehavior,
       Gerrit.RESTClientBehavior,
       Gerrit.URLEncodingBehavior,
     ],
@@ -234,7 +235,8 @@
     },
 
     _computeProjectURL: function(project) {
-      return '/q/status:open+project:' + this.encodeURL(project, false);
+      return this.getBaseUrl() + '/q/status:open+project:' +
+        this.encodeURL(project, false);
     },
 
     _computeBranchURL: function(project, branch) {
@@ -244,15 +246,16 @@
       } else {
         status = this.change.status.toLowerCase();
       }
-      return '/q/project:' + this.encodeURL(project, false) +
+      return this.getBaseUrl() + '/q/project:' +
+        this.encodeURL(project, false) +
           ' branch:' + this.encodeURL(branch, false) +
-          ' status:' + this.encodeURL(status, false);
+              ' status:' + this.encodeURL(status, false);
     },
 
     _computeTopicHref: function(topic) {
       var encodedTopic = encodeURIComponent('\"' + topic + '\"');
-      return '/q/topic:' + encodeURIComponent(encodedTopic) +
-          '+(status:open OR status:merged)';
+      return this.getBaseUrl() + '/q/topic:' +
+        encodeURIComponent(encodedTopic) + '+(status:open OR status:merged)';
     },
 
     _handleTopicRemoved: function() {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 22f742d..cec9487 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -14,10 +14,11 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 8a317ab..8ce7a83 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -162,6 +162,7 @@
     },
 
     behaviors: [
+      Gerrit.BaseUrlBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.RESTClientBehavior,
@@ -588,7 +589,7 @@
     },
 
     _computeChangePermalink: function(changeNum) {
-      return '/' + changeNum;
+      return this.getBaseUrl() + '/' + changeNum;
     },
 
     _computeChangeStatus: function(change, patchNum) {
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 13daffd..0e640a7 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index ecd4dcc..f1fb0fd 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -16,7 +16,11 @@
 
   Polymer({
     is: 'gr-comment-list',
-    behaviors: [Gerrit.PathListBehavior],
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.PathListBehavior,
+    ],
 
     properties: {
       changeNum: Number,
@@ -31,7 +35,8 @@
     },
 
     _computeFileDiffURL: function(file, changeNum, patchNum) {
-      return '/c/' + changeNum + '/' + patchNum + '/' + file;
+      return this.getBaseUrl() + '/c/' + changeNum +
+        '/' + patchNum + '/' + file;
     },
 
     _isOnParent: function(comment) {
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
index bcd9053..0e97d36 100644
--- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.html
@@ -14,9 +14,9 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-input/iron-input.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 75d86f4..b20c82c 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index d06a695..73dd969 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -111,6 +111,7 @@
     },
 
     behaviors: [
+      Gerrit.BaseUrlBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.PatchSetBehavior,
       Gerrit.URLEncodingBehavior,
@@ -604,7 +605,7 @@
     },
 
     _computeDiffURL: function(changeNum, patchRange, path) {
-      return this.encodeURL('/c/' + changeNum + '/' +
+      return this.encodeURL(this.getBaseUrl() + '/c/' + changeNum + '/' +
           this._patchRangeStr(patchRange) + '/' + path, true);
     },
 
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
index e7d7cf9..c9d5cd8 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.html
@@ -14,8 +14,9 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-related-changes-list">
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
index 5a9642e..8501b20 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -62,6 +62,7 @@
     },
 
     behaviors: [
+      Gerrit.BaseUrlBehavior,
       Gerrit.RESTClientBehavior,
     ],
 
@@ -160,7 +161,7 @@
     },
 
     _computeChangeURL: function(changeNum, patchNum) {
-      var urlStr = '/c/' + changeNum;
+      var urlStr = this.getBaseUrl() + '/c/' + changeNum;
       if (patchNum != null) {
         urlStr += '/' + patchNum;
       }
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index f9ec01c..842de22 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -15,7 +15,7 @@
 -->
 
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
 <link rel="import" href="../../../bower_components/iron-selector/iron-selector.html">
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index dc66bb6..1e6596b 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index c9abac2..487074e 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -96,6 +96,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
+
     observers: [
       '_accountLoaded(_account)',
     ],
@@ -114,14 +118,23 @@
     },
 
     _handleLocationChange: function(e) {
-      this._loginURL = '/login/' + encodeURIComponent(
-          window.location.pathname +
-          window.location.search +
-          window.location.hash);
+      if (this.getBaseUrl()) {
+        // Strip the canonical path from the path since needing canonical in
+        // the path is uneeded and breaks the url.
+        this._loginURL = this.getBaseUrl() + '/login/' + encodeURIComponent(
+            '/' + window.location.pathname.substring(this.getBaseUrl().length) +
+            window.location.search +
+            window.location.hash);
+      } else {
+        this._loginURL = '/login/' + encodeURIComponent(
+            window.location.pathname +
+            window.location.search +
+            window.location.hash);
+      }
     },
 
     _computeRelativeURL: function(path) {
-      return '//' + window.location.host + path;
+      return '//' + window.location.host + this.getBaseUrl() + path;
     },
 
     _computeLinks: function(defaultLinks, userLinks, adminLinks) {
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
index d10567c..d5d33ab 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
 
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 780f3b9..1f96014 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -79,6 +79,10 @@
       },
     },
 
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
+
     get performanceTiming() {
       return window.performance.timing;
     },
@@ -154,11 +158,11 @@
       var page = '';
       var pathname = this._getPathname();
       if (pathname.indexOf('/q/') === 0) {
-        page = '/q/';
+        page = this.getBaseUrl() + '/q/';
       } else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view
-        page = '/c/';
+        page = this.getBaseUrl() + '/c/';
       } else if (pathname.match(DIFF_VIEW_REGEX)) { // diff view
-        page = '/c//COMMIT_MSG';
+        page = this.getBaseUrl() + '/c//COMMIT_MSG';
       } else {
         // Ignore other page changes.
         return;
@@ -172,7 +176,7 @@
     },
 
     _getPathname: function() {
-      return window.location.pathname;
+      return '/' + window.location.pathname.substring(this.getBaseUrl().length);
     },
 
     /**
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.html b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
index bd79419..5a494b1 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.html
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.html
@@ -13,6 +13,7 @@
 See the License for the specific language governing permissions and
 limitations under the License.
 -->
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 <link rel="import" href="../gr-reporting/gr-reporting.html">
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index eb650ca..1e3481f 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -33,6 +33,11 @@
     getReporting().pageLoaded();
   };
 
+  var base = window.Gerrit.BaseUrlBehavior.getBaseUrl();
+  if (base) {
+    page.base(base);
+  }
+
   window.addEventListener('WebComponentsReady', function() {
     getReporting().timeEnd('WebComponentsReady');
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 9ec52d2..1204170 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -14,11 +14,12 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../../../bower_components/polymer/polymer.html">
-<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-url-encoding-behavior.html">
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="../../../behaviors/rest-client-behavior.html">
+<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
+<link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
 <link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index b795d64..fcbbe23 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -106,6 +106,7 @@
     },
 
     behaviors: [
+      Gerrit.BaseUrlBehavior,
       Gerrit.KeyboardShortcutBehavior,
       Gerrit.RESTClientBehavior,
       Gerrit.URLEncodingBehavior,
@@ -502,8 +503,8 @@
     },
 
     _getDiffURL: function(changeNum, patchRange, path) {
-      return '/c/' + changeNum + '/' + this._patchRangeStr(patchRange) + '/' +
-          this.encodeURL(path, true);
+      return this.getBaseUrl() + '/c/' + changeNum + '/' +
+          this._patchRangeStr(patchRange) + '/' + this.encodeURL(path, true);
     },
 
     _computeDiffURL: function(changeNum, patchRangeRecord, path) {
@@ -528,7 +529,7 @@
     },
 
     _getChangePath: function(changeNum, patchRange, revisions) {
-      var base = '/c/' + changeNum + '/';
+      var base = this.getBaseUrl() + '/c/' + changeNum + '/';
 
       // The change may not have loaded yet, making revisions unavailable.
       if (!revisions) {
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 6811397..bc88b14 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -14,8 +14,9 @@
 limitations under the License.
 -->
 
-<link rel="import" href="../bower_components/polymer/polymer.html">
+<link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
+<link rel="import" href="../bower_components/polymer/polymer.html">
 <link rel="import" href="../styles/app-theme.html">
 <link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html">
 
@@ -151,7 +152,7 @@
             rel="noopener" target="_blank">Send feedback</a>
         <template is="dom-if" if="[[_computeShowGwtUiLink(_serverConfig)]]">
           |
-          <a id="gwtLink" href$="/?polygerrit=0#[[_path]]" rel="external">Old UI</a>
+          <a id="gwtLink" href$="[[computeGwtUrl(_path)]]" rel="external">Old UI</a>
         </template>
         | Press &ldquo;?&rdquo; for keyboard shortcuts
       </div>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index c24e2aa..acf38c7 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -71,6 +71,7 @@
     ],
 
     behaviors: [
+      Gerrit.BaseUrlBehavior,
       Gerrit.KeyboardShortcutBehavior,
     ],
 
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html
index 5aacc77..2eb835d 100644
--- a/polygerrit-ui/app/elements/gr-app_test.html
+++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -72,8 +72,10 @@
       element._path = '/test/path';
       flush(function() {
         var gwtLink = element.$$('#gwtLink');
-        assert.equal(gwtLink.href,
-            'http://' + location.host + '/?polygerrit=0#/test/path');
+        assert.equal(
+          gwtLink.href,
+          'http://' + location.host + element.getBaseUrl() + '/?polygerrit=0#/test/path'
+        );
         done();
       });
     });
@@ -89,8 +91,11 @@
 
       flush(function() {
         var gwtLink = element.$$('#gwtLink');
-        assert.equal(gwtLink.href,
-            'http://' + location.host + '/?polygerrit=0#/c/1/1/testfile.txt@2');
+        assert.equal(
+          gwtLink.href,
+          'http://' + location.host + element.getBaseUrl() +
+            '/?polygerrit=0#/c/1/1/testfile.txt@2'
+        );
         done();
       });
     });
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
index 8d89692..20b6e3f 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../gr-account-label/gr-account-label.html">
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
index 3ff4ace..69beb78 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -25,10 +25,14 @@
       },
     },
 
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
+
     _computeOwnerLink: function(account) {
       if (!account) { return; }
       var accountID = account.email || account._account_id;
-      return '/q/owner:' + encodeURIComponent(accountID);
+      return this.getBaseUrl() + '/q/owner:' + encodeURIComponent(accountID);
     },
 
     _computeShowEmail: function(account) {
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
index db07de4..0a555cf 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <link rel="import" href="../../../bower_components/iron-dropdown/iron-dropdown.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
index 9b87f03..e84357f 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -56,6 +56,10 @@
       _hasAvatars: String,
     },
 
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
+
     attached: function() {
       this.$.restAPI.getConfig().then(function(cfg) {
         this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars);
@@ -79,7 +83,7 @@
     },
 
     _computeURLHelper: function(host, path) {
-      return '//' + host + path;
+      return '//' + host + this.getBaseUrl() + path;
     },
 
     _computeRelativeURL: function(path) {
diff --git a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html b/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html
index 2e624cc..2c8be31a 100644
--- a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html
+++ b/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 
 <dom-module id="gr-placeholder">
@@ -44,7 +45,7 @@
       <h1>[[title]]</h1>
       <section>
         This page is not yet implemented in PolyGerrit. View it in the
-        <a id="gwtLink" href$="/?polygerrit=0#[[path]]" rel="external">
+        <a id="gwtLink" href$="[[computeGwtUrl(path)]]" rel="external">
         Old UI</a>
       </section>
     </main>
diff --git a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.js b/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.js
index 0ea2979..9b60061 100644
--- a/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.js
+++ b/polygerrit-ui/app/elements/shared/gr-placeholder/gr-placeholder.js
@@ -21,5 +21,9 @@
       path: String,
       title: String,
     },
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+    ],
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
index 07ff632..1e5fdaa 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html
@@ -14,6 +14,7 @@
 limitations under the License.
 -->
 
+<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
 <link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
 <script src="../../../bower_components/es6-promise/dist/es6-promise.min.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 7cb29a1..0e3019a 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -82,7 +82,11 @@
 
   Polymer({
     is: 'gr-rest-api-interface',
-    behaviors: [Gerrit.PathListBehavior],
+
+    behaviors: [
+      Gerrit.BaseUrlBehavior,
+      Gerrit.PathListBehavior,
+    ],
 
     /**
      * Fired when an server error occurs.
@@ -152,7 +156,7 @@
     },
 
     _urlWithParams: function(url, opt_params) {
-      if (!opt_params) { return url; }
+      if (!opt_params) { return this.getBaseUrl() + url; }
 
       var params = [];
       for (var p in opt_params) {
@@ -167,7 +171,7 @@
             encodeURIComponent(values[i]));
         }
       }
-      return url + '?' + params.join('&');
+      return this.getBaseUrl() + url + '?' + params.join('&');
     },
 
     getResponseObject: function(response) {
@@ -694,7 +698,7 @@
         }
         options.body = opt_body;
       }
-      return fetch(url, options).then(function(response) {
+      return fetch(this.getBaseUrl() + url, options).then(function(response) {
         if (!response.ok) {
           if (opt_errFn) {
             opt_errFn.call(opt_ctx || null, response);
@@ -907,7 +911,7 @@
     },
 
     _fetchB64File: function(url) {
-      return fetch(url, {credentials: 'same-origin'}).then(function(response) {
+      return fetch(this.getBaseUrl() + url, {credentials: 'same-origin'}).then(function(response) {
         var type = response.headers.get('X-FYI-Content-Type');
         return response.text()
           .then(function(text) {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index ca50bbc..3770d4d 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -114,6 +114,8 @@
 
   // Behaviors tests.
   [
+    'base-url-behavior/base-url-behavior_test.html',
+    'rest-client-behavior/rest-client-behavior_test.html',
     'gr-change-table-behavior/gr-change-table-behavior_test.html',
     'gr-patch-set-behavior/gr-patch-set-behavior_test.html',
     'gr-path-list-behavior/gr-path-list-behavior_test.html',