Commit 07da4f56 authored by Max Woolf's avatar Max Woolf

Add mutation to create compliance frameworks

Adds a createComplianceFramework GraphQL mutation
to create new compliance frameworks to a namespace
that the current user is an owner of.
parent 054dbb3b
...@@ -4111,6 +4111,56 @@ type CreateClusterAgentPayload { ...@@ -4111,6 +4111,56 @@ type CreateClusterAgentPayload {
errors: [String!]! errors: [String!]!
} }
"""
Autogenerated input type of CreateComplianceFramework
"""
input CreateComplianceFrameworkInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Color to represent the compliance framework as a hexadecimal value. e.g. #ABC123.
"""
color: String!
"""
Description of the compliance framework.
"""
description: String!
"""
Name of the compliance framework.
"""
name: String!
"""
Full path of the namespace to add the compliance framework to.
"""
namespacePath: ID!
}
"""
Autogenerated return type of CreateComplianceFramework
"""
type CreateComplianceFrameworkPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The created compliance framework.
"""
framework: ComplianceFramework
}
""" """
Autogenerated input type of CreateCustomEmoji Autogenerated input type of CreateCustomEmoji
""" """
...@@ -14535,6 +14585,7 @@ type Mutation { ...@@ -14535,6 +14585,7 @@ type Mutation {
createBoard(input: CreateBoardInput!): CreateBoardPayload createBoard(input: CreateBoardInput!): CreateBoardPayload
createBranch(input: CreateBranchInput!): CreateBranchPayload createBranch(input: CreateBranchInput!): CreateBranchPayload
createClusterAgent(input: CreateClusterAgentInput!): CreateClusterAgentPayload createClusterAgent(input: CreateClusterAgentInput!): CreateClusterAgentPayload
createComplianceFramework(input: CreateComplianceFrameworkInput!): CreateComplianceFrameworkPayload
""" """
. Available only when feature flag `custom_emoji` is enabled . Available only when feature flag `custom_emoji` is enabled
......
...@@ -11300,6 +11300,150 @@ ...@@ -11300,6 +11300,150 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "CreateComplianceFrameworkInput",
"description": "Autogenerated input type of CreateComplianceFramework",
"fields": null,
"inputFields": [
{
"name": "namespacePath",
"description": "Full path of the namespace to add the compliance framework to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "name",
"description": "Name of the compliance framework.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "description",
"description": "Description of the compliance framework.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "color",
"description": "Color to represent the compliance framework as a hexadecimal value. e.g. #ABC123.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"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": "CreateComplianceFrameworkPayload",
"description": "Autogenerated return type of CreateComplianceFramework",
"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": "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
},
{
"name": "framework",
"description": "The created compliance framework.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFramework",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "CreateCustomEmojiInput", "name": "CreateCustomEmojiInput",
...@@ -40776,6 +40920,33 @@ ...@@ -40776,6 +40920,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "createComplianceFramework",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "CreateComplianceFrameworkInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "CreateComplianceFrameworkPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "createCustomEmoji", "name": "createCustomEmoji",
"description": ". Available only when feature flag `custom_emoji` is enabled", "description": ". Available only when feature flag `custom_emoji` is enabled",
...@@ -692,6 +692,16 @@ Autogenerated return type of CreateClusterAgent. ...@@ -692,6 +692,16 @@ Autogenerated return type of CreateClusterAgent.
| `clusterAgent` | ClusterAgent | Cluster agent created after mutation | | `clusterAgent` | ClusterAgent | Cluster agent created after mutation |
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
### CreateComplianceFrameworkPayload
Autogenerated return type of CreateComplianceFramework.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `framework` | ComplianceFramework | The created compliance framework. |
### CreateCustomEmojiPayload ### CreateCustomEmojiPayload
Autogenerated return type of CreateCustomEmoji. Autogenerated return type of CreateCustomEmoji.
......
...@@ -12,6 +12,7 @@ module EE ...@@ -12,6 +12,7 @@ module EE
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::ComplianceManagement::Frameworks::Update
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Create
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 Create < BaseMutation
graphql_name 'CreateComplianceFramework'
field :framework,
Types::ComplianceManagement::ComplianceFrameworkType,
null: true,
description: 'The created compliance framework.'
argument :namespace_path, GraphQL::ID_TYPE,
required: true,
description: 'Full path of the namespace to add the compliance framework to.'
argument :name, GraphQL::STRING_TYPE,
required: true,
description: 'Name of the compliance framework.'
argument :description, GraphQL::STRING_TYPE,
required: true,
description: 'Description of the compliance framework.'
argument :color, GraphQL::STRING_TYPE,
required: true,
description: 'Color to represent the compliance framework as a hexadecimal value. e.g. #ABC123.'
def resolve(**args)
service = ::ComplianceManagement::Frameworks::CreateService.new(namespace: namespace(args[:namespace_path]),
params: args,
current_user: current_user).execute
service.success? ? success(service) : error(service)
end
private
def success(service)
{ framework: service.payload[:framework], errors: [] }
end
def error(service)
errors = [service.message]
model_errors = service.payload.try(:full_messages).to_a
{ errors: (errors + model_errors).flatten }
end
def namespace(namespace_path)
::Gitlab::Graphql::Loaders::FullPathModelLoader.new(Namespace, namespace_path).find.sync
end
end
end
end
end
...@@ -5,7 +5,6 @@ module EE ...@@ -5,7 +5,6 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
condition(:custom_compliance_frameworks_enabled) { License.feature_available?(:custom_compliance_frameworks) }
condition(:over_storage_limit, scope: :subject) { @subject.over_storage_limit? } condition(:over_storage_limit, scope: :subject) { @subject.over_storage_limit? }
rule { admin & is_gitlab_com }.enable :update_subscription_limit rule { admin & is_gitlab_com }.enable :update_subscription_limit
...@@ -13,10 +12,6 @@ module EE ...@@ -13,10 +12,6 @@ module EE
rule { over_storage_limit }.policy do rule { over_storage_limit }.policy do
prevent :create_projects prevent :create_projects
end end
rule { (owner | admin) & custom_compliance_frameworks_enabled }.policy do
enable :create_custom_compliance_frameworks
end
end end
end end
end end
...@@ -6,15 +6,13 @@ module ComplianceManagement ...@@ -6,15 +6,13 @@ module ComplianceManagement
attr_reader :namespace, :params, :current_user, :framework attr_reader :namespace, :params, :current_user, :framework
def initialize(namespace:, params:, current_user:) def initialize(namespace:, params:, current_user:)
@namespace = namespace.root_ancestor @namespace = namespace&.root_ancestor
@params = params @params = params
@current_user = current_user @current_user = current_user
@framework = ComplianceManagement::Framework.new @framework = ComplianceManagement::Framework.new
end end
def execute def execute
return ServiceResponse.error(message: _('Feature not available')) unless permitted?
framework.assign_attributes( framework.assign_attributes(
namespace: namespace, namespace: namespace,
name: params[:name], name: params[:name],
...@@ -22,13 +20,15 @@ module ComplianceManagement ...@@ -22,13 +20,15 @@ module ComplianceManagement
color: params[:color] color: params[:color]
) )
return ServiceResponse.error(message: 'Not permitted to create framework') unless permitted?
framework.save ? success : error framework.save ? success : error
end end
private private
def permitted? def permitted?
can?(current_user, :create_custom_compliance_frameworks, namespace) can? current_user, :manage_compliance_framework, framework
end end
def success def success
......
---
title: Add compliance framework creation mutation
merge_request: 48250
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::ComplianceManagement::Frameworks::Create do
let_it_be(:current_user) { create(:user) }
let_it_be(:namespace) { create(:namespace) }
let(:params) { valid_params }
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
subject { mutation.resolve(params) }
describe '#resolve' do
context 'feature is unlicensed' do
before do
stub_licensed_features(custom_compliance_frameworks: false)
end
it 'does not create a new compliance framework' do
expect { subject }.not_to change { namespace.compliance_management_frameworks.count }
end
it 'returns useful error messages' do
expect(subject[:errors]).to include('Not permitted to create framework')
end
end
context 'feature is licensed' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
end
context 'feature flag is disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
end
it 'does not create a new compliance framework' do
expect { subject }.not_to change { namespace.compliance_management_frameworks.count }
end
it 'returns useful error messages' do
expect(subject[:errors]).to include 'Not permitted to create framework'
end
end
context 'current_user is not namespace owner' do
it 'does not create a new compliance framework' do
expect { subject }.not_to change { namespace.compliance_management_frameworks.count }
end
it 'returns useful error messages' do
expect(subject[:errors]).to include 'Not permitted to create framework'
end
end
context 'current_user is group owner' do
let_it_be(:namespace) { create(:group) }
before do
namespace.add_owner(current_user)
end
it 'creates a new compliance framework' do
expect { subject }.to change { namespace.compliance_management_frameworks.count }.by 1
end
end
context 'current_user is namespace owner' do
let(:current_user) { namespace.owner }
context 'framework parameters are valid' do
it 'creates a new compliance framework' do
expect { subject }.to change { namespace.compliance_management_frameworks.count }.by 1
end
end
context 'namespace does not exist' do
let(:params) { valid_params.merge(namespace_path: 'not_a_path') }
it 'returns useful error messages' do
expect(subject[:errors]).to include 'Not permitted to create framework'
end
end
context 'framework parameters are invalid' do
let(:params) { valid_params.merge(color: 'notacolor') }
it 'does not create a new compliance framework' do
expect { subject }.not_to change { namespace.compliance_management_frameworks.count }
end
it 'returns useful error messages' do
expect(subject[:errors]).to include 'Color must be a valid color code'
end
end
end
end
end
private
def valid_params
{
namespace_path: namespace.full_path,
name: 'GDPR',
description: 'Example description',
color: '#abc123'
}
end
end
...@@ -23,40 +23,6 @@ RSpec.describe NamespacePolicy do ...@@ -23,40 +23,6 @@ RSpec.describe NamespacePolicy do
end end
end end
context 'custom_compliance_frameworks_enabled' do
let(:current_user) { owner }
context 'is licensed' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
end
context 'current_user is namespace owner' do
it { is_expected.to be_allowed(:create_custom_compliance_frameworks) }
end
context 'current_user is not namespace owner' do
let(:current_user) { build_stubbed(:user) }
it { is_expected.to be_disallowed(:create_custom_compliance_frameworks) }
end
context 'current_user is administrator', :enable_admin_mode do
let(:current_user) { build_stubbed(:admin) }
it { is_expected.to be_allowed(:create_custom_compliance_frameworks) }
end
end
context 'not licensed' do
before do
stub_licensed_features(custom_compliance_frameworks: false)
end
it { is_expected.to be_disallowed(:create_custom_compliance_frameworks) }
end
end
context ':over_storage_limit' do context ':over_storage_limit' do
let(:current_user) { owner } let(:current_user) { owner }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Create a Compliance Framework' do
include GraphqlHelpers
let_it_be(:namespace) { create(:namespace) }
let_it_be(:current_user) { namespace.owner }
let(:mutation) do
graphql_mutation(
:create_compliance_framework,
namespace_path: namespace.full_path,
name: 'GDPR',
description: 'Example Description',
color: '#ABC123'
)
end
subject { post_graphql_mutation(mutation, current_user: current_user) }
def mutation_response
graphql_mutation_response(:create_compliance_framework)
end
shared_examples 'a mutation that creates a compliance framework' do
it 'creates a new compliance framework' do
expect { subject }.to change { namespace.compliance_management_frameworks.count }.by 1
end
it 'returns the newly created framework' do
subject
expect(mutation_response['framework']['color']).to eq '#ABC123'
expect(mutation_response['framework']['name']).to eq 'GDPR'
expect(mutation_response['framework']['description']).to eq 'Example Description'
end
end
context '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 errors in the response', errors: ['Not permitted to create framework']
end
context 'feature is licensed' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
end
context 'feature is disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
end
it_behaves_like 'a mutation that returns errors in the response', errors: ['Not permitted to create framework']
end
context 'current_user is namespace owner' do
it_behaves_like 'a mutation that creates a compliance framework'
end
context 'current_user is group owner' do
let_it_be(:namespace) { create(:group) }
let_it_be(:current_user) { create(:user) }
before do
namespace.add_owner(current_user)
end
it_behaves_like 'a mutation that creates a compliance framework'
end
context 'current_user is not namespace owner' do
let_it_be(:current_user) { create(:user) }
it 'does not create a new compliance framework' do
expect { subject }.not_to change { namespace.compliance_management_frameworks.count }
end
it_behaves_like 'a mutation that returns errors in the response', errors: ['Not permitted to create framework']
end
end
end
...@@ -24,7 +24,7 @@ RSpec.describe ComplianceManagement::Frameworks::CreateService do ...@@ -24,7 +24,7 @@ RSpec.describe ComplianceManagement::Frameworks::CreateService do
end end
it 'responds with an error message' do it 'responds with an error message' do
expect(subject.execute.message).to eq('Feature not available') expect(subject.execute.message).to eq('Not permitted to create framework')
end end
end end
......
...@@ -11784,9 +11784,6 @@ msgstr "" ...@@ -11784,9 +11784,6 @@ msgstr ""
msgid "Feature flag was successfully removed." msgid "Feature flag was successfully removed."
msgstr "" msgstr ""
msgid "Feature not available"
msgstr ""
msgid "FeatureFlags|%d user" msgid "FeatureFlags|%d user"
msgid_plural "FeatureFlags|%d users" msgid_plural "FeatureFlags|%d users"
msgstr[0] "" msgstr[0] ""
......
...@@ -6,7 +6,7 @@ module RuboCop ...@@ -6,7 +6,7 @@ module RuboCop
class IDType < RuboCop::Cop::Cop class IDType < RuboCop::Cop::Cop
MSG = 'Do not use GraphQL::ID_TYPE, use a specific GlobalIDType instead' MSG = 'Do not use GraphQL::ID_TYPE, use a specific GlobalIDType instead'
WHITELISTED_ARGUMENTS = %i[iid full_path project_path group_path target_project_path].freeze WHITELISTED_ARGUMENTS = %i[iid full_path project_path group_path target_project_path namespace_path].freeze
def_node_search :graphql_id_type?, <<~PATTERN def_node_search :graphql_id_type?, <<~PATTERN
(send nil? :argument (_ #does_not_match?) (const (const nil? :GraphQL) :ID_TYPE) ...) (send nil? :argument (_ #does_not_match?) (const (const nil? :GraphQL) :ID_TYPE) ...)
......
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