Commit cfcd84cb authored by Max Woolf's avatar Max Woolf

Add ability to update compliance frameworks via GraphQL

Adds a new UpdateService and updateComplianceFramework
mutation
parent 5559a68e
...@@ -14593,6 +14593,7 @@ type Mutation { ...@@ -14593,6 +14593,7 @@ type Mutation {
updateBoard(input: UpdateBoardInput!): UpdateBoardPayload updateBoard(input: UpdateBoardInput!): UpdateBoardPayload
updateBoardEpicUserPreferences(input: UpdateBoardEpicUserPreferencesInput!): UpdateBoardEpicUserPreferencesPayload updateBoardEpicUserPreferences(input: UpdateBoardEpicUserPreferencesInput!): UpdateBoardEpicUserPreferencesPayload
updateBoardList(input: UpdateBoardListInput!): UpdateBoardListPayload updateBoardList(input: UpdateBoardListInput!): UpdateBoardListPayload
updateComplianceFramework(input: UpdateComplianceFrameworkInput!): UpdateComplianceFrameworkPayload
updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload
updateDevopsAdoptionSegment(input: UpdateDevopsAdoptionSegmentInput!): UpdateDevopsAdoptionSegmentPayload updateDevopsAdoptionSegment(input: UpdateDevopsAdoptionSegmentInput!): UpdateDevopsAdoptionSegmentPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
...@@ -23359,6 +23360,56 @@ type UpdateBoardPayload { ...@@ -23359,6 +23360,56 @@ type UpdateBoardPayload {
errors: [String!]! errors: [String!]!
} }
"""
Autogenerated input type of UpdateComplianceFramework
"""
input UpdateComplianceFrameworkInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
New color representation of the compliance framework in hex format. e.g. #FCA121
"""
color: String
"""
New description for the compliance framework
"""
description: String
"""
The global ID of the compliance framework to update
"""
id: ComplianceManagementFrameworkID!
"""
New name for the compliance framework
"""
name: String
}
"""
Autogenerated return type of UpdateComplianceFramework
"""
type UpdateComplianceFrameworkPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The compliance framework after mutation
"""
complianceFramework: ComplianceFramework
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
""" """
Autogenerated input type of UpdateContainerExpirationPolicy Autogenerated input type of UpdateContainerExpirationPolicy
""" """
......
...@@ -43261,6 +43261,33 @@ ...@@ -43261,6 +43261,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "updateComplianceFramework",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateComplianceFrameworkInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateComplianceFrameworkPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "updateContainerExpirationPolicy", "name": "updateContainerExpirationPolicy",
"description": null, "description": null,
...@@ -68342,6 +68369,138 @@ ...@@ -68342,6 +68369,138 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "UpdateComplianceFrameworkInput",
"description": "Autogenerated input type of UpdateComplianceFramework",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global ID of the compliance framework to update",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ComplianceManagementFrameworkID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "name",
"description": "New name for the compliance framework",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "description",
"description": "New description for the compliance framework",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "color",
"description": "New color representation of the compliance framework in hex format. e.g. #FCA121",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "UpdateComplianceFrameworkPayload",
"description": "Autogenerated return type of UpdateComplianceFramework",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "complianceFramework",
"description": "The compliance framework after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFramework",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "UpdateContainerExpirationPolicyInput", "name": "UpdateContainerExpirationPolicyInput",
...@@ -3554,6 +3554,16 @@ Autogenerated return type of UpdateBoard. ...@@ -3554,6 +3554,16 @@ Autogenerated return type of UpdateBoard.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
### UpdateComplianceFrameworkPayload
Autogenerated return type of UpdateComplianceFramework.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `complianceFramework` | ComplianceFramework | The compliance framework after mutation |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### UpdateContainerExpirationPolicyPayload ### UpdateContainerExpirationPolicyPayload
Autogenerated return type of UpdateContainerExpirationPolicy. Autogenerated return type of UpdateContainerExpirationPolicy.
......
...@@ -11,6 +11,7 @@ module EE ...@@ -11,6 +11,7 @@ module EE
mount_mutation ::Mutations::Clusters::AgentTokens::Create mount_mutation ::Mutations::Clusters::AgentTokens::Create
mount_mutation ::Mutations::Clusters::AgentTokens::Delete mount_mutation ::Mutations::Clusters::AgentTokens::Delete
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Destroy mount_mutation ::Mutations::ComplianceManagement::Frameworks::Destroy
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Update
mount_mutation ::Mutations::Issues::SetIteration mount_mutation ::Mutations::Issues::SetIteration
mount_mutation ::Mutations::Issues::SetWeight mount_mutation ::Mutations::Issues::SetWeight
mount_mutation ::Mutations::Issues::SetEpic mount_mutation ::Mutations::Issues::SetEpic
......
# frozen_string_literal: true
module Mutations
module ComplianceManagement
module Frameworks
class Update < ::Mutations::BaseMutation
graphql_name 'UpdateComplianceFramework'
authorize :manage_compliance_framework
argument :id,
::Types::GlobalIDType[::ComplianceManagement::Framework],
required: true,
description: 'The global ID of the compliance framework to update'
argument :name,
GraphQL::STRING_TYPE,
required: false,
description: 'New name for the compliance framework'
argument :description,
GraphQL::STRING_TYPE,
required: false,
description: 'New description for the compliance framework'
argument :color,
GraphQL::STRING_TYPE,
required: false,
description: 'New color representation of the compliance framework in hex format. e.g. #FCA121'
field :compliance_framework,
Types::ComplianceManagement::ComplianceFrameworkType,
null: true,
description: "The compliance framework after mutation"
def resolve(id:, **args)
framework = authorized_find!(id: id)
::ComplianceManagement::Frameworks::UpdateService.new(framework: framework,
current_user: current_user,
params: args).execute
{ compliance_framework: framework, errors: errors_on_object(framework) }
end
private
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::ComplianceManagement::Framework)
end
end
end
end
end
# frozen_string_literal: true
module ComplianceManagement
module Frameworks
class UpdateService < BaseService
attr_reader :framework, :current_user, :params
def initialize(framework:, current_user:, params:)
@framework = framework
@current_user = current_user
@params = params
end
def execute
return error unless permitted?
framework.update(params) ? success : error
end
def success
ServiceResponse.success(payload: { framework: framework })
end
def error
ServiceResponse.error(message: _('Failed to update framework'), payload: framework.errors )
end
private
def permitted?
can? current_user, :manage_compliance_framework, framework
end
end
end
end
---
title: Add ability to update compliance frameworks via GraphQL
merge_request: 49157
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::ComplianceManagement::Frameworks::Update do
include GraphqlHelpers
let_it_be(:framework) { create(:compliance_framework) }
let(:user) { framework.namespace.owner }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:params) do
{
name: 'New Name',
description: 'New Description',
color: '#AAAAA1'
}
end
subject { mutation.resolve(id: global_id_of(framework), **params) }
context 'feature is enabled and licensed' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
stub_feature_flags(ff_custom_compliance_frameworks: true)
end
context 'parameters are valid' do
it 'returns the new object' do
response = subject[:compliance_framework]
expect(response.name).to eq('New Name')
expect(response.description).to eq('New Description')
expect(response.color).to eq('#AAAAA1')
end
it 'returns no errors' do
expect(subject[:errors]).to be_empty
end
context 'current_user is not authorized to update framework' do
let_it_be(:user) { create(:user) }
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
context 'parameters are invalid' do
let(:params) do
{
name: '',
description: '',
color: 'AAAAA1'
}
end
it 'does not change the framework attributes' do
expect { subject }.not_to change { framework.name }
expect { subject }.not_to change { framework.description }
expect { subject }.not_to change { framework.color }
end
it 'returns validation errors' do
expect(subject[:errors]).to contain_exactly("Name can't be blank", "Description can't be blank", "Color must be a valid color code")
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Update a compliance framework' do
include GraphqlHelpers
let_it_be(:framework) { create(:compliance_framework) }
let(:mutation) { graphql_mutation(:update_compliance_framework, { id: global_id_of(framework), **params }) }
let(:current_user) { framework.namespace.owner }
let(:params) do
{
name: 'New Name',
description: 'New Description',
color: '#AAC112'
}
end
subject { post_graphql_mutation(mutation, current_user: current_user) }
def mutation_response
graphql_mutation_response(:update_compliance_framework)
end
context 'feature is unlicensed' do
before do
stub_licensed_features(custom_compliance_frameworks: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]
end
context 'feature is licensed but disabled' do
before do
stub_licensed_features(custom_compliance_frameworks: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]
end
context 'feature is licensed and enabled' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
stub_feature_flags(ff_custom_compliance_frameworks: true)
end
context 'with valid params' do
it 'returns an empty array of errors' do
subject
expect(mutation_response['errors']).to be_empty
end
it 'returns the updated framework' do
subject
expect(mutation_response['complianceFramework']['name']).to eq 'New Name'
expect(mutation_response['complianceFramework']['description']).to eq 'New Description'
expect(mutation_response['complianceFramework']['color']).to eq '#AAC112'
end
context 'current_user is not permitted to update framework' do
let_it_be(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]
end
end
context 'with invalid params' do
let(:params) do
{
name: '',
description: '',
color: 'NOTACOLOR'
}
end
it 'returns an array of errors' do
subject
expect(mutation_response['errors']).to contain_exactly "Color must be a valid color code", "Description can't be blank", "Name can't be blank"
end
it 'does not update the framework' do
expect { subject }.not_to change { framework.name }
expect { subject }.not_to change { framework.description }
expect { subject }.not_to change { framework.color }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ComplianceManagement::Frameworks::UpdateService do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:framework) { create(:compliance_framework, namespace: namespace) }
let(:current_user) { namespace.owner }
let(:params) { { color: '#000001', description: 'New Description', name: 'New Name' } }
subject { described_class.new(framework: framework, current_user: current_user, params: params) }
shared_examples 'a failed update request' do
it 'does not update the compliance framework' do
expect { subject.execute }.not_to change { framework.name }
expect { subject.execute }.not_to change { framework.description }
expect { subject.execute }.not_to change { framework.color }
end
it 'is unsuccessful' do
expect(subject.execute.success?).to be false
end
end
context 'feature is disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
end
it_behaves_like 'a failed update request'
end
context 'feature is licensed but disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
stub_licensed_features(custom_compliance_frameworks: true)
end
it_behaves_like 'a failed update request'
end
context 'current_user is not the namespace owner' do
let(:current_user) { create(:user) }
it_behaves_like 'a failed update request'
end
context 'when feature is enabled and licensed' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
stub_feature_flags(ff_custom_compliance_frameworks: true)
end
context 'with an invalid param passed' do
let(:params) { { color: '0001', description: '', name: 'New Name' } }
it 'is unsuccessful' do
expect(subject.execute.success?).to be false
end
it 'has appropriate errors' do
expect(subject.execute.payload.full_messages).to contain_exactly 'Color must be a valid color code', "Description can't be blank"
end
end
context 'with valid params passed' do
it 'updates the compliance framework with valid params' do
subject.execute
expect(framework.name).to eq('New Name')
expect(framework.color).to eq('#000001')
expect(framework.description).to eq('New Description')
end
it 'is successful' do
expect(subject.execute.success?).to be true
end
end
end
end
...@@ -11679,6 +11679,9 @@ msgstr "" ...@@ -11679,6 +11679,9 @@ msgstr ""
msgid "Failed to update environment!" msgid "Failed to update environment!"
msgstr "" msgstr ""
msgid "Failed to update framework"
msgstr ""
msgid "Failed to update issue status" msgid "Failed to update issue status"
msgstr "" msgstr ""
......
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