From db146c80a99f60092ae2c92d4283943d4dcd9d31 Mon Sep 17 00:00:00 2001 From: Hitesh Raghuvanshi Date: Mon, 13 Jan 2025 17:03:55 +0530 Subject: [PATCH 1/2] Adding mutation for creating compliance controls Changelog: added EE: true --- doc/api/graphql/reference/index.md | 70 +++++++++++ doc/user/compliance/audit_event_types.md | 1 + ee/app/graphql/ee/types/mutation_type.rb | 2 + .../create.rb | 54 +++++++++ .../compliance_requirement_type.rb | 5 + ...pliance_requirements_control_input_type.rb | 19 +++ .../compliance_requirements_control_type.rb | 30 +++++ .../compliance_requirements_control_policy.rb | 9 ++ .../create_service.rb | 65 +++++++++++ ...created_compliance_requirement_control.yml | 10 ++ .../compliance_requirement_type_spec.rb | 1 + ...ce_requirements_control_input_type_spec.rb | 9 ++ ...mpliance_requirements_control_type_spec.rb | 18 +++ .../create_spec.rb | 99 ++++++++++++++++ .../create_spec.rb | 89 ++++++++++++++ .../create_service_spec.rb | 109 ++++++++++++++++++ locale/gitlab.pot | 6 + 17 files changed, 596 insertions(+) create mode 100644 ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create.rb create mode 100644 ee/app/graphql/types/compliance_management/compliance_requirements_control_input_type.rb create mode 100644 ee/app/graphql/types/compliance_management/compliance_requirements_control_type.rb create mode 100644 ee/app/policies/compliance_management/compliance_framework/compliance_requirements_control_policy.rb create mode 100644 ee/app/services/compliance_management/compliance_framework/compliance_requirements_controls/create_service.rb create mode 100644 ee/config/audit_events/types/created_compliance_requirement_control.yml create mode 100644 ee/spec/graphql/ee/types/compliance_management/compliance_requirements_control_input_type_spec.rb create mode 100644 ee/spec/graphql/ee/types/compliance_management/compliance_requirements_control_type_spec.rb create mode 100644 ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create_spec.rb create mode 100644 ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create_spec.rb create mode 100644 ee/spec/services/compliance_management/compliance_framework/compliance_requirements_controls/create_service_spec.rb diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b1fb1f542e7c07..f6f140dcbcadf7 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3736,6 +3736,30 @@ Input type: `CreateComplianceRequirementInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `requirement` | [`ComplianceRequirement`](#compliancerequirement) | Created compliance requirement. | +### `Mutation.createComplianceRequirementsControl` + +DETAILS: +**Introduced** in GitLab 17.8. +**Status**: Experiment. + +Input type: `CreateComplianceRequirementsControlInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `complianceRequirementId` | [`ComplianceManagementComplianceFrameworkComplianceRequirementID!`](#compliancemanagementcomplianceframeworkcompliancerequirementid) | Global ID of the compliance requirement of the new control. | +| `params` | [`ComplianceRequirementsControlInput!`](#compliancerequirementscontrolinput) | Parameters to create the compliance requirement control. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `requirementsControl` | [`ComplianceRequirementsControl`](#compliancerequirementscontrol) | Created compliance requirements control. | + ### `Mutation.createContainerProtectionRepositoryRule` Creates a repository protection rule to restrict access to a project's container registry. @@ -13368,6 +13392,29 @@ The edge type for [`ComplianceRequirement`](#compliancerequirement). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`ComplianceRequirement`](#compliancerequirement) | The item at the end of the edge. | +#### `ComplianceRequirementsControlConnection` + +The connection type for [`ComplianceRequirementsControl`](#compliancerequirementscontrol). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[ComplianceRequirementsControlEdge]`](#compliancerequirementscontroledge) | A list of edges. | +| `nodes` | [`[ComplianceRequirementsControl]`](#compliancerequirementscontrol) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `ComplianceRequirementsControlEdge` + +The edge type for [`ComplianceRequirementsControl`](#compliancerequirementscontrol). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`ComplianceRequirementsControl`](#compliancerequirementscontrol) | The item at the end of the edge. | + #### `ComplianceStandardsAdherenceConnection` The connection type for [`ComplianceStandardsAdherence`](#compliancestandardsadherence). @@ -21699,6 +21746,7 @@ Represents a ComplianceRequirement associated with a ComplianceFramework. | Name | Type | Description | | ---- | ---- | ----------- | +| `complianceRequirementsControls` | [`ComplianceRequirementsControlConnection`](#compliancerequirementscontrolconnection) | Compliance controls of the compliance requirement. (see [Connections](#connections)) | | `controlExpression` | [`String`](#string) | Control expression of the compliance requirement. | | `description` | [`String!`](#string) | Description of the compliance requirement. | | `id` | [`ID!`](#id) | Compliance requirement ID. | @@ -21715,6 +21763,19 @@ Lists down all the possible types of requirement controls. | ---- | ---- | ----------- | | `controlExpressions` | [`[ControlExpression!]!`](#controlexpression) | List of requirement controls. | +### `ComplianceRequirementsControl` + +Represents a ComplianceRequirementsControl associated with a ComplianceRequirement. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `controlType` | [`String!`](#string) | Type of the compliance control. | +| `expression` | [`String`](#string) | Expression of the compliance control. | +| `id` | [`ID!`](#id) | Compliance requirements control ID. | +| `name` | [`String!`](#string) | Name of the compliance control. | + ### `ComplianceStandardsAdherence` Compliance standards adherence for a project. @@ -44758,6 +44819,15 @@ Attributes for defining a CI/CD variable. | `description` | [`String`](#string) | New description for the compliance requirement. | | `name` | [`String`](#string) | New name for the compliance requirement. | +### `ComplianceRequirementsControlInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `expression` | [`String`](#string) | Expression of the compliance control. | +| `name` | [`String`](#string) | New name for the compliance requirement control. | + ### `ComplianceStandardsAdherenceInput` #### Arguments diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 4feaf5ffe54917..041c86b0c4eeeb 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -148,6 +148,7 @@ Audit event types belong to the following product categories. | [`create_compliance_framework`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74292) | A compliance framework is successfully created | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) | Group | | [`create_status_check`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84624) | An external status check is created | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/355805) | Project | | [`created_compliance_requirement`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169485) | A requirement is added to a compliance framework | **{check-circle}** Yes | GitLab [17.6](https://gitlab.com/gitlab-org/gitlab/-/issues/470695) | Group | +| [`created_compliance_requirement_control`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177557) | A control is added to a compliance requirement. | **{check-circle}** Yes | GitLab [17.8](https://gitlab.com/gitlab-org/gitlab/-/issues/512381) | Group | | [`delete_status_check`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84624) | An external status check is deleted | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/355805) | Project | | [`destroy_compliance_framework`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74292) | A compliance framework is successfully deleted | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) | Group | | [`destroyed_compliance_requirement`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170380) | A compliance framework requirement is destroyed | **{check-circle}** Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/470695) | Group | diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 55f115170559ec..9c2b43177349c1 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -265,6 +265,8 @@ def self.authorization_scopes experiment: { milestone: '17.7' } mount_mutation ::Mutations::Ai::SelfHostedModels::ConnectionCheck, experiment: { milestone: '17.7' } + mount_mutation ::Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::Create, + experiment: { milestone: '17.8' } prepend(Types::DeprecatedMutations) end diff --git a/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create.rb b/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create.rb new file mode 100644 index 00000000000000..8b15f9ed18c689 --- /dev/null +++ b/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Mutations + module ComplianceManagement + module ComplianceFramework + module ComplianceRequirementsControls + class Create < BaseMutation + graphql_name 'CreateComplianceRequirementsControl' + + authorize :admin_compliance_framework + + field :requirements_control, + Types::ComplianceManagement::ComplianceRequirementsControlType, + null: true, + description: 'Created compliance requirements control.' + + argument :compliance_requirement_id, + ::Types::GlobalIDType[::ComplianceManagement::ComplianceFramework::ComplianceRequirement], + required: true, + description: 'Global ID of the compliance requirement of the new control.' + + argument :params, Types::ComplianceManagement::ComplianceRequirementsControlInputType, + required: true, + description: 'Parameters to create the compliance requirement control.' + + def resolve(args) + requirement = authorized_find!(id: args[:compliance_requirement_id]) + + service = ::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::CreateService.new( + requirement: requirement, + params: args[:params].to_h, + current_user: current_user + ).execute + + service.success? ? success(service) : error(service) + end + + private + + def success(service) + { requirements_control: service.payload[:control], errors: [] } + end + + def error(service) + errors = [service.message] + model_errors = service.payload.try(:full_messages).to_a + + { errors: (errors + model_errors).flatten } + end + end + end + end + end +end diff --git a/ee/app/graphql/types/compliance_management/compliance_requirement_type.rb b/ee/app/graphql/types/compliance_management/compliance_requirement_type.rb index b1ec0f358a1432..d86f2c5b62ea77 100644 --- a/ee/app/graphql/types/compliance_management/compliance_requirement_type.rb +++ b/ee/app/graphql/types/compliance_management/compliance_requirement_type.rb @@ -27,6 +27,11 @@ class ComplianceRequirementType < Types::BaseObject field :requirement_type, GraphQL::Types::String, null: false, description: 'Type of the compliance requirement.' + + field :compliance_requirements_controls, + ::Types::ComplianceManagement::ComplianceRequirementsControlType.connection_type, + null: true, + description: 'Compliance controls of the compliance requirement.' end end end diff --git a/ee/app/graphql/types/compliance_management/compliance_requirements_control_input_type.rb b/ee/app/graphql/types/compliance_management/compliance_requirements_control_input_type.rb new file mode 100644 index 00000000000000..5628409e9274b4 --- /dev/null +++ b/ee/app/graphql/types/compliance_management/compliance_requirements_control_input_type.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Types + module ComplianceManagement + class ComplianceRequirementsControlInputType < BaseInputObject + graphql_name 'ComplianceRequirementsControlInput' + + argument :name, + GraphQL::Types::String, + required: false, + description: 'New name for the compliance requirement control.' + + argument :expression, + GraphQL::Types::String, + required: false, + description: 'Expression of the compliance control.' + end + end +end diff --git a/ee/app/graphql/types/compliance_management/compliance_requirements_control_type.rb b/ee/app/graphql/types/compliance_management/compliance_requirements_control_type.rb new file mode 100644 index 00000000000000..50dda62457f475 --- /dev/null +++ b/ee/app/graphql/types/compliance_management/compliance_requirements_control_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# rubocop: disable Graphql/AuthorizeTypes -- because ComplianceRequirementsControlType is, and should only be, accessible via ComplianceRequirementType + +module Types + module ComplianceManagement + class ComplianceRequirementsControlType < Types::BaseObject + graphql_name 'ComplianceRequirementsControl' + description 'Represents a ComplianceRequirementsControl associated with a ComplianceRequirement' + + field :id, GraphQL::Types::ID, + null: false, + description: 'Compliance requirements control ID.' + + field :name, GraphQL::Types::String, + null: false, + description: 'Name of the compliance control.' + + field :expression, GraphQL::Types::String, + null: true, + description: 'Expression of the compliance control.' + + field :control_type, GraphQL::Types::String, + null: false, + description: 'Type of the compliance control.' + end + end +end + +# rubocop: enable Graphql/AuthorizeTypes diff --git a/ee/app/policies/compliance_management/compliance_framework/compliance_requirements_control_policy.rb b/ee/app/policies/compliance_management/compliance_framework/compliance_requirements_control_policy.rb new file mode 100644 index 00000000000000..96be3ca8ea65f5 --- /dev/null +++ b/ee/app/policies/compliance_management/compliance_framework/compliance_requirements_control_policy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module ComplianceManagement + module ComplianceFramework + class ComplianceRequirementsControlPolicy < BasePolicy + delegate { @subject.compliance_requirement.framework } + end + end +end diff --git a/ee/app/services/compliance_management/compliance_framework/compliance_requirements_controls/create_service.rb b/ee/app/services/compliance_management/compliance_framework/compliance_requirements_controls/create_service.rb new file mode 100644 index 00000000000000..9d57268ccfb54d --- /dev/null +++ b/ee/app/services/compliance_management/compliance_framework/compliance_requirements_controls/create_service.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module ComplianceManagement + module ComplianceFramework + module ComplianceRequirementsControls + class CreateService < BaseService + attr_reader :params, :current_user, :requirement, :control + + def initialize(requirement:, params:, current_user:) + @requirement = requirement + @params = params + @current_user = current_user + @control = ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl.new + end + + def execute + control.assign_attributes( + compliance_requirement: requirement, + namespace_id: requirement.namespace.id, + name: params[:name], + expression: params[:expression], + control_type: 'internal' + ) + + return ServiceResponse.error(message: _('Not permitted to create compliance control')) unless permitted? + + return error unless control.save + + audit_create + success + end + + private + + def permitted? + can?(current_user, :admin_compliance_framework, requirement.framework) + end + + def success + ServiceResponse.success(payload: { control: control }) + end + + def audit_create + audit_context = { + name: 'created_compliance_requirement_control', + author: current_user, + scope: requirement.namespace, + target: control, + message: "Created compliance control #{control.name} for requirement #{requirement.name}", + additional_details: { + framework: requirement.framework.name, + control_type: control.control_type + } + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end + + def error + ServiceResponse.error(message: _('Failed to create compliance requirement control'), payload: control.errors) + end + end + end + end +end diff --git a/ee/config/audit_events/types/created_compliance_requirement_control.yml b/ee/config/audit_events/types/created_compliance_requirement_control.yml new file mode 100644 index 00000000000000..8cf52d9c90eed6 --- /dev/null +++ b/ee/config/audit_events/types/created_compliance_requirement_control.yml @@ -0,0 +1,10 @@ +--- +name: created_compliance_requirement_control +description: A control is added to a compliance requirement. +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/512381 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177557 +milestone: '17.8' +feature_category: compliance_management +saved_to_database: true +streamed: true +scope: [Group] diff --git a/ee/spec/graphql/ee/types/compliance_management/compliance_requirement_type_spec.rb b/ee/spec/graphql/ee/types/compliance_management/compliance_requirement_type_spec.rb index 849eb582c4d6a3..b8f15802a46dca 100644 --- a/ee/spec/graphql/ee/types/compliance_management/compliance_requirement_type_spec.rb +++ b/ee/spec/graphql/ee/types/compliance_management/compliance_requirement_type_spec.rb @@ -11,6 +11,7 @@ description controlExpression requirementType + complianceRequirementsControls ] it 'has the correct fields' do diff --git a/ee/spec/graphql/ee/types/compliance_management/compliance_requirements_control_input_type_spec.rb b/ee/spec/graphql/ee/types/compliance_management/compliance_requirements_control_input_type_spec.rb new file mode 100644 index 00000000000000..589775e0c872b7 --- /dev/null +++ b/ee/spec/graphql/ee/types/compliance_management/compliance_requirements_control_input_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Types::ComplianceManagement::ComplianceRequirementsControlInputType, feature_category: :compliance_management do + it { expect(described_class.graphql_name).to eq('ComplianceRequirementsControlInput') } + + it { expect(described_class.arguments.keys).to match_array(%w[name expression]) } +end diff --git a/ee/spec/graphql/ee/types/compliance_management/compliance_requirements_control_type_spec.rb b/ee/spec/graphql/ee/types/compliance_management/compliance_requirements_control_type_spec.rb new file mode 100644 index 00000000000000..b03463cf76dec9 --- /dev/null +++ b/ee/spec/graphql/ee/types/compliance_management/compliance_requirements_control_type_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['ComplianceRequirementsControl'], feature_category: :compliance_management do + subject { described_class } + + fields = %w[ + id + name + expression + control_type + ] + + it 'has the correct fields' do + is_expected.to have_graphql_fields(fields) + end +end diff --git a/ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create_spec.rb b/ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create_spec.rb new file mode 100644 index 00000000000000..a68f36c0b68768 --- /dev/null +++ b/ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::Create, + feature_category: :compliance_management do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:namespace) { create(:group) } + let_it_be(:requirement) do + create(:compliance_requirement, framework: create(:compliance_framework, namespace: namespace)) + end + + let(:params) { valid_params } + let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) } + + subject(:mutate) { mutation.resolve(**params) } + + describe '#resolve' do + shared_examples 'resource not available' do + it 'raises error' do + expect { mutate }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + before do + stub_licensed_features(custom_compliance_frameworks: true) + end + + context 'when current_user is not group namespace owner' do + it_behaves_like 'resource not available' + end + + context 'when current_user is group owner' do + before_all do + namespace.add_owner(current_user) + end + + context 'when all arguments are valid' do + it 'creates a new compliance requirement control' do + expect { mutate }.to change { requirement.compliance_requirements_controls.count }.by 1 + end + end + + context 'when control parameters are invalid' do + subject(:mutate) { mutation.resolve(**invalid_name_params) } + + it 'does not create a new compliance control' do + expect { mutate }.not_to change { requirement.compliance_requirements_controls.count } + end + + it 'returns error for name' do + expect(mutate[:errors]).to include "Name can't be blank" + end + end + end + + context 'when current_user is personal namespace owner' do + let_it_be(:namespace) { create(:user_namespace) } + + let(:current_user) { namespace.owner } + + context 'when control parameters are valid' do + it_behaves_like 'resource not available' + end + end + end + + private + + def valid_params + { + compliance_requirement_id: requirement.to_gid, + params: { + name: 'minimum_approvals_required_2', + expression: control_expression + } + } + end + + def invalid_name_params + { + compliance_requirement_id: requirement.to_gid, + params: { + name: '', + expression: control_expression + } + } + end + + def control_expression + { + operator: "=", + field: "minimum_approvals_required", + value: 2 + }.to_json + end +end diff --git a/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create_spec.rb b/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create_spec.rb new file mode 100644 index 00000000000000..402c14243b6076 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/create_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Create a Compliance Requirement Control', feature_category: :compliance_management do + include GraphqlHelpers + + let_it_be(:namespace) { create(:group) } + let_it_be(:current_user) { create(:user) } + let_it_be(:requirement) do + create(:compliance_requirement, framework: create(:compliance_framework, namespace: namespace)) + end + + let(:mutation) do + graphql_mutation( + :create_compliance_requirements_control, + compliance_requirement_id: requirement.to_gid, + params: { + name: 'minimum_approvals_required_2', + expression: control_expression + } + ) + end + + subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:create_compliance_requirements_control) + end + + shared_examples 'a mutation that creates a compliance requirement control' do + it 'creates a new compliance requirement control' do + expect { mutate }.to change { requirement.compliance_requirements_controls.count }.by 1 + end + + it 'returns the newly created requirement control', :aggregate_failures do + mutate + + expect(mutation_response['requirementsControl']['name']).to eq 'minimum_approvals_required_2' + expect(mutation_response['requirementsControl']['expression']).to eq control_expression + expect(mutation_response['requirementsControl']['controlType']).to eq 'internal' + end + end + + context 'when framework feature is unlicensed' do + before do + stub_licensed_features(custom_compliance_frameworks: false) + post_graphql_mutation(mutation, current_user: current_user) + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when feature is licensed' do + before do + stub_licensed_features(custom_compliance_frameworks: true, evaluate_group_level_compliance_pipeline: true) + end + + context 'when current_user is group owner' do + before_all do + namespace.add_owner(current_user) + end + + it_behaves_like 'a mutation that creates a compliance requirement control' + end + + context 'when current_user is not a group owner' do + context 'when current_user is group owner' do + before_all do + namespace.add_maintainer(current_user) + end + + it 'does not create a new compliance requirement control' do + expect { mutate }.not_to change { requirement.compliance_requirements_controls.count } + end + + it_behaves_like 'a mutation that returns a top-level access error' + end + end + end + + def control_expression + { + operator: "=", + field: "minimum_approvals_required", + value: 2 + }.to_json + end +end diff --git a/ee/spec/services/compliance_management/compliance_framework/compliance_requirements_controls/create_service_spec.rb b/ee/spec/services/compliance_management/compliance_framework/compliance_requirements_controls/create_service_spec.rb new file mode 100644 index 00000000000000..ffe0775994297b --- /dev/null +++ b/ee/spec/services/compliance_management/compliance_framework/compliance_requirements_controls/create_service_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::CreateService, + feature_category: :compliance_management do + let_it_be_with_refind(:namespace) { create(:group) } + let_it_be(:current_user) { create(:user, owner_of: namespace) } + let_it_be(:requirement) do + create(:compliance_requirement, framework: create(:compliance_framework, namespace: namespace)) + end + + let(:params) do + { + name: 'minimum_approvals_required_2', + expression: control_expression + } + end + + context 'when custom_compliance_frameworks is disabled' do + before do + stub_licensed_features(custom_compliance_frameworks: false) + end + + subject(:control_creator) do + described_class.new(requirement: requirement, params: params, current_user: current_user) + end + + it 'does not create a new compliance control' do + expect { control_creator.execute }.not_to change { requirement.compliance_requirements_controls.count } + end + + it 'responds with an error message' do + expect(control_creator.execute.message).to eq('Not permitted to create compliance control') + end + end + + context 'when custom_compliance_frameworks is enabled' do + before do + stub_licensed_features(custom_compliance_frameworks: true) + end + + context 'when using invalid parameters' do + subject(:control_creator) do + described_class.new(requirement: requirement, params: params.except(:name), current_user: current_user) + end + + let(:response) { control_creator.execute } + + it 'responds with an error service response' do + expect(response.success?).to be_falsey + expect(response.payload.messages[:name]).to contain_exactly "can't be blank" + end + end + + context 'when creating a compliance control for a namespace that current_user is not the owner of' do + subject(:control_creator) do + described_class.new(requirement: requirement, params: params, current_user: create(:user)) + end + + it 'responds with an error service response' do + expect(control_creator.execute.success?).to be false + end + + it 'does not create a new compliance control' do + expect { control_creator.execute }.not_to change { requirement.compliance_requirements_controls.count } + end + end + + context 'when using parameters for a valid compliance control' do + subject(:control_creator) do + described_class.new(requirement: requirement, params: params, current_user: current_user) + end + + it 'audits the changes' do + expect { control_creator.execute } + .to change { AuditEvent.where("details LIKE ?", "%created_compliance_requirement_control%").count }.by(1) + end + + it 'creates a new compliance control' do + expect { control_creator.execute }.to change { requirement.compliance_requirements_controls.count }.by(1) + end + + it 'responds with a successful service response' do + expect(control_creator.execute.success?).to be true + end + + it 'has the expected attributes' do + control = control_creator.execute.payload[:control] + + expect(control.attributes).to include( + "name" => "minimum_approvals_required_2", + "compliance_requirement_id" => requirement.id, + "namespace_id" => namespace.id, + "expression" => control_expression, + "control_type" => "internal" + ) + end + end + end + + def control_expression + { + operator: "=", + field: "minimum_approvals_required", + value: 2 + }.to_json + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c051346b11ef85..72ec454eda8da7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23802,6 +23802,9 @@ msgstr "" msgid "Failed to create compliance requirement" msgstr "" +msgid "Failed to create compliance requirement control" +msgstr "" + msgid "Failed to create framework" msgstr "" @@ -37820,6 +37823,9 @@ msgstr "" msgid "Not found." msgstr "" +msgid "Not permitted to create compliance control" +msgstr "" + msgid "Not permitted to destroy framework" msgstr "" -- GitLab From a586a745bb502c9bae16cff9e6622da26d0933f4 Mon Sep 17 00:00:00 2001 From: Hitesh Raghuvanshi Date: Wed, 15 Jan 2025 10:20:50 +0530 Subject: [PATCH 2/2] Updated milestone version --- doc/api/graphql/reference/index.md | 2 +- doc/user/compliance/audit_event_types.md | 2 +- ee/app/graphql/ee/types/mutation_type.rb | 2 +- .../types/created_compliance_requirement_control.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index f6f140dcbcadf7..dafdb521da120e 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3739,7 +3739,7 @@ Input type: `CreateComplianceRequirementInput` ### `Mutation.createComplianceRequirementsControl` DETAILS: -**Introduced** in GitLab 17.8. +**Introduced** in GitLab 17.9. **Status**: Experiment. Input type: `CreateComplianceRequirementsControlInput` diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 041c86b0c4eeeb..b33cf8caa24a1e 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -148,7 +148,7 @@ Audit event types belong to the following product categories. | [`create_compliance_framework`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74292) | A compliance framework is successfully created | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) | Group | | [`create_status_check`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84624) | An external status check is created | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/355805) | Project | | [`created_compliance_requirement`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169485) | A requirement is added to a compliance framework | **{check-circle}** Yes | GitLab [17.6](https://gitlab.com/gitlab-org/gitlab/-/issues/470695) | Group | -| [`created_compliance_requirement_control`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177557) | A control is added to a compliance requirement. | **{check-circle}** Yes | GitLab [17.8](https://gitlab.com/gitlab-org/gitlab/-/issues/512381) | Group | +| [`created_compliance_requirement_control`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177557) | A control is added to a compliance requirement. | **{check-circle}** Yes | GitLab [17.9](https://gitlab.com/gitlab-org/gitlab/-/issues/512381) | Group | | [`delete_status_check`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84624) | An external status check is deleted | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/355805) | Project | | [`destroy_compliance_framework`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74292) | A compliance framework is successfully deleted | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) | Group | | [`destroyed_compliance_requirement`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170380) | A compliance framework requirement is destroyed | **{check-circle}** Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/470695) | Group | diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 9c2b43177349c1..0b860a589b086a 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -266,7 +266,7 @@ def self.authorization_scopes mount_mutation ::Mutations::Ai::SelfHostedModels::ConnectionCheck, experiment: { milestone: '17.7' } mount_mutation ::Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::Create, - experiment: { milestone: '17.8' } + experiment: { milestone: '17.9' } prepend(Types::DeprecatedMutations) end diff --git a/ee/config/audit_events/types/created_compliance_requirement_control.yml b/ee/config/audit_events/types/created_compliance_requirement_control.yml index 8cf52d9c90eed6..74104c35ae3a6d 100644 --- a/ee/config/audit_events/types/created_compliance_requirement_control.yml +++ b/ee/config/audit_events/types/created_compliance_requirement_control.yml @@ -3,7 +3,7 @@ name: created_compliance_requirement_control description: A control is added to a compliance requirement. introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/512381 introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177557 -milestone: '17.8' +milestone: '17.9' feature_category: compliance_management saved_to_database: true streamed: true -- GitLab