Merge branch 'stable-2.14'

* stable-2.14:
  Add support for setting defaults access rights for root projects
  Update bazlets to latest stable-2.14 to use 2.14.7 release API

Change-Id: I3a3024f944d65220487d54fc8c347754fdbac0d6
diff --git a/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/DefaultAccessRights.java b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/DefaultAccessRights.java
new file mode 100644
index 0000000..c91c1e2
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/DefaultAccessRights.java
@@ -0,0 +1,197 @@
+// Copyright (C) 2018 Ericsson
+//
+// 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.ericsson.gerrit.plugins.projectgroupstructure;
+
+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.common.data.RefConfigSection;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+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.group.InternalGroup;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.RefPattern;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.Set;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Set defaults access rights for root projects.
+ *
+ * <p>Default access rights are read from <review_site>/data/project-group-structure/project.config.
+ * The format of that file is the same as regular project.config except that group, in addition to
+ * be a group name, can be set to token ${owner} instead which will be replaced by the group owning
+ * the project.
+ */
+@Singleton
+public class DefaultAccessRights implements NewProjectCreatedListener {
+  private static final Logger log = LoggerFactory.getLogger(DefaultAccessRights.class);
+  private static final String OWNER_TOKEN = "${owner}";
+
+  private final GroupCache groupCache;
+  private final ProjectCache projectCache;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final FileBasedConfig defaultAccessRightsConfig;
+
+  @Inject
+  public DefaultAccessRights(
+      MetaDataUpdate.User metaDataUpdateFactory,
+      ProjectCache projectCache,
+      GroupCache groupCache,
+      @PluginData Path dataDir) {
+    this.groupCache = groupCache;
+    this.projectCache = projectCache;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    defaultAccessRightsConfig =
+        new FileBasedConfig(dataDir.resolve(ProjectConfig.PROJECT_CONFIG).toFile(), FS.DETECTED);
+    try {
+      defaultAccessRightsConfig.load();
+    } catch (IOException | ConfigInvalidException e) {
+      // Swallow the exception to allow the plugin to load, we still want the
+      // project structure to be enforced even if defaults access rights will
+      // not be set.
+      log.error(
+          "Failed to load default access rights config {}, no access right will be set on root projects: {}",
+          defaultAccessRightsConfig.getFile().getAbsolutePath(),
+          e.getMessage(),
+          e);
+    }
+  }
+
+  @Override
+  public void onNewProjectCreated(NewProjectCreatedListener.Event event) {
+    String projectName = event.getProjectName();
+    // only set default access rights for root projects, if configured.
+    if (projectName.contains("/") || defaultAccessRightsConfig.getSections().isEmpty()) {
+      return;
+    }
+
+    ProjectState project = projectCache.get(Project.NameKey.parse(projectName));
+    if (project == null) {
+      log.error("Could not retrieve projet {} from cache", projectName);
+      return;
+    }
+
+    try (MetaDataUpdate md = metaDataUpdateFactory.create(project.getProject().getNameKey())) {
+      ProjectConfig config = ProjectConfig.read(md);
+      setAccessRights(config, project);
+      md.setMessage("Set default access rights\n");
+      config.commit(md);
+    } catch (Exception e) {
+      log.error("Failed to set defauts access rights {}", e.getMessage(), e);
+    }
+  }
+
+  private void setAccessRights(ProjectConfig config, ProjectState project) {
+    for (String refName : defaultAccessRightsConfig.getSubsections(ProjectConfig.ACCESS)) {
+      if (RefConfigSection.isValid(refName) && isValidRegex(refName)) {
+        AccessSection as = config.getAccessSection(refName, true);
+        for (String varName :
+            defaultAccessRightsConfig.getStringList(
+                ProjectConfig.ACCESS, refName, "exclusiveGroupPermissions")) {
+          for (String n : varName.split("[, \t]{1,}")) {
+            if (Permission.isPermission(n)) {
+              as.getPermission(n, true).setExclusiveGroup(true);
+            }
+          }
+        }
+        String ownerGroupName = getOwerGroupName(project);
+        for (String value : defaultAccessRightsConfig.getNames(ProjectConfig.ACCESS, refName)) {
+          if (Permission.isPermission(value)) {
+            Permission perm = as.getPermission(value, true);
+            setPermissionRules(ownerGroupName, perm, refName, value);
+          } else {
+            log.error("Invalid permission {}", value);
+          }
+        }
+      }
+    }
+  }
+
+  private String getOwerGroupName(ProjectState project) {
+    Set<AccountGroup.UUID> owners = project.getAllOwners();
+    if (!owners.isEmpty()) {
+      Optional<InternalGroup> owner = groupCache.get(owners.iterator().next());
+      if (owner.isPresent()) {
+        return owner.get().getName();
+      }
+    }
+    return String.format("no owners for project %s", project.getProject().getName());
+  }
+
+  private boolean isValidRegex(String refPattern) {
+    try {
+      RefPattern.validateRegExp(refPattern);
+    } catch (InvalidNameException e) {
+      log.error("Invalid ref name: " + e.getMessage());
+      return false;
+    }
+    return true;
+  }
+
+  private void setPermissionRules(
+      String ownerGroupName, Permission perm, String refName, String value) {
+    for (String ruleString :
+        defaultAccessRightsConfig.getStringList(ProjectConfig.ACCESS, refName, value)) {
+      PermissionRule rule;
+      try {
+        rule =
+            PermissionRule.fromString(
+                ruleString.replaceAll(Pattern.quote(OWNER_TOKEN), ownerGroupName),
+                Permission.hasRange(value));
+
+      } catch (IllegalArgumentException notRule) {
+        log.error(
+            "Invalid rule in {}{}.{}: {}",
+            ProjectConfig.ACCESS,
+            refName != null ? "." + refName : "",
+            value,
+            notRule.getMessage());
+        continue;
+      }
+
+      if (rule.getGroup().getUUID() == null) {
+        // this means that group is not already in the groups file, so
+        // we need to check if group exist if if it does, get its
+        // uuid.
+        Optional<InternalGroup> group =
+            groupCache.get(new AccountGroup.NameKey(rule.getGroup().getName()));
+
+        if (!group.isPresent()) {
+          log.error("Group {} not found", rule.getGroup().getName());
+          continue;
+        }
+        rule.getGroup().setUUID(group.get().getGroupUUID());
+      }
+      perm.add(rule);
+    }
+  }
+}
diff --git a/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/Module.java b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/Module.java
index 088f253..ca9b88c 100644
--- a/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/Module.java
+++ b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/Module.java
@@ -14,6 +14,7 @@
 
 package com.ericsson.gerrit.plugins.projectgroupstructure;
 
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.validators.ProjectCreationValidationListener;
 import com.google.inject.AbstractModule;
@@ -23,5 +24,6 @@
   protected void configure() {
     DynamicSet.bind(binder(), ProjectCreationValidationListener.class)
         .to(ProjectCreationValidator.class);
+    DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(DefaultAccessRights.class);
   }
 }
diff --git a/src/main/resources/Documentation/config.md b/src/main/resources/Documentation/config.md
index a444519..ac9a12e 100644
--- a/src/main/resources/Documentation/config.md
+++ b/src/main/resources/Documentation/config.md
@@ -1,2 +1,26 @@
-The only configuration required is to grant "Create-group" and "Create-project"
-global permissions to "Registered Users" group.
\ No newline at end of file
+# Config
+The only configuration required is to grant `Create-group` and `Create-project`
+global permissions to `Registered Users` group.
+
+## Default Access Rights
+
+This plugin can set default access rights for newly created root projects if configured.
+
+Default access rights are read from `<review_site>/data/@PLUGIN@/project.config`.
+The format of that file is the same as regular project.config except that group, in addition to
+be a group name, can be set to token `${owner}` instead which will be replaced by the group owning
+the project.
+
+Example of default access rights config file:
+
+```
+[access "refs/*"]
+  read = group ${owner}
+  read = group existing group
+[access "refs/heads/*"]
+  create = group ${owner}
+  push = group ${owner}
+  label-Code-Review = -2..+2 group ${owner}
+  submit = group ${owner}
+
+```
\ No newline at end of file
diff --git a/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/DefaultAccessRightsIT.java b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/DefaultAccessRightsIT.java
new file mode 100644
index 0000000..3c413a3
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/DefaultAccessRightsIT.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2018 Ericsson
+//
+// 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.ericsson.gerrit.plugins.projectgroupstructure;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+import com.google.gerrit.acceptance.TestPlugin;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.Permission;
+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 java.nio.file.Files;
+import org.junit.Before;
+import org.junit.Test;
+
+@TestPlugin(
+  name = "project-group-structure",
+  sysModule = "com.ericsson.gerrit.plugins.projectgroupstructure.Module"
+)
+public class DefaultAccessRightsIT extends LightweightPluginDaemonTest {
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    String defaultAccessRights =
+        "[access \"refs/*\"]\n"
+            + "  read = group ${owner}\n"
+            + "  read = group unexiting group\n"
+            + "  push = group Administrators\n"
+            + "  exclusiveGroupPermissions = read andInvalidOne\n"
+            + "[access \"refs/heads/*\"]\n"
+            + "  create = group ${owner}\n"
+            + "  push = group ${owner}\n"
+            + "  label-Code-Review = -2..+2 group ${owner}\n"
+            + "  submit = group ${owner}\n"
+            + "  invalidPermission = group ${owner}\n"
+            + "  editTopicName = group\n"
+            + "[access \"/invalidrefs\"]\n"
+            + "[access \"refs/invalidregex/${username(((((\"]\n";
+    Files.write(
+        tempDataDir.newFile(ProjectConfig.PROJECT_CONFIG).toPath(), defaultAccessRights.getBytes());
+    super.setUp();
+    // These access rights are mandatory configuration for this plugin as
+    // documented in config.md
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.CREATE_GROUP);
+    allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.CREATE_PROJECT);
+  }
+
+  @Test
+  public void shouldConfigureForRootProject() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.permissionsOnly = true;
+    String projectName = name("someProject");
+    userRestSession.put("/projects/" + projectName, in).assertCreated();
+
+    ProjectState projectState = projectCache.get(new Project.NameKey(projectName));
+    AccountGroup.UUID ownerUUID = projectState.getOwners().iterator().next();
+    ProjectConfig projectConfig = projectState.getConfig();
+
+    assertThat(projectConfig.getAccessSections().size()).isEqualTo(2);
+
+    AccessSection refsSection = projectConfig.getAccessSection("refs/*");
+    assertThat(refsSection.getPermissions().size()).isEqualTo(3);
+    assertThat(refsSection.getPermission(Permission.OWNER).getRules().get(0).getGroup().getUUID())
+        .isEqualTo(ownerUUID);
+    assertThat(refsSection.getPermission(Permission.READ).getRules().get(0).getGroup().getUUID())
+        .isEqualTo(ownerUUID);
+    assertThat(refsSection.getPermission(Permission.READ).getExclusiveGroup()).isTrue();
+    assertThat(refsSection.getPermission(Permission.PUSH).getRules().get(0).getGroup().getName())
+        .isEqualTo("Administrators");
+
+    AccessSection refsHeadsSection = projectConfig.getAccessSection("refs/heads/*");
+    assertThat(refsHeadsSection.getPermissions().size()).isEqualTo(4);
+    assertThat(
+            refsHeadsSection
+                .getPermission(Permission.CREATE)
+                .getRules()
+                .get(0)
+                .getGroup()
+                .getUUID())
+        .isEqualTo(ownerUUID);
+    assertThat(
+            refsHeadsSection.getPermission(Permission.PUSH).getRules().get(0).getGroup().getUUID())
+        .isEqualTo(ownerUUID);
+    assertThat(
+            refsHeadsSection
+                .getPermission(Permission.LABEL + "Code-Review")
+                .getRules()
+                .get(0)
+                .getGroup()
+                .getUUID())
+        .isEqualTo(ownerUUID);
+    assertThat(
+            refsHeadsSection
+                .getPermission(Permission.SUBMIT)
+                .getRules()
+                .get(0)
+                .getGroup()
+                .getUUID())
+        .isEqualTo(ownerUUID);
+  }
+
+  @Test
+  public void shoudNotConfigureForNonRootProject() throws Exception {
+    ProjectInput in = new ProjectInput();
+    in.permissionsOnly = false;
+    String projectName = name("some/project");
+    adminRestSession.put("/projects/" + Url.encode(projectName), in).assertCreated();
+
+    assertThat(
+            projectCache
+                .get(new Project.NameKey(projectName))
+                .getConfig()
+                .getAccessSections()
+                .size())
+        .isEqualTo(0);
+  }
+}