Commit 89d6a3f8 authored by Phil Hughes's avatar Phil Hughes

Merge branch '341418-add-pretty-error-messages-from-json-schema' into 'master'

Return pretty error validation messages for Security Policy

See merge request gitlab-org/gitlab!78916
parents 1f2f7466 7e0e5a43
...@@ -34,6 +34,7 @@ export default { ...@@ -34,6 +34,7 @@ export default {
data() { data() {
return { return {
error: '', error: '',
errorMessages: [],
newPolicyType: POLICY_TYPE_COMPONENT_OPTIONS.container.value, newPolicyType: POLICY_TYPE_COMPONENT_OPTIONS.container.value,
}; };
}, },
...@@ -77,8 +78,8 @@ export default { ...@@ -77,8 +78,8 @@ export default {
}, },
}, },
methods: { methods: {
setError(error) { setError(errors) {
this.error = error; [this.error, ...this.errorMessages] = errors.split('\n');
}, },
handleNewPolicyType(type) { handleNewPolicyType(type) {
this.newPolicyType = type; this.newPolicyType = type;
...@@ -92,8 +93,18 @@ export default { ...@@ -92,8 +93,18 @@ export default {
<template> <template>
<section class="policy-editor"> <section class="policy-editor">
<gl-alert v-if="error" dissmissable="true" variant="danger" @dismiss="setError('')"> <gl-alert
{{ error }} v-if="error"
:title="error"
dissmissable="true"
variant="danger"
@dismiss="setError('')"
>
<ul v-if="errorMessages.length" class="gl-mb-0! gl-ml-5">
<li v-for="errorMessage in errorMessages" :key="errorMessage">
{{ errorMessage }}
</li>
</ul>
</gl-alert> </gl-alert>
<header class="gl-pb-5"> <header class="gl-pb-5">
<h3>{{ $options.i18n.title }}</h3> <h3>{{ $options.i18n.title }}</h3>
......
...@@ -10,7 +10,7 @@ import { DEFAULT_MR_TITLE, SECURITY_POLICY_ACTIONS } from './constants'; ...@@ -10,7 +10,7 @@ import { DEFAULT_MR_TITLE, SECURITY_POLICY_ACTIONS } from './constants';
*/ */
const checkForErrors = ({ errors }) => { const checkForErrors = ({ errors }) => {
if (errors?.length) { if (errors?.length) {
throw new Error(errors); throw new Error(errors.join('\n'));
} }
}; };
......
...@@ -38,10 +38,11 @@ module Mutations ...@@ -38,10 +38,11 @@ module Mutations
result = commit_policy(project, args) result = commit_policy(project, args)
error_message = result[:status] == :error ? result[:message] : nil error_message = result[:status] == :error ? result[:message] : nil
error_details = result[:status] == :error ? result[:details] : nil
{ {
branch: result[:branch], branch: result[:branch],
errors: [error_message].compact errors: [error_message, *error_details].compact
} }
end end
......
...@@ -11,6 +11,7 @@ module Security ...@@ -11,6 +11,7 @@ module Security
POLICY_PATH = '.gitlab/security-policies/policy.yml' POLICY_PATH = '.gitlab/security-policies/policy.yml'
POLICY_SCHEMA_PATH = 'ee/app/validators/json_schemas/security_orchestration_policy.json' POLICY_SCHEMA_PATH = 'ee/app/validators/json_schemas/security_orchestration_policy.json'
POLICY_SCHEMA = JSONSchemer.schema(Rails.root.join(POLICY_SCHEMA_PATH))
AVAILABLE_POLICY_TYPES = %i{scan_execution_policy scan_result_policy}.freeze AVAILABLE_POLICY_TYPES = %i{scan_execution_policy scan_result_policy}.freeze
belongs_to :project, inverse_of: :security_orchestration_policy_configuration belongs_to :project, inverse_of: :security_orchestration_policy_configuration
...@@ -44,9 +45,13 @@ module Security ...@@ -44,9 +45,13 @@ module Security
end end
def policy_configuration_valid?(policy = policy_hash) def policy_configuration_valid?(policy = policy_hash)
JSONSchemer POLICY_SCHEMA.valid?(policy.to_h.deep_stringify_keys)
.schema(Rails.root.join(POLICY_SCHEMA_PATH)) end
.valid?(policy.to_h.deep_stringify_keys)
def policy_configuration_validation_errors(policy = policy_hash)
POLICY_SCHEMA
.validate(policy.to_h.deep_stringify_keys)
.map { |error| JSONSchemer::Errors.pretty(error) }
end end
def policy_last_updated_by def policy_last_updated_by
......
...@@ -8,8 +8,10 @@ module Security ...@@ -8,8 +8,10 @@ module Security
return error('Security Policy Project does not exist') unless policy_configuration.present? return error('Security Policy Project does not exist') unless policy_configuration.present?
result = commit_policy(process_policy_yaml) process_policy_result = process_policy
return process_policy_result if process_policy_result[:status] != :success
result = commit_policy(process_policy_result[:policy_hash])
return error(result[:message], :bad_request) if result[:status] != :success return error(result[:message], :bad_request) if result[:status] != :success
success({ branch: branch_name }) success({ branch: branch_name })
...@@ -19,9 +21,8 @@ module Security ...@@ -19,9 +21,8 @@ module Security
private private
def process_policy_yaml def process_policy
policy = Gitlab::Config::Loader::Yaml.new(params[:policy_yaml]).load! ProcessPolicyService.new(
updated_policy = ProcessPolicyService.new(
policy_configuration: policy_configuration, policy_configuration: policy_configuration,
params: { params: {
operation: params[:operation], operation: params[:operation],
...@@ -30,11 +31,11 @@ module Security ...@@ -30,11 +31,11 @@ module Security
type: policy.delete(:type)&.to_sym type: policy.delete(:type)&.to_sym
} }
).execute ).execute
YAML.dump(updated_policy.deep_stringify_keys)
end end
def commit_policy(policy_yaml) def commit_policy(policy_hash)
policy_yaml = YAML.dump(policy_hash.deep_stringify_keys)
return create_commit(::Files::UpdateService, policy_yaml) if policy_configuration.policy_configuration_exists? return create_commit(::Files::UpdateService, policy_yaml) if policy_configuration.policy_configuration_exists?
create_commit(::Files::CreateService, policy_yaml) create_commit(::Files::CreateService, policy_yaml)
...@@ -68,6 +69,10 @@ module Security ...@@ -68,6 +69,10 @@ module Security
@branch_name ||= "update-policy-#{Time.now.to_i}" @branch_name ||= "update-policy-#{Time.now.to_i}"
end end
def policy
@policy ||= Gitlab::Config::Loader::Yaml.new(params[:policy_yaml]).load!
end
attr_reader :project, :policy_configuration attr_reader :project, :policy_configuration
end end
end end
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
module Security module Security
module SecurityOrchestrationPolicies module SecurityOrchestrationPolicies
class ProcessPolicyService class ProcessPolicyService
include BaseServiceUtility
def initialize(policy_configuration:, params:) def initialize(policy_configuration:, params:)
@policy_configuration = policy_configuration @policy_configuration = policy_configuration
@params = params @params = params
...@@ -14,8 +16,8 @@ module Security ...@@ -14,8 +16,8 @@ module Security
name = params[:name] name = params[:name]
operation = params[:operation] operation = params[:operation]
raise StandardError, "Invalid policy type" unless Security::OrchestrationPolicyConfiguration::AVAILABLE_POLICY_TYPES.include?(type) return error("Invalid policy type", :bad_request) unless Security::OrchestrationPolicyConfiguration::AVAILABLE_POLICY_TYPES.include?(type)
raise StandardError, "Name should be same as the policy name" if name && operation != :replace && policy[:name] != name return error("Name should be same as the policy name", :bad_request) if name && operation != :replace && policy[:name] != name
policy_hash = policy_configuration.policy_hash.dup || {} policy_hash = policy_configuration.policy_hash.dup || {}
...@@ -25,13 +27,17 @@ module Security ...@@ -25,13 +27,17 @@ module Security
when :remove then remove_from_policy_hash(policy_hash, policy, type) when :remove then remove_from_policy_hash(policy_hash, policy, type)
end end
raise StandardError, "Invalid policy yaml" unless policy_configuration.policy_configuration_valid?(policy_hash) return error('Invalid policy YAML', :bad_request, pass_back: { details: policy_configuration_validation_errors(policy_hash) }) unless policy_configuration_valid?(policy_hash)
policy_hash success(policy_hash: policy_hash)
rescue StandardError => e
error(e.message)
end end
private private
delegate :policy_configuration_validation_errors, :policy_configuration_valid?, to: :policy_configuration
def append_to_policy_hash(policy_hash, policy, type) def append_to_policy_hash(policy_hash, policy, type)
if policy_hash[type].blank? if policy_hash[type].blank?
policy_hash[type] = [policy] policy_hash[type] = [policy]
......
...@@ -70,7 +70,17 @@ describe('PolicyEditor component', () => { ...@@ -70,7 +70,17 @@ describe('PolicyEditor component', () => {
await nextTick(); await nextTick();
const alert = findAlert(); const alert = findAlert();
expect(alert.exists()).toBe(true); expect(alert.exists()).toBe(true);
expect(alert.text()).toBe(errorMessage); expect(alert.props('title')).toBe(errorMessage);
});
it('shows an alert with details when multiline "error" is emitted from the component', async () => {
const errorMessages = 'title\ndetail1';
findNeworkPolicyEditor().vm.$emit('error', errorMessages);
await nextTick();
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.props('title')).toBe('title');
expect(alert.text()).toBe('detail1');
}); });
it.each` it.each`
......
...@@ -180,6 +180,46 @@ RSpec.describe Security::OrchestrationPolicyConfiguration do ...@@ -180,6 +180,46 @@ RSpec.describe Security::OrchestrationPolicyConfiguration do
end end
end end
describe '#policy_configuration_validation_errors' do
subject { security_orchestration_policy_configuration.policy_configuration_validation_errors }
context 'when file is invalid' do
let(:policy_yaml) do
build(:orchestration_policy_yaml, scan_execution_policy:
[build(:scan_execution_policy, rules: [{ type: 'pipeline', branches: 'production' }])])
end
it { is_expected.to eq(["property '/scan_execution_policy/0/rules/0/branches' is not of type: array"]) }
end
context 'when file is valid' do
it { is_expected.to eq([]) }
end
context 'when policy is passed as argument' do
let_it_be(:policy_yaml) { nil }
let_it_be(:policy) { { scan_execution_policy: [build(:scan_execution_policy)] } }
context 'when scan type is secret_detection' do
it 'returns false if extra fields are present' do
invalid_policy = policy.deep_dup
invalid_policy[:scan_execution_policy][0][:actions][0][:scan] = 'secret_detection'
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(invalid_policy)).to eq(
["property '/scan_execution_policy/0/actions/0' is invalid: error_type=maxProperties"]
)
end
it 'returns true if extra fields are not present' do
valid_policy = policy.deep_dup
valid_policy[:scan_execution_policy][0][:actions][0] = { scan: 'secret_detection' }
expect(security_orchestration_policy_configuration.policy_configuration_validation_errors(valid_policy)).to eq([])
end
end
end
end
describe '#active_scan_execution_policies' do describe '#active_scan_execution_policies' do
let(:enforce_dast_yaml) { build(:orchestration_policy_yaml, scan_execution_policy: [build(:scan_execution_policy)]) } let(:enforce_dast_yaml) { build(:orchestration_policy_yaml, scan_execution_policy: [build(:scan_execution_policy)]) }
let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') } let(:policy_yaml) { fixture_file('security_orchestration.yml', dir: 'ee') }
......
...@@ -46,5 +46,15 @@ RSpec.describe 'Create scan execution policy for a project' do ...@@ -46,5 +46,15 @@ RSpec.describe 'Create scan execution policy for a project' do
expect(branch).not_to be_nil expect(branch).not_to be_nil
expect(commit.message).to eq('Add a new policy to .gitlab/security-policies/policy.yml') expect(commit.message).to eq('Add a new policy to .gitlab/security-policies/policy.yml')
end end
context 'when provided policy is invalid' do
let_it_be(:policy_yaml) { build(:scan_execution_policy, name: policy_name).merge(type: 'scan_execution_policy', rules: [{ type: 'invalid_type' }]).to_yaml }
it 'returns error with detailed information' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to eq(['Invalid policy YAML', "property '/scan_execution_policy/0/rules/0/type' is not one of: [\"pipeline\", \"schedule\"]"])
end
end
end end
end end
...@@ -41,7 +41,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::PolicyCommitService do ...@@ -41,7 +41,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::PolicyCommitService do
response = service.execute response = service.execute
expect(response[:status]).to eq(:error) expect(response[:status]).to eq(:error)
expect(response[:message]).to eq("Invalid policy yaml") expect(response[:message]).to eq("Invalid policy YAML")
expect(response[:details]).to eq(["property '/scan_execution_policy/0' is missing required keys: name, enabled, rules, actions"])
end end
end end
......
...@@ -29,24 +29,34 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -29,24 +29,34 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
let(:policy_name) { 'invalid' } let(:policy_name) { 'invalid' }
let(:policy) { { name: 'invalid', invalid_field: 'invalid' } } let(:policy) { { name: 'invalid', invalid_field: 'invalid' } }
it 'raises StandardError' do it 'returns error' do
expect { service.execute }.to raise_error(StandardError, 'Invalid policy yaml') result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid policy YAML')
expect(result[:details]).to eq(["property '/scan_execution_policy/0' is missing required keys: enabled, rules, actions"])
end end
end end
context 'when policy name is not same as in policy' do context 'when policy name is not same as in policy' do
let(:policy_name) { 'invalid' } let(:policy_name) { 'invalid' }
it 'raises StandardError' do it 'returns error' do
expect { service.execute }.to raise_error(StandardError, 'Name should be same as the policy name') result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Name should be same as the policy name')
end end
end end
context 'when type is invalid' do context 'when type is invalid' do
let(:type) { :invalid_type } let(:type) { :invalid_type }
it 'raises StandardError' do it 'returns error' do
expect { service.execute }.to raise_error(StandardError, 'Invalid policy type') result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid policy type')
end end
end end
...@@ -59,7 +69,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -59,7 +69,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
it 'appends the new policy' do it 'appends the new policy' do
result = service.execute result = service.execute
expect(result[:scan_execution_policy].count).to eq(3) expect(result[:status]).to eq(:success)
expect(result.dig(:policy_hash, :scan_execution_policy).count).to eq(3)
end end
end end
...@@ -68,8 +79,11 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -68,8 +79,11 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
allow(policy_configuration).to receive(:policy_hash).and_return(Gitlab::Config::Loader::Yaml.new(repository_with_existing_policy_yaml).load!) allow(policy_configuration).to receive(:policy_hash).and_return(Gitlab::Config::Loader::Yaml.new(repository_with_existing_policy_yaml).load!)
end end
it 'raises StandardError' do it 'returns error' do
expect { service.execute }.to raise_error(StandardError, 'Policy already exists with same name') result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Policy already exists with same name')
end end
end end
...@@ -81,7 +95,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -81,7 +95,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
it 'appends the new policy' do it 'appends the new policy' do
result = service.execute result = service.execute
expect(result[:scan_execution_policy].count).to eq(1) expect(result[:status]).to eq(:success)
expect(result.dig(:policy_hash, :scan_execution_policy).count).to eq(1)
end end
end end
end end
...@@ -98,8 +113,11 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -98,8 +113,11 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
allow(policy_configuration).to receive(:policy_hash).and_return(Gitlab::Config::Loader::Yaml.new(repository_policy_yaml).load!) allow(policy_configuration).to receive(:policy_hash).and_return(Gitlab::Config::Loader::Yaml.new(repository_policy_yaml).load!)
end end
it 'raises StandardError' do it 'returns error' do
expect { service.execute }.to raise_error(StandardError, 'Policy does not exist') result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Policy does not exist')
end end
end end
...@@ -109,7 +127,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -109,7 +127,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
it 'does not modify the policy name' do it 'does not modify the policy name' do
result = service.execute result = service.execute
expect(result[:scan_execution_policy].first).to eq(policy_yaml) expect(result.dig(:policy_hash, :scan_execution_policy).first).to eq(policy_yaml)
end end
end end
...@@ -117,7 +135,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -117,7 +135,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
it 'replaces the policy' do it 'replaces the policy' do
result = service.execute result = service.execute
expect(result[:scan_execution_policy].first[:enabled]).to be_falsey expect(result.dig(:policy_hash, :scan_execution_policy).first[:enabled]).to be_falsey
end end
end end
...@@ -129,7 +147,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -129,7 +147,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
it 'updates the policy name' do it 'updates the policy name' do
result = service.execute result = service.execute
expect(result[:scan_execution_policy].first[:name]).to eq('Updated Policy') expect(result.dig(:policy_hash, :scan_execution_policy).first[:name]).to eq('Updated Policy')
end end
end end
...@@ -138,8 +156,11 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -138,8 +156,11 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
Gitlab::Config::Loader::Yaml.new(build(:scan_execution_policy, name: 'Scheduled DAST', enabled: false).to_yaml).load! Gitlab::Config::Loader::Yaml.new(build(:scan_execution_policy, name: 'Scheduled DAST', enabled: false).to_yaml).load!
end end
it 'raises StandardError' do it 'returns error' do
expect { service.execute }.to raise_error(StandardError, 'Policy already exists with same name') result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Policy already exists with same name')
end end
end end
end end
...@@ -152,8 +173,11 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -152,8 +173,11 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
allow(policy_configuration).to receive(:policy_hash).and_return(Gitlab::Config::Loader::Yaml.new(repository_policy_yaml).load!) allow(policy_configuration).to receive(:policy_hash).and_return(Gitlab::Config::Loader::Yaml.new(repository_policy_yaml).load!)
end end
it 'raises StandardError' do it 'returns error' do
expect { service.execute }.to raise_error(StandardError, 'Policy does not exist') result = service.execute
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Policy does not exist')
end end
end end
...@@ -165,7 +189,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do ...@@ -165,7 +189,8 @@ RSpec.describe Security::SecurityOrchestrationPolicies::ProcessPolicyService do
it 'removes the policy' do it 'removes the policy' do
result = service.execute result = service.execute
expect(result[:scan_execution_policy].count).to eq(1) expect(result[:status]).to eq(:success)
expect(result.dig(:policy_hash, :scan_execution_policy).count).to eq(1)
end end
end end
end end
......
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