diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d54afe84c25d9fda6be156e096592f837c727303..78f2f584a8a0a6cdce7410cca50ade91a6743c6d 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5616,6 +5616,28 @@ Input type: `ProjectSetComplianceFrameworkInput` | <a id="mutationprojectsetcomplianceframeworkerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationprojectsetcomplianceframeworkproject"></a>`project` | [`Project`](#project) | Project after mutation. | +### `Mutation.projectSetContinuousVulnerabilityScanning` + +Enable/disable Continuous Vulnerability Scanning for the given project. + +Input type: `ProjectSetContinuousVulnerabilityScanningInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationprojectsetcontinuousvulnerabilityscanningclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationprojectsetcontinuousvulnerabilityscanningenable"></a>`enable` | [`Boolean!`](#boolean) | Desired status for Continuous Vulnerability Scanning feature. | +| <a id="mutationprojectsetcontinuousvulnerabilityscanningprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationprojectsetcontinuousvulnerabilityscanningclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationprojectsetcontinuousvulnerabilityscanningcontinuousvulnerabilityscanningenabled"></a>`continuousVulnerabilityScanningEnabled` | [`Boolean!`](#boolean) | Whether feature is enabled. | +| <a id="mutationprojectsetcontinuousvulnerabilityscanningerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.projectSetLocked` Input type: `ProjectSetLockedInput` diff --git a/ee/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql b/ee/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql new file mode 100644 index 0000000000000000000000000000000000000000..79f4316d106097d1779c275f086c5fd218cca6e6 --- /dev/null +++ b/ee/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql @@ -0,0 +1,8 @@ +mutation ProjectSetContinuousVulnerabilityScanning( + $input: ProjectSetContinuousVulnerabilityScanningInput! +) { + projectSetContinuousVulnerabilityScanning(input: $input) { + continuousVulnerabilityScanningEnabled + errors + } +} diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index b80d0ea8eb6747b4f7cbff820bfb2df26ece85eb..6fa6ee1ae077b63befa7cd4249024e948f262ede 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -128,6 +128,7 @@ module MutationType mount_mutation ::Mutations::AuditEvents::Streaming::InstanceHeaders::Destroy mount_mutation ::Mutations::AuditEvents::Streaming::InstanceEventTypeFilters::Create mount_mutation ::Mutations::AuditEvents::Streaming::InstanceEventTypeFilters::Destroy + mount_mutation ::Mutations::Security::CiConfiguration::ProjectSetContinuousVulnerabilityScanning prepend(Types::DeprecatedMutations) end diff --git a/ee/app/graphql/mutations/security/ci_configuration/project_set_continuous_vulnerability_scanning.rb b/ee/app/graphql/mutations/security/ci_configuration/project_set_continuous_vulnerability_scanning.rb new file mode 100644 index 0000000000000000000000000000000000000000..a3955bfbe18c56d6362973c8fe98d4b3e9353ffa --- /dev/null +++ b/ee/app/graphql/mutations/security/ci_configuration/project_set_continuous_vulnerability_scanning.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Mutations + module Security + module CiConfiguration + class ProjectSetContinuousVulnerabilityScanning < BaseMutation + graphql_name 'ProjectSetContinuousVulnerabilityScanning' + + include FindsProject + + description <<~DESC + Enable/disable Continuous Vulnerability Scanning for the given project. + DESC + + argument :project_path, GraphQL::Types::ID, + required: true, + description: 'Full path of the project.' + + argument :enable, GraphQL::Types::Boolean, + required: true, + description: 'Desired status for Continuous Vulnerability Scanning feature.' + + field :continuous_vulnerability_scanning_enabled, GraphQL::Types::Boolean, + null: false, + description: 'Whether feature is enabled.' + + authorize :enable_continuous_vulnerability_scans + + def resolve(project_path:, enable:) + project = authorized_find!(project_path) + + enabled = ::Security::Configuration::ProjectSetContinuousVulnerabilityScanningService + .execute(project: project, enable: enable) + + { continuous_vulnerability_scanning_enabled: enabled, errors: [] } + end + end + end + end +end diff --git a/ee/app/models/project_security_setting.rb b/ee/app/models/project_security_setting.rb index bfc9bae8b8c56a212126843eb15fd97682aec45c..a243f324675751bdcc4a092991a53bb171f83a63 100644 --- a/ee/app/models/project_security_setting.rb +++ b/ee/app/models/project_security_setting.rb @@ -23,9 +23,7 @@ def auto_fix_enabled_types end end - # TODO: the caller for this method is a graphql mutation implemented as part of - # https://gitlab.com/gitlab-org/gitlab/-/issues/424374 def set_continuous_vulnerability_scans!(enabled:) - update!(continuous_vulnerability_scans_enabled: enabled) + enabled if update!(continuous_vulnerability_scans_enabled: enabled) end end diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 5c44dae6bc1c8126f4a1f5f76224a49234a2ff18..938d70f7dd982e2d9986adc51e8652b8a4830876 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -570,6 +570,10 @@ module ProjectPolicy .default_project_deletion_protection end + condition(:continuous_vulnerability_scanning_available) do + ::Feature.enabled?(:dependency_scanning_on_advisory_ingestion) + end + rule { needs_new_sso_session }.policy do prevent :read_project end @@ -700,6 +704,10 @@ module ProjectPolicy rule do (maintainer | owner | admin) & pages_multiple_versions_available end.enable :pages_multiple_versions + + rule { continuous_vulnerability_scanning_available & can?(:developer_access) }.policy do + enable :enable_continuous_vulnerability_scans + end end override :lookup_access_level! diff --git a/ee/app/services/security/configuration/project_set_continuous_vulnerability_scanning_service.rb b/ee/app/services/security/configuration/project_set_continuous_vulnerability_scanning_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..334f7d6a47d2e3d7671405545804327b515bd2e2 --- /dev/null +++ b/ee/app/services/security/configuration/project_set_continuous_vulnerability_scanning_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Security + module Configuration + class ProjectSetContinuousVulnerabilityScanningService + def self.execute(project:, enable:) + project.security_setting.set_continuous_vulnerability_scans!(enabled: enable) + end + end + end +end diff --git a/ee/spec/models/project_security_setting_spec.rb b/ee/spec/models/project_security_setting_spec.rb index c19ba38f8311e8c4feb60bbdf88ad318bda2d55b..aa61f3c4efad527ebb38edd92f110eadb0267db6 100644 --- a/ee/spec/models/project_security_setting_spec.rb +++ b/ee/spec/models/project_security_setting_spec.rb @@ -67,8 +67,8 @@ with_them do let(:setting) { create(:project_security_setting, continuous_vulnerability_scans_enabled: value_before) } - specify do - setting.set_continuous_vulnerability_scans!(enabled: enabled) + it 'updates the attribute and returns the new value' do + expect(setting.set_continuous_vulnerability_scans!(enabled: enabled)).to eq(value_after) expect(setting.reload.continuous_vulnerability_scans_enabled).to eq(value_after) end end diff --git a/ee/spec/requests/api/graphql/mutations/security/configuration/project_set_continuous_vulnerability_scanning_spec.rb b/ee/spec/requests/api/graphql/mutations/security/configuration/project_set_continuous_vulnerability_scanning_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f506b651045f01a954c3e7ce1a79d25bffae5e78 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/security/configuration/project_set_continuous_vulnerability_scanning_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting Project Continuous Vulnerability Scanning', feature_category: :software_composition_analysis do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:security_setting) { create(:project_security_setting, continuous_vulnerability_scans_enabled: value_before) } + let(:project) { security_setting.project } + let(:mutation_name) { :project_set_continuous_vulnerability_scanning } + let(:mutation) do + graphql_mutation( + mutation_name, + project_path: project.full_path, + enable: enable + ) + end + + let(:value_before) { false } + let(:enable) { true } + + context 'when the user does not have permission' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not enable cvs' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { security_setting.reload.continuous_vulnerability_scans_enabled } + end + end + + context 'when the user has permission' do + before do + project.add_developer(current_user) + end + + context 'and feature is enabled' do + before do + stub_feature_flags(dependency_scanning_on_advisory_ingestion: true) + end + + where(:value_before, :enable, :value_after) do + true | false | false + true | true | true + false | true | true + false | false | false + end + + with_them do + it 'updates the project setting and returns the new value' do + post_graphql_mutation(mutation, current_user: current_user) + + response = graphql_mutation_response(mutation_name) + expect(response).to include({ 'continuousVulnerabilityScanningEnabled' => value_after, 'errors' => [] }) + + expect(security_setting.reload.continuous_vulnerability_scans_enabled).to eq(value_after) + end + end + end + + context 'and feature is disabled' do + before do + stub_feature_flags(dependency_scanning_on_advisory_ingestion: false) + end + + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not enable cvs' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { security_setting.reload.continuous_vulnerability_scans_enabled } + end + end + end +end diff --git a/ee/spec/services/security/configuration/project_set_continuous_vulnerability_scanning_service_spec.rb b/ee/spec/services/security/configuration/project_set_continuous_vulnerability_scanning_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..027cf3d2fc08d1baca4d0f18fdcbe45402e52292 --- /dev/null +++ b/ee/spec/services/security/configuration/project_set_continuous_vulnerability_scanning_service_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::Configuration::ProjectSetContinuousVulnerabilityScanningService, feature_category: :software_composition_analysis do + describe '#execute' do + let_it_be(:security_setting) { create(:project_security_setting, continuous_vulnerability_scans_enabled: false) } + let_it_be(:project) { security_setting.project } + + it 'returns attribute value' do + expect(described_class.execute(project: project, enable: true)).to eq(true) + expect(described_class.execute(project: project, enable: false)).to eq(false) + end + + it 'changes the attribute' do + expect { described_class.execute(project: project, enable: true) } + .to change { security_setting.reload.continuous_vulnerability_scans_enabled } + .from(false).to(true) + expect { described_class.execute(project: project, enable: true) } + .not_to change { security_setting.reload.continuous_vulnerability_scans_enabled } + expect { described_class.execute(project: project, enable: false) } + .to change { security_setting.reload.continuous_vulnerability_scans_enabled } + .from(true).to(false) + expect { described_class.execute(project: project, enable: false) } + .not_to change { security_setting.reload.continuous_vulnerability_scans_enabled } + end + end +end