blob: 331c96870ba59c8d0fc380bea36e2e8a9bc7bd41 [file] [log] [blame]
// 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 com.google.common.base.Charsets;
import com.google.common.hash.Hashing;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.annotations.PluginCanonicalWebUrl;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.api.groups.Groups;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.permissions.ProjectPermission;
import com.google.gerrit.server.project.CreateProjectArgs;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
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 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 =
"Since the \"Rights Inherit From\" field is empty, "
+ "\"%s\" is considered a root project whose parent is \"%s\". "
+ "Root project names cannot contain slashes."
+ SEE_DOCUMENTATION_MSG;
private static final String REGULAR_PROJECT_NOT_ALLOWED_AS_ROOT_MSG =
"Regular projects are not allowed as root.\n\n"
+ "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"
+ "\"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;
static final String DELEGATE_PROJECT_CREATION_TO = "delegateProjectCreationTo";
static final String DISABLE_GRANTING_PROJECT_OWNERSHIP = "disableGrantingProjectOwnership";
private final Groups groups;
private final String documentationUrl;
private final AllProjectsNameProvider allProjectsName;
private final Provider<CurrentUser> self;
private final PermissionBackend permissionBackend;
private final PluginConfigFactory cfg;
private final String pluginName;
@Inject
public ProjectCreationValidator(
Groups groups,
@PluginCanonicalWebUrl String url,
AllProjectsNameProvider allProjectsName,
Provider<CurrentUser> self,
PermissionBackend permissionBackend,
PluginConfigFactory cfg,
@PluginName String pluginName) {
this.groups = groups;
this.documentationUrl = url + "Documentation/index.html";
this.allProjectsName = allProjectsName;
this.self = self;
this.permissionBackend = permissionBackend;
this.cfg = cfg;
this.pluginName = pluginName;
}
@Override
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));
}
Project.NameKey newParent = args.newParent;
try {
permissionBackend.user(self.get()).check(GlobalPermission.ADMINISTRATE_SERVER);
// Admins can bypass any rules to support creating projects that doesn't
// comply with the new naming rules. New projects structures have to
// comply but we need to be able to add new project to an existing non
// compliant structure.
log.debug("admin is creating project, bypassing all rules");
return;
} catch (AuthException | PermissionBackendException e) {
// continuing
}
if (allProjectsName.get().equals(newParent)) {
validateRootProject(name, args.permissionsOnly);
} else {
validateProject(name, newParent);
}
// If we reached that point, it means we allow project creation. Make the
// user an owner if not already by inheritance.
if (!isOwner(newParent) && !configDisableGrantingOwnership(newParent)) {
args.ownerIds.add(createGroup(name + "-admins"));
}
}
private boolean isOwner(Project.NameKey project) {
try {
permissionBackend.user(self.get()).project(project).check(ProjectPermission.WRITE_CONFIG);
} catch (AuthException | PermissionBackendException noWriter) {
try {
permissionBackend.user(self.get()).check(GlobalPermission.ADMINISTRATE_SERVER);
} catch (AuthException | PermissionBackendException noAdmin) {
return false;
}
}
return true;
}
private boolean configDisableGrantingOwnership(Project.NameKey parentCtrl)
throws ValidationException {
try {
return cfg.getFromProjectConfigWithInheritance(parentCtrl, pluginName)
.getBoolean(DISABLE_GRANTING_PROJECT_OWNERSHIP, false);
} catch (NoSuchProjectException e) {
log.error("Failed to check project config for {}: {}", parentCtrl.get(), e.getMessage(), e);
throw new ValidationException(AN_ERROR_OCCURRED_MSG);
}
}
private AccountGroup.UUID createGroup(String name) throws ValidationException {
try {
GroupInfo groupInfo = null;
try {
groupInfo = groups.create(name).get();
} catch (ResourceConflictException e) {
// name already exists, make sure it is unique by adding a abbreviated
// sha1
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 = groups.create(nameWithSha1).get();
}
return AccountGroup.UUID.parse(groupInfo.id);
} catch (RestApiException 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 {
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,
name,
allProjectsName.get(),
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("allowing creation of root project {}", name);
}
private void validateProject(String name, Project.NameKey parentCtrl) throws ValidationException {
log.debug("validating name prefix of {}", name);
String prefix = parentCtrl.get() + "/";
if (!name.startsWith(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));
}
if (!isOwner(parentCtrl) && !isInDelegatingGroup(parentCtrl)) {
log.debug("rejecting creation of {}: user is not owner of {}", name, parentCtrl.get());
throw new ValidationException(
String.format(MUST_BE_OWNER_TO_CREATE_PROJECT_MSG, parentCtrl.get(), documentationUrl));
}
log.debug("allowing creation of project {}", name);
}
private boolean isInDelegatingGroup(Project.NameKey parentCtrl) {
try {
GroupReference delegateProjectCreationTo =
cfg.getFromProjectConfigWithInheritance(parentCtrl, pluginName)
.getGroupReference(DELEGATE_PROJECT_CREATION_TO);
if (delegateProjectCreationTo == null) {
return false;
}
log.debug("delegateProjectCreationTo: {}", delegateProjectCreationTo);
GroupMembership effectiveGroups = self.get().getEffectiveGroups();
return effectiveGroups.contains(delegateProjectCreationTo.getUUID());
} catch (NoSuchProjectException e) {
log.error("isInDelegatingGroup with error ({}): {}", e.getClass().getName(), e.getMessage());
return false;
}
}
}