Allow project creation to delegating users
A user is a delegating user if it is in the delegating group
specified in project.config
Change-Id: I4f92b28a6a6ed318840becc6bc07f2412fbc0672
diff --git a/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidator.java b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidator.java
index a80c12b..d211bba 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidator.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidator.java
@@ -16,7 +16,9 @@
import com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
+import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.AuthException;
@@ -25,13 +27,16 @@
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.group.CreateGroup;
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.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
@@ -78,23 +83,32 @@
"Project name must start with parent project name, e.g. %s."
+ SEE_DOCUMENTATION_MSG;
+ /* package */ static final String DELEGATE_PROJECT_CREATION_TO =
+ "delegateProjectCreationTo";
+
private final CreateGroup.Factory createGroupFactory;
private final String documentationUrl;
private final AllProjectsNameProvider allProjectsName;
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
+ private final PluginConfigFactory cfg;
+ private final String pluginName;
@Inject
public ProjectCreationValidator(CreateGroup.Factory createGroupFactory,
@PluginCanonicalWebUrl String url,
AllProjectsNameProvider allProjectsName,
Provider<CurrentUser> self,
- PermissionBackend permissionBackend) {
+ PermissionBackend permissionBackend,
+ PluginConfigFactory cfg,
+ @PluginName String pluginName) {
this.createGroupFactory = createGroupFactory;
this.documentationUrl = url + "Documentation/index.html";
this.allProjectsName = allProjectsName;
this.self = self;
this.permissionBackend = permissionBackend;
+ this.cfg = cfg;
+ this.pluginName = pluginName;
}
@Override
@@ -180,7 +194,7 @@
String.format(PROJECT_MUST_START_WITH_PARENT_NAME_MSG, prefix + name,
documentationUrl));
}
- if (!parentCtrl.isOwner()) {
+ if (!parentCtrl.isOwner() && !isInDelegatingGroup(parentCtrl)) {
log.debug("rejecting creation of {}: user is not owner of {}", name,
parent.getName());
throw new ValidationException(
@@ -189,4 +203,24 @@
}
log.debug("allowing creation of project {}", name);
}
+
+ private boolean isInDelegatingGroup(ProjectControl parentCtrl) {
+ try {
+ GroupReference delegateProjectCreationTo = cfg
+ .getFromProjectConfigWithInheritance(
+ parentCtrl.getProject().getNameKey(), pluginName)
+ .getGroupReference(DELEGATE_PROJECT_CREATION_TO);
+ if (delegateProjectCreationTo == null) {
+ return false;
+ }
+ log.debug("delegateProjectCreationTo: {}", delegateProjectCreationTo);
+ GroupMembership effectiveGroups =
+ parentCtrl.getUser().getEffectiveGroups();
+ return effectiveGroups.contains(delegateProjectCreationTo.getUUID());
+ } catch (NoSuchProjectException e) {
+ log.error("isInDelegatingGroup with error ({}): {}",
+ e.getClass().getName(), e.getMessage());
+ return false;
+ }
+ }
}
diff --git a/src/main/resources/Documentation/about.md b/src/main/resources/Documentation/about.md
index 36998dd..b59d97f 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -15,3 +15,62 @@
root project and the project names must start with root project name, e.g.
`some-organization/some-project`.
+Delegating group
+----------------
+Project creation can also be delegated to non-owner users by configuring
+`delegateProjectCreationTo` in the `project.config` of
+`refs/meta/config` branch of the parent project.
+
+The value of `delegateProjectCreationTo` must be set to a
+[group reference](@URL@Documentation/dev-plugins.html#configuring-groups).
+
+`project.config` file
+```
+[plugin "@PLUGIN@"]
+delegateProjectCreationTo = group group_name
+```
+
+`groups` file
+```
+group_uuid group_name
+```
+
+The UUID of a group can be found on the "General" tab of the group's page.
+
+If creating-project is delegated to built-in groups, e.g. "Registered Users"
+group, then the value is as following:
+
+`project.config` file
+```
+[plugin "@PLUGIN@"]
+delegateProjectCreationTo = group Registered Users
+```
+
+`groups` file
+```
+global:Registered-Users Registered Users
+```
+
+A way to edit `project.config` and `groups` file is from Gerrit UI.
+For example, to delegate project creation under `orgA` root project to
+`orgA-project-creators` group:
+
+- From main menu, click `People` -> `List Groups`
+- Type `orgA-project-creators` as the filter then click on
+`orgA-project-creators` group
+- Copy the group UUID (example: 3d2bef3b667a577f2dd5232e0848c526efd11b1f)
+- From main menu, click `Projects` -> `List`
+- Type `orgA` as the filter then click on `orgA` project
+- Click `Edit Config` button
+- Add the following then click `Save` -> `Close`:
+ ```
+ [plugin "@PLUGIN@"]
+ delegateProjectCreationTo = orgA-project-creators
+ ```
+- Click `Add...` button then type and open `groups` file
+- Add the following then click `Save` -> `Close`:
+ ```
+ 3d2bef3b667a577f2dd5232e0848c526efd11b1f orgA-project-creators
+ ```
+- Click `Publish` button, review, vote and submit the change to apply new
+configuration
diff --git a/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java
index f62d8ce..cd35085 100644
--- a/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java
+++ b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java
@@ -24,11 +24,13 @@
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestPlugin;
import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.api.groups.GroupApi;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.ProjectState;
import org.junit.Before;
@@ -40,6 +42,9 @@
)
public class ProjectCreationValidatorIT extends LightweightPluginDaemonTest {
+ private static final String PLUGIN_NAME = "project-group-structure";
+
+ @Override
@Before
public void setUp() throws Exception {
super.setUp();
@@ -186,4 +191,219 @@
assertThat(r.getEntityContent())
.contains("Regular projects are not allowed as root");
}
+
+ @Test
+ public void shouldAllowCreationIfUserIsInDelegatingGroup() throws Exception {
+ String ownerGroup = name("groupA");
+ gApi.groups().create(ownerGroup);
+
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ in.owners = Lists.newArrayList(ownerGroup);
+ adminRestSession.put("/projects/" + parent, in).assertCreated();
+
+ in = new ProjectInput();
+ in.parent = parent;
+ RestResponse r = userRestSession
+ .put("/projects/" + Url.encode(parent + "/childProject"), in);
+ r.assertConflict();
+ assertThat(r.getEntityContent())
+ .contains("You must be owner of the parent project");
+
+ // the user is in the delegating group
+ String delegatingGroup = name("groupB");
+ GroupApi dGroup = gApi.groups().create(delegatingGroup);
+ dGroup.addMembers(user.username);
+ // the group is in the project.config
+ Project.NameKey parentNameKey = new Project.NameKey(parent);
+ ProjectConfig cfg = projectCache.checkedGet(parentNameKey).getConfig();
+ String gId = gApi.groups().id(delegatingGroup).get().id;
+ cfg.getPluginConfig(PLUGIN_NAME)
+ .setGroupReference(
+ ProjectCreationValidator.DELEGATE_PROJECT_CREATION_TO,
+ new GroupReference(AccountGroup.UUID.parse(gId), delegatingGroup));
+ saveProjectConfig(parentNameKey, cfg);
+ userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in).assertCreated();
+ }
+
+ @Test
+ public void shouldBlockCreationIfGroupRefIsNotUsed() throws Exception {
+ String ownerGroup = name("groupA");
+ gApi.groups().create(ownerGroup);
+
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ in.owners = Lists.newArrayList(ownerGroup);
+ adminRestSession.put("/projects/" + parent, in).assertCreated();
+
+ in = new ProjectInput();
+ in.parent = parent;
+ RestResponse r = userRestSession
+ .put("/projects/" + Url.encode(parent + "/childProject"), in);
+ r.assertConflict();
+ assertThat(r.getEntityContent())
+ .contains("You must be owner of the parent project");
+
+ // the user is in the delegating group
+ String delegatingGroup = name("groupB");
+ GroupApi dGroup = gApi.groups().create(delegatingGroup);
+ dGroup.addMembers(user.username);
+ // the group is in the project.config
+ Project.NameKey parentNameKey = new Project.NameKey(parent);
+ ProjectConfig cfg = projectCache.checkedGet(parentNameKey).getConfig();
+ cfg.getPluginConfig(PLUGIN_NAME)
+ .setString(ProjectCreationValidator.DELEGATE_PROJECT_CREATION_TO, delegatingGroup);
+ saveProjectConfig(parentNameKey, cfg);
+ userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in).assertConflict();
+ }
+
+ @Test
+ public void shouldAllowCreationIfUserIsInDelegatingGroupNested() throws Exception {
+ String ownerGroup = name("groupA");
+ gApi.groups().create(ownerGroup);
+
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ in.owners = Lists.newArrayList(ownerGroup);
+ adminRestSession.put("/projects/" + parent, in).assertCreated();
+
+ in = new ProjectInput();
+ in.parent = parent;
+ RestResponse r = userRestSession
+ .put("/projects/" + Url.encode(parent + "/childProject"), in);
+ r.assertConflict();
+ assertThat(r.getEntityContent())
+ .contains("You must be owner of the parent project");
+
+ // the user is in the nested delegating group
+ String delegatingGroup = name("groupB");
+ GroupApi dGroup = gApi.groups().create(delegatingGroup);
+
+ String nestedGroup = name("groupC");
+ GroupApi nGroup = gApi.groups().create(nestedGroup);
+ nGroup.addMembers(user.username);
+
+ dGroup.addGroups(nestedGroup);
+ // the group is in the project.config
+ Project.NameKey parentNameKey = new Project.NameKey(parent);
+ ProjectConfig cfg = projectCache.checkedGet(parentNameKey).getConfig();
+ String gId = gApi.groups().id(delegatingGroup).get().id;
+ cfg.getPluginConfig(PLUGIN_NAME)
+ .setGroupReference(
+ ProjectCreationValidator.DELEGATE_PROJECT_CREATION_TO,
+ new GroupReference(AccountGroup.UUID.parse(gId), delegatingGroup));
+ saveProjectConfig(parentNameKey, cfg);
+ userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in).assertCreated();
+ }
+
+ @Test
+ public void shouldBlockCreationIfUserIsNotInDelegatingGroup() throws Exception {
+ String ownerGroup = name("groupA");
+ gApi.groups().create(ownerGroup);
+
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ in.owners = Lists.newArrayList(ownerGroup);
+ adminRestSession.put("/projects/" + parent, in).assertCreated();
+
+ in = new ProjectInput();
+ in.parent = parent;
+ RestResponse r = userRestSession
+ .put("/projects/" + Url.encode(parent + "/childProject"), in);
+ r.assertConflict();
+ assertThat(r.getEntityContent())
+ .contains("You must be owner of the parent project");
+
+ // the user is in the delegating group
+ String delegatingGroup = name("groupB");
+ gApi.groups().create(delegatingGroup);
+ // The user is not added to the delegated group
+ // the group is in the project.config
+ Project.NameKey parentNameKey = new Project.NameKey(parent);
+ ProjectConfig cfg = projectCache.checkedGet(parentNameKey).getConfig();
+ String gId = gApi.groups().id(delegatingGroup).get().id;
+ cfg.getPluginConfig(PLUGIN_NAME).setGroupReference(
+ ProjectCreationValidator.DELEGATE_PROJECT_CREATION_TO,
+ new GroupReference(AccountGroup.UUID.parse(gId), delegatingGroup));
+ saveProjectConfig(parentNameKey, cfg);
+ userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in)
+ .assertConflict();
+ }
+
+ @Test
+ public void shouldBlockCreationIfDelegatingGroupDoesNotExist() throws Exception {
+ String ownerGroup = name("groupA");
+ gApi.groups().create(ownerGroup);
+
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ in.owners = Lists.newArrayList(ownerGroup);
+ adminRestSession.put("/projects/" + parent, in).assertCreated();
+
+ in = new ProjectInput();
+ in.parent = parent;
+ RestResponse r = userRestSession
+ .put("/projects/" + Url.encode(parent + "/childProject"), in);
+ r.assertConflict();
+ assertThat(r.getEntityContent())
+ .contains("You must be owner of the parent project");
+
+ // The delegating group is not created
+ String delegatingGroup = name("groupB");
+ // the group is in the project.config
+ Project.NameKey parentNameKey = new Project.NameKey(parent);
+ ProjectConfig cfg = projectCache.checkedGet(parentNameKey).getConfig();
+ String gId = "fake-gId";
+ cfg.getPluginConfig(PLUGIN_NAME).setGroupReference(
+ ProjectCreationValidator.DELEGATE_PROJECT_CREATION_TO,
+ new GroupReference(AccountGroup.UUID.parse(gId), delegatingGroup));
+ saveProjectConfig(parentNameKey, cfg);
+ userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in).assertConflict();
+ }
+
+ @Test
+ public void shouldNotBlockCreationIfDelegatingGroupIsRenamed()
+ throws Exception {
+ String ownerGroup = name("groupA");
+ gApi.groups().create(ownerGroup);
+
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ in.owners = Lists.newArrayList(ownerGroup);
+ adminRestSession.put("/projects/" + parent, in).assertCreated();
+
+ in = new ProjectInput();
+ in.parent = parent;
+ RestResponse r = userRestSession
+ .put("/projects/" + Url.encode(parent + "/childProject"), in);
+ r.assertConflict();
+ assertThat(r.getEntityContent())
+ .contains("You must be owner of the parent project");
+
+ // the user is in the delegating group
+ String delegatingGroup = name("groupB");
+ GroupApi dGroup = gApi.groups().create(delegatingGroup);
+ dGroup.addMembers(user.username);
+ // the group is in the project.config
+ Project.NameKey parentNameKey = new Project.NameKey(parent);
+ ProjectConfig cfg = projectCache.checkedGet(parentNameKey).getConfig();
+
+ String gId = gApi.groups().id(delegatingGroup).get().id;
+ cfg.getPluginConfig("project-group-structure").setGroupReference(
+ ProjectCreationValidator.DELEGATE_PROJECT_CREATION_TO,
+ new GroupReference(AccountGroup.UUID.parse(gId), delegatingGroup));
+ saveProjectConfig(parentNameKey, cfg);
+
+ String newDelegatingGroup = name("groupC");
+ gApi.groups().id(delegatingGroup).name(newDelegatingGroup);
+
+ userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in)
+ .assertCreated();
+ }
}