com.ericsson.gerrit.plugins.projectgroupstructure.ProjectCreationValidator.java Source code

Java tutorial

Introduction

Here is the source code for com.ericsson.gerrit.plugins.projectgroupstructure.ProjectCreationValidator.java

Source

// 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.extensions.annotations.PluginCanonicalWebUrl;
import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.common.GroupInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
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.CurrentUser;
import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.group.CreateGroup;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.CreateProjectArgs;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.validators.ProjectCreationValidationListener;
import com.google.gerrit.server.validators.ValidationException;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
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 = "Root project name cannot contains 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 CreateGroup.Factory createGroupFactory;
    private final String documentationUrl;
    private final AllProjectsNameProvider allProjectsName;
    private final Provider<CurrentUser> self;
    private final PermissionBackend permissionBackend;
    private final PluginConfigFactory cfg;
    private final String pluginName;
    private final ProjectControl.GenericFactory projectControlFactory;

    @Inject
    public ProjectCreationValidator(CreateGroup.Factory createGroupFactory, @PluginCanonicalWebUrl String url,
            AllProjectsNameProvider allProjectsName, Provider<CurrentUser> self,
            PermissionBackend permissionBackend, PluginConfigFactory cfg, @PluginName String pluginName,
            ProjectControl.GenericFactory projectControlFactory) {
        this.createGroupFactory = createGroupFactory;
        this.documentationUrl = url + "Documentation/index.html";
        this.allProjectsName = allProjectsName;
        this.self = self;
        this.permissionBackend = permissionBackend;
        this.cfg = cfg;
        this.pluginName = pluginName;
        this.projectControlFactory = projectControlFactory;
    }

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

        ProjectControl parentCtrl;
        try {
            parentCtrl = projectControlFactory.controlFor(args.newParent, self.get());
        } catch (NoSuchProjectException | IOException e) {
            log.error("Failed to create project {}; Cannot retrieve info about parent project {}: {}", name,
                    args.newParent.get(), e.getMessage(), e);
            throw new ValidationException(AN_ERROR_OCCURRED_MSG);
        }

        try {
            permissionBackend.user(self).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(parentCtrl.getProject().getNameKey())) {
            validateRootProject(name, args.permissionsOnly);
        } else {
            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 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());
            } 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 = createGroupFactory.create(nameWithSha1).apply(TopLevelResource.INSTANCE,
                        new GroupInput());
            }
            return AccountGroup.UUID.parse(groupInfo.id);
        } catch (RestApiException | OrmException | IOException | ConfigInvalidException 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, 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, 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);
            throw new ValidationException(
                    String.format(PROJECT_MUST_START_WITH_PARENT_NAME_MSG, prefix + name, documentationUrl));
        }
        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));
        }
        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;
        }
    }
}