Commit 9cbae32c authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch...

Merge branch '347069-update-graphql-mutation-to-assign-security-policy-project-to-group' into 'master'

Update mutation to create project and assign security policy to a group

See merge request gitlab-org/gitlab!83191
parents 134094a4 039cc718
...@@ -26,4 +26,8 @@ class BaseContainerService ...@@ -26,4 +26,8 @@ class BaseContainerService
def group_container? def group_container?
container.is_a?(::Group) container.is_a?(::Group)
end end
def namespace_container?
container.is_a?(::Namespace)
end
end end
...@@ -4336,7 +4336,7 @@ Input type: `ScanExecutionPolicyCommitInput` ...@@ -4336,7 +4336,7 @@ Input type: `ScanExecutionPolicyCommitInput`
### `Mutation.securityPolicyProjectAssign` ### `Mutation.securityPolicyProjectAssign`
Assigns the specified project(`security_policy_project_id`) as security policy project for the given project(`project_path`). If the project already has a security policy project, this reassigns the project's security policy project with the given `security_policy_project_id`. Assigns the specified project(`security_policy_project_id`) as security policy project for the given project(`full_path`). If the project already has a security policy project, this reassigns the project's security policy project with the given `security_policy_project_id`.
Input type: `SecurityPolicyProjectAssignInput` Input type: `SecurityPolicyProjectAssignInput`
...@@ -4345,7 +4345,8 @@ Input type: `SecurityPolicyProjectAssignInput` ...@@ -4345,7 +4345,8 @@ Input type: `SecurityPolicyProjectAssignInput`
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <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="mutationsecuritypolicyprojectassignprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project. | | <a id="mutationsecuritypolicyprojectassignfullpath"></a>`fullPath` | [`String`](#string) | Full path of the project. |
| <a id="mutationsecuritypolicyprojectassignprojectpath"></a>`projectPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Use `fullPath`. Deprecated in 14.10. |
| <a id="mutationsecuritypolicyprojectassignsecuritypolicyprojectid"></a>`securityPolicyProjectId` | [`ProjectID!`](#projectid) | ID of the security policy project. | | <a id="mutationsecuritypolicyprojectassignsecuritypolicyprojectid"></a>`securityPolicyProjectId` | [`ProjectID!`](#projectid) | ID of the security policy project. |
#### Fields #### Fields
...@@ -4357,7 +4358,7 @@ Input type: `SecurityPolicyProjectAssignInput` ...@@ -4357,7 +4358,7 @@ Input type: `SecurityPolicyProjectAssignInput`
### `Mutation.securityPolicyProjectCreate` ### `Mutation.securityPolicyProjectCreate`
Creates and assigns a security policy project for the given project(`project_path`). Creates and assigns a security policy project for the given project (`full_path`).
Input type: `SecurityPolicyProjectCreateInput` Input type: `SecurityPolicyProjectCreateInput`
...@@ -4366,7 +4367,8 @@ Input type: `SecurityPolicyProjectCreateInput` ...@@ -4366,7 +4367,8 @@ Input type: `SecurityPolicyProjectCreateInput`
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="mutationsecuritypolicyprojectcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <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. | | <a id="mutationsecuritypolicyprojectcreatefullpath"></a>`fullPath` | [`String`](#string) | Full path of the project. |
| <a id="mutationsecuritypolicyprojectcreateprojectpath"></a>`projectPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Use `fullPath`. Deprecated in 14.10. |
#### Fields #### Fields
...@@ -4,7 +4,7 @@ module Mutations ...@@ -4,7 +4,7 @@ module Mutations
module FindsProjectOrGroupForSecurityPolicies module FindsProjectOrGroupForSecurityPolicies
private private
def find_object(args) def find_object(**args)
full_path = args[:project_path].presence || args[:full_path] full_path = args[:project_path].presence || args[:full_path]
if full_path.blank? if full_path.blank?
......
...@@ -5,15 +5,20 @@ module Mutations ...@@ -5,15 +5,20 @@ module Mutations
class AssignSecurityPolicyProject < BaseMutation class AssignSecurityPolicyProject < BaseMutation
graphql_name 'SecurityPolicyProjectAssign' graphql_name 'SecurityPolicyProjectAssign'
description 'Assigns the specified project(`security_policy_project_id`) as security policy project '\ description 'Assigns the specified project(`security_policy_project_id`) as security policy project '\
'for the given project(`project_path`). If the project already has a security policy project, '\ 'for the given project(`full_path`). If the project already has a security policy project, '\
'this reassigns the project\'s security policy project with the given `security_policy_project_id`' 'this reassigns the project\'s security policy project with the given `security_policy_project_id`'
include FindsProject include FindsProjectOrGroupForSecurityPolicies
authorize :update_security_orchestration_policy_project authorize :update_security_orchestration_policy_project
argument :full_path, GraphQL::Types::String,
required: false,
description: 'Full path of the project.'
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: false,
deprecated: { reason: 'Use `fullPath`', milestone: '14.10' },
description: 'Full path of the project.' description: 'Full path of the project.'
argument :security_policy_project_id, ::Types::GlobalIDType[::Project], argument :security_policy_project_id, ::Types::GlobalIDType[::Project],
...@@ -21,12 +26,12 @@ module Mutations ...@@ -21,12 +26,12 @@ module Mutations
description: 'ID of the security policy project.' description: 'ID of the security policy project.'
def resolve(args) def resolve(args)
project = authorized_find!(args[:project_path]) project_or_group = authorized_find!(**args)
policy_project = find_policy_project(args[:security_policy_project_id]) policy_project = find_policy_project(args[:security_policy_project_id])
raise_resource_not_available_error! unless policy_project.present? raise_resource_not_available_error! unless policy_project.present?
result = assign_project(project, policy_project) result = assign_project(project_or_group, policy_project)
{ {
errors: result.success? ? [] : [result.message] errors: result.success? ? [] : [result.message]
} }
...@@ -41,9 +46,9 @@ module Mutations ...@@ -41,9 +46,9 @@ module Mutations
::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(id, expected_type: Project)) ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(id, expected_type: Project))
end end
def assign_project(project, policy_project) def assign_project(project_or_group, policy_project)
::Security::Orchestration::AssignService ::Security::Orchestration::AssignService
.new(project, current_user, policy_project_id: policy_project.id) .new(container: project_or_group, current_user: current_user, params: { policy_project_id: policy_project.id })
.execute .execute
end end
end end
......
...@@ -4,14 +4,19 @@ module Mutations ...@@ -4,14 +4,19 @@ module Mutations
module SecurityPolicy module SecurityPolicy
class CreateSecurityPolicyProject < BaseMutation class CreateSecurityPolicyProject < BaseMutation
graphql_name 'SecurityPolicyProjectCreate' graphql_name 'SecurityPolicyProjectCreate'
description 'Creates and assigns a security policy project for the given project(`project_path`)' description 'Creates and assigns a security policy project for the given project (`full_path`)'
include FindsProject include FindsProjectOrGroupForSecurityPolicies
authorize :update_security_orchestration_policy_project authorize :update_security_orchestration_policy_project
argument :full_path, GraphQL::Types::String,
required: false,
description: 'Full path of the project.'
argument :project_path, GraphQL::Types::ID, argument :project_path, GraphQL::Types::ID,
required: true, required: false,
deprecated: { reason: 'Use `fullPath`', milestone: '14.10' },
description: 'Full path of the project.' description: 'Full path of the project.'
field :project, Types::ProjectType, field :project, Types::ProjectType,
...@@ -19,9 +24,9 @@ module Mutations ...@@ -19,9 +24,9 @@ module Mutations
description: 'Security Policy Project that was created.' description: 'Security Policy Project that was created.'
def resolve(args) def resolve(args)
project = authorized_find!(args[:project_path]) project_or_group = authorized_find!(**args)
result = create_project(project) result = create_project(project_or_group)
return { project: nil, errors: [result[:message]] } if result[:status] == :error return { project: nil, errors: [result[:message]] } if result[:status] == :error
{ {
...@@ -32,9 +37,9 @@ module Mutations ...@@ -32,9 +37,9 @@ module Mutations
private private
def create_project(project) def create_project(project_or_group)
::Security::SecurityOrchestrationPolicies::ProjectCreateService ::Security::SecurityOrchestrationPolicies::ProjectCreateService
.new(project: project, current_user: current_user) .new(container: project_or_group, current_user: current_user)
.execute .execute
end end
end end
......
...@@ -5,13 +5,14 @@ module EE ...@@ -5,13 +5,14 @@ module EE
module CreateService module CreateService
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
attr_reader :security_policy_target_project_id attr_reader :security_policy_target_project_id, :security_policy_target_namespace_id
override :initialize override :initialize
def initialize(user, params) def initialize(user, params)
super super
@security_policy_target_project_id = @params.delete(:security_policy_target_project_id) @security_policy_target_project_id = @params.delete(:security_policy_target_project_id)
@security_policy_target_namespace_id = @params.delete(:security_policy_target_namespace_id)
end end
override :execute override :execute
...@@ -66,12 +67,19 @@ module EE ...@@ -66,12 +67,19 @@ module EE
end end
def create_security_policy_configuration_if_exists def create_security_policy_configuration_if_exists
return unless security_policy_target_project_id.present? security_policy_target = find_security_policy_target
return if security_policy_target.blank?
if (security_policy_target_project = ::Project.find(security_policy_target_project_id)) ::Security::Orchestration::AssignService
::Security::Orchestration::AssignService .new(container: security_policy_target, current_user: current_user, params: { policy_project_id: project.id })
.new(security_policy_target_project, current_user, policy_project_id: project.id)
.execute .execute
end
def find_security_policy_target
if security_policy_target_project_id.present?
::Project.find_by_id(security_policy_target_project_id)
elsif security_policy_target_namespace_id.present?
::Namespace.find_by_id(security_policy_target_namespace_id)
end end
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module Security module Security
module Orchestration module Orchestration
class AssignService < ::BaseService class AssignService < ::BaseContainerService
def execute def execute
res = create_or_update_security_policy_configuration res = create_or_update_security_policy_configuration
...@@ -11,7 +11,7 @@ module Security ...@@ -11,7 +11,7 @@ module Security
rescue ActiveRecord::RecordNotFound => _ rescue ActiveRecord::RecordNotFound => _
error(_('Policy project doesn\'t exist')) error(_('Policy project doesn\'t exist'))
rescue ActiveRecord::RecordInvalid => _ rescue ActiveRecord::RecordInvalid => _
error(_('Couldn\'t assign policy to project')) error(_('Couldn\'t assign policy to project or group'))
end end
private private
...@@ -24,18 +24,18 @@ module Security ...@@ -24,18 +24,18 @@ module Security
policy_project = Project.find(policy_project_id) policy_project = Project.find(policy_project_id)
if has_existing_policy? if has_existing_policy?
project.security_orchestration_policy_configuration.update!( container.security_orchestration_policy_configuration.update!(
security_policy_management_project_id: policy_project.id security_policy_management_project_id: policy_project.id
) )
else else
project.create_security_orchestration_policy_configuration! do |p| container.create_security_orchestration_policy_configuration! do |p|
p.security_policy_management_project_id = policy_project.id p.security_policy_management_project_id = policy_project.id
end end
end end
end end
def unassign_policy_project def unassign_policy_project
project.security_orchestration_policy_configuration.delete container.security_orchestration_policy_configuration.delete
end end
def success def success
...@@ -47,7 +47,7 @@ module Security ...@@ -47,7 +47,7 @@ module Security
end end
def has_existing_policy? def has_existing_policy?
project.security_orchestration_policy_configuration.present? container.security_orchestration_policy_configuration.present?
end end
def policy_project_id def policy_project_id
......
...@@ -2,21 +2,23 @@ ...@@ -2,21 +2,23 @@
module Security module Security
module SecurityOrchestrationPolicies module SecurityOrchestrationPolicies
class ProjectCreateService < ::BaseProjectService class ProjectCreateService < ::BaseContainerService
ACCESS_LEVELS_TO_ADD = [Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER].freeze ACCESS_LEVELS_TO_ADD = [Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER].freeze
README_TEMPLATE_PATH = Rails.root.join('ee', 'app', 'views', 'projects', 'security', 'policies', 'readme.md.tt') README_TEMPLATE_PATH = Rails.root.join('ee', 'app', 'views', 'projects', 'security', 'policies', 'readme.md.tt')
def execute def execute
return error('Security Policy project already exists.') if project.security_orchestration_policy_configuration.present? return error('Security Policy project already exists.') if container.security_orchestration_policy_configuration.present?
policy_project = ::Projects::CreateService.new(current_user, create_project_params).execute policy_project = ::Projects::CreateService.new(current_user, create_project_params).execute
return error(policy_project.errors.full_messages.join(',')) unless policy_project.saved? return error(policy_project.errors.full_messages.join(',')) unless policy_project.saved?
members = add_members(policy_project) if project_container?
errors = members.flat_map { |member| member.errors.full_messages } 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? return error('Project was created and assigned as security policy project, but failed adding users to the project.') if errors.any?
end
success(policy_project: policy_project) success(policy_project: policy_project)
end end
...@@ -24,18 +26,21 @@ module Security ...@@ -24,18 +26,21 @@ module Security
private private
def add_members(policy_project) def add_members(policy_project)
developers_and_maintainers = project.team.members_with_access_levels(ACCESS_LEVELS_TO_ADD)
members_to_add = developers_and_maintainers - policy_project.team.members members_to_add = developers_and_maintainers - policy_project.team.members
policy_project.add_users(members_to_add, :developer) policy_project.add_users(members_to_add, :developer)
end end
def developers_and_maintainers
container.team.members_with_access_levels(ACCESS_LEVELS_TO_ADD)
end
def create_project_params def create_project_params
{ {
visibility_level: project.visibility_level, creator: current_user,
security_policy_target_project_id: project.id, visibility_level: container.visibility_level,
name: "#{project.name} - Security policy project", name: "#{container.name} - Security policy project",
description: "This project is automatically generated to manage security policies for the project.", description: "This project is automatically generated to manage security policies for the project.",
namespace_id: project.namespace_id, namespace_id: namespace_id,
initialize_with_readme: true, initialize_with_readme: true,
container_registry_enabled: false, container_registry_enabled: false,
packages_enabled: false, packages_enabled: false,
...@@ -44,14 +49,28 @@ module Security ...@@ -44,14 +49,28 @@ module Security
wiki_enabled: false, wiki_enabled: false,
snippets_enabled: false, snippets_enabled: false,
readme_template: readme_template readme_template: readme_template
} }.merge(security_policy_target_id)
end
def security_policy_target_id
if project_container?
{ security_policy_target_project_id: container.id }
elsif namespace_container?
{ security_policy_target_namespace_id: container.id }
end
end
def namespace_id
if project_container?
container.namespace_id
elsif namespace_container?
container.id
end
end end
def readme_template def readme_template
ERB.new(File.read(README_TEMPLATE_PATH), trim_mode: '<>').result(binding) ERB.new(File.read(README_TEMPLATE_PATH), trim_mode: '<>').result(binding)
end end
attr_reader :project
end end
end end
end end
# Security Policy Project for <%= @project.name %> # Security Policy Project for <%= @container.name %>
This project is automatically generated to manage security policies for the project. This project is automatically generated to manage security policies for the project.
...@@ -35,7 +35,7 @@ You can read more about the format and policies schema in the [documentation](ht ...@@ -35,7 +35,7 @@ You can read more about the format and policies schema in the [documentation](ht
## Default branch protection settings ## Default branch protection settings
This project is preconfigured with the default branch set as a protected branch, and only [project](<%= @project.web_url %>) This project is preconfigured with the default branch set as a protected branch, and only maintainers/owners of
maintainers/owners have permission to merge into that branch. This overrides any default branch protection both at the [<%= @container.name %>](<%= @container.web_url %>) have permission to merge into that branch. This overrides any default branch protection both at the
[group level](https://docs.gitlab.com/ee/user/group/index.html#change-the-default-branch-protection-of-a-group) and at the [group level](https://docs.gitlab.com/ee/user/group/index.html#change-the-default-branch-protection-of-a-group) and at the
[instance level](https://docs.gitlab.com/ee/user/admin_area/settings/visibility_and_access_controls.html#default-branch-protection). [instance level](https://docs.gitlab.com/ee/user/admin_area/settings/visibility_and_access_controls.html#default-branch-protection).
...@@ -7,35 +7,60 @@ RSpec.describe Mutations::SecurityPolicy::AssignSecurityPolicyProject do ...@@ -7,35 +7,60 @@ RSpec.describe Mutations::SecurityPolicy::AssignSecurityPolicyProject do
describe '#resolve' do describe '#resolve' do
let_it_be(:owner) { create(:user) } let_it_be(:owner) { create(:user) }
let_it_be(:user) { create(:user) } let_it_be(:maintainer) { create(:user) }
let_it_be(:namespace) { create(:group) }
let_it_be(:project) { create(:project, namespace: owner.namespace) } let_it_be(:project) { create(:project, namespace: owner.namespace) }
let_it_be(:policy_project) { create(:project) } let_it_be(:policy_project) { create(:project) }
let_it_be(:policy_project_id) { GitlabSchema.id_from_object(policy_project) } let_it_be(:policy_project_id) { GitlabSchema.id_from_object(policy_project) }
let(:current_user) { owner } let(:current_user) { owner }
subject { mutation.resolve(project_path: project.full_path, security_policy_project_id: policy_project_id) } subject { mutation.resolve(full_path: container.full_path, security_policy_project_id: policy_project_id) }
context 'when permission is set for user' do shared_context 'assigns security policy project' do
before do context 'when licensed feature is available' do
stub_licensed_features(security_orchestration_policies: true) before do
end stub_licensed_features(security_orchestration_policies: true)
end
context 'when user is an owner of the project' do context 'when user is an owner of the container' do
it 'assigns the security policy project' do before do
result = subject container.add_owner(owner)
end
expect(result[:errors]).to be_empty it 'assigns the security policy project' do
expect(project.security_orchestration_policy_configuration).not_to be_nil result = subject
expect(project.security_orchestration_policy_configuration.security_policy_management_project).to eq(policy_project)
expect(result[:errors]).to be_empty
expect(container.security_orchestration_policy_configuration).not_to be_nil
expect(container.security_orchestration_policy_configuration.security_policy_management_project).to eq(policy_project)
end
end
context 'when user is not an owner' do
let(:current_user) { maintainer }
before do
container.add_maintainer(maintainer)
end
it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end end
end end
context 'when user is not an owner' do context 'when policy_project_id is invalid' do
let(:current_user) { user } let_it_be(:policy_project_id) { 'invalid' }
it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when feature is not licensed' do
before do before do
project.add_maintainer(user) stub_licensed_features(security_orchestration_policies: false)
end end
it 'raises exception' do it 'raises exception' do
...@@ -44,21 +69,44 @@ RSpec.describe Mutations::SecurityPolicy::AssignSecurityPolicyProject do ...@@ -44,21 +69,44 @@ RSpec.describe Mutations::SecurityPolicy::AssignSecurityPolicyProject do
end end
end end
context 'when policy_project_id is invalid' do context 'when both fullPath and projectPath are not provided' do
let_it_be(:policy_project_id) { 'invalid' } subject { mutation.resolve(security_policy_project_id: policy_project_id) }
before do
stub_licensed_features(security_orchestration_policies: true)
end
it 'raises exception' do it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end end
end end
context 'when feature is not licensed' do context 'for project' do
before do let(:container) { project }
stub_licensed_features(security_orchestration_policies: false)
it_behaves_like 'assigns security policy project'
end
context 'for namespace' do
let(:container) { namespace }
context 'when feature is enabled' do
before do
stub_feature_flags(group_level_security_policies: namespace)
end
it_behaves_like 'assigns security policy project'
end end
it 'raises exception' do context 'when feature is disabled' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) before do
stub_licensed_features(security_orchestration_policies: true)
stub_feature_flags(group_level_security_policies: false)
end
it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end end
end end
end end
......
...@@ -6,34 +6,51 @@ RSpec.describe Mutations::SecurityPolicy::CreateSecurityPolicyProject do ...@@ -6,34 +6,51 @@ RSpec.describe Mutations::SecurityPolicy::CreateSecurityPolicyProject do
describe '#resolve' do describe '#resolve' do
let_it_be(:owner) { create(:user) } let_it_be(:owner) { create(:user) }
let_it_be(:user) { create(:user) } let_it_be(:maintainer) { create(:user) }
let_it_be(:namespace) { create(:group) }
let_it_be(:project) { create(:project, namespace: owner.namespace) } let_it_be(:project) { create(:project, namespace: owner.namespace) }
let(:current_user) { owner } let(:current_user) { owner }
subject { mutation.resolve(project_path: project.full_path) } subject { mutation.resolve(full_path: container.full_path) }
context 'when permission is set for user' do shared_context 'creates security policy project' do
before do context 'when licensed feature is available' do
stub_licensed_features(security_orchestration_policies: true) before do
end stub_licensed_features(security_orchestration_policies: true)
end
context 'when user is an owner of the container' do
let(:current_user) { owner }
context 'when user is an owner of the project' do before do
let(:current_user) { owner } namespace.add_owner(owner)
end
it 'returns project' do it 'returns project' do
result = subject result = subject
expect(result[:errors]).to be_empty expect(result[:errors]).to be_empty
expect(result[:project]).to eq(Project.last) expect(result[:project]).to eq(Project.last)
end
end end
end
context 'when user is not an owner' do context 'when user is not an owner' do
let(:current_user) { user } let(:current_user) { maintainer }
before do
container.add_maintainer(maintainer)
end
it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
context 'when feature is not licensed' do
before do before do
project.add_maintainer(user) stub_licensed_features(security_orchestration_policies: false)
end end
it 'raises exception' do it 'raises exception' do
...@@ -42,13 +59,44 @@ RSpec.describe Mutations::SecurityPolicy::CreateSecurityPolicyProject do ...@@ -42,13 +59,44 @@ RSpec.describe Mutations::SecurityPolicy::CreateSecurityPolicyProject do
end end
end end
context 'when feature is not licensed' do context 'when both fullPath and projectPath are not provided' do
subject { mutation.resolve({}) }
before do before do
stub_licensed_features(security_orchestration_policies: false) stub_licensed_features(security_orchestration_policies: true)
end end
it 'raises exception' do it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
context 'for project' do
let(:container) { project }
it_behaves_like 'creates security policy project'
end
context 'for namespace' do
let(:container) { namespace }
context 'when feature is enabled' do
before do
stub_feature_flags(group_level_security_policies: namespace)
end
it_behaves_like 'creates security policy project'
end
context 'when feature is disabled' do
before do
stub_licensed_features(security_orchestration_policies: true)
stub_feature_flags(group_level_security_policies: false)
end
it 'raises exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end end
end end
end end
......
...@@ -1934,4 +1934,62 @@ RSpec.describe GroupPolicy do ...@@ -1934,4 +1934,62 @@ RSpec.describe GroupPolicy do
expect_disallowed(*owner_permissions) expect_disallowed(*owner_permissions)
end end
end end
describe 'security complience policy' do
context 'when licensed feature is available' do
before do
stub_licensed_features(security_orchestration_policies: false)
end
context 'with developer or maintainer role' do
where(role: %w[maintainer developer])
with_them do
let(:current_user) { public_send(role) }
it { is_expected.to be_disallowed(:security_orchestration_policies) }
it { is_expected.to be_disallowed(:update_security_orchestration_policy_project) }
end
end
context 'with owner role' do
where(role: %w[owner])
with_them do
let(:current_user) { public_send(role) }
it { is_expected.to be_disallowed(:security_orchestration_policies) }
it { is_expected.to be_disallowed(:update_security_orchestration_policy_project) }
end
end
end
context 'when licensed feature is available' do
before do
stub_licensed_features(security_orchestration_policies: true)
end
context 'with developer or maintainer role' do
where(role: %w[maintainer developer])
with_them do
let(:current_user) { public_send(role) }
it { is_expected.to be_allowed(:security_orchestration_policies) }
it { is_expected.to be_disallowed(:update_security_orchestration_policy_project) }
end
end
context 'with owner role' do
where(role: %w[owner])
with_them do
let(:current_user) { public_send(role) }
it { is_expected.to be_allowed(:security_orchestration_policies) }
it { is_expected.to be_allowed(:update_security_orchestration_policy_project) }
end
end
end
end
end end
...@@ -2,12 +2,13 @@ ...@@ -2,12 +2,13 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Assigns scan execution policy project to a project' do RSpec.describe 'Assigns scan execution policy project to a project/namespace' do
include GraphqlHelpers include GraphqlHelpers
let_it_be_with_refind(:owner) { create(:user) } let_it_be_with_refind(:owner) { create(:user) }
let_it_be_with_refind(:user) { create(:user) } let_it_be_with_refind(:maintainer) { create(:user) }
let_it_be_with_refind(:project) { create(:project, namespace: owner.namespace) } let_it_be_with_refind(:project) { create(:project, namespace: owner.namespace) }
let_it_be_with_refind(:namespace) { create(:group) }
let_it_be_with_refind(:policy_project) { create(:project) } let_it_be_with_refind(:policy_project) { create(:project) }
let_it_be_with_refind(:policy_project_id) { GitlabSchema.id_from_object(policy_project) } let_it_be_with_refind(:policy_project_id) { GitlabSchema.id_from_object(policy_project) }
...@@ -16,7 +17,7 @@ RSpec.describe 'Assigns scan execution policy project to a project' do ...@@ -16,7 +17,7 @@ RSpec.describe 'Assigns scan execution policy project to a project' do
subject { post_graphql_mutation(mutation, current_user: current_user) } subject { post_graphql_mutation(mutation, current_user: current_user) }
def mutation def mutation
variables = { project_path: project.full_path, security_policy_project_id: policy_project_id.to_s } variables = { full_path: container.full_path, security_policy_project_id: policy_project_id.to_s }
graphql_mutation(:security_policy_project_assign, variables) do graphql_mutation(:security_policy_project_assign, variables) do
<<-QL.strip_heredoc <<-QL.strip_heredoc
...@@ -29,48 +30,82 @@ RSpec.describe 'Assigns scan execution policy project to a project' do ...@@ -29,48 +30,82 @@ RSpec.describe 'Assigns scan execution policy project to a project' do
graphql_mutation_response(:security_policy_project_assign) graphql_mutation_response(:security_policy_project_assign)
end end
context 'when licensed feature is available' do shared_context 'assigns security policy project' do
before do context 'when licensed feature is available' do
stub_licensed_features(security_orchestration_policies: true) before do
end stub_licensed_features(security_orchestration_policies: true)
end
context 'when user is an owner of the container' do
before do
container.add_owner(owner)
end
context 'when user is an owner of the project' do it 'assigns the security policy project', :aggregate_failures do
it 'assigns the security policy project', :aggregate_failures do subject
subject
orchestration_policy_configuration = project.security_orchestration_policy_configuration orchestration_policy_configuration = container.security_orchestration_policy_configuration
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty expect(mutation_response['errors']).to be_empty
expect(orchestration_policy_configuration.security_policy_management_project).to eq(policy_project) expect(orchestration_policy_configuration.security_policy_management_project).to eq(policy_project)
end
end end
end
context 'when user is not an owner' do context 'when user is not an owner' do
let(:current_user) { user } let(:current_user) { maintainer }
before do before do
project.add_maintainer(user) project.add_maintainer(maintainer)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end end
it_behaves_like 'a mutation that returns top-level errors', context 'when policy_project_id is invalid' do
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] let_it_be_with_refind(:policy_project_id) { "gid://gitlab/Project/#{non_existing_record_id}" }
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end end
context 'when policy_project_id is invalid' do context 'when feature is not licensed' do
let_it_be_with_refind(:policy_project_id) { "gid://gitlab/Project/#{non_existing_record_id}" } before do
stub_licensed_features(security_orchestration_policies: false)
end
it_behaves_like 'a mutation that returns top-level errors', it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end end
end end
context 'when feature is not licensed' do context 'for project' do
before do let(:container) { project }
stub_licensed_features(security_orchestration_policies: false)
it_behaves_like 'assigns security policy project'
end
context 'for namespace' do
let(:container) { namespace }
context 'when feature is enabled' do
before do
stub_feature_flags(group_level_security_policies: namespace)
end
it_behaves_like 'assigns security policy project'
end end
it_behaves_like 'a mutation that returns top-level errors', context 'when feature is disabled' do
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] before do
stub_licensed_features(security_orchestration_policies: true)
stub_feature_flags(group_level_security_policies: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end end
end end
...@@ -2,19 +2,20 @@ ...@@ -2,19 +2,20 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Creates and assigns scan execution policy project to a project' do RSpec.describe 'Creates and assigns scan execution policy project to a project/namespace' do
include GraphqlHelpers include GraphqlHelpers
let_it_be_with_refind(:owner) { create(:user) } let_it_be_with_refind(:owner) { create(:user) }
let_it_be_with_refind(:user) { create(:user) } let_it_be_with_refind(:maintainer) { create(:user) }
let_it_be_with_refind(:project) { create(:project, namespace: owner.namespace) } let_it_be_with_refind(:project) { create(:project, namespace: owner.namespace) }
let_it_be_with_refind(:namespace) { create(:group) }
let(:current_user) { owner } let(:current_user) { owner }
subject { post_graphql_mutation(mutation, current_user: current_user) } subject { post_graphql_mutation(mutation, current_user: current_user) }
def mutation def mutation
variables = { project_path: project.full_path } variables = { full_path: container.full_path }
graphql_mutation(:security_policy_project_create, variables) do graphql_mutation(:security_policy_project_create, variables) do
<<-QL.strip_heredoc <<-QL.strip_heredoc
...@@ -31,46 +32,80 @@ RSpec.describe 'Creates and assigns scan execution policy project to a project' ...@@ -31,46 +32,80 @@ RSpec.describe 'Creates and assigns scan execution policy project to a project'
graphql_mutation_response(:security_policy_project_create) graphql_mutation_response(:security_policy_project_create)
end end
context 'when licensed feature is available' do shared_examples 'creates security policy project' do
before do context 'when licensed feature is available' do
# TODO: investigate too many qeuries issue as part of Project Management Database and Query Performance before do
# Epic: https://gitlab.com/groups/gitlab-org/-/epics/5804 # TODO: investigate too many qeuries issue as part of Project Management Database and Query Performance
# Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/348344 # Epic: https://gitlab.com/groups/gitlab-org/-/epics/5804
stub_const('Gitlab::QueryLimiting::Transaction::THRESHOLD', 130) # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/348344
stub_licensed_features(security_orchestration_policies: true) stub_const('Gitlab::QueryLimiting::Transaction::THRESHOLD', 140)
end stub_licensed_features(security_orchestration_policies: true)
end
context 'when user is an owner of the project' do context 'when user is an owner of the container' do
it 'creates and assigns the security policy project', :aggregate_failures do it 'creates and assigns the security policy project', :aggregate_failures do
expect { subject }.to change { ::Project.count }.by(1) expect { subject }.to change { ::Project.count }.by(1)
orchestration_policy_configuration = project.security_orchestration_policy_configuration orchestration_policy_configuration = container.security_orchestration_policy_configuration
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
expect(mutation_response['errors']).to be_empty expect(mutation_response['errors']).to be_empty
expect(mutation_response.dig('project', 'id')).to eq(orchestration_policy_configuration.security_policy_management_project.to_gid.to_s) expect(mutation_response.dig('project', 'id')).to eq(orchestration_policy_configuration.security_policy_management_project.to_gid.to_s)
expect(mutation_response.dig('project', 'name')).to eq("#{project.name} - Security policy project") expect(mutation_response.dig('project', 'name')).to eq("#{container.name} - Security policy project")
end
end end
end
context 'when user is not an owner' do context 'when user is not an owner' do
let(:current_user) { user } let(:current_user) { maintainer }
before do
project.add_maintainer(maintainer)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end
context 'when feature is not licensed' do
before do before do
project.add_maintainer(user) stub_licensed_features(security_orchestration_policies: false)
end end
it_behaves_like 'a mutation that returns top-level errors', it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end end
end end
context 'when feature is not licensed' do context 'for project' do
let(:container) { project }
it_behaves_like 'creates security policy project'
end
context 'for namespace' do
let(:container) { namespace }
before do before do
stub_licensed_features(security_orchestration_policies: false) namespace.add_owner(owner)
end
context 'when feature is enabled' do
before do
stub_feature_flags(group_level_security_policies: namespace)
end
it_behaves_like 'creates security policy project'
end end
it_behaves_like 'a mutation that returns top-level errors', context 'when feature is disabled' do
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR] before do
stub_licensed_features(security_orchestration_policies: true)
stub_feature_flags(group_level_security_policies: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end end
end end
...@@ -373,18 +373,36 @@ RSpec.describe Projects::CreateService, '#execute' do ...@@ -373,18 +373,36 @@ RSpec.describe Projects::CreateService, '#execute' do
end end
context 'security policy configuration' do context 'security policy configuration' do
let_it_be(:security_policy_target_project) { create(:project) } context 'with security_policy_target_project_id' do
let_it_be(:security_policy_target_project) { create(:project) }
before do before do
opts[:security_policy_target_project_id] = security_policy_target_project.id opts[:security_policy_target_project_id] = security_policy_target_project.id
stub_licensed_features(security_orchestration_policies: true)
end
stub_licensed_features(security_orchestration_policies: true) it 'creates security policy configuration for the project' do
expect(::Security::Orchestration::AssignService).to receive_message_chain(:new, :execute)
create_project(user, opts)
end
end end
it 'creates security policy configuration for the project' do context 'with security_policy_target_namespace_id' do
expect(::Security::Orchestration::AssignService).to receive_message_chain(:new, :execute) let_it_be(:security_policy_target_namespace) { create(:namespace) }
create_project(user, opts) before do
opts[:security_policy_target_namespace_id] = security_policy_target_namespace.id
stub_licensed_features(security_orchestration_policies: true)
end
it 'creates security policy configuration for the project' do
expect(::Security::Orchestration::AssignService).to receive_message_chain(:new, :execute)
create_project(user, opts)
end
end end
end end
......
...@@ -5,76 +5,99 @@ require 'spec_helper' ...@@ -5,76 +5,99 @@ require 'spec_helper'
RSpec.describe Security::Orchestration::AssignService do RSpec.describe Security::Orchestration::AssignService do
let_it_be(:project, reload: true) { create(:project) } let_it_be(:project, reload: true) { create(:project) }
let_it_be(:another_project) { create(:project) } let_it_be(:another_project) { create(:project) }
let_it_be(:namespace, reload: true) { create(:group) }
let_it_be(:another_namespace) { create(:group) }
let_it_be(:policy_project) { create(:project) } let_it_be(:policy_project) { create(:project) }
let_it_be(:new_policy_project) { create(:project) } let_it_be(:new_policy_project) { create(:project) }
let(:container) { project }
let(:another_container) { another_project }
describe '#execute' do describe '#execute' do
subject(:service) do subject(:service) do
described_class.new(project, nil, policy_project_id: policy_project.id).execute described_class.new(container: container, current_user: nil, params: { policy_project_id: policy_project.id }).execute
end end
before do before do
service service
end end
it 'assigns policy project to project' do shared_examples 'assigns policy project' do
expect(service).to be_success it 'assigns policy project to container' do
expect( expect(service).to be_success
project.security_orchestration_policy_configuration.security_policy_management_project_id expect(
).to eq(policy_project.id) container.security_orchestration_policy_configuration.security_policy_management_project_id
end ).to eq(policy_project.id)
end
it 'updates project with new policy project' do it 'updates container with new policy project' do
repeated_service = repeated_service =
described_class.new(project, nil, policy_project_id: new_policy_project.id).execute described_class.new(container: container, current_user: nil, params: { policy_project_id: new_policy_project.id }).execute
expect(repeated_service).to be_success expect(repeated_service).to be_success
expect( expect(
project.security_orchestration_policy_configuration.security_policy_management_project_id container.security_orchestration_policy_configuration.security_policy_management_project_id
).to eq(new_policy_project.id) ).to eq(new_policy_project.id)
end end
it 'assigns same policy to different projects' do it 'assigns same policy to different container' do
repeated_service = repeated_service =
described_class.new(another_project, nil, policy_project_id: policy_project.id).execute described_class.new(container: another_container, current_user: nil, params: { policy_project_id: policy_project.id }).execute
expect(repeated_service).to be_success expect(repeated_service).to be_success
end end
it 'unassigns project' do it 'unassigns project' do
expect { described_class.new(project, nil, policy_project_id: nil).execute }.to change { expect { described_class.new(container: container, current_user: nil, params: { policy_project_id: nil }).execute }.to change {
project.reload.security_orchestration_policy_configuration container.reload.security_orchestration_policy_configuration
}.to(nil) }.to(nil)
end end
it 'returns error when db has problem' do
dbl_error = double('ActiveRecord')
dbl =
double(
'Security::OrchestrationPolicyConfiguration',
security_orchestration_policy_configuration: dbl_error
)
it 'returns error when db has problem' do allow(dbl_error).to receive(:update!).and_raise(ActiveRecord::RecordInvalid)
dbl_error = double('ActiveRecord')
dbl =
double(
'Security::OrchestrationPolicyConfiguration',
security_orchestration_policy_configuration: dbl_error
)
allow(dbl_error).to receive(:update!).and_raise(ActiveRecord::RecordInvalid) allow_next_instance_of(described_class) do |instance|
allow(instance).to receive(:has_existing_policy?).and_return(true)
allow(instance).to receive(:container).and_return(dbl)
end
allow_next_instance_of(described_class) do |instance| repeated_service =
allow(instance).to receive(:has_existing_policy?).and_return(true) described_class.new(container: container, current_user: nil, params: { policy_project_id: new_policy_project.id }).execute
allow(instance).to receive(:project).and_return(dbl)
expect(repeated_service).to be_error
end end
repeated_service = describe 'with invalid project id' do
described_class.new(project, nil, policy_project_id: new_policy_project.id).execute subject(:service) { described_class.new(container: container, current_user: nil, params: { policy_project_id: 345 }).execute }
it 'does not change policy project' do
expect(service).to be_error
expect(repeated_service).to be_error expect { service }.not_to change { container.security_orchestration_policy_configuration }
end
end
end end
describe 'with invalid project id' do context 'for project' do
subject(:service) { described_class.new(project, nil, policy_project_id: 345).execute } let(:container) { project }
let(:another_container) { another_project }
it_behaves_like 'assigns policy project'
end
it 'assigns policy project to project' do context 'for namespace' do
expect(service).to be_error let(:container) { namespace }
let(:another_container) { another_namespace }
expect { service }.not_to change { project.security_orchestration_policy_configuration } it_behaves_like 'assigns policy project'
end
end end
end end
end end
...@@ -4,15 +4,18 @@ require 'spec_helper' ...@@ -4,15 +4,18 @@ require 'spec_helper'
RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do
describe '#execute' do describe '#execute' do
let_it_be(:project) { create(:project) } let_it_be_with_refind(:project) { create(:project) }
let_it_be(:current_user) { project.first_owner }
subject(:service) { described_class.new(project: project, current_user: current_user) } let_it_be(:owner) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be(:developer) { create(:user) }
context 'when security_orchestration_policies_configuration does not exist for project' do let(:current_user) { container.first_owner }
let_it_be(:maintainer) { create(:user) } let(:container) { project }
let_it_be(:developer) { create(:user) }
subject(:service) { described_class.new(container: container, current_user: current_user) }
context 'when security_orchestration_policies_configuration does not exist for project' do
before do before do
project.add_maintainer(maintainer) project.add_maintainer(maintainer)
project.add_developer(developer) project.add_developer(developer)
...@@ -31,10 +34,32 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do ...@@ -31,10 +34,32 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do
end end
end end
context 'when security_orchestration_policies_configuration does not exist for namespace' do
let(:group) { create(:group) }
let(:container) { group }
before do
group.add_owner(owner)
group.add_maintainer(maintainer)
group.add_developer(developer)
end
it 'creates policy project with maintainers and developers from target group as developers', :aggregate_failures do
response = service.execute
policy_project = response[:policy_project]
expect(group.reload.security_orchestration_policy_configuration.security_policy_management_project).to eq(policy_project)
expect(policy_project.namespace).to eq(group)
expect(policy_project.owner).to eq(group)
expect(MembersFinder.new(policy_project, nil).execute.map(&:user)).to contain_exactly(owner, maintainer, developer)
expect(policy_project.container_registry_access_level).to eq(ProjectFeature::DISABLED)
expect(policy_project.repository.readme.data).to include('# Security Policy Project for')
expect(policy_project.repository.readme.data).to include('## Default branch protection settings')
end
end
context 'when adding users to security policy project fails' do context 'when adding users to security policy project fails' do
let_it_be(:project) { create(:project) } let(:current_user) { project.first_owner }
let_it_be(:current_user) { project.first_owner }
let_it_be(:maintainer) { create(:user) }
before do before do
project.add_maintainer(maintainer) project.add_maintainer(maintainer)
...@@ -54,8 +79,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do ...@@ -54,8 +79,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProjectCreateService do
end end
context 'when project creation fails' do context 'when project creation fails' do
let_it_be(:project) { create(:project) } let(:current_user) { create(:user) }
let_it_be(:current_user) { create(:user) }
it 'returns error' do it 'returns error' do
response = service.execute response = service.execute
......
...@@ -10309,7 +10309,7 @@ msgstr "" ...@@ -10309,7 +10309,7 @@ msgstr ""
msgid "Could not upload your designs as one or more files uploaded are not supported." msgid "Could not upload your designs as one or more files uploaded are not supported."
msgstr "" msgstr ""
msgid "Couldn't assign policy to project" msgid "Couldn't assign policy to project or group"
msgstr "" msgstr ""
msgid "Country" msgid "Country"
......
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