Merge branch 'stable-2.13' into stable-2.14
* stable-2.13:
Change copyright to AOSP
Change-Id: I26c16e7a912d9e7edecfe2fa501315d07a9ca88b
diff --git a/.buckconfig b/.buckconfig
deleted file mode 100644
index 0a84b64..0000000
--- a/.buckconfig
+++ /dev/null
@@ -1,15 +0,0 @@
-[alias]
- project-group-structure = //:project-group-structure
- plugin = //:project-group-structure
- src = //:project-group-structure-sources
-
-[java]
- src_roots = java, resources
-
-[project]
- ignore = .git
-
-[cache]
- mode = dir
- dir = buck-out/cache
-
diff --git a/.gitignore b/.gitignore
index d781c5f..72f041f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,6 @@
-/.buckd
-/.buckversion
/.classpath
+/.primary_build_tool
/.project
/.settings/
-/.watchmanconfig
-/buck-out
-/bucklets
+/bazel-*
/eclipse-out/
diff --git a/BUCK b/BUCK
deleted file mode 100644
index dfd8810..0000000
--- a/BUCK
+++ /dev/null
@@ -1,42 +0,0 @@
-include_defs('//bucklets/gerrit_plugin.bucklet')
-include_defs('//bucklets/java_sources.bucklet')
-
-SOURCES = glob(['src/main/java/**/*.java'])
-RESOURCES = glob(['src/main/resources/**/*'])
-
-TEST_DEPS = GERRIT_TESTS + GERRIT_PLUGIN_API + [
- ':project-group-structure__plugin',
-]
-
-gerrit_plugin(
- name = 'project-group-structure',
- srcs = SOURCES,
- resources = RESOURCES,
- manifest_entries = [
- 'Gerrit-PluginName: project-group-structure',
- 'Gerrit-ApiType: plugin',
- 'Gerrit-Module: com.ericsson.gerrit.plugins.projectgroupstructure.Module',
- 'Implementation-Title: project-group-structure plugin',
- 'Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/project-group-structure',
- 'Implementation-Vendor: Ericsson',
- ],
-)
-
-java_test(
- name = 'project-group-structure-tests',
- srcs = glob(['src/test/java/**/*.java']),
- labels = ['project-group-structure'],
- deps = TEST_DEPS,
-)
-
-java_sources(
- name = 'project-group-structure-sources',
- srcs = SOURCES + RESOURCES,
-)
-
-# this is required for bucklets/tools/eclipse/project.py to work
-java_library(
- name = 'classpath',
- deps = TEST_DEPS,
-)
-
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..41af36e
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,38 @@
+load("//tools/bzl:junit.bzl", "junit_tests")
+load(
+ "//tools/bzl:plugin.bzl",
+ "gerrit_plugin",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+)
+
+gerrit_plugin(
+ name = "project-group-structure",
+ srcs = glob(["src/main/java/**/*.java"]),
+ manifest_entries = [
+ "Gerrit-PluginName: project-group-structure",
+ "Gerrit-Module: com.ericsson.gerrit.plugins.projectgroupstructure.Module",
+ "Implementation-Title: project-group-structure plugin",
+ "Implementation-URL: https://gerrit-review.googlesource.com/#/admin/projects/plugins/project-group-structure",
+ "Implementation-Vendor: Ericsson",
+ ],
+ resources = glob(["src/main/resources/**/*"]),
+)
+
+junit_tests(
+ name = "project_group_structure_tests",
+ srcs = glob(["src/test/java/**/*.java"]),
+ tags = ["project-group-structure"],
+ deps = [
+ ":project-group-structure__plugin_test_deps",
+ ],
+)
+
+java_library(
+ name = "project-group-structure__plugin_test_deps",
+ testonly = 1,
+ visibility = ["//visibility:public"],
+ exports = PLUGIN_DEPS + PLUGIN_TEST_DEPS + [
+ ":project-group-structure__plugin",
+ ],
+)
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..0f5da43
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,26 @@
+workspace(name = "project_group_structure")
+
+load("//:bazlets.bzl", "load_bazlets")
+
+load_bazlets(
+ commit = "11ce7521051ca73598d099aa8a396c9ffe932a74",
+ # local_path = "/home/<user>/projects/bazlets",
+)
+
+#Snapshot Plugin API
+#load(
+# "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+# "gerrit_api_maven_local",
+#)
+
+# Load snapshot Plugin API
+#gerrit_api_maven_local()
+
+# Release Plugin API
+load(
+ "@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+ "gerrit_api",
+)
+
+# Load release Plugin API
+gerrit_api()
diff --git a/bazlets.bzl b/bazlets.bzl
new file mode 100644
index 0000000..e14e488
--- /dev/null
+++ b/bazlets.bzl
@@ -0,0 +1,17 @@
+NAME = "com_googlesource_gerrit_bazlets"
+
+def load_bazlets(
+ commit,
+ local_path = None
+ ):
+ if not local_path:
+ native.git_repository(
+ name = NAME,
+ remote = "https://gerrit.googlesource.com/bazlets",
+ commit = commit,
+ )
+ else:
+ native.local_repository(
+ name = NAME,
+ path = local_path,
+ )
diff --git a/lib/gerrit/BUCK b/lib/gerrit/BUCK
deleted file mode 100644
index 950a90d..0000000
--- a/lib/gerrit/BUCK
+++ /dev/null
@@ -1,21 +0,0 @@
-include_defs('//bucklets/maven_jar.bucklet')
-
-VER = '2.13'
-REPO = MAVEN_CENTRAL
-
-maven_jar(
- name = 'plugin-api',
- id = 'com.google.gerrit:gerrit-plugin-api:' + VER,
- sha1 = 'e25d55b8f41627c4ae6b9d2069ec398638b219a3',
- license = 'Apache2.0',
- attach_source = False,
- repository = REPO,
-)
-maven_jar(
- name = 'acceptance-framework',
- id = 'com.google.gerrit:gerrit-acceptance-framework:' + VER,
- sha1 = 'a6913a61196a8fccdb45e761f43a0b7e21867c90',
- license = 'Apache2.0',
- attach_source = False,
- repository = REPO,
-)
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..466fb55
--- /dev/null
+++ b/src/main/java/com/ericsson/gerrit/plugins/projectgroupstructure/DefaultAccessRights.java
@@ -0,0 +1,191 @@
+// Copyright (C) 2018 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.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.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.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()) {
+ return groupCache.get(owners.iterator().next()).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.
+ AccountGroup group = groupCache.get(new AccountGroup.NameKey(rule.getGroup().getName()));
+
+ if (group == null) {
+ log.error("Group {} not found", rule.getGroup().getName());
+ continue;
+ }
+ rule.getGroup().setUUID(group.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 5e80a96..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,15 +14,16 @@
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;
-
class Module extends AbstractModule {
@Override
protected void configure() {
DynamicSet.bind(binder(), ProjectCreationValidationListener.class)
.to(ProjectCreationValidator.class);
+ DynamicSet.bind(binder(), NewProjectCreatedListener.class).to(DefaultAccessRights.class);
}
}
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 d11afca..e422f17 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.ResourceConflictException;
@@ -24,37 +26,38 @@
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.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.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;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Singleton;
-
+import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.IOException;
-
@Singleton
-public class ProjectCreationValidator
- implements ProjectCreationValidationListener {
- private static final Logger log =
- LoggerFactory.getLogger(ProjectCreationValidator.class);
+public class ProjectCreationValidator implements ProjectCreationValidationListener {
+ private static final Logger log = LoggerFactory.getLogger(ProjectCreationValidator.class);
private static final String AN_ERROR_OCCURRED_MSG =
"An error occurred while creating project, please contact Gerrit support";
- private static final String SEE_DOCUMENTATION_MSG =
- "\n\nSee documentation for more info: %s";
+ private static final String SEE_DOCUMENTATION_MSG = "\n\nSee documentation for more info: %s";
private static final String MUST_BE_OWNER_TO_CREATE_PROJECT_MSG =
"You must be owner of the parent project \"%s\" to create a nested project."
+ SEE_DOCUMENTATION_MSG;
+ private static final String PROJECT_CANNOT_CONTAINS_SPACES_MSG =
+ "Project name cannot contains spaces." + SEE_DOCUMENTATION_MSG;
+
private static final String ROOT_PROJECT_CANNOT_CONTAINS_SLASHES_MSG =
"Root project name cannot contains slashes." + SEE_DOCUMENTATION_MSG;
@@ -63,34 +66,48 @@
+ "Please create a root parent project (project with option "
+ "\"Only serve as parent for other projects\") that will hold "
+ "all your access rights and then create your regular project that "
- + "inherits rights from your root project.\n\n" + "Example:\n"
+ + "inherits rights from your root project.\n\n"
+ + "Example:\n"
+ "\"someOrganization\"->parent project\n"
+ "\"someOrganization/someProject\"->regular project."
+ SEE_DOCUMENTATION_MSG;
private static final String PROJECT_MUST_START_WITH_PARENT_NAME_MSG =
- "Project name must start with parent project name, e.g. %s."
- + SEE_DOCUMENTATION_MSG;
+ "Project name must start with parent project name, e.g. %s." + SEE_DOCUMENTATION_MSG;
+
+ static final String DELEGATE_PROJECT_CREATION_TO = "delegateProjectCreationTo";
+
+ static final String DISABLE_GRANTING_PROJECT_OWNERSHIP = "disableGrantingProjectOwnership";
private final CreateGroup.Factory createGroupFactory;
private final String documentationUrl;
private final AllProjectsNameProvider allProjectsName;
+ private final PluginConfigFactory cfg;
+ private final String pluginName;
@Inject
- public ProjectCreationValidator(CreateGroup.Factory createGroupFactory,
+ public ProjectCreationValidator(
+ CreateGroup.Factory createGroupFactory,
@PluginCanonicalWebUrl String url,
- AllProjectsNameProvider allProjectsName) {
+ AllProjectsNameProvider allProjectsName,
+ PluginConfigFactory cfg,
+ @PluginName String pluginName) {
this.createGroupFactory = createGroupFactory;
this.documentationUrl = url + "Documentation/index.html";
this.allProjectsName = allProjectsName;
-
+ this.cfg = cfg;
+ this.pluginName = pluginName;
}
@Override
- public void validateNewProject(CreateProjectArgs args)
- throws ValidationException {
+ public void validateNewProject(CreateProjectArgs args) throws ValidationException {
String name = args.getProjectName();
log.debug("validating creation of {}", name);
+ if (name.contains(" ")) {
+ throw new ValidationException(
+ String.format(PROJECT_CANNOT_CONTAINS_SPACES_MSG, documentationUrl));
+ }
+
ProjectControl parentCtrl = args.newParent;
if (parentCtrl.getUser().getCapabilities().canAdministrateServer()) {
// Admins can bypass any rules to support creating projects that doesn't
@@ -103,74 +120,110 @@
if (allProjectsName.get().equals(parentCtrl.getProject().getNameKey())) {
validateRootProject(name, args.permissionsOnly);
- args.ownerIds.add(createGroup(name + "-admins"));
- return;
+ } else {
+ validateProject(name, parentCtrl);
}
- validateProject(name, parentCtrl);
+ // If we reached that point, it means we allow project creation. Make the
+ // user an owner if not already by inheritance.
+ if (!parentCtrl.isOwner() && !configDisableGrantingOwnership(parentCtrl)) {
+ args.ownerIds.add(createGroup(name + "-admins"));
+ }
}
- private AccountGroup.UUID createGroup(String name)
+ private boolean configDisableGrantingOwnership(ProjectControl parentCtrl)
throws ValidationException {
try {
+ return cfg.getFromProjectConfigWithInheritance(
+ parentCtrl.getProject().getNameKey(), pluginName)
+ .getBoolean(DISABLE_GRANTING_PROJECT_OWNERSHIP, false);
+ } catch (NoSuchProjectException e) {
+ log.error(
+ "Failed to check project config for "
+ + parentCtrl.getProject().getName()
+ + ": "
+ + e.getMessage(),
+ e);
+ throw new ValidationException(AN_ERROR_OCCURRED_MSG);
+ }
+ }
+
+ private AccountGroup.UUID createGroup(String name) throws ValidationException {
+ try {
GroupInfo groupInfo = null;
try {
- groupInfo = createGroupFactory.create(name)
- .apply(TopLevelResource.INSTANCE, new GroupInput());
+ groupInfo =
+ createGroupFactory.create(name).apply(TopLevelResource.INSTANCE, new GroupInput());
} catch (ResourceConflictException e) {
// name already exists, make sure it is unique by adding a abbreviated
// sha1
- String nameWithSha1 = name + "-" + Hashing.sha1()
- .hashString(name, Charsets.UTF_8).toString().substring(0, 7);
+ String nameWithSha1 =
+ name
+ + "-"
+ + Hashing.sha256().hashString(name, Charsets.UTF_8).toString().substring(0, 7);
log.info(
"Failed to create group name {} because of a conflict: {}, trying to create {} instead",
- name, e.getMessage(), nameWithSha1);
- groupInfo = createGroupFactory.create(nameWithSha1)
- .apply(TopLevelResource.INSTANCE, new GroupInput());
+ name,
+ e.getMessage(),
+ nameWithSha1);
+ groupInfo =
+ createGroupFactory
+ .create(nameWithSha1)
+ .apply(TopLevelResource.INSTANCE, new GroupInput());
}
return AccountGroup.UUID.parse(groupInfo.id);
- } catch (RestApiException | OrmException | IOException e ) {
+ } catch (RestApiException | OrmException | IOException e) {
log.error("Failed to create project " + name + ": " + e.getMessage(), e);
throw new ValidationException(AN_ERROR_OCCURRED_MSG);
}
}
- private void validateRootProject(String name, boolean permissionOnly)
- throws ValidationException {
+ private void validateRootProject(String name, boolean permissionOnly) throws ValidationException {
log.debug("validating root project name {}", name);
if (name.contains("/")) {
log.debug("rejecting creation of {}: name contains slashes", name);
- throw new ValidationException(String
- .format(ROOT_PROJECT_CANNOT_CONTAINS_SLASHES_MSG, documentationUrl));
+ throw new ValidationException(
+ String.format(ROOT_PROJECT_CANNOT_CONTAINS_SLASHES_MSG, documentationUrl));
}
if (!permissionOnly) {
- log.debug("rejecting creation of {}: missing permissions only option",
- name);
- throw new ValidationException(String
- .format(REGULAR_PROJECT_NOT_ALLOWED_AS_ROOT_MSG, documentationUrl));
+ log.debug("rejecting creation of {}: missing permissions only option", name);
+ throw new ValidationException(
+ String.format(REGULAR_PROJECT_NOT_ALLOWED_AS_ROOT_MSG, documentationUrl));
}
log.debug("allowing creation of root project {}", name);
}
- private void validateProject(String name, ProjectControl parentCtrl)
- throws ValidationException {
+ private void validateProject(String name, ProjectControl parentCtrl) throws ValidationException {
log.debug("validating name prefix of {}", name);
Project parent = parentCtrl.getProject();
String prefix = parent.getName() + "/";
if (!name.startsWith(prefix)) {
- log.debug("rejecting creation of {}: name is not starting with {}", name,
- prefix);
+ log.debug("rejecting creation of {}: name is not starting with {}", name, prefix);
throw new ValidationException(
- String.format(PROJECT_MUST_START_WITH_PARENT_NAME_MSG, prefix + name,
- documentationUrl));
+ String.format(PROJECT_MUST_START_WITH_PARENT_NAME_MSG, prefix + name, documentationUrl));
}
- if (!parentCtrl.isOwner()) {
- log.debug("rejecting creation of {}: user is not owner of {}", name,
- parent.getName());
+ if (!parentCtrl.isOwner() && !isInDelegatingGroup(parentCtrl)) {
+ log.debug("rejecting creation of {}: user is not owner of {}", name, parent.getName());
throw new ValidationException(
- String.format(MUST_BE_OWNER_TO_CREATE_PROJECT_MSG, parent.getName(),
- documentationUrl));
+ String.format(MUST_BE_OWNER_TO_CREATE_PROJECT_MSG, parent.getName(), documentationUrl));
}
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..216ccc2 100644
--- a/src/main/resources/Documentation/about.md
+++ b/src/main/resources/Documentation/about.md
@@ -1,5 +1,7 @@
This plugin enforce a project group structure and restrict project creation
-within this structure to project group owners only.
+within this structure to project group owners only. Besides the rules the plugin
+enforce for the project group structure, it also enforce other naming
+rules like project name cannot contain spaces.
To start creating a project group structure, simply create a root project, i.e.
a project which inherits rights from `All-Projects`. The root project name
@@ -15,3 +17,79 @@
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
+
+Ownership of a project created by delegated user is given automatically to that
+user by adding him to a group named `<root-project-name>-admins`. It is
+possible to disable granting the ownership by configuring
+`disableGrantingProjectOwnership` in the `project.config` of
+`refs/meta/config` branch of the parent project:
+
+```
+[plugin "@PLUGIN@"]
+disableGrantingProjectOwnership = true
+```
diff --git a/src/main/resources/Documentation/build.md b/src/main/resources/Documentation/build.md
index bedf322..630c6fb 100644
--- a/src/main/resources/Documentation/build.md
+++ b/src/main/resources/Documentation/build.md
@@ -1,72 +1,92 @@
-Build
-=====
+# Build
-This plugin is built with Buck.
+This plugin can be built with Bazel, and two build modes are supported:
-Buck
-----
+* Standalone
+* In Gerrit tree
-Two build modes are supported: Standalone and in Gerrit tree.
-The standalone build mode is recommended, as this mode doesn't require
-the Gerrit tree to exist locally.
+Standalone build mode is recommended, as this mode doesn't require local Gerrit
+tree to exist. Moreover, there are some limitations and additional manual steps
+required when building in Gerrit tree mode (see corresponding sections).
-Build standalone
-----------------
-
-Clone bucklets library:
-
-```
- git clone https://gerrit.googlesource.com/bucklets
-
-```
-and link it to @PLUGIN@ plugin directory:
-
-```
- cd @PLUGIN@ && ln -s ../bucklets .
-```
-
-Add link to the .buckversion file:
-
-```
- cd @PLUGIN@ && ln -s bucklets/buckversion .buckversion
-```
-
-Add link to the .watchmanconfig file:
-```
- cd @PLUGIN@ && ln -s bucklets/watchmanconfig .watchmanconfig
-```
+## Build standalone
To build the plugin, issue the following command:
-
```
- buck build plugin
+ bazel build @PLUGIN@
```
The output is created in
```
- buck-out/gen/@PLUGIN@.jar
+ bazel-genfiles/@PLUGIN@.jar
```
-Build in Gerrit tree
---------------------
-
-Clone or link this plugin to the plugins directory of Gerrit's source
-tree, and issue the command:
+To package the plugin sources run:
```
- buck build plugins/@PLUGIN@
+ bazel build lib@PLUGIN@__plugin-src.jar
```
-The output is created in
+The output is created in:
```
- buck-out/gen/plugins/@PLUGIN@/@PLUGIN@.jar
+ bazel-bin/lib@PLUGIN@__plugin-src.jar
+```
+
+To execute the tests run:
+
+```
+ bazel test //...
```
This project can be imported into the Eclipse IDE:
```
+ ./tools/eclipse/project.sh
+```
+
+## Build in Gerrit tree
+
+Clone or link this plugin to the plugins directory of Gerrit's
+source tree. From Gerrit source tree issue the command:
+
+```
+ bazel build plugins/@PLUGIN@
+```
+
+Note that due to a [known issue in Bazel][bazelissue], if the plugin
+has previously been built in standalone mode, it is necessary to clean
+the workspace before building in-tree:
+
+```
+ cd plugins/@PLUGIN@
+ bazel clean --expunge
+```
+
+The output is created in
+
+```
+ bazel-genfiles/plugins/@PLUGIN@/@PLUGIN@.jar
+```
+
+This project can be imported into the Eclipse IDE:
+Add the plugin name to the `CUSTOM_PLUGINS` in `tools/bzl/plugins.bzl`, and
+execute:
+
+```
./tools/eclipse/project.py
```
+
+To execute the tests run:
+
+```
+ bazel test --test_tag_filters=@PLUGIN@ //...
+```
+
+
+[Back to @PLUGIN@ documentation index][index]
+
+[index]: index.html
+[bazelissue]: https://github.com/bazelbuild/bazel/issues/2797
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..2746ce7
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/DefaultAccessRightsIT.java
@@ -0,0 +1,137 @@
+// Copyright (C) 2018 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.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);
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java
new file mode 100644
index 0000000..92d8820
--- /dev/null
+++ b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorIT.java
@@ -0,0 +1,473 @@
+// Copyright (C) 2016 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.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.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.hash.Hashing;
+import com.google.gerrit.acceptance.LightweightPluginDaemonTest;
+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;
+import org.junit.Test;
+
+@TestPlugin(
+ name = "project-group-structure",
+ sysModule = "com.ericsson.gerrit.plugins.projectgroupstructure.Module"
+)
+public class ProjectCreationValidatorIT extends LightweightPluginDaemonTest {
+
+ private static final String PLUGIN_NAME = "project-group-structure";
+
+ @Override
+ @Before
+ public void setUp() throws Exception {
+ 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 shouldProjectWithASpaceInTheirName() throws Exception {
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ RestResponse r = userRestSession.put("/projects/" + Url.encode("project with space"), in);
+ r.assertConflict();
+ assertThat(r.getEntityContent()).contains("Project name cannot contains spaces");
+ }
+
+ @Test
+ public void shouldAllowAnyUsersToCreateUnderAllProjects() throws Exception {
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ adminRestSession.put("/projects/" + name("someProject"), in).assertCreated();
+
+ userRestSession.put("/projects/" + name("someOtherProject"), in).assertCreated();
+ }
+
+ @Test
+ public void shouldBlockRootProjectWithSlashesInTheirName() throws Exception {
+ // Root project is OK without slash
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ userRestSession.put("/projects/" + parent, in).assertCreated();
+
+ // Creation is rejected when root project name contains slashes
+ RestResponse r = userRestSession.put("/projects/" + Url.encode("a/parentProject"), in);
+ r.assertConflict();
+ assertThat(r.getEntityContent()).contains("Root project name cannot contains slashes");
+ }
+
+ @Test
+ public void shouldBlockProjectWithParentNotPartOfProjectName() throws Exception {
+ // Root project is OK without parent part of the name
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ String parent = name("parentProject");
+ userRestSession.put("/projects/" + parent, in).assertCreated();
+
+ // Creation is rejected when project name does not start with parent
+ in = new ProjectInput();
+ in.parent = parent;
+ RestResponse r = userRestSession.put("/projects/childProject", in);
+ r.assertConflict();
+ assertThat(r.getEntityContent()).contains("Project name must start with parent project name");
+
+ // Creation is OK when project name starts with parent
+ userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in).assertCreated();
+
+ // Creation is rejected when project name does not start with nested parent
+ String nestedParent = parent + "/childProject";
+ in.parent = nestedParent;
+ r = userRestSession.put("/projects/grandchild", in);
+ r.assertConflict();
+ assertThat(r.getEntityContent()).contains("Project name must start with parent project name");
+
+ // Creation is OK when project name starts with nested parent
+ userRestSession
+ .put("/projects/" + Url.encode(nestedParent + "/grandchild"), in)
+ .assertCreated();
+ }
+
+ @Test
+ public void shouldBlockCreationToNonOwnersOfParentProject() throws Exception {
+ String ownerGroup = name("groupA");
+ GroupApi g = 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();
+
+ // Creation is rejected when user is not owner of parent
+ 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");
+
+ // Creation is OK when user is owner of parent
+ g.addMembers(user.username);
+ userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in).assertCreated();
+ }
+
+ @Test
+ public void shouldAllowAdminsToByPassAnyRule() throws Exception {
+ // Root project with a slash
+ String parent = name("orgA/parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ adminRestSession.put("/projects/" + Url.encode(parent), in).assertCreated();
+
+ // Child project without name of parent as prefix
+ in = new ProjectInput();
+ in.parent = parent;
+ adminRestSession.put("/projects/" + Url.encode("orgA/childProject"), in).assertCreated();
+ }
+
+ @Test
+ public void shouldMakeUserOwnerOnRootProjectCreation() throws Exception {
+ // normal case, when <project-name>-admins group does not exist
+ String rootProject = name("rootProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ userRestSession.put("/projects/" + rootProject, in).assertCreated();
+ ProjectState projectState = projectCache.get(new Project.NameKey(rootProject));
+ assertThat(projectState.getOwners().size()).isEqualTo(1);
+ assertThat(projectState.getOwners())
+ .contains(groupCache.get(new AccountGroup.NameKey(rootProject + "-admins")).getGroupUUID());
+
+ // case when <project-name>-admins group already exists
+ rootProject = name("rootProject2");
+ String existingGroupName = rootProject + "-admins";
+ gApi.groups().create(existingGroupName);
+ userRestSession.put("/projects/" + rootProject, in).assertCreated();
+ projectState = projectCache.get(new Project.NameKey(rootProject));
+ assertThat(projectState.getOwners().size()).isEqualTo(1);
+ String expectedOwnerGroup =
+ existingGroupName
+ + "-"
+ + Hashing.sha256()
+ .hashString(existingGroupName, Charsets.UTF_8)
+ .toString()
+ .substring(0, 7);
+ assertThat(projectState.getOwners())
+ .contains(groupCache.get(new AccountGroup.NameKey(expectedOwnerGroup)).getGroupUUID());
+ }
+
+ @Test
+ public void shouldBlockRootCodeProject() throws Exception {
+ RestResponse r = userRestSession.put("/projects/" + Url.encode("project1"));
+ r.assertConflict();
+ 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 shouldMakeUserOwnerIfNotAlreadyOwnerByInheritance() throws Exception {
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ adminRestSession.put("/projects/" + parent, in).assertCreated();
+
+ String delegatingGroup = name("someGroup");
+ GroupApi dGroup = gApi.groups().create(delegatingGroup);
+ dGroup.addMembers(user.username);
+ 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);
+
+ // normal case, when <project-name>-admins group does not exist
+ in = new ProjectInput();
+ in.parent = parent;
+ String childProject = parent + "/childProject";
+ userRestSession.put("/projects/" + Url.encode(childProject), in).assertCreated();
+ ProjectState projectState = projectCache.get(new Project.NameKey(childProject));
+ assertThat(projectState.getOwners().size()).isEqualTo(1);
+ assertThat(projectState.getOwners())
+ .contains(
+ groupCache.get(new AccountGroup.NameKey(childProject + "-admins")).getGroupUUID());
+
+ // case when <project-name>-admins group already exists
+ String childProject2 = parent + "/childProject2";
+ String existingGroupName = childProject2 + "-admins";
+ gApi.groups().create(existingGroupName);
+ userRestSession.put("/projects/" + Url.encode(childProject2), in).assertCreated();
+ projectState = projectCache.get(new Project.NameKey(childProject2));
+ assertThat(projectState.getOwners().size()).isEqualTo(1);
+ String expectedOwnerGroup =
+ existingGroupName
+ + "-"
+ + Hashing.sha256()
+ .hashString(existingGroupName, Charsets.UTF_8)
+ .toString()
+ .substring(0, 7);
+ assertThat(projectState.getOwners())
+ .contains(groupCache.get(new AccountGroup.NameKey(expectedOwnerGroup)).getGroupUUID());
+ }
+
+ @Test
+ public void shouldNotMakeUserOwnerIfNotAlreadyOwnerByInheritanceAndGrantingIsDisabled()
+ throws Exception {
+ String parent = name("parentProject");
+ ProjectInput in = new ProjectInput();
+ in.permissionsOnly = true;
+ adminRestSession.put("/projects/" + parent, in).assertCreated();
+
+ String delegatingGroup = name("someGroup");
+ GroupApi dGroup = gApi.groups().create(delegatingGroup);
+ dGroup.addMembers(user.username);
+ 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));
+ cfg.getPluginConfig(PLUGIN_NAME)
+ .setBoolean(ProjectCreationValidator.DISABLE_GRANTING_PROJECT_OWNERSHIP, true);
+ saveProjectConfig(parentNameKey, cfg);
+
+ in = new ProjectInput();
+ in.parent = parent;
+ String childProject = parent + "/childProject";
+ userRestSession.put("/projects/" + Url.encode(childProject), in).assertCreated();
+ ProjectState projectState = projectCache.get(new Project.NameKey(childProject));
+ assertThat(projectState.getOwners().size()).isEqualTo(0);
+ }
+
+ @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();
+ }
+}
diff --git a/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorTest.java b/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorTest.java
deleted file mode 100644
index b61f1aa..0000000
--- a/src/test/java/com/ericsson/gerrit/plugins/projectgroupstructure/ProjectCreationValidatorTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright (C) 2016 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.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.common.base.Charsets;
-import com.google.common.collect.Lists;
-import com.google.common.hash.Hashing;
-import com.google.gerrit.acceptance.PluginDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.common.data.GlobalCapability;
-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.project.ProjectState;
-
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-
-@Ignore
-public class ProjectCreationValidatorTest extends PluginDaemonTest {
-
- @Before
- public void setUp() throws Exception {
- // 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 shouldAllowAnyUsersToCreateUnderAllProjects() throws Exception {
- ProjectInput in = new ProjectInput();
- in.permissionsOnly = true;
- adminRestSession.put("/projects/" + name("someProject"), in)
- .assertCreated();
-
- userRestSession.put("/projects/" + name("someOtherProject"), in)
- .assertCreated();
- }
-
- @Test
- public void shouldBlockRootProjectWithSlashesInTheirName() throws Exception {
- // Root project is OK without slash
- String parent = name("parentProject");
- ProjectInput in = new ProjectInput();
- in.permissionsOnly = true;
- userRestSession.put("/projects/" + parent, in).assertCreated();
-
- // Creation is rejected when root project name contains slashes
- RestResponse r =
- userRestSession.put("/projects/" + Url.encode("a/parentProject"), in);
- r.assertConflict();
- assertThat(r.getEntityContent())
- .contains("Root project name cannot contains slashes");
- }
-
- @Test
- public void shouldBlockProjectWithParentNotPartOfProjectName()
- throws Exception {
- // Root project is OK without parent part of the name
- ProjectInput in = new ProjectInput();
- in.permissionsOnly = true;
- String parent = name("parentProject");
- userRestSession.put("/projects/" + parent, in).assertCreated();
-
- // Creation is rejected when project name does not start with parent
- in = new ProjectInput();
- in.parent = parent;
- RestResponse r = userRestSession.put("/projects/childProject", in);
- r.assertConflict();
- assertThat(r.getEntityContent())
- .contains("Project name must start with parent project name");
-
- // Creation is OK when project name starts with parent
- userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in)
- .assertCreated();
-
- // Creation is rejected when project name does not start with nested parent
- String nestedParent = parent + "/childProject";
- in.parent = nestedParent;
- r = userRestSession.put("/projects/grandchild", in);
- r.assertConflict();
- assertThat(r.getEntityContent())
- .contains("Project name must start with parent project name");
-
- // Creation is OK when project name starts with nested parent
- userRestSession
- .put("/projects/" + Url.encode(nestedParent + "/grandchild"), in)
- .assertCreated();
- }
-
- @Test
- public void shouldBlockCreationToNonOwnersOfParentProject() throws Exception {
- String ownerGroup = name("groupA");
- GroupApi g = 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();
-
- // Creation is rejected when user is not owner of parent
- 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");
-
- // Creation is OK when user is owner of parent
- g.addMembers(user.username);
- userRestSession.put("/projects/" + Url.encode(parent + "/childProject"), in)
- .assertCreated();
- }
-
- @Test
- public void shouldAllowAdminsToByPassAnyRule() throws Exception {
- // Root project with a slash
- String parent = name("orgA/parentProject");
- ProjectInput in = new ProjectInput();
- in.permissionsOnly = true;
- adminRestSession.put("/projects/" + Url.encode(parent), in).assertCreated();
-
- // Child project without name of parent as prefix
- in = new ProjectInput();
- in.parent = parent;
- adminRestSession.put("/projects/" + Url.encode("orgA/childProject"), in)
- .assertCreated();
- }
-
- @Test
- public void shouldMakeUserOwnerOnRootProjectCreation() throws Exception {
- // normal case, when <project-name>-admins group does not exist
- String rootProject = name("rootProject");
- ProjectInput in = new ProjectInput();
- in.permissionsOnly = true;
- userRestSession.put("/projects/" + rootProject, in).assertCreated();
- ProjectState projectState =
- projectCache.get(new Project.NameKey(rootProject));
- assertThat(projectState.getOwners().size()).isEqualTo(1);
- assertThat(projectState.getOwners()).contains(groupCache
- .get(new AccountGroup.NameKey(rootProject + "-admins")).getGroupUUID());
-
- // case when <project-name>-admins group already exists
- rootProject = name("rootProject2");
- String existingGroupName = rootProject + "-admins";
- gApi.groups().create(existingGroupName);
- userRestSession.put("/projects/" + rootProject, in).assertCreated();
- projectState = projectCache.get(new Project.NameKey(rootProject));
- assertThat(projectState.getOwners().size()).isEqualTo(1);
- String expectedOwnerGroup = existingGroupName + "-"
- + Hashing.sha1().hashString(existingGroupName, Charsets.UTF_8)
- .toString().substring(0, 7);
- assertThat(projectState.getOwners()).contains(groupCache
- .get(new AccountGroup.NameKey(expectedOwnerGroup)).getGroupUUID());
- }
-
- @Test
- public void shouldBlockRootCodeProject() throws Exception {
- RestResponse r = userRestSession.put("/projects/" + Url.encode("project1"));
- r.assertConflict();
- assertThat(r.getEntityContent())
- .contains("Regular projects are not allowed as root");
- }
-}
diff --git a/tools/bazel.rc b/tools/bazel.rc
new file mode 100644
index 0000000..4ed16cf
--- /dev/null
+++ b/tools/bazel.rc
@@ -0,0 +1,2 @@
+build --workspace_status_command=./tools/workspace-status.sh
+test --build_tests_only
diff --git a/tools/bzl/BUILD b/tools/bzl/BUILD
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tools/bzl/BUILD
diff --git a/tools/bzl/classpath.bzl b/tools/bzl/classpath.bzl
new file mode 100644
index 0000000..d5764f7
--- /dev/null
+++ b/tools/bzl/classpath.bzl
@@ -0,0 +1,4 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:classpath.bzl",
+ "classpath_collector",
+)
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl
new file mode 100644
index 0000000..3af7e58
--- /dev/null
+++ b/tools/bzl/junit.bzl
@@ -0,0 +1,4 @@
+load(
+ "@com_googlesource_gerrit_bazlets//tools:junit.bzl",
+ "junit_tests",
+)
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
new file mode 100644
index 0000000..a2e438f
--- /dev/null
+++ b/tools/bzl/plugin.bzl
@@ -0,0 +1,6 @@
+load(
+ "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl",
+ "gerrit_plugin",
+ "PLUGIN_DEPS",
+ "PLUGIN_TEST_DEPS",
+)
diff --git a/tools/eclipse/BUILD b/tools/eclipse/BUILD
new file mode 100644
index 0000000..57b7b83
--- /dev/null
+++ b/tools/eclipse/BUILD
@@ -0,0 +1,9 @@
+load("//tools/bzl:classpath.bzl", "classpath_collector")
+
+classpath_collector(
+ name = "main_classpath_collect",
+ testonly = 1,
+ deps = [
+ "//:project-group-structure__plugin_test_deps",
+ ],
+)
diff --git a/tools/eclipse/project.sh b/tools/eclipse/project.sh
new file mode 100755
index 0000000..be0fd7a
--- /dev/null
+++ b/tools/eclipse/project.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Copyright (C) 2017 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.
+`bazel query @com_googlesource_gerrit_bazlets//tools/eclipse:project --output location | sed s/BUILD:.*//`project.py -n project-group-structure -r .
diff --git a/tools/sonar/sonar.sh b/tools/sonar/sonar.sh
new file mode 100755
index 0000000..39df185
--- /dev/null
+++ b/tools/sonar/sonar.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Copyright (C) 2018 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.
+`bazel query @com_googlesource_gerrit_bazlets//tools/sonar:sonar --output location | sed s/BUILD:.*//`sonar.py
diff --git a/tools/workspace-status.sh b/tools/workspace-status.sh
new file mode 100755
index 0000000..5320776
--- /dev/null
+++ b/tools/workspace-status.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# This script will be run by bazel when the build process starts to
+# generate key-value information that represents the status of the
+# workspace. The output should be like
+#
+# KEY1 VALUE1
+# KEY2 VALUE2
+#
+# If the script exits with non-zero code, it's considered as a failure
+# and the output will be discarded.
+
+function rev() {
+ cd $1; git describe --always --match "v[0-9].*" --dirty
+}
+
+echo STABLE_BUILD_PROJECT-GROUP-STRUCTURE_LABEL $(rev .)