Allow multiple groups to own a project

Project ownership is now managed as an access right rather than
as part of the project entity's basic properties.  This permits
more than one group to be granted the Owner access value, thus
allowing more than group to manage the properties of the project.

Bug: GERRIT-247
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/project-setup.txt b/Documentation/project-setup.txt
index baa2f48..c48a906 100644
--- a/Documentation/project-setup.txt
+++ b/Documentation/project-setup.txt
@@ -44,13 +44,11 @@
   (project_id
    ,use_contributor_agreements
    ,submit_type
-   ,owner_group_id
    ,name)
   VALUES
   (nextval('project_id')
   ,'Y'
   ,'M'
-  ,(SELECT admin_group_id FROM system_config)
   ,'new/project');
 
   INSERT INTO branches
diff --git a/src/main/java/com/google/gerrit/client/admin/ProjectAdminService.java b/src/main/java/com/google/gerrit/client/admin/ProjectAdminService.java
index ac7f754..aa68b6f 100644
--- a/src/main/java/com/google/gerrit/client/admin/ProjectAdminService.java
+++ b/src/main/java/com/google/gerrit/client/admin/ProjectAdminService.java
@@ -38,10 +38,6 @@
       AsyncCallback<ProjectDetail> callback);
 
   @SignInRequired
-  void changeProjectOwner(Project.Id projectId, String newOwnerName,
-      AsyncCallback<VoidResult> callback);
-
-  @SignInRequired
   void deleteRight(Set<ProjectRight.Key> ids, AsyncCallback<VoidResult> callback);
 
   @SignInRequired
diff --git a/src/main/java/com/google/gerrit/client/admin/ProjectDetail.java b/src/main/java/com/google/gerrit/client/admin/ProjectDetail.java
index 4b83065..7caeb93 100644
--- a/src/main/java/com/google/gerrit/client/admin/ProjectDetail.java
+++ b/src/main/java/com/google/gerrit/client/admin/ProjectDetail.java
@@ -40,7 +40,6 @@
       throws OrmException {
     project = g.getProject();
     groups = new HashMap<AccountGroup.Id, AccountGroup>();
-    wantGroup(project.getOwnerGroupId());
 
     rights = new ArrayList<ProjectRight>();
     for (final ProjectRight p : g.getRights()) {
diff --git a/src/main/java/com/google/gerrit/client/admin/ProjectInfoPanel.java b/src/main/java/com/google/gerrit/client/admin/ProjectInfoPanel.java
index 85274d8..f88fb66 100644
--- a/src/main/java/com/google/gerrit/client/admin/ProjectInfoPanel.java
+++ b/src/main/java/com/google/gerrit/client/admin/ProjectInfoPanel.java
@@ -44,11 +44,6 @@
   private Project.Id projectId;
   private Project project;
 
-  private Panel ownerPanel;
-  private NpTextBox ownerTxtBox;
-  private SuggestBox ownerTxt;
-  private Button changeOwner;
-
   private Panel submitTypePanel;
   private ListBox submitType;
 
@@ -69,7 +64,6 @@
     });
 
     final FlowPanel body = new FlowPanel();
-    initOwner(body);
     initDescription(body);
     initSubmitType(body);
     initAgreements(body);
@@ -82,7 +76,6 @@
   @Override
   protected void onLoad() {
     enableForm(false);
-    changeOwner.setEnabled(false);
     saveProject.setEnabled(false);
     super.onLoad();
     refresh();
@@ -93,7 +86,6 @@
         new GerritCallback<ProjectDetail>() {
           public void onSuccess(final ProjectDetail result) {
             enableForm(true);
-            changeOwner.setEnabled(false);
             saveProject.setEnabled(false);
             display(result);
           }
@@ -102,42 +94,11 @@
 
   private void enableForm(final boolean on) {
     submitType.setEnabled(on);
-    ownerTxtBox.setEnabled(on);
     descTxt.setEnabled(on);
     useContributorAgreements.setEnabled(on);
     useSignedOffBy.setEnabled(on);
   }
 
-  private void initOwner(final Panel body) {
-    ownerPanel = new VerticalPanel();
-    ownerPanel.add(new SmallHeading(Util.C.headingOwner()));
-
-    ownerTxtBox = new NpTextBox();
-    ownerTxtBox.setVisibleLength(60);
-    ownerTxt = new SuggestBox(new AccountGroupSuggestOracle(), ownerTxtBox);
-    ownerPanel.add(ownerTxt);
-
-    changeOwner = new Button(Util.C.buttonChangeGroupOwner());
-    changeOwner.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        final String newOwner = ownerTxt.getText().trim();
-        if (newOwner.length() > 0) {
-          Util.PROJECT_SVC.changeProjectOwner(projectId, newOwner,
-              new GerritCallback<VoidResult>() {
-                public void onSuccess(final VoidResult result) {
-                  changeOwner.setEnabled(false);
-                }
-              });
-        }
-      }
-    });
-    ownerPanel.add(changeOwner);
-    body.add(ownerPanel);
-
-    new TextSaveButtonListener(ownerTxtBox, changeOwner);
-  }
-
   private void initDescription(final Panel body) {
     final VerticalPanel vp = new VerticalPanel();
     vp.add(new SmallHeading(Util.C.headingDescription()));
@@ -206,15 +167,8 @@
 
   void display(final ProjectDetail result) {
     project = result.project;
-    final AccountGroup owner = result.groups.get(project.getOwnerGroupId());
-    if (owner != null) {
-      ownerTxt.setText(owner.getName());
-    } else {
-      ownerTxt.setText(Util.M.deletedGroup(project.getOwnerGroupId().get()));
-    }
 
     final boolean isall = ProjectRight.WILD_PROJECT.equals(project.getId());
-    ownerPanel.setVisible(!isall);
     submitTypePanel.setVisible(!isall);
     agreementsPanel.setVisible(!isall);
     useContributorAgreements.setVisible(Common.getGerritConfig()
@@ -236,7 +190,6 @@
     }
 
     enableForm(false);
-    changeOwner.setEnabled(false);
     saveProject.setEnabled(false);
 
     Util.PROJECT_SVC.changeProjectSettings(project,
diff --git a/src/main/java/com/google/gerrit/client/admin/ProjectRightsPanel.java b/src/main/java/com/google/gerrit/client/admin/ProjectRightsPanel.java
index 3147f7b..f1892a6 100644
--- a/src/main/java/com/google/gerrit/client/admin/ProjectRightsPanel.java
+++ b/src/main/java/com/google/gerrit/client/admin/ProjectRightsPanel.java
@@ -65,11 +65,11 @@
   private SuggestBox nameTxt;
 
   public ProjectRightsPanel(final Project.Id toShow) {
+    projectId = toShow;
+
     final FlowPanel body = new FlowPanel();
     initRights(body);
     initWidget(body);
-
-    projectId = toShow;
   }
 
   @Override
@@ -119,6 +119,15 @@
     }
     for (final ApprovalType at : Common.getGerritConfig().getActionTypes()) {
       final ApprovalCategory c = at.getCategory();
+      if (ProjectRight.WILD_PROJECT.equals(projectId)
+          && ApprovalCategory.OWN.equals(c.getId())) {
+        // Giving out control of the WILD_PROJECT to other groups beyond
+        // Administrators is dangerous. Having control over WILD_PROJECT
+        // is about the same as having Administrator access as users are
+        // able to affect grants in all projects on the system.
+        //
+        continue;
+      }
       catBox.addItem(c.getName(), c.getId().get());
     }
     if (catBox.getItemCount() > 0) {
diff --git a/src/main/java/com/google/gerrit/client/changes/ChangeDetailServiceImpl.java b/src/main/java/com/google/gerrit/client/changes/ChangeDetailServiceImpl.java
index 372cd28..d75da08 100644
--- a/src/main/java/com/google/gerrit/client/changes/ChangeDetailServiceImpl.java
+++ b/src/main/java/com/google/gerrit/client/changes/ChangeDetailServiceImpl.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.client.data.PatchSetDetail;
 import com.google.gerrit.client.data.ProjectCache;
 import com.google.gerrit.client.reviewdb.Account;
+import com.google.gerrit.client.reviewdb.ApprovalCategory;
 import com.google.gerrit.client.reviewdb.Change;
 import com.google.gerrit.client.reviewdb.PatchSet;
 import com.google.gerrit.client.reviewdb.Project;
@@ -69,8 +70,7 @@
               me.equals(change.getOwner())
                   || me.equals(patch.getUploader())
                   || Common.getGroupCache().isAdministrator(me)
-                  || Common.getGroupCache().isInGroup(me,
-                      proj.getOwnerGroupId());
+                  || canPerform(me, projEnt, ApprovalCategory.OWN, (short) 1);
         }
         final ChangeDetail d = new ChangeDetail();
 
diff --git a/src/main/java/com/google/gerrit/client/data/ProjectCache.java b/src/main/java/com/google/gerrit/client/data/ProjectCache.java
index 34632cf..6178dfd 100644
--- a/src/main/java/com/google/gerrit/client/data/ProjectCache.java
+++ b/src/main/java/com/google/gerrit/client/data/ProjectCache.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.data;
 
+import com.google.gerrit.client.reviewdb.AccountGroup;
+import com.google.gerrit.client.reviewdb.ApprovalCategory;
 import com.google.gerrit.client.reviewdb.Project;
 import com.google.gerrit.client.reviewdb.ProjectRight;
 import com.google.gerrit.client.reviewdb.ReviewDb;
@@ -22,8 +24,10 @@
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.Set;
 
 /** Cache of project information, including access rights. */
 @SuppressWarnings("serial")
@@ -177,12 +181,22 @@
   public static class Entry {
     private final Project project;
     private final Collection<ProjectRight> rights;
+    private final Set<AccountGroup.Id> owners;
 
     protected Entry(final ReviewDb db, final Project p) throws OrmException {
       project = p;
       rights =
           Collections.unmodifiableCollection(db.projectRights().byProject(
               project.getId()).toList());
+
+      final HashSet<AccountGroup.Id> groups = new HashSet<AccountGroup.Id>();
+      for (final ProjectRight right : rights) {
+        if (ApprovalCategory.OWN.equals(right.getApprovalCategoryId())
+            && right.getMaxValue() > 0) {
+          groups.add(right.getAccountGroupId());
+        }
+      }
+      owners = Collections.unmodifiableSet(groups);
     }
 
     public Project getProject() {
@@ -192,5 +206,9 @@
     public Collection<ProjectRight> getRights() {
       return rights;
     }
+
+    public Set<AccountGroup.Id> getOwners() {
+      return owners;
+    }
   }
 }
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/ApprovalCategory.java b/src/main/java/com/google/gerrit/client/reviewdb/ApprovalCategory.java
index 783453b..6801de4 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/ApprovalCategory.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/ApprovalCategory.java
@@ -31,6 +31,10 @@
   public static final ApprovalCategory.Id READ =
       new ApprovalCategory.Id("READ");
 
+  /** Id of the special "Own" category; manages a project. */
+  public static final ApprovalCategory.Id OWN =
+      new ApprovalCategory.Id("OWN");
+
   /** Id of the special "Push Annotated Tag" action (and category). */
   public static final ApprovalCategory.Id PUSH_TAG =
       new ApprovalCategory.Id("pTAG");
@@ -67,6 +71,14 @@
     protected void set(String newValue) {
       id = newValue;
     }
+
+    /** True if the right can inherit from {@link ProjectRight#WILD_PROJECT}. */
+    public boolean canInheritFromWildProject() {
+      if (OWN.equals(this)) {
+        return false;
+      }
+      return true;
+    }
   }
 
   /** Internal short unique identifier for this category. */
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/Project.java b/src/main/java/com/google/gerrit/client/reviewdb/Project.java
index 2637a46..f32a874 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/Project.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/Project.java
@@ -124,9 +124,6 @@
   protected String description;
 
   @Column
-  protected AccountGroup.Id ownerGroupId;
-
-  @Column
   protected boolean useContributorAgreements;
 
   @Column
@@ -165,14 +162,6 @@
     description = d;
   }
 
-  public AccountGroup.Id getOwnerGroupId() {
-    return ownerGroupId;
-  }
-
-  public void setOwnerGroupId(final AccountGroup.Id id) {
-    ownerGroupId = id;
-  }
-
   public boolean isUseContributorAgreements() {
     return useContributorAgreements;
   }
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/ProjectAccess.java b/src/main/java/com/google/gerrit/client/reviewdb/ProjectAccess.java
index fda6ad1..7b6030e 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/ProjectAccess.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/ProjectAccess.java
@@ -31,9 +31,6 @@
   @Query("ORDER BY name")
   ResultSet<Project> all() throws OrmException;
 
-  @Query("WHERE ownerGroupId = ?")
-  ResultSet<Project> ownedByGroup(AccountGroup.Id groupId) throws OrmException;
-
   @Query("WHERE name.name >= ? AND name.name <= ? ORDER BY name LIMIT ?")
   ResultSet<Project> suggestByName(String nameA, String nameB, int limit)
       throws OrmException;
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/ProjectRightAccess.java b/src/main/java/com/google/gerrit/client/reviewdb/ProjectRightAccess.java
index 9c1d707..3623b7e 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/ProjectRightAccess.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/ProjectRightAccess.java
@@ -28,10 +28,7 @@
   @Query("WHERE key.projectId = ?")
   ResultSet<ProjectRight> byProject(Project.Id id) throws OrmException;
 
-  @Query("WHERE key.groupId = ?")
-  ResultSet<ProjectRight> byGroup(AccountGroup.Id id) throws OrmException;
-
-  @Query("WHERE key.categoryId = ?")
-  ResultSet<ProjectRight> byApprovalCategory(ApprovalCategory.Id id)
-      throws OrmException;
+  @Query("WHERE key.categoryId = ? AND key.groupId = ?")
+  ResultSet<ProjectRight> byCategoryGroup(ApprovalCategory.Id cat,
+      AccountGroup.Id group) throws OrmException;
 }
diff --git a/src/main/java/com/google/gerrit/client/rpc/BaseServiceImplementation.java b/src/main/java/com/google/gerrit/client/rpc/BaseServiceImplementation.java
index 36cc33e..c667d65 100644
--- a/src/main/java/com/google/gerrit/client/rpc/BaseServiceImplementation.java
+++ b/src/main/java/com/google/gerrit/client/rpc/BaseServiceImplementation.java
@@ -97,27 +97,25 @@
 
   public static boolean canRead(final Account.Id who,
       final Project.NameKey projectKey) {
-    final ProjectCache.Entry e = Common.getProjectCache().get(projectKey);
-    return canRead(who, e);
+    return canRead(who, Common.getProjectCache().get(projectKey));
   }
 
   public static boolean canRead(final Account.Id who, final ProjectCache.Entry e) {
-    if (e == null) {
-      // Unexpected, a project disappearing. But claim its not available.
-      //
-      return false;
-    }
-    final Set<AccountGroup.Id> myGroups = Common.getGroupCache().getEffectiveGroups(who);
-    return canPerform(myGroups, e, ApprovalCategory.READ, (short) 1, true);
+    return canPerform(who, e, ApprovalCategory.READ, (short) 1);
+  }
+
+  public static boolean canPerform(final Account.Id who,
+      final ProjectCache.Entry e, final ApprovalCategory.Id actionId,
+      final short requireValue) {
+    Set<AccountGroup.Id> groups = Common.getGroupCache().getEffectiveGroups(who);
+    return canPerform(groups, e, actionId, requireValue);
   }
 
   public static boolean canPerform(final Set<AccountGroup.Id> myGroups,
       final ProjectCache.Entry e, final ApprovalCategory.Id actionId,
-      final short requireValue, final boolean assumeOwner) {
-    if (assumeOwner && myGroups.contains(e.getProject().getOwnerGroupId())) {
-      // Ownership implies full access.
-      //
-      return true;
+      final short requireValue) {
+    if (e == null) {
+      return false;
     }
 
     int val = Integer.MIN_VALUE;
@@ -138,7 +136,7 @@
         }
       }
     }
-    if (val == Integer.MIN_VALUE) {
+    if (val == Integer.MIN_VALUE && actionId.canInheritFromWildProject()) {
       for (final ProjectRight pr : Common.getProjectCache().getWildcardRights()) {
         if (actionId.equals(pr.getApprovalCategoryId())
             && myGroups.contains(pr.getAccountGroupId())) {
diff --git a/src/main/java/com/google/gerrit/server/GerritServer.java b/src/main/java/com/google/gerrit/server/GerritServer.java
index a726ef0..19cd52c 100644
--- a/src/main/java/com/google/gerrit/server/GerritServer.java
+++ b/src/main/java/com/google/gerrit/server/GerritServer.java
@@ -473,7 +473,6 @@
         new Project(new Project.NameKey("-- All Projects --"),
             ProjectRight.WILD_PROJECT);
     proj.setDescription("Rights inherited by all other projects");
-    proj.setOwnerGroupId(sConfig.adminGroupId);
     proj.setUseContributorAgreements(false);
     c.projects().insert(Collections.singleton(proj));
   }
@@ -519,6 +518,21 @@
     c.projectRights().insert(Collections.singleton(approve));
   }
 
+  private void initOwnerCategory(final ReviewDb c) throws OrmException {
+    final Transaction txn = c.beginTransaction();
+    final ApprovalCategory cat;
+    final ArrayList<ApprovalCategoryValue> vals;
+
+    cat = new ApprovalCategory(ApprovalCategory.OWN, "Owner");
+    cat.setPosition((short) -1);
+    cat.setFunctionName(NoOpFunction.NAME);
+    vals = new ArrayList<ApprovalCategoryValue>();
+    vals.add(value(cat, 1, "Administer All Settings"));
+    c.approvalCategories().insert(Collections.singleton(cat), txn);
+    c.approvalCategoryValues().insert(vals, txn);
+    txn.commit();
+  }
+
   private void initReadCategory(final ReviewDb c) throws OrmException {
     final Transaction txn = c.beginTransaction();
     final ApprovalCategory cat;
@@ -631,13 +645,14 @@
 
         initSystemConfig(c);
         sConfig = c.systemConfig().get(new SystemConfig.Key());
-        initWildCardProject(c);
+        initOwnerCategory(c);
         initReadCategory(c);
         initVerifiedCategory(c);
         initCodeReviewCategory(c);
         initSubmitCategory(c);
         initPushTagCategory(c);
         initPushUpdateBranchCategory(c);
+        initWildCardProject(c);
       }
 
       if (sVer.versionNbr == 2) {
diff --git a/src/main/java/com/google/gerrit/server/ProjectAdminServiceImpl.java b/src/main/java/com/google/gerrit/server/ProjectAdminServiceImpl.java
index dac91f3..aef08e1 100644
--- a/src/main/java/com/google/gerrit/server/ProjectAdminServiceImpl.java
+++ b/src/main/java/com/google/gerrit/server/ProjectAdminServiceImpl.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.client.reviewdb.Project;
 import com.google.gerrit.client.reviewdb.ProjectRight;
 import com.google.gerrit.client.reviewdb.ReviewDb;
+import com.google.gerrit.client.reviewdb.AccountGroup.Id;
 import com.google.gerrit.client.rpc.BaseServiceImplementation;
 import com.google.gerrit.client.rpc.Common;
 import com.google.gerrit.client.rpc.InvalidNameException;
@@ -74,17 +75,12 @@
   public void ownedProjects(final AsyncCallback<List<Project>> callback) {
     run(callback, new Action<List<Project>>() {
       public List<Project> run(ReviewDb db) throws OrmException {
-        final List<Project> result;
-        if (Common.getGroupCache().isAdministrator(Common.getAccountId())) {
-          result = db.projects().all().toList();
-        } else {
-          result = myOwnedProjects(db);
-          Collections.sort(result, new Comparator<Project>() {
-            public int compare(final Project a, final Project b) {
-              return a.getName().compareTo(b.getName());
-            }
-          });
-        }
+        final List<Project> result = myOwnedProjects(db);
+        Collections.sort(result, new Comparator<Project>() {
+          public int compare(final Project a, final Project b) {
+            return a.getName().compareTo(b.getName());
+          }
+        });
         return result;
       }
     });
@@ -153,50 +149,14 @@
     });
   }
 
-  public void changeProjectOwner(final Project.Id projectId,
-      final String newOwnerName, final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        assertAmProjectOwner(db, projectId);
-        final Project project = db.projects().get(projectId);
-        if (project == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-        if (ProjectRight.WILD_PROJECT.equals(projectId)) {
-          // This is *not* a good idea to change away from administrators.
-          //
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        final AccountGroup owner =
-            db.accountGroups().get(new AccountGroup.NameKey(newOwnerName));
-        if (owner == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        project.setOwnerGroupId(owner.getId());
-        db.projects().update(Collections.singleton(project));
-        Common.getProjectCache().invalidate(project);
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
   public void deleteRight(final Set<ProjectRight.Key> keys,
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
       public VoidResult run(final ReviewDb db) throws OrmException, Failure {
         final Set<Project.Id> owned = ids(myOwnedProjects(db));
-        Boolean amAdmin = null;
         for (final ProjectRight.Key k : keys) {
           if (!owned.contains(k.getProjectId())) {
-            if (amAdmin == null) {
-              amAdmin =
-                  Common.getGroupCache().isAdministrator(Common.getAccountId());
-            }
-            if (!amAdmin) {
-              throw new Failure(new NoSuchEntityException());
-            }
+            throw new Failure(new NoSuchEntityException());
           }
         }
         for (final ProjectRight.Key k : keys) {
@@ -215,6 +175,17 @@
       final ApprovalCategory.Id categoryId, final String groupName,
       final short amin, final short amax,
       final AsyncCallback<ProjectDetail> callback) {
+    if (ProjectRight.WILD_PROJECT.equals(projectId)
+        && ApprovalCategory.OWN.equals(categoryId)) {
+      // Giving out control of the WILD_PROJECT to other groups beyond
+      // Administrators is dangerous. Having control over WILD_PROJECT
+      // is about the same as having Administrator access as users are
+      // able to affect grants in all projects on the system.
+      //
+      callback.onFailure(new NoSuchEntityException());
+      return;
+    }
+
     final short min, max;
     if (amin <= amax) {
       min = amin;
@@ -295,7 +266,6 @@
       public Set<Branch.NameKey> run(ReviewDb db) throws OrmException, Failure {
         final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>();
         final Set<Project.Id> owned = ids(myOwnedProjects(db));
-        Boolean amAdmin = null;
         for (final Branch.NameKey k : ids) {
           final ProjectCache.Entry e;
 
@@ -304,13 +274,7 @@
             throw new Failure(new NoSuchEntityException());
           }
           if (!owned.contains(e.getProject().getId())) {
-            if (amAdmin == null) {
-              amAdmin =
-                  Common.getGroupCache().isAdministrator(Common.getAccountId());
-            }
-            if (!amAdmin) {
-              throw new Failure(new NoSuchEntityException());
-            }
+            throw new Failure(new NoSuchEntityException());
           }
         }
         for (final Branch.NameKey k : ids) {
@@ -475,25 +439,44 @@
     if (p == null) {
       throw new Failure(new NoSuchEntityException());
     }
-    final Account.Id me = Common.getAccountId();
-    if (!Common.getGroupCache().isInGroup(me, p.getProject().getOwnerGroupId())
-        && !Common.getGroupCache().isAdministrator(me)) {
+    if (Common.getGroupCache().isAdministrator(Common.getAccountId())) {
+      return;
+    }
+    final Set<Id> myGroups = myGroups();
+    if (!canPerform(myGroups, p, ApprovalCategory.OWN, (short) 1)) {
       throw new Failure(new NoSuchEntityException());
     }
   }
 
   private List<Project> myOwnedProjects(final ReviewDb db) throws OrmException {
-    final Account.Id me = Common.getAccountId();
+    if (Common.getGroupCache().isAdministrator(Common.getAccountId())) {
+      return db.projects().all().toList();
+    }
+
+    final Set<AccountGroup.Id> myGroups = myGroups();
+    final HashSet<Project.Id> projects = new HashSet<Project.Id>();
+    for (final AccountGroup.Id groupId : myGroups) {
+      for (final ProjectRight r : db.projectRights().byCategoryGroup(
+          ApprovalCategory.OWN, groupId)) {
+        projects.add(r.getProjectId());
+      }
+    }
+
+    final ProjectCache projectCache = Common.getProjectCache();
     final List<Project> own = new ArrayList<Project>();
-    for (final AccountGroup.Id groupId : Common.getGroupCache()
-        .getEffectiveGroups(me)) {
-      for (final Project g : db.projects().ownedByGroup(groupId)) {
-        own.add(g);
+    for (Project.Id id : projects) {
+      final ProjectCache.Entry cacheEntry = projectCache.get(id);
+      if (canPerform(myGroups, cacheEntry, ApprovalCategory.OWN, (short) 1)) {
+        own.add(cacheEntry.getProject());
       }
     }
     return own;
   }
 
+  private Set<Id> myGroups() {
+    return Common.getGroupCache().getEffectiveGroups(Common.getAccountId());
+  }
+
   private static Set<Project.Id> ids(final Collection<Project> projectList) {
     final HashSet<Project.Id> r = new HashSet<Project.Id>();
     for (final Project project : projectList) {
diff --git a/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index 5865f49..82b49ec 100644
--- a/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.client.reviewdb.Account;
+import com.google.gerrit.client.reviewdb.AccountGroup;
 import com.google.gerrit.client.reviewdb.AccountGroupMember;
 import com.google.gerrit.client.reviewdb.AccountProjectWatch;
 import com.google.gerrit.client.reviewdb.Change;
@@ -48,9 +49,10 @@
           // Try to mark interested owners with a TO and not a BCC line.
           //
           final Set<Account.Id> owners = new HashSet<Account.Id>();
-          for (AccountGroupMember m : db.accountGroupMembers().byGroup(
-              project.getOwnerGroupId())) {
-            owners.add(m.getAccountId());
+          for (AccountGroup.Id g : getProjectOwners()) {
+            for (AccountGroupMember m : db.accountGroupMembers().byGroup(g)) {
+              owners.add(m.getAccountId());
+            }
           }
 
           // BCC anyone who has interest in this project's changes
diff --git a/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index c3d5799..2e8cda1 100644
--- a/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.data.ProjectCache;
 import com.google.gerrit.client.reviewdb.Account;
+import com.google.gerrit.client.reviewdb.AccountGroup;
 import com.google.gerrit.client.reviewdb.AccountProjectWatch;
 import com.google.gerrit.client.reviewdb.Change;
 import com.google.gerrit.client.reviewdb.ChangeApproval;
@@ -44,12 +45,14 @@
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
+import java.util.Set;
 
 import javax.servlet.http.HttpServletRequest;
 
@@ -515,6 +518,14 @@
     return r != null ? r.getProject() : null;
   }
 
+  /** Get the groups which own the project. */
+  protected Set<AccountGroup.Id> getProjectOwners() {
+    final ProjectCache.Entry r;
+
+    r = Common.getProjectCache().get(change.getDest().getParentKey());
+    return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
+  }
+
   /** Schedule this message for delivery to the listed accounts. */
   protected void add(final RecipientType rt, final Collection<Account.Id> list) {
     for (final Account.Id id : list) {
diff --git a/src/main/java/com/google/gerrit/server/patch/PatchDetailServiceImpl.java b/src/main/java/com/google/gerrit/server/patch/PatchDetailServiceImpl.java
index b0d9445..557973a 100644
--- a/src/main/java/com/google/gerrit/server/patch/PatchDetailServiceImpl.java
+++ b/src/main/java/com/google/gerrit/server/patch/PatchDetailServiceImpl.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.data.ApprovalType;
 import com.google.gerrit.client.data.PatchScript;
 import com.google.gerrit.client.data.PatchScriptSettings;
@@ -408,7 +407,7 @@
 
         if (!me.equals(change.getOwner()) && !me.equals(patch.getUploader())
             && !Common.getGroupCache().isAdministrator(me)
-            && !Common.getGroupCache().isInGroup(me, proj.getOwnerGroupId())) {
+            && !canPerform(me, projEnt, ApprovalCategory.OWN, (short) 1)) {
           // The user doesn't have permission to abandon the change
           throw new Failure(new NoSuchEntityException());
         }
diff --git a/src/main/java/com/google/gerrit/server/ssh/AbstractCommand.java b/src/main/java/com/google/gerrit/server/ssh/AbstractCommand.java
index e1bbb99..94b7790 100644
--- a/src/main/java/com/google/gerrit/server/ssh/AbstractCommand.java
+++ b/src/main/java/com/google/gerrit/server/ssh/AbstractCommand.java
@@ -147,7 +147,7 @@
   protected boolean canPerform(final ProjectCache.Entry project,
       final ApprovalCategory.Id actionId, final short val) {
     return BaseServiceImplementation.canPerform(getGroups(), project, actionId,
-        val, false);
+        val);
   }
 
   protected void assertIsAdministrator() throws Failure {
diff --git a/src/main/webapp/WEB-INF/sql/index_generic.sql b/src/main/webapp/WEB-INF/sql/index_generic.sql
index fcd56ba..0e508af 100644
--- a/src/main/webapp/WEB-INF/sql/index_generic.sql
+++ b/src/main/webapp/WEB-INF/sql/index_generic.sql
@@ -160,21 +160,14 @@
 -- *********************************************************************
 -- ProjectAccess
 --    @PrimaryKey covers: all, suggestByName
---    covers:             ownedByGroup
-CREATE INDEX projects_ownedByGroup
-ON projects (owner_group_id);
 
 
 -- *********************************************************************
 -- ProjectRightAccess
 --    @PrimaryKey covers: byProject
---    covers:             byGroup
-CREATE INDEX project_rights_byGroup
-ON project_rights (group_id);
-
---    covers:             byApprovalCategory
-CREATE INDEX project_rights_byCat
-ON project_rights (category_id);
+--    covers:             byCategoryGroup
+CREATE INDEX project_rights_byCatGroup
+ON project_rights (category_id, group_id);
 
 
 -- *********************************************************************
diff --git a/src/main/webapp/WEB-INF/sql/index_postgres.sql b/src/main/webapp/WEB-INF/sql/index_postgres.sql
index 5cda3d6..52e1695 100644
--- a/src/main/webapp/WEB-INF/sql/index_postgres.sql
+++ b/src/main/webapp/WEB-INF/sql/index_postgres.sql
@@ -186,20 +186,14 @@
 -- ProjectAccess
 --    @PrimaryKey covers: all, suggestByName
 --    covers:             ownedByGroup
-CREATE INDEX projects_ownedByGroup
-ON projects (owner_group_id);
 
 
 -- *********************************************************************
 -- ProjectRightAccess
 --    @PrimaryKey covers: byProject
---    covers:             byGroup
-CREATE INDEX project_rights_byGroup
-ON project_rights (group_id);
-
---    covers:             byApprovalCategory
-CREATE INDEX project_rights_byCat
-ON project_rights (category_id);
+--    covers:             byCategoryGroup
+CREATE INDEX project_rights_byCatGroup
+ON project_rights (category_id, group_id);
 
 
 -- *********************************************************************
diff --git a/src/main/webapp/WEB-INF/sql/upgrade014_015_mysql.sql b/src/main/webapp/WEB-INF/sql/upgrade014_015_mysql.sql
deleted file mode 100644
index 7a5d628..0000000
--- a/src/main/webapp/WEB-INF/sql/upgrade014_015_mysql.sql
+++ /dev/null
@@ -1,13 +0,0 @@
--- Upgrade: schema_version 14 to 15
---
-ALTER TABLE patch_comments ADD parent_uuid VARCHAR(40);
-
-CREATE TABLE account_patch_reviews
-(account_id INTEGER NOT NULL DEFAULT(0),
-change_id INTEGER NOT NULL DEFAULT(0),
-patch_set_id INTEGER NOT NULL DEFAULT(0),
-file_name VARCHAR(255) NOT NULL DEFAULT(''),
-PRIMARY KEY (account_id, change_id, patch_set_id, file_name)
-);
-
-UPDATE schema_version SET version_nbr = 15;
diff --git a/src/main/webapp/WEB-INF/sql/upgrade014_015_part1_mysql.sql b/src/main/webapp/WEB-INF/sql/upgrade014_015_part1_mysql.sql
new file mode 100644
index 0000000..41d512f
--- /dev/null
+++ b/src/main/webapp/WEB-INF/sql/upgrade014_015_part1_mysql.sql
@@ -0,0 +1,36 @@
+-- Upgrade: schema_version 14 to 15
+--
+ALTER TABLE patch_comments ADD parent_uuid VARCHAR(40);
+
+CREATE TABLE account_patch_reviews
+(account_id INTEGER NOT NULL DEFAULT(0),
+change_id INTEGER NOT NULL DEFAULT(0),
+patch_set_id INTEGER NOT NULL DEFAULT(0),
+file_name VARCHAR(255) NOT NULL DEFAULT(''),
+PRIMARY KEY (account_id, change_id, patch_set_id, file_name)
+);
+
+INSERT INTO approval_categories
+(name, position, function_name, category_id)
+VALUES
+('Owner', -1, 'NoOp', 'OWN'); 
+
+INSERT INTO approval_category_values
+(category_id, value, name)
+VALUES
+('OWN', 1, 'Administer All Settings');
+
+INSERT INTO project_rights
+(project_id, category_id, group_id, min_value, max_value)
+SELECT p.project_id, 'OWN', p.owner_group_id, 0, 1
+FROM projects p
+AND p.project_id <> 0;
+
+DROP INDEX projects_ownedByGroup ON projects;
+DROP INDEX project_rights_byCat ON project_rights;
+DROP INDEX project_rights_byGroup ON project_rights;
+
+CREATE INDEX project_rights_byCatGroup
+ON project_rights (category_id, group_id);
+
+UPDATE schema_version SET version_nbr = 15;
diff --git a/src/main/webapp/WEB-INF/sql/upgrade014_015_part1_postgres.sql b/src/main/webapp/WEB-INF/sql/upgrade014_015_part1_postgres.sql
new file mode 100644
index 0000000..57e184a
--- /dev/null
+++ b/src/main/webapp/WEB-INF/sql/upgrade014_015_part1_postgres.sql
@@ -0,0 +1,38 @@
+-- Upgrade: schema_version 14 to 15
+--
+ALTER TABLE patch_comments ADD parent_uuid VARCHAR(40);
+
+CREATE TABLE account_patch_reviews
+(account_id INTEGER NOT NULL DEFAULT(0),
+change_id INTEGER NOT NULL DEFAULT(0),
+patch_set_id INTEGER NOT NULL DEFAULT(0),
+file_name VARCHAR(255) NOT NULL DEFAULT(''),
+PRIMARY KEY (account_id, change_id, patch_set_id, file_name)
+);
+
+ALTER TABLE account_patch_reviews OWNER TO gerrit2;
+
+INSERT INTO approval_categories
+(name, position, function_name, category_id)
+VALUES
+('Owner', -1, 'NoOp', 'OWN');
+
+INSERT INTO approval_category_values
+(category_id, value, name)
+VALUES
+('OWN', 1, 'Administer All Settings');
+
+INSERT INTO project_rights
+(project_id, category_id, group_id, min_value, max_value)
+SELECT p.project_id, 'OWN', p.owner_group_id, 1, 1
+FROM projects p
+AND p.project_id <> 0;
+
+DROP INDEX projects_ownedByGroup;
+DROP INDEX project_rights_byCat;
+DROP INDEX project_rights_byGroup;
+
+CREATE INDEX project_rights_byCatGroup
+ON project_rights (category_id, group_id);
+
+UPDATE schema_version SET version_nbr = 15;
diff --git a/src/main/webapp/WEB-INF/sql/upgrade014_015_part2.sql b/src/main/webapp/WEB-INF/sql/upgrade014_015_part2.sql
new file mode 100644
index 0000000..8b728c4
--- /dev/null
+++ b/src/main/webapp/WEB-INF/sql/upgrade014_015_part2.sql
@@ -0,0 +1 @@
+ALTER TABLE projects DROP COLUMN owner_group_id;
diff --git a/src/main/webapp/WEB-INF/sql/upgrade014_015_postgres.sql b/src/main/webapp/WEB-INF/sql/upgrade014_015_postgres.sql
deleted file mode 100644
index e60b799..0000000
--- a/src/main/webapp/WEB-INF/sql/upgrade014_015_postgres.sql
+++ /dev/null
@@ -1,15 +0,0 @@
--- Upgrade: schema_version 14 to 15
---
-ALTER TABLE patch_comments ADD parent_uuid VARCHAR(40);
-
-CREATE TABLE account_patch_reviews
-(account_id INTEGER NOT NULL DEFAULT(0),
-change_id INTEGER NOT NULL DEFAULT(0),
-patch_set_id INTEGER NOT NULL DEFAULT(0),
-file_name VARCHAR(255) NOT NULL DEFAULT(''),
-PRIMARY KEY (account_id, change_id, patch_set_id, file_name)
-);
-
-ALTER TABLE account_patch_reviews OWNER TO gerrit2;
-
-UPDATE schema_version SET version_nbr = 15;