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();
+  }
 }