Commit 91b925fc authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '255340-graphql-create-compliance-framework' into 'master'

Add ability to create compliance frameworks via GraphQL

See merge request gitlab-org/gitlab!48250
parents 1130ebc5 07da4f56
......@@ -4121,6 +4121,56 @@ type CreateClusterAgentPayload {
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
"""
......@@ -14545,6 +14595,7 @@ type Mutation {
createBoard(input: CreateBoardInput!): CreateBoardPayload
createBranch(input: CreateBranchInput!): CreateBranchPayload
createClusterAgent(input: CreateClusterAgentInput!): CreateClusterAgentPayload
createComplianceFramework(input: CreateComplianceFrameworkInput!): CreateComplianceFrameworkPayload
"""
Available only when feature flag `custom_emoji` is enabled.
......
......@@ -11320,6 +11320,150 @@
"enumValues": 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",
"name": "CreateCustomEmojiInput",
......@@ -40796,6 +40940,33 @@
"isDeprecated": false,
"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",
"description": " Available only when feature flag `custom_emoji` is enabled.",
......@@ -693,6 +693,16 @@ Autogenerated return type of CreateClusterAgent.
| `clusterAgent` | ClusterAgent | Cluster agent created after 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
Autogenerated return type of CreateCustomEmoji.
......
......@@ -12,6 +12,7 @@ module EE
mount_mutation ::Mutations::Clusters::AgentTokens::Delete
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Destroy
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Update
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Create
mount_mutation ::Mutations::Issues::SetIteration
mount_mutation ::Mutations::Issues::SetWeight
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
extend ActiveSupport::Concern
prepended do
condition(:custom_compliance_frameworks_enabled) { License.feature_available?(:custom_compliance_frameworks) }
condition(:over_storage_limit, scope: :subject) { @subject.over_storage_limit? }
rule { admin & is_gitlab_com }.enable :update_subscription_limit
......@@ -13,10 +12,6 @@ module EE
rule { over_storage_limit }.policy do
prevent :create_projects
end
rule { (owner | admin) & custom_compliance_frameworks_enabled }.policy do
enable :create_custom_compliance_frameworks
end
end
end
end
......@@ -6,15 +6,13 @@ module ComplianceManagement
attr_reader :namespace, :params, :current_user, :framework
def initialize(namespace:, params:, current_user:)
@namespace = namespace.root_ancestor
@namespace = namespace&.root_ancestor
@params = params
@current_user = current_user
@framework = ComplianceManagement::Framework.new
end
def execute
return ServiceResponse.error(message: _('Feature not available')) unless permitted?
framework.assign_attributes(
namespace: namespace,
name: params[:name],
......@@ -22,13 +20,15 @@ module ComplianceManagement
color: params[:color]
)
return ServiceResponse.error(message: 'Not permitted to create framework') unless permitted?
framework.save ? success : error
end
private
def permitted?
can?(current_user, :create_custom_compliance_frameworks, namespace)
can? current_user, :manage_compliance_framework, framework
end
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
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
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
end
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
......
......@@ -11805,9 +11805,6 @@ msgstr ""
msgid "Feature flag was successfully removed."
msgstr ""
msgid "Feature not available"
msgstr ""
msgid "FeatureFlags|%d user"
msgid_plural "FeatureFlags|%d users"
msgstr[0] ""
......
......@@ -6,7 +6,7 @@ module RuboCop
class IDType < RuboCop::Cop::Cop
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
(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