Add a Change Owner group

Add a new system 'CHANGE_OWNER' (the patch set owner) group to allow
configuration of this group with gerrit label permissions.

To keeps it consistent with how the existing system groups
(Project Owner, Admin, etc..) are configured the Change Owner
group's logic for existing change context permissions like
Abandon, Rebase, View Drafts, etc. is untouched by this patch.
In other words an Admin can only apply this groups to a label
permission.

Bug: issue 1993
Change-Id: Ibb35b8172ce50319cfc2fca3b661fdaf7e245e5d
This commit is contained in:
Khai Do
2013-10-11 18:14:31 +02:00
committed by Shawn Pearce
parent 8ba03ff10b
commit 4245e6f8db
13 changed files with 391 additions and 16 deletions

View File

@@ -10,10 +10,19 @@ users.
System Groups
-------------
Gerrit comes with 4 system groups, with special access privileges
and membership management. The identity of these groups is set
in the `system_config` table within the database, so the groups
can be renamed after installation if desired.
Gerrit comes with following system groups:
* Administrators
* Anonymous Users
* Change Owner
* Non-Interactive Users
* Project Owners
* Registered Users
The system groups are assigned special access and membership management
privileges. The identity of these groups is set in the `system_config`
table within the database, so the groups can be renamed after installation
if desired.
[[administrators]]
@@ -85,6 +94,18 @@ avoid the need to initially configure access rights for
newly created child projects.
[[change_owner]]
Change Owner
~~~~~~~~~~~~
Access rights assigned to this group are always evaluated within the
context of a change to which the access rights apply. These rights
therefore apply to the user who is the owner of this change.
It is typical to assign a label to this group, allowing the change
owner to vote on his change, but not actually cause it to become
approved or rejected.
[[registered_users]]
Registered Users
~~~~~~~~~~~~~~~~

View File

@@ -122,6 +122,16 @@ public class AccountCreator {
"Administrators");
}
public TestAccount user()
throws UnsupportedEncodingException, OrmException, JSchException {
return create("user", "user@example.com", "User");
}
public TestAccount user2()
throws UnsupportedEncodingException, OrmException, JSchException {
return create("user2", "user2@example.com", "User2");
}
private AccountExternalId.Key getEmailKey(String email) {
return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
}

View File

@@ -0,0 +1,167 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.acceptance.rest.change;
import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
import static com.google.gerrit.acceptance.git.GitUtil.createProject;
import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
import static com.google.gerrit.common.data.Permission.LABEL;
import static org.junit.Assert.assertEquals;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.AccountCreator;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.RestSession;
import com.google.gerrit.acceptance.SshSession;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.git.PushOneCommit;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.jcraft.jsch.JSchException;
import org.apache.http.HttpStatus;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
public class ChangeOwnerIT extends AbstractDaemonTest {
@Inject
private AccountCreator accounts;
@Inject
private SchemaFactory<ReviewDb> reviewDbProvider;
@Inject
private MetaDataUpdate.Server metaDataUpdateFactory;
@Inject
private ProjectCache projectCache;
@Inject
private GroupCache groupCache;
private TestAccount owner;
private TestAccount dev;
private RestSession sessionOwner;
private RestSession sessionDev;
private Git git;
private ReviewDb db;
private Project.NameKey project;
@Before
public void setUp() throws Exception {
newProject();
owner = accounts.user();
sessionOwner = new RestSession(server, owner);
SshSession sshSession = new SshSession(server, owner);
initSsh(owner);
// need to initialize intern session
createProject(sshSession, "foo");
git = cloneProject(sshSession.getUrl() + "/" + project.get());
sshSession.close();
dev = accounts.user2();
sessionDev = new RestSession(server, dev);
db = reviewDbProvider.open();
}
@After
public void cleanup() {
db.close();
}
@Test
public void testChangeOwner_OwnerACLNotGranted() throws GitAPIException,
IOException, OrmException, ConfigInvalidException {
approve(sessionOwner, createChange(), HttpStatus.SC_FORBIDDEN);
}
@Test
public void testChangeOwner_OwnerACLGranted() throws GitAPIException,
IOException, OrmException, ConfigInvalidException {
grantApproveToChangeOwner();
approve(sessionOwner, createChange(), HttpStatus.SC_OK);
}
@Test
public void testChangeOwner_NotOwnerACLGranted() throws GitAPIException,
IOException, OrmException, ConfigInvalidException {
grantApproveToChangeOwner();
approve(sessionDev, createChange(), HttpStatus.SC_FORBIDDEN);
}
private void approve(RestSession s, String changeId, int expected)
throws IOException {
RestResponse r =
s.post("/changes/" + changeId + "/revisions/current/review",
new ReviewInput().label("Code-Review", 2));
assertEquals(expected, r.getStatusCode());
r.consume();
}
private void grantApproveToChangeOwner() throws IOException,
ConfigInvalidException {
MetaDataUpdate md = metaDataUpdateFactory.create(project);
md.setMessage(String.format("Grant approve to change owner"));
ProjectConfig config = ProjectConfig.read(md);
AccessSection s = config.getAccessSection("refs/heads/*", true);
Permission p = s.getPermission(LABEL + "Code-Review", true);
AccountGroup changeOwnerGroup = groupCache
.get(new AccountGroup.NameKey("Change Owner"));
PermissionRule rule = new PermissionRule(config
.resolve(changeOwnerGroup));
rule.setMin(-2);
rule.setMax(+2);
p.add(rule);
config.commit(md);
projectCache.evict(config.getProject());
}
private String createChange() throws GitAPIException,
IOException {
PushOneCommit push = new PushOneCommit(db, owner.getIdent());
return push.to(git, "refs/for/master").getChangeId();
}
private void newProject() throws UnsupportedEncodingException,
OrmException, JSchException, IOException {
TestAccount admin = accounts.admin();
initSsh(admin);
project = new Project.NameKey("p");
SshSession sshSession = new SshSession(server, admin);
createProject(sshSession, project.get());
sshSession.close();
}
}

View File

@@ -69,6 +69,7 @@ public class SystemGroupsIT extends AbstractDaemonTest {
String result = session.exec("gerrit ls-groups");
assertTrue(result.contains("Administrators"));
assertTrue(result.contains("Anonymous Users"));
assertTrue(result.contains("Change Owner"));
assertTrue(result.contains("Non-Interactive Users"));
assertTrue(result.contains("Project Owners"));
assertTrue(result.contains("Registered Users"));
@@ -85,6 +86,7 @@ public class SystemGroupsIT extends AbstractDaemonTest {
Set<String> names = result.keySet();
assertTrue(names.contains("Administrators"));
assertTrue(names.contains("Anonymous Users"));
assertTrue(names.contains("Change Owner"));
assertTrue(names.contains("Non-Interactive Users"));
assertTrue(names.contains("Project Owners"));
assertTrue(names.contains("Registered Users"));
@@ -100,6 +102,7 @@ public class SystemGroupsIT extends AbstractDaemonTest {
}
assertTrue(names.contains("Administrators"));
assertTrue(names.contains("Anonymous Users"));
assertTrue(names.contains("Change Owner"));
assertTrue(names.contains("Non-Interactive Users"));
assertTrue(names.contains("Project Owners"));
assertTrue(names.contains("Registered Users"));

View File

@@ -149,6 +149,10 @@ public final class AccountGroup {
public static final AccountGroup.UUID PROJECT_OWNERS =
new AccountGroup.UUID("global:Project-Owners");
/** Common UUID assigned to the "Change Owner" placeholder group. */
public static final AccountGroup.UUID CHANGE_OWNER =
new AccountGroup.UUID("global:Change-Owner");
/** Common UUID assigned to the "Anonymous Users" group. */
public static final AccountGroup.UUID ANONYMOUS_USERS =
new AccountGroup.UUID("global:Anonymous-Users");

View File

@@ -292,12 +292,12 @@ public class ChangeControl {
/** All value ranges of any allowed label permission. */
public List<PermissionRange> getLabelRanges() {
return getRefControl().getLabelRanges();
return getRefControl().getLabelRanges(isOwner());
}
/** The range of permitted values associated with a label permission. */
public PermissionRange getRange(String permission) {
return getRefControl().getRange(permission);
return getRefControl().getRange(permission, isOwner());
}
/** Can this user add a patch set to this change? */

View File

@@ -462,9 +462,19 @@ public class ProjectControl {
return match(rule.getGroup().getUUID());
}
boolean match(PermissionRule rule, boolean isChangeOwner) {
return match(rule.getGroup().getUUID(), isChangeOwner);
}
boolean match(AccountGroup.UUID uuid) {
return match(uuid, false);
}
boolean match(AccountGroup.UUID uuid, boolean isChangeOwner) {
if (AccountGroup.PROJECT_OWNERS.equals(uuid)) {
return isDeclaredOwner();
} else if (AccountGroup.CHANGE_OWNER.equals(uuid)) {
return isChangeOwner;
} else {
return user.getEffectiveGroups().contains(uuid);
}

View File

@@ -377,14 +377,14 @@ public class RefControl {
}
/** All value ranges of any allowed label permission. */
public List<PermissionRange> getLabelRanges() {
public List<PermissionRange> getLabelRanges(boolean isChangeOwner) {
List<PermissionRange> r = new ArrayList<PermissionRange>();
for (Map.Entry<String, List<PermissionRule>> e : relevant.getDeclaredPermissions()) {
if (Permission.isLabel(e.getKey())) {
int min = 0;
int max = 0;
for (PermissionRule rule : e.getValue()) {
if (projectControl.match(rule)) {
if (projectControl.match(rule, isChangeOwner)) {
min = Math.min(min, rule.getMin());
max = Math.max(max, rule.getMax());
}
@@ -399,8 +399,13 @@ public class RefControl {
/** The range of permitted values associated with a label permission. */
public PermissionRange getRange(String permission) {
return getRange(permission, false);
}
/** The range of permitted values associated with a label permission. */
public PermissionRange getRange(String permission, boolean isChangeOwner) {
if (Permission.hasRange(permission)) {
return toRange(permission, access(permission));
return toRange(permission, access(permission, isChangeOwner));
}
return null;
}
@@ -516,6 +521,12 @@ public class RefControl {
/** Rules for the given permission, or the empty list. */
private List<PermissionRule> access(String permissionName) {
return access(permissionName, false);
}
/** Rules for the given permission, or the empty list. */
private List<PermissionRule> access(String permissionName,
boolean isChangeOwner) {
List<PermissionRule> rules = effective.get(permissionName);
if (rules != null) {
return rules;
@@ -529,7 +540,7 @@ public class RefControl {
}
if (rules.size() == 1) {
if (!projectControl.match(rules.get(0))) {
if (!projectControl.match(rules.get(0), isChangeOwner)) {
rules = Collections.emptyList();
}
effective.put(permissionName, rules);
@@ -538,7 +549,7 @@ public class RefControl {
List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
for (PermissionRule rule : rules) {
if (projectControl.match(rule)) {
if (projectControl.match(rule, isChangeOwner)) {
mine.add(rule);
}
}

View File

@@ -52,6 +52,7 @@ public class SchemaCreator {
private AccountGroup registered;
private AccountGroup owners;
private AccountGroup batch;
private AccountGroup changeOwner;
@Inject
public SchemaCreator(SitePaths site,
@@ -149,6 +150,14 @@ public class SchemaCreator {
c.accountGroupNames().insert(
Collections.singleton(new AccountGroupName(owners)));
changeOwner = newGroup(c, "Change Owner", AccountGroup.CHANGE_OWNER);
changeOwner.setDescription("The owner of a change");
changeOwner.setOwnerGroupUUID(admin.getGroupUUID());
changeOwner.setType(AccountGroup.Type.SYSTEM);
c.accountGroups().insert(Collections.singleton(changeOwner));
c.accountGroupNames().insert(
Collections.singleton(new AccountGroupName(changeOwner)));
final SystemConfig s = SystemConfig.create();
try {
s.sitePath = site_path.getCanonicalPath();

View File

@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
public static final Class<Schema_85> C = Schema_85.class;
public static final Class<Schema_86> C = Schema_86.class;
public static class Module extends AbstractModule {
@Override

View File

@@ -0,0 +1,118 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.schema;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.AccountGroupName;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.account.GroupUUID;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
public class Schema_86 extends SchemaVersion {
private final AllProjectsName allProjects;
private final GitRepositoryManager mgr;
private final PersonIdent serverUser;
@Inject
Schema_86(Provider<Schema_85> prior,
AllProjectsName allProjects,
GitRepositoryManager mgr,
@GerritPersonIdent PersonIdent serverUser) {
super(prior);
this.allProjects = allProjects;
this.mgr = mgr;
this.serverUser = serverUser;
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
Repository git;
try {
git = mgr.openRepository(allProjects);
} catch (IOException e) {
throw new OrmException(e);
}
try {
MetaDataUpdate md =
new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
ProjectConfig config = ProjectConfig.read(md);
// Create the CHANGE OWNER group.
AccountGroup.UUID adminGroupUUID = findAdminGroup(db, config);
createGroup(db, "Change Owner", adminGroupUUID,
"The owner of a change");
} catch (IOException e) {
throw new OrmException(e);
} catch (ConfigInvalidException e) {
throw new OrmException(e);
} finally {
git.close();
}
}
private AccountGroup createGroup(ReviewDb db, String groupName,
AccountGroup.UUID adminGroupUUID, String description) throws OrmException {
AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId());
AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
AccountGroup.UUID uuid = GroupUUID.make(groupName, serverUser);
AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
group.setOwnerGroupUUID(adminGroupUUID);
group.setDescription(description);
group.setType(AccountGroup.Type.SYSTEM);
AccountGroupName gn = new AccountGroupName(group);
// first insert the group name to validate that the group name hasn't
// already been used to create another group
db.accountGroupNames().insert(Collections.singleton(gn));
db.accountGroups().insert(Collections.singleton(group));
return group;
}
private static AccountGroup.UUID findAdminGroup(
ReviewDb db, ProjectConfig cfg) {
List<PermissionRule> rules = cfg
.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
.getPermission(GlobalCapability.ADMINISTRATE_SERVER)
.getRules();
for (PermissionRule rule : rules) {
if (rule.getAction() == Action.ALLOW) {
return rule.getGroup().getUUID();
}
}
throw new IllegalStateException("no administrator group found");
}
}

View File

@@ -22,6 +22,7 @@ import static com.google.gerrit.common.data.Permission.READ;
import static com.google.gerrit.common.data.Permission.SUBMIT;
import static com.google.gerrit.server.project.Util.ANONYMOUS;
import static com.google.gerrit.server.project.Util.REGISTERED;
import static com.google.gerrit.server.project.Util.CHANGE_OWNER;
import static com.google.gerrit.server.project.Util.ADMIN;
import static com.google.gerrit.server.project.Util.DEVS;
import static com.google.gerrit.server.project.Util.grant;
@@ -428,4 +429,24 @@ public class RefControlTest {
assertFalse("u can't vote -2", range.contains(-2));
assertFalse("u can't vote 2", range.contains(2));
}
public void testUnblockRangeForChangeOwner() {
grant(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
ProjectControl u = util.user(local, DEVS);
PermissionRange range = u.controlForRef("refs/heads/master")
.getRange(LABEL + "Code-Review", true);
assertTrue("u can vote -2", range.contains(-2));
assertTrue("u can vote +2", range.contains(2));
}
public void testUnblockRangeForNotChangeOwner() {
grant(local, LABEL + "Code-Review", -2, +2, CHANGE_OWNER, "refs/heads/*");
ProjectControl u = util.user(local, DEVS);
PermissionRange range = u.controlForRef("refs/heads/master")
.getRange(LABEL + "Code-Review");
assertFalse("u can vote -2", range.contains(-2));
assertFalse("u can vote +2", range.contains(2));
}
}

View File

@@ -54,10 +54,11 @@ import java.util.Map;
import java.util.Set;
public class Util {
public static final AccountGroup.UUID ANONYMOUS = AccountGroup.ANONYMOUS_USERS;
public static final AccountGroup.UUID REGISTERED = AccountGroup.REGISTERED_USERS;
public static final AccountGroup.UUID ADMIN = new AccountGroup.UUID("test.admin");
public static final AccountGroup.UUID DEVS = new AccountGroup.UUID("test.devs");
public static AccountGroup.UUID ANONYMOUS = AccountGroup.ANONYMOUS_USERS;
public static AccountGroup.UUID CHANGE_OWNER = AccountGroup.CHANGE_OWNER;
public static AccountGroup.UUID REGISTERED = AccountGroup.REGISTERED_USERS;
public static AccountGroup.UUID ADMIN = new AccountGroup.UUID("test.admin");
public static AccountGroup.UUID DEVS = new AccountGroup.UUID("test.devs");
public static final LabelType CR = category("Code-Review",
value(2, "Looks good to me, approved"),