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