Commit b3b8bd08 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'sk/329422-create-security-policy-project' into 'master'

Create GraphQL mutation to create Security Policy Project

See merge request gitlab-org/gitlab!64589
parents a38e3d6c b4eb7472
...@@ -3631,6 +3631,25 @@ Input type: `SecurityPolicyProjectAssignInput` ...@@ -3631,6 +3631,25 @@ Input type: `SecurityPolicyProjectAssignInput`
| <a id="mutationsecuritypolicyprojectassignclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationsecuritypolicyprojectassignclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsecuritypolicyprojectassignerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationsecuritypolicyprojectassignerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.securityPolicyProjectCreate`
Input type: `SecurityPolicyProjectCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationsecuritypolicyprojectcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsecuritypolicyprojectcreateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationsecuritypolicyprojectcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationsecuritypolicyprojectcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationsecuritypolicyprojectcreateproject"></a>`project` | [`Project`](#project) | Security Policy Project that was created. |
### `Mutation.terraformStateDelete` ### `Mutation.terraformStateDelete`
Input type: `TerraformStateDeleteInput` Input type: `TerraformStateDeleteInput`
......
...@@ -82,6 +82,7 @@ module EE ...@@ -82,6 +82,7 @@ module EE
mount_mutation ::Mutations::AppSec::Fuzzing::API::CiConfiguration::Create mount_mutation ::Mutations::AppSec::Fuzzing::API::CiConfiguration::Create
mount_mutation ::Mutations::SecurityPolicy::CommitScanExecutionPolicy mount_mutation ::Mutations::SecurityPolicy::CommitScanExecutionPolicy
mount_mutation ::Mutations::SecurityPolicy::AssignSecurityPolicyProject mount_mutation ::Mutations::SecurityPolicy::AssignSecurityPolicyProject
mount_mutation ::Mutations::SecurityPolicy::CreateSecurityPolicyProject
prepend(Types::DeprecatedMutations) prepend(Types::DeprecatedMutations)
end end
......
# frozen_string_literal: true
module Mutations
module SecurityPolicy
class CreateSecurityPolicyProject < BaseMutation
include FindsProject
graphql_name 'SecurityPolicyProjectCreate'
authorize :security_orchestration_policies
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'Full path of the project.'
field :project, Types::ProjectType,
null: true,
description: 'Security Policy Project that was created.'
def resolve(args)
project = authorized_find!(args[:project_path])
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless allowed?(project)
result = create_project(project)
return { project: nil, errors: [result[:message]] } if result[:status] == :error
{
project: result[:policy_project],
errors: []
}
end
private
def allowed?(project)
Feature.enabled?(:security_orchestration_policies_configuration, project)
end
def create_project(project)
::Security::SecurityOrchestrationPolicies::ProjectCreateService
.new(project: project, current_user: current_user)
.execute
end
end
end
end
# frozen_string_literal: true
module Security
module SecurityOrchestrationPolicies
class ProjectCreateService < ::BaseProjectService
def execute
return error('Security Policy project already exists.') if project.security_orchestration_policy_configuration.present?
policy_project = ::Projects::CreateService.new(current_user, create_project_params).execute
return error(policy_project.errors.full_messages.join(',')) unless policy_project.saved?
project.create_security_orchestration_policy_configuration! do |p|
p.security_policy_management_project_id = policy_project.id
end
create_protected_branch(policy_project)
members = add_members(policy_project)
errors = members.flat_map { |member| member.errors.full_messages }
return error('Project was created and assigned as security policy project, but failed adding users to the project.') if errors.any?
success(policy_project: policy_project)
end
private
def create_protected_branch(policy_project)
params = {
name: policy_project.default_branch_or_main,
push_access_levels_attributes: [{ access_level: Gitlab::Access::NO_ACCESS }],
merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]
}
ProtectedBranches::CreateService
.new(policy_project, current_user, params)
.execute(skip_authorization: true)
end
def add_members(policy_project)
members_to_add = project.team.maintainers - policy_project.team.members
policy_project.add_users(members_to_add, :developer)
end
def create_project_params
{
visibility_level: project.visibility_level,
name: "#{project.name} - Security policy project",
description: "This project is automatically generated to manage security policies for the project.",
namespace_id: project.namespace.id,
initialize_with_readme: true,
container_registry_enabled: false,
packages_enabled: false,
requirements_enabled: false,
builds_enabled: false,
wiki_enabled: false,
snippets_enabled: false
}
end
attr_reader :project
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::SecurityPolicy::CreateSecurityPolicyProject do
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
subject { mutation.resolve(project_path: project.full_path) }
context 'when feature is enabled and permission is set for user' do
before do
project.add_maintainer(user)
stub_licensed_features(security_orchestration_policies: true)
stub_feature_flags(security_orchestration_policies_configuration: true)
end
it 'returns project' do
result = subject
expect(result[:errors]).to be_empty
expect(result[:project]).to eq(Project.last)
end
end
context 'when feature is disabled' do
before do
stub_licensed_features(security_orchestration_policies: true)
stub_feature_flags(security_orchestration_policies_configuration: false)
end
it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when permission is not enabled' do
before do
stub_licensed_features(security_orchestration_policies: false)
end
it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do
describe '#execute' do
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { project.owner }
subject(:service) { described_class.new(project: project, current_user: current_user) }
context 'when security_orchestration_policies_configuration does not exist for project' do
let_it_be(:maintainer) { create(:user) }
before do
project.add_maintainer(maintainer)
end
it 'creates new project' do
response = service.execute
policy_project = response[:policy_project]
expect(project.security_orchestration_policy_configuration.security_policy_management_project).to eq(policy_project)
expect(policy_project.namespace).to eq(project.namespace)
expect(policy_project.protected_branches.map(&:name)).to contain_exactly(project.default_branch_or_main)
expect(policy_project.team.developers).to contain_exactly(maintainer)
end
end
context 'when adding users to security policy project fails' do
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { project.owner }
let_it_be(:maintainer) { create(:user) }
before do
project.add_maintainer(maintainer)
errors = ActiveModel::Errors.new(ProjectMember.new).tap { |e| e.add(:source, "cannot be nil") }
allow_next_instance_of(ProjectMember) do |instance|
allow(instance).to receive(:errors).and_return(errors)
end
end
it 'returns error' do
response = service.execute
expect(response[:status]).to eq(:error)
expect(response[:message]).to eq('Project was created and assigned as security policy project, but failed adding users to the project.')
end
end
context 'when project creation fails' do
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
it 'returns error' do
response = service.execute
expect(response[:status]).to eq(:error)
expect(response[:message]).to eq('Namespace is not valid')
end
end
context 'when security_orchestration_policies_configuration already exists for project' do
let_it_be(:policy_configuration) { create(:security_orchestration_policy_configuration, project: project) }
it 'returns error' do
response = service.execute
expect(response[:status]).to eq(:error)
expect(response[:message]).to eq('Security Policy project already exists.')
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment