diff --git a/app/assets/javascripts/security_configuration/graphql/set_container_scanning_for_registry.graphql b/app/assets/javascripts/security_configuration/graphql/set_container_scanning_for_registry.graphql new file mode 100644 index 0000000000000000000000000000000000000000..843f0edbe0251e7aa9d58b9073d038aab0eca457 --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/set_container_scanning_for_registry.graphql @@ -0,0 +1,6 @@ +mutation SetContainerScanningForRegistry($input: SetContainerScanningForRegistryInput!) { + setContainerScanningForRegistry(input: $input) { + containerScanningForRegistryEnabled + errors + } +} diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d8c0ac3daa1e4194a1ea4759b2eb77044e7b857d..6ac9cb548cd2b51aacd0224838ee42e26f84ce8b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -7825,6 +7825,28 @@ Input type: `SecurityTrainingUpdateInput` | <a id="mutationsecuritytrainingupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationsecuritytrainingupdatetraining"></a>`training` | [`ProjectSecurityTraining`](#projectsecuritytraining) | Represents the training entity subject to mutation. | +### `Mutation.setContainerScanningForRegistry` + +Enable/disable Container Scanning on Container Registry for the given project or group. + +Input type: `SetContainerScanningForRegistryInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationsetcontainerscanningforregistryclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationsetcontainerscanningforregistryenable"></a>`enable` | [`Boolean!`](#boolean) | Desired status for Container Scanning on Container Registry feature. | +| <a id="mutationsetcontainerscanningforregistrynamespacepath"></a>`namespacePath` | [`ID!`](#id) | Full path of the namespace (project or group). | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationsetcontainerscanningforregistryclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationsetcontainerscanningforregistrycontainerscanningforregistryenabled"></a>`containerScanningForRegistryEnabled` | [`Boolean`](#boolean) | Whether the feature is enabled. | +| <a id="mutationsetcontainerscanningforregistryerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.setPreReceiveSecretDetection` Enable/disable pre-receive secret detection for the given project. diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index e1c7941aa447f3a59e01f611dae5ad6450025d6b..f51bc21a93532be0d2659fbf4960b74fe55c3a9d 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -149,6 +149,7 @@ module MutationType mount_mutation ::Mutations::AuditEvents::Streaming::InstanceEventTypeFilters::Destroy mount_mutation ::Mutations::Security::CiConfiguration::ProjectSetContinuousVulnerabilityScanning mount_mutation ::Mutations::Security::CiConfiguration::SetPreReceiveSecretDetection + mount_mutation ::Mutations::Security::CiConfiguration::SetContainerScanningForRegistry mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Destroy mount_mutation ::Mutations::AuditEvents::Instance::GoogleCloudLoggingConfigurations::Update mount_mutation ::Mutations::DependencyProxy::Packages::Settings::Update diff --git a/ee/app/graphql/mutations/security/ci_configuration/set_container_scanning_for_registry.rb b/ee/app/graphql/mutations/security/ci_configuration/set_container_scanning_for_registry.rb new file mode 100644 index 0000000000000000000000000000000000000000..cc110bcf45050c443515ebc4e900aadd748e0ed6 --- /dev/null +++ b/ee/app/graphql/mutations/security/ci_configuration/set_container_scanning_for_registry.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Mutations + module Security + module CiConfiguration + class SetContainerScanningForRegistry < BaseMutation + graphql_name 'SetContainerScanningForRegistry' + + include FindsNamespace + + description <<~DESC + Enable/disable Container Scanning on Container Registry for the given project or group. + DESC + + argument :namespace_path, GraphQL::Types::ID, + required: true, + description: 'Full path of the namespace (project or group).' + + argument :enable, GraphQL::Types::Boolean, + required: true, + description: 'Desired status for Container Scanning on Container Registry feature.' + + field :container_scanning_for_registry_enabled, GraphQL::Types::Boolean, + null: true, + description: 'Whether the feature is enabled.' + + authorize :enable_container_scanning_for_registry + + def resolve(namespace_path:, enable:) + namespace = find_namespace(namespace_path) + + response = ::Security::Configuration::SetContainerScanningForRegistryService + .execute(namespace: namespace, enable: enable) + + { container_scanning_for_registry_enabled: response.payload[:enabled], errors: response.errors } + end + + private + + def find_namespace(namespace_path) + namespace = authorized_find!(namespace_path) + # This will be removed following the completion of https://gitlab.com/gitlab-org/gitlab/-/issues/451430 + unless namespace.is_a? Project + raise_resource_not_available_error! 'Setting only available for Project namespaces.' + end + + namespace + end + end + end + end +end diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 65c26c830b0145c7e57d93f9e78dd41156b182c5..9427d209c130b12d197263447c890de4e6660209 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -769,6 +769,14 @@ module GroupPolicy rule { pre_receive_secret_detection_available & can?(:maintainer_access) }.policy do enable :enable_pre_receive_secret_detection end + + condition(:container_scanning_for_registry_available) do + ::Feature.enabled?(:container_scanning_for_registry) + end + + rule { container_scanning_for_registry_available & can?(:maintainer_access) }.policy do + enable :enable_container_scanning_for_registry + end end override :lookup_access_level! diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 3191ba2943ccdd981a47a7732ae683d69e01f224..b80cc52aa8c2d2dfe51386a6394298b6f4c6fc3b 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -981,6 +981,14 @@ module ProjectPolicy rule { pre_receive_secret_detection_available & can?(:maintainer_access) }.policy do enable :enable_pre_receive_secret_detection end + + condition(:container_scanning_for_registry_available) do + ::Feature.enabled?(:container_scanning_for_registry) + end + + rule { container_scanning_for_registry_available & can?(:maintainer_access) }.policy do + enable :enable_container_scanning_for_registry + end end override :lookup_access_level! diff --git a/ee/app/services/security/configuration/set_container_scanning_for_registry_service.rb b/ee/app/services/security/configuration/set_container_scanning_for_registry_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..ca1f157de21b8538b3066ef2db56e147b0e12927 --- /dev/null +++ b/ee/app/services/security/configuration/set_container_scanning_for_registry_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Security + module Configuration + class SetContainerScanningForRegistryService + def self.execute(namespace:, enable:) + # At present, the security_setting feature is exclusively accessible for projects. + # Following the resolution of https://gitlab.com/gitlab-org/gitlab/-/issues/451430, + # this feature will also be available at the group level. + ServiceResponse.success( + payload: { + enabled: namespace.security_setting.set_container_scanning_for_registry!( + enabled: enable + ), + errors: [] + }) + rescue StandardError => e + ServiceResponse.error( + message: e.message, + payload: { enabled: nil } + ) + end + end + end +end diff --git a/ee/config/feature_flags/wip/container_scanning_for_registry.yml b/ee/config/feature_flags/wip/container_scanning_for_registry.yml new file mode 100644 index 0000000000000000000000000000000000000000..6c6e4a20db4a0c1f4a237b93099fca81aa96142b --- /dev/null +++ b/ee/config/feature_flags/wip/container_scanning_for_registry.yml @@ -0,0 +1,9 @@ +--- +name: container_scanning_for_registry +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/442890 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147537 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/443827 +milestone: '17.0' +group: group::composition analysis +type: wip +default_enabled: false diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 2c801ac8a61e1d770cbad52c409d9ec674c6bb2c..790219fbe749ef95cd91fa66bdf9e279db01498b 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -3640,4 +3640,25 @@ def create_member_role(member, abilities = member_role_abilities) it { is_expected.to match_expected_result } end end + + describe 'enable_container_scanning_for_registry' do + using RSpec::Parameterized::TableSyntax + + where(:container_scanning_for_registry, :current_user, :match_expected_result) do + true | ref(:owner) | be_allowed(:enable_container_scanning_for_registry) + true | ref(:maintainer) | be_allowed(:enable_container_scanning_for_registry) + true | ref(:developer) | be_disallowed(:enable_container_scanning_for_registry) + false | ref(:owner) | be_disallowed(:enable_container_scanning_for_registry) + false | ref(:maintainer) | be_disallowed(:enable_container_scanning_for_registry) + false | ref(:developer) | be_disallowed(:enable_container_scanning_for_registry) + end + + with_them do + before do + stub_feature_flags(container_scanning_for_registry: container_scanning_for_registry) + end + + it { is_expected.to match_expected_result } + end + end end diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 299e6776e0edc7f2faf8e71b60f626eef5a42c32..3e10496743ae238cca76099e7fafa021d79c977d 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -3807,4 +3807,27 @@ def create_member_role(member, abilities = member_role_abilities) it { is_expected.to match_expected_result } end end + + describe 'enable_container_scanning_for_registry' do + using RSpec::Parameterized::TableSyntax + + where(:container_scanning_for_registry, :current_user, :match_expected_result) do + true | ref(:owner) | be_allowed(:enable_container_scanning_for_registry) + true | ref(:maintainer) | be_allowed(:enable_container_scanning_for_registry) + true | ref(:developer) | be_disallowed(:enable_container_scanning_for_registry) + true | ref(:non_member) | be_disallowed(:enable_container_scanning_for_registry) + false | ref(:owner) | be_disallowed(:enable_container_scanning_for_registry) + false | ref(:maintainer) | be_disallowed(:enable_container_scanning_for_registry) + false | ref(:developer) | be_disallowed(:enable_container_scanning_for_registry) + false | ref(:non_member) | be_disallowed(:enable_container_scanning_for_registry) + end + + with_them do + before do + stub_feature_flags(container_scanning_for_registry: container_scanning_for_registry) + end + + it { is_expected.to match_expected_result } + end + end end diff --git a/ee/spec/requests/api/graphql/mutations/security/configuration/set_container_scanning_for_registry_spec.rb b/ee/spec/requests/api/graphql/mutations/security/configuration/set_container_scanning_for_registry_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7d6702db5aabf2d9ed9c28424a330da32e5b81f2 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/security/configuration/set_container_scanning_for_registry_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting Project and Group Container Scanning for Registry', feature_category: :software_composition_analysis do + using RSpec::Parameterized::TableSyntax + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:security_setting) { create(:project_security_setting, container_scanning_for_registry_enabled: value_before) } + let(:mutation_name) { :set_container_scanning_for_registry } + + let(:value_before) { false } + let(:enable) { true } + + context 'with project' do + let(:project) { security_setting.project } + let(:mutation) do + graphql_mutation( + mutation_name, + namespace_path: project.full_path, + enable: enable + ) + end + + 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 container scanning for registry' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { security_setting.reload.container_scanning_for_registry_enabled } + end + end + + context 'when the user has permission' do + before do + project.add_maintainer(current_user) + 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 namespace 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({ 'containerScanningForRegistryEnabled' => value_after, 'errors' => [] }) + + expect(security_setting.reload.container_scanning_for_registry_enabled).to eq(value_after) + end + end + + context 'when an invalid value is provided' do + let(:enable) { true } + let(:value_before) { false } + + before do + allow(::Security::Configuration::SetContainerScanningForRegistryService).to receive(:execute).and_return( + ServiceResponse.error(message: 'failed', payload: { enabled: nil }) + ) + end + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + response = graphql_mutation_response(mutation_name) + expect(response).to include({ 'containerScanningForRegistryEnabled' => nil, 'errors' => be_present }) + + expect(security_setting.reload.container_scanning_for_registry_enabled).to eq(false) + end + end + end + end + + context 'with group' do + let(:group) { create(:group) } + let(:mutation) do + graphql_mutation( + mutation_name, + namespace_path: group.full_path, + enable: enable + ) + end + + 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 container scanning for registry' do + expect { post_graphql_mutation(mutation, current_user: current_user) } + .not_to change { security_setting.reload.container_scanning_for_registry_enabled } + end + end + + context 'when the user has permission' do + before do + group.add_maintainer(current_user) + end + + it 'raises ResourceNotAvailable' do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include('Setting only available for Project namespaces.') + end + end + end +end diff --git a/ee/spec/services/security/configuration/set_container_scanning_for_registry_service_spec.rb b/ee/spec/services/security/configuration/set_container_scanning_for_registry_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6caa41909b08ffc9df930209f1a96de0c4d4d5b8 --- /dev/null +++ b/ee/spec/services/security/configuration/set_container_scanning_for_registry_service_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::Configuration::SetContainerScanningForRegistryService, feature_category: :software_composition_analysis do + describe '#execute' do + let_it_be(:security_setting) { create(:project_security_setting, container_scanning_for_registry_enabled: false) } + + context 'when namespace is project' do + let_it_be(:namespace) { security_setting.project } + + it 'returns attribute value' do + expect(described_class.execute(namespace: namespace, + enable: true)).to have_attributes(errors: be_blank, payload: include(enabled: true)) + expect(described_class.execute(namespace: namespace, + enable: false)).to have_attributes(errors: be_blank, payload: include(enabled: false)) + end + + it 'changes the attribute' do + expect { described_class.execute(namespace: namespace, enable: true) } + .to change { security_setting.reload.container_scanning_for_registry_enabled } + .from(false).to(true) + expect { described_class.execute(namespace: namespace, enable: true) } + .not_to change { security_setting.reload.container_scanning_for_registry_enabled } + expect { described_class.execute(namespace: namespace, enable: false) } + .to change { security_setting.reload.container_scanning_for_registry_enabled } + .from(true).to(false) + expect { described_class.execute(namespace: namespace, enable: false) } + .not_to change { security_setting.reload.container_scanning_for_registry_enabled } + end + + context 'when fields are invalid' do + context 'when repository_path_pattern is invalid' do + it 'returns nil and error' do + expect(described_class.execute(namespace: namespace, + enable: nil)).to have_attributes(errors: be_present, payload: include(enabled: nil)) + end + + it 'does not change the attribute' do + expect { described_class.execute(namespace: namespace, enable: nil) } + .not_to change { security_setting.reload.container_scanning_for_registry_enabled } + end + end + end + end + end +end