From dfd78b8d774926305508cd0396c831bf990b97b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20=C4=8Cavoj?= <mcavoj@gitlab.com> Date: Fri, 26 Jul 2024 21:14:37 +0000 Subject: [PATCH] Refactor CI ProjectConfig sources to use static precedence This change refactors sources for ProjectConfig to use a static list of sources instead of manipulating it in EE to make the precedence of sources clearer. EE-only configs return `nil` for CE. --- .../gitlab/feature_available_usage.yml | 1 - .rubocop_todo/gitlab/strong_memoize_attr.yml | 1 - ee/lib/ee/gitlab/ci/project_config.rb | 32 -- .../ee/gitlab/ci/project_config/compliance.rb | 40 +++ .../pipeline_execution_policy_forced.rb | 41 +++ .../project_config/security_policy_default.rb | 54 ++++ ee/lib/gitlab/ci/project_config/compliance.rb | 44 --- .../pipeline_execution_policy_forced.rb | 39 --- .../project_config/security_policy_default.rb | 52 ---- .../lib/ee/gitlab/ci/project_config_spec.rb | 287 ++++++++++++++++++ ee/spec/lib/gitlab/ci/project_config_spec.rb | 283 ----------------- lib/gitlab/ci/project_config.rb | 12 +- lib/gitlab/ci/project_config/compliance.rb | 27 ++ .../pipeline_execution_policy_forced.rb | 23 ++ .../project_config/security_policy_default.rb | 23 ++ lib/gitlab/ci/project_config/source.rb | 3 +- spec/lib/gitlab/ci/project_config_spec.rb | 10 +- 17 files changed, 513 insertions(+), 459 deletions(-) delete mode 100644 ee/lib/ee/gitlab/ci/project_config.rb create mode 100644 ee/lib/ee/gitlab/ci/project_config/compliance.rb create mode 100644 ee/lib/ee/gitlab/ci/project_config/pipeline_execution_policy_forced.rb create mode 100644 ee/lib/ee/gitlab/ci/project_config/security_policy_default.rb delete mode 100644 ee/lib/gitlab/ci/project_config/compliance.rb delete mode 100644 ee/lib/gitlab/ci/project_config/pipeline_execution_policy_forced.rb delete mode 100644 ee/lib/gitlab/ci/project_config/security_policy_default.rb create mode 100644 ee/spec/lib/ee/gitlab/ci/project_config_spec.rb delete mode 100644 ee/spec/lib/gitlab/ci/project_config_spec.rb create mode 100644 lib/gitlab/ci/project_config/compliance.rb create mode 100644 lib/gitlab/ci/project_config/pipeline_execution_policy_forced.rb create mode 100644 lib/gitlab/ci/project_config/security_policy_default.rb diff --git a/.rubocop_todo/gitlab/feature_available_usage.yml b/.rubocop_todo/gitlab/feature_available_usage.yml index 3833fe08cac75..bfdfba161363c 100644 --- a/.rubocop_todo/gitlab/feature_available_usage.yml +++ b/.rubocop_todo/gitlab/feature_available_usage.yml @@ -99,7 +99,6 @@ Gitlab/FeatureAvailableUsage: - 'ee/lib/ee/gitlab/gon_helper.rb' - 'ee/lib/ee/gitlab/tree_summary.rb' - 'ee/lib/gitlab/alert_management.rb' - - 'ee/lib/gitlab/ci/project_config/compliance.rb' - 'ee/lib/gitlab/code_owners.rb' - 'ee/lib/gitlab/path_locks_finder.rb' - 'ee/spec/models/ee/project_spec.rb' diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml index 06b7d974b432f..8d7df60bdcf57 100644 --- a/.rubocop_todo/gitlab/strong_memoize_attr.yml +++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml @@ -396,7 +396,6 @@ Gitlab/StrongMemoizeAttr: - 'ee/lib/gitlab/ci/minutes/cached_quota.rb' - 'ee/lib/gitlab/ci/minutes/gitlab_contribution_cost_factor.rb' - 'ee/lib/gitlab/ci/parsers/security/container_scanning.rb' - - 'ee/lib/gitlab/ci/project_config/compliance.rb' - 'ee/lib/gitlab/ci/reports/license_scanning/reports_comparer.rb' - 'ee/lib/gitlab/ci/reports/metrics/reports_comparer.rb' - 'ee/lib/gitlab/code_owners/entry.rb' diff --git a/ee/lib/ee/gitlab/ci/project_config.rb b/ee/lib/ee/gitlab/ci/project_config.rb deleted file mode 100644 index dae0913f684d0..0000000000000 --- a/ee/lib/ee/gitlab/ci/project_config.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -module EE - module Gitlab - module Ci - module ProjectConfig - extend ::Gitlab::Utils::Override - - private - - override :sources - def sources - # SecurityPolicyDefault should come last. It is only necessary if no other source is available. - sources = [::Gitlab::Ci::ProjectConfig::Compliance].concat(super) - # PipelineExecutionPolicyForced must come before AutoDevops because it handles - # the empty CI config case. - # We want to run Pipeline Execution Policies instead of AutoDevops (if they are present). - insert_before_autodevops(sources, ::Gitlab::Ci::ProjectConfig::PipelineExecutionPolicyForced) - sources.concat([::Gitlab::Ci::ProjectConfig::SecurityPolicyDefault]) - end - - def insert_before_autodevops(sources, new_source) - auto_devops_source_index = sources.find_index do |source| - source == ::Gitlab::Ci::ProjectConfig::AutoDevops - end - - sources.insert(auto_devops_source_index, new_source) - end - end - end - end -end diff --git a/ee/lib/ee/gitlab/ci/project_config/compliance.rb b/ee/lib/ee/gitlab/ci/project_config/compliance.rb new file mode 100644 index 0000000000000..d03fb0c132d91 --- /dev/null +++ b/ee/lib/ee/gitlab/ci/project_config/compliance.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module EE + module Gitlab + module Ci + module ProjectConfig + module Compliance + extend ::Gitlab::Utils::Override + include ::Gitlab::Utils::StrongMemoize + + override :content + def content + return unless available? + return unless pipeline_configuration_full_path.present? + return if pipeline_source_bridge && pipeline_source == :parent_pipeline + + return if [:security_orchestration_policy, :ondemand_dast_scan].include?(pipeline_source) + + path_file, path_project = pipeline_configuration_full_path.split('@', 2) + YAML.dump('include' => [{ 'project' => path_project, 'file' => path_file }]) + end + strong_memoize_attr :content + + private + + def pipeline_configuration_full_path + return unless project + + project.compliance_pipeline_configuration_full_path + end + strong_memoize_attr :pipeline_configuration_full_path + + def available? + project.licensed_feature_available?(:evaluate_group_level_compliance_pipeline) + end + end + end + end + end +end diff --git a/ee/lib/ee/gitlab/ci/project_config/pipeline_execution_policy_forced.rb b/ee/lib/ee/gitlab/ci/project_config/pipeline_execution_policy_forced.rb new file mode 100644 index 0000000000000..5bb596079fec8 --- /dev/null +++ b/ee/lib/ee/gitlab/ci/project_config/pipeline_execution_policy_forced.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module EE + module Gitlab + module Ci + module ProjectConfig + # This class is responsible for generating the content of the `.gitlab-ci.yml` + # file that will trigger the execution of the pipeline execution policy jobs. + # + # If there are Pipeline Execution Policies defined for a project, + # this source will generate a dummy job that will force the creation + # of the project pipeline, so that the execution policy jobs can be merged + # into it. The dummy job will be removed in `MergeJobs` step of the pipeline chain. + # + # This source should be loaded before AutoDevops source. + module PipelineExecutionPolicyForced + extend ::Gitlab::Utils::Override + include ::Gitlab::Utils::StrongMemoize + + DUMMY_CONTENT = { + 'Pipeline execution policy trigger' => { + 'stage' => ::Gitlab::Ci::Config::EdgeStagesInjector::PRE_PIPELINE, + 'script' => ['echo "Forcing project pipeline to run policy jobs."'] + } + }.freeze + + override :content + def content + return if ::Feature.disabled?(:pipeline_execution_policy_type, project.group) + return unless has_pipeline_execution_policies + + # Create a dummy job to ensure that project pipeline gets created. + # Pipeline execution policy jobs will be merged onto the project pipeline. + YAML.dump(DUMMY_CONTENT) + end + strong_memoize_attr :content + end + end + end + end +end diff --git a/ee/lib/ee/gitlab/ci/project_config/security_policy_default.rb b/ee/lib/ee/gitlab/ci/project_config/security_policy_default.rb new file mode 100644 index 0000000000000..3a07a561268b4 --- /dev/null +++ b/ee/lib/ee/gitlab/ci/project_config/security_policy_default.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module EE + module Gitlab + module Ci + module ProjectConfig + module SecurityPolicyDefault + extend ::Gitlab::Utils::Override + include ::Gitlab::Utils::StrongMemoize + + override :content + def content + return unless triggered_for_branch + return unless ::Enums::Ci::Pipeline.ci_and_security_orchestration_sources.key?(pipeline_source) + return unless project.licensed_feature_available?(:security_orchestration_policies) + return unless active_scan_execution_policies? + + # We merge the security scans with the pipeline configuration in ee/lib/ee/gitlab/ci/config_ee.rb. + # An empty config with no content is enough to trigger the merge process when the Auto DevOps is disabled + # and no .gitlab-ci.yml is present. + YAML.dump(nil) + end + strong_memoize_attr :content + + private + + def active_scan_execution_policies? + return false unless ref + + service = ::Security::SecurityOrchestrationPolicies::PolicyBranchesService.new(project: project) + + ::Gitlab::Security::Orchestration::ProjectPolicyConfigurations + .new(project).all + .to_a + .flat_map(&:active_scan_execution_policies_for_pipelines) + .any? { |policy| policy_applicable?(policy) && applicable_for_branch?(service, policy) } + end + + def policy_applicable?(policy) + ::Security::SecurityOrchestrationPolicies::PolicyScopeChecker + .new(project: project) + .policy_applicable?(policy) + end + + def applicable_for_branch?(service, policy) + applicable_branches = service.scan_execution_branches(policy[:rules]) + + ref.in?(applicable_branches) + end + end + end + end + end +end diff --git a/ee/lib/gitlab/ci/project_config/compliance.rb b/ee/lib/gitlab/ci/project_config/compliance.rb deleted file mode 100644 index 5015b9445fc6f..0000000000000 --- a/ee/lib/gitlab/ci/project_config/compliance.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class ProjectConfig - class Compliance < Gitlab::Ci::ProjectConfig::Source - def content - strong_memoize(:content) do - next unless available? - next unless pipeline_configuration_full_path.present? - next if pipeline_source_bridge && pipeline_source == :parent_pipeline - - next if [:security_orchestration_policy, :ondemand_dast_scan].include?(pipeline_source) - - path_file, path_project = pipeline_configuration_full_path.split('@', 2) - YAML.dump('include' => [{ 'project' => path_project, 'file' => path_file }]) - end - end - - def internal_include_prepended? - true - end - - def source - :compliance_source - end - - private - - def pipeline_configuration_full_path - strong_memoize(:pipeline_configuration_full_path) do - next unless project - - project.compliance_pipeline_configuration_full_path - end - end - - def available? - project.feature_available?(:evaluate_group_level_compliance_pipeline) - end - end - end - end -end diff --git a/ee/lib/gitlab/ci/project_config/pipeline_execution_policy_forced.rb b/ee/lib/gitlab/ci/project_config/pipeline_execution_policy_forced.rb deleted file mode 100644 index a2242a5c528af..0000000000000 --- a/ee/lib/gitlab/ci/project_config/pipeline_execution_policy_forced.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class ProjectConfig - # This class is responsible for generating the content of the `.gitlab-ci.yml` - # file that will trigger the execution of the pipeline execution policy jobs. - # - # If there are Pipeline Execution Policies defined for a project, - # this source will generate a dummy job that will force the creation - # of the project pipeline, so that the execution policy jobs can be merged - # into it. The dummy job will be removed in `MergeJobs` step of the pipeline chain. - # - # This source should be loaded before AutoDevops source. - class PipelineExecutionPolicyForced < Gitlab::Ci::ProjectConfig::Source - DUMMY_CONTENT = { - 'Pipeline execution policy trigger' => { - 'stage' => ::Gitlab::Ci::Config::EdgeStagesInjector::PRE_PIPELINE, - 'script' => ['echo "Forcing project pipeline to run policy jobs."'] - } - }.freeze - - def content - return if ::Feature.disabled?(:pipeline_execution_policy_type, @project.group) - return unless @has_pipeline_execution_policies - - # Create a dummy job to ensure that project pipeline gets created. - # Pipeline execution policy jobs will be merged onto the project pipeline. - YAML.dump(DUMMY_CONTENT) - end - strong_memoize_attr :content - - def source - :pipeline_execution_policy_forced - end - end - end - end -end diff --git a/ee/lib/gitlab/ci/project_config/security_policy_default.rb b/ee/lib/gitlab/ci/project_config/security_policy_default.rb deleted file mode 100644 index e975c68c207c5..0000000000000 --- a/ee/lib/gitlab/ci/project_config/security_policy_default.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - class ProjectConfig - class SecurityPolicyDefault < Gitlab::Ci::ProjectConfig::Source - def content - return unless @triggered_for_branch - return unless Enums::Ci::Pipeline.ci_and_security_orchestration_sources.key?(@pipeline_source) - return unless @project.licensed_feature_available?(:security_orchestration_policies) - return unless active_scan_execution_policies? - - # We merge the security scans with the pipeline configuration in ee/lib/ee/gitlab/ci/config_ee.rb. - # An empty config with no content is enough to trigger the merge process when the Auto DevOps is disabled - # and no .gitlab-ci.yml is present. - YAML.dump(nil) - end - strong_memoize_attr :content - - def source - :security_policies_default_source - end - - private - - def active_scan_execution_policies? - return false unless @ref - - service = ::Security::SecurityOrchestrationPolicies::PolicyBranchesService.new(project: @project) - - ::Gitlab::Security::Orchestration::ProjectPolicyConfigurations - .new(@project).all - .to_a - .flat_map(&:active_scan_execution_policies_for_pipelines) - .any? { |policy| policy_applicable?(policy) && applicable_for_branch?(service, policy) } - end - - def policy_applicable?(policy) - ::Security::SecurityOrchestrationPolicies::PolicyScopeChecker - .new(project: @project) - .policy_applicable?(policy) - end - - def applicable_for_branch?(service, policy) - applicable_branches = service.scan_execution_branches(policy[:rules]) - - @ref.in?(applicable_branches) - end - end - end - end -end diff --git a/ee/spec/lib/ee/gitlab/ci/project_config_spec.rb b/ee/spec/lib/ee/gitlab/ci/project_config_spec.rb new file mode 100644 index 0000000000000..4ef86b8dacb9f --- /dev/null +++ b/ee/spec/lib/ee/gitlab/ci/project_config_spec.rb @@ -0,0 +1,287 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_composition do + let_it_be_with_reload(:project) { create(:project, :empty_repo) } + let(:sha) { '123456' } + let(:content) { nil } + let(:source) { :push } + let(:bridge) { nil } + let(:triggered_for_branch) { true } + let(:ref) { 'master' } + let(:has_pipeline_execution_policies) { false } + + subject(:config) do + described_class.new( + project: project, + sha: sha, + custom_content: content, + pipeline_source: source, + pipeline_source_bridge: bridge, + triggered_for_branch: triggered_for_branch, + ref: ref, + has_pipeline_execution_policies: has_pipeline_execution_policies + ) + end + + context 'when config is Compliance' do + let(:content_result) do + <<~CICONFIG + --- + include: + - project: compliance/hippa + file: ".compliance-gitlab-ci.yml" + CICONFIG + end + + shared_examples 'does not include compliance pipeline configuration content' do + it do + expect(config.source).not_to eq(:compliance_source) + expect(config.content).not_to eq(content_result) + end + end + + context 'when project has compliance label defined' do + let_it_be(:compliance_group) { create(:group, :private, name: "compliance") } + let_it_be(:compliance_project) { create(:project, namespace: compliance_group, name: "hippa") } + + context 'when feature is available' do + before do + stub_licensed_features(evaluate_group_level_compliance_pipeline: true) + end + + context 'when compliance pipeline configuration is defined' do + let(:framework) do + create( + :compliance_framework, + namespace: compliance_group, + pipeline_configuration_full_path: ".compliance-gitlab-ci.yml@compliance/hippa" + ) + end + + let!(:framework_project_setting) do + create(:compliance_framework_project_setting, project: project, + compliance_management_framework: framework) + end + + it 'includes compliance pipeline configuration content' do + expect(config.source).to eq(:compliance_source) + expect(config.content).to eq(content_result) + end + + context 'when pipeline is downstream of a bridge' do + let(:bridge) { create(:ci_bridge) } + + it 'does include compliance pipeline configuration' do + expect(config.source).to eq(:compliance_source) + expect(config.content).to eq(content_result) + end + + context 'when pipeline source is parent pipeline' do + let(:source) { :parent_pipeline } + + it_behaves_like 'does not include compliance pipeline configuration content' + end + end + + context 'when the source is on-demand dast scan' do + let(:source) { :ondemand_dast_scan } + let(:content) { "---\ninclude:\n- template: DAST-On-Demand-Scan.gitlab-ci.yml\n" } + let(:content_result) do + <<~CICONFIG + --- + include: + - template: DAST-On-Demand-Scan.gitlab-ci.yml + CICONFIG + end + + it 'does not include compliance pipeline configuration' do + expect(config.source).to eq(:parameter_source) + expect(config.content).to eq(content_result) + end + end + end + + context 'when compliance pipeline configuration is not defined' do + let(:framework) { create(:compliance_framework, namespace: compliance_group) } + let!(:framework_project_setting) do + create(:compliance_framework_project_setting, project: project, + compliance_management_framework: framework) + end + + it_behaves_like 'does not include compliance pipeline configuration content' + end + + context 'when compliance pipeline configuration is empty' do + let(:framework) do + create(:compliance_framework, namespace: compliance_group, pipeline_configuration_full_path: '') + end + + let!(:framework_project_setting) do + create(:compliance_framework_project_setting, project: project, + compliance_management_framework: framework) + end + + it_behaves_like 'does not include compliance pipeline configuration content' + end + end + + context 'when feature is not licensed' do + before do + stub_licensed_features(evaluate_group_level_compliance_pipeline: false) + end + + it_behaves_like 'does not include compliance pipeline configuration content' + end + end + + context 'when project does not have compliance label defined' do + context 'when feature is available' do + before do + stub_licensed_features(evaluate_group_level_compliance_pipeline: true) + end + + it_behaves_like 'does not include compliance pipeline configuration content' + end + end + end + + context 'when config is SecurityPolicyDefault' do + let_it_be_with_reload(:project) { create(:project, :repository) } + let!(:security_policy_configuration) do + create(:security_orchestration_policy_configuration, project: project) + end + + let(:policy) { build(:scan_execution_policy, enabled: true, rules: [rule]) } + let(:branches) { %w[master production] } + + before do + allow(project).to receive(:all_security_orchestration_policy_configurations) + .and_return([security_policy_configuration]) + + allow(security_policy_configuration).to receive(:active_scan_execution_policies).and_return([policy]) + end + + context 'when policies should be enforced' do + context 'when security_orchestration_policies feature is available' do + before do + stub_licensed_features(security_orchestration_policies: true) + end + + let(:security_policy_default_content) { YAML.dump(nil) } + + context 'when auto devops is not enabled' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + context 'when active policies includes a rule with pipeline type' do + let(:rule) { { type: 'pipeline', branches: branches } } + + context 'when policy applies to the pipeline\'s branch' do + it 'includes security policies default pipeline configuration content' do + expect(config.source).to eq(:security_policies_default_source) + expect(config.content).to eq(security_policy_default_content) + end + end + end + end + end + end + + context 'when policies should not be enforced' do + let(:rule) { { type: 'pipeline', branches: branches } } + let(:licensed_security_orchestration_policies) { true } + + before do + stub_licensed_features(security_orchestration_policies: licensed_security_orchestration_policies) + end + + shared_examples 'does not include security policies default pipeline configuration content' do + context 'when auto devops is not enabled' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it 'does not include security policies default pipeline configuration content' do + expect(config.source).to eq(nil) + end + end + end + + context 'when security_orchestration_policies feature is not available' do + let(:licensed_security_orchestration_policies) { false } + + it_behaves_like 'does not include security policies default pipeline configuration content' + end + + context 'when is not triggered for branch' do + let(:triggered_for_branch) { false } + + it_behaves_like 'does not include security policies default pipeline configuration content' + end + + context 'when auto devops is enabled' do + it 'does not include security policies default pipeline configuration content' do + expect(config.source).to eq(:auto_devops_source) + end + end + + context 'when auto devops is not enabled' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + context 'when active policies does not include a rule with pipeline type' do + let(:rule) { { type: 'schedule', branches: branches, cadence: '*/20 * * * *' } } + + it 'does not include security policies default pipeline configuration content' do + expect(config.source).to eq(nil) + end + end + + context 'when policy does not apply to the branch' do + let(:rule) { { type: 'pipeline', branches: ['main'] } } + + it 'does not include security policies default pipeline configuration content' do + expect(config.source).to eq(nil) + end + end + + context 'when the policy should not be enforced to the pipeline source' do + Enums::Ci::Pipeline.dangling_sources.except(:security_orchestration_policy).each_key do |source| + context "when pipeline source is #{source}" do + let(:source) { source } + + it_behaves_like 'does not include security policies default pipeline configuration content' + end + end + end + end + end + end + + context 'when config is PipelineExecutionPolicyForced' do + let(:has_pipeline_execution_policies) { true } + + shared_examples_for 'forces the pipeline creation by including dummy content' do + let(:expected_content) { YAML.dump(Gitlab::Ci::ProjectConfig::PipelineExecutionPolicyForced::DUMMY_CONTENT) } + + it 'includes dummy job to force the pipeline creation' do + expect(config.source).to eq(:pipeline_execution_policy_forced) + expect(config.content).to eq(expected_content) + end + end + + it_behaves_like 'forces the pipeline creation by including dummy content' + + context 'when auto devops is not enabled' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it_behaves_like 'forces the pipeline creation by including dummy content' + end + end +end diff --git a/ee/spec/lib/gitlab/ci/project_config_spec.rb b/ee/spec/lib/gitlab/ci/project_config_spec.rb deleted file mode 100644 index 98231d315aabc..0000000000000 --- a/ee/spec/lib/gitlab/ci/project_config_spec.rb +++ /dev/null @@ -1,283 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ::Gitlab::Ci::ProjectConfig, feature_category: :continuous_integration do - let_it_be_with_reload(:project) { create(:project, :repository, ci_config_path: nil) } - let(:sha) { '123456' } - let(:content) { nil } - let(:source) { :push } - let(:bridge) { nil } - let(:triggered_for_branch) { true } - let(:ref) { 'master' } - let(:security_policies) { {} } - let(:has_pipeline_execution_policies) { false } - - let(:content_result) do - <<~CICONFIG - --- - include: - - project: compliance/hippa - file: ".compliance-gitlab-ci.yml" - CICONFIG - end - - subject(:config) do - described_class.new( - project: project, - sha: sha, - custom_content: content, - pipeline_source: source, - pipeline_source_bridge: bridge, - triggered_for_branch: triggered_for_branch, - ref: ref, - has_pipeline_execution_policies: has_pipeline_execution_policies - ) - end - - shared_examples 'does not include compliance pipeline configuration content' do - it do - expect(config.source).not_to eq(:compliance_source) - expect(config.content).not_to eq(content_result) - end - end - - context 'when project has compliance label defined' do - let(:compliance_group) { create(:group, :private, name: "compliance") } - let(:compliance_project) { create(:project, namespace: compliance_group, name: "hippa") } - - context 'when feature is available' do - before do - stub_licensed_features(evaluate_group_level_compliance_pipeline: true) - end - - context 'when compliance pipeline configuration is defined' do - let(:framework) do - create( - :compliance_framework, - namespace: compliance_group, - pipeline_configuration_full_path: ".compliance-gitlab-ci.yml@compliance/hippa" - ) - end - - let!(:framework_project_setting) do - create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework) - end - - it 'includes compliance pipeline configuration content' do - expect(config.source).to eq(:compliance_source) - expect(config.content).to eq(content_result) - end - - context 'when pipeline is downstream of a bridge' do - let(:bridge) { create(:ci_bridge) } - - it 'does include compliance pipeline configuration' do - expect(config.source).to eq(:compliance_source) - expect(config.content).to eq(content_result) - end - - context 'when pipeline source is parent pipeline' do - let(:source) { :parent_pipeline } - - it_behaves_like 'does not include compliance pipeline configuration content' - end - end - - context 'when the source is on-demand dast scan' do - let(:source) { :ondemand_dast_scan } - let(:content) { "---\ninclude:\n- template: DAST-On-Demand-Scan.gitlab-ci.yml\n" } - let(:content_result) do - <<~CICONFIG - --- - include: - - template: DAST-On-Demand-Scan.gitlab-ci.yml - CICONFIG - end - - it 'does not include compliance pipeline configuration' do - expect(config.source).to eq(:parameter_source) - expect(config.content).to eq(content_result) - end - end - end - - context 'when compliance pipeline configuration is not defined' do - let(:framework) { create(:compliance_framework, namespace: compliance_group) } - let!(:framework_project_setting) do - create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework) - end - - it_behaves_like 'does not include compliance pipeline configuration content' - end - - context 'when compliance pipeline configuration is empty' do - let(:framework) do - create(:compliance_framework, namespace: compliance_group, pipeline_configuration_full_path: '') - end - - let!(:framework_project_setting) do - create(:compliance_framework_project_setting, project: project, compliance_management_framework: framework) - end - - it_behaves_like 'does not include compliance pipeline configuration content' - end - end - - context 'when feature is not licensed' do - before do - stub_licensed_features(evaluate_group_level_compliance_pipeline: false) - end - - it_behaves_like 'does not include compliance pipeline configuration content' - end - end - - context 'when project does not have compliance label defined' do - context 'when feature is available' do - before do - stub_licensed_features(evaluate_group_level_compliance_pipeline: true) - end - - it_behaves_like 'does not include compliance pipeline configuration content' - end - - context 'when project has active scan_execution policies' do - let(:security_policies) { { enabled: true, policies: [security_policy_configuration] } } - let!(:security_policy_configuration) do - create(:security_orchestration_policy_configuration, project: project) - end - - let(:policy) { build(:scan_execution_policy, enabled: true, rules: [rule]) } - let(:branches) { %w[master production] } - - before do - allow(project).to receive(:all_security_orchestration_policy_configurations) - .and_return([security_policy_configuration]) - - allow(security_policy_configuration).to receive(:active_scan_execution_policies).and_return([policy]) - end - - context 'when policies should be enforced' do - context 'when security_orchestration_policies feature is available' do - before do - stub_licensed_features(security_orchestration_policies: true) - end - - let(:security_policy_default_content) { YAML.dump(nil) } - - context 'when auto devops is not enabled' do - before do - stub_application_setting(auto_devops_enabled: false) - end - - context 'when active policies includes a rule with pipeline type' do - let(:rule) { { type: 'pipeline', branches: branches } } - - context 'when policy applies to the pipeline\'s branch' do - it 'includes security policies default pipeline configuration content' do - expect(config.source).to eq(:security_policies_default_source) - expect(config.content).to eq(security_policy_default_content) - end - end - end - end - end - end - - context 'when policies should not be enforced' do - let(:rule) { { type: 'pipeline', branches: branches } } - let(:licensed_security_orchestration_policies) { true } - - before do - stub_licensed_features(security_orchestration_policies: licensed_security_orchestration_policies) - end - - shared_examples 'does not include security policies default pipeline configuration content' do - context 'when auto devops is not enabled' do - before do - stub_application_setting(auto_devops_enabled: false) - end - - it 'does not include security policies default pipeline configuration content' do - expect(config.source).to eq(nil) - end - end - end - - context 'when security_orchestration_policies feature is not available' do - let(:licensed_security_orchestration_policies) { false } - - it_behaves_like 'does not include security policies default pipeline configuration content' - end - - context 'when is not triggered for branch' do - let(:triggered_for_branch) { false } - - it_behaves_like 'does not include security policies default pipeline configuration content' - end - - context 'when auto devops is enabled' do - it 'does not include security policies default pipeline configuration content' do - expect(config.source).to eq(:auto_devops_source) - end - end - - context 'when auto devops is not enabled' do - before do - stub_application_setting(auto_devops_enabled: false) - end - - context 'when active policies does not include a rule with pipeline type' do - let(:rule) { { type: 'schedule', branches: branches, cadence: '*/20 * * * *' } } - - it 'does not include security policies default pipeline configuration content' do - expect(config.source).to eq(nil) - end - end - - context 'when policy does not apply to the branch' do - let(:rule) { { type: 'pipeline', branches: ['main'] } } - - it 'does not include security policies default pipeline configuration content' do - expect(config.source).to eq(nil) - end - end - - context 'when the policy should not be enforced to the pipeline source' do - Enums::Ci::Pipeline.dangling_sources.except(:security_orchestration_policy).each_key do |source| - context "when pipeline source is #{source}" do - let(:source) { source } - - it_behaves_like 'does not include security policies default pipeline configuration content' - end - end - end - end - end - end - - context 'when project has active pipeline execution policies' do - shared_examples_for 'forces the pipeline creation by including dummy content' do - let(:expected_content) { YAML.dump(Gitlab::Ci::ProjectConfig::PipelineExecutionPolicyForced::DUMMY_CONTENT) } - - it 'includes dummy job to force the pipeline creation' do - expect(config.source).to eq(:pipeline_execution_policy_forced) - expect(config.content).to eq(expected_content) - end - end - - let(:has_pipeline_execution_policies) { true } - - it_behaves_like 'forces the pipeline creation by including dummy content' - - context 'when auto devops is not enabled' do - before do - stub_application_setting(auto_devops_enabled: false) - end - - it_behaves_like 'forces the pipeline creation by including dummy content' - end - end - end -end diff --git a/lib/gitlab/ci/project_config.rb b/lib/gitlab/ci/project_config.rb index 72ced07619929..dd00c2b254244 100644 --- a/lib/gitlab/ci/project_config.rb +++ b/lib/gitlab/ci/project_config.rb @@ -6,19 +6,25 @@ module Ci class ProjectConfig # The order of sources is important: # - EE uses Compliance first since it must be used first if compliance templates are enabled. - # (see ee/lib/ee/gitlab/ci/project_config.rb) # - Parameter is used by on-demand security scanning which passes the actual CI YAML to use as argument. # - Bridge is used for downstream pipelines since the config is defined in the bridge job. If lower in priority, # it would evaluate the project's YAML file instead. # - Repository / ExternalProject / Remote: their order is not important between each other. + # - EE uses PipelineExecutionPolicyForced and it must come before AutoDevops because + # it handles the empty CI config case. + # We want to run Pipeline Execution Policies instead of AutoDevops (if they are present). # - AutoDevops is used as default option if nothing else is found and if AutoDevops is enabled. + # - EE uses SecurityPolicyDefault and it should come last. It is only necessary if no other source is available. SOURCES = [ + ProjectConfig::Compliance, ProjectConfig::Parameter, ProjectConfig::Bridge, ProjectConfig::Repository, ProjectConfig::ExternalProject, ProjectConfig::Remote, - ProjectConfig::AutoDevops + ProjectConfig::PipelineExecutionPolicyForced, + ProjectConfig::AutoDevops, + ProjectConfig::SecurityPolicyDefault ].freeze def initialize( @@ -58,5 +64,3 @@ def sources end end end - -Gitlab::Ci::ProjectConfig.prepend_mod_with('Gitlab::Ci::ProjectConfig') diff --git a/lib/gitlab/ci/project_config/compliance.rb b/lib/gitlab/ci/project_config/compliance.rb new file mode 100644 index 0000000000000..bbc058817e666 --- /dev/null +++ b/lib/gitlab/ci/project_config/compliance.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class ProjectConfig + class Compliance < Gitlab::Ci::ProjectConfig::Source + # rubocop:disable Gitlab/NoCodeCoverageComment -- overridden and tested in EE + # :nocov: + def content + nil + end + # :nocov: + # rubocop:enable Gitlab/NoCodeCoverageComment + + def internal_include_prepended? + true + end + + def source + :compliance_source + end + end + end + end +end + +Gitlab::Ci::ProjectConfig::Compliance.prepend_mod diff --git a/lib/gitlab/ci/project_config/pipeline_execution_policy_forced.rb b/lib/gitlab/ci/project_config/pipeline_execution_policy_forced.rb new file mode 100644 index 0000000000000..2582d816d0a86 --- /dev/null +++ b/lib/gitlab/ci/project_config/pipeline_execution_policy_forced.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class ProjectConfig + class PipelineExecutionPolicyForced < Gitlab::Ci::ProjectConfig::Source + # rubocop:disable Gitlab/NoCodeCoverageComment -- overridden and tested in EE + # :nocov: + def content + nil + end + # :nocov: + # rubocop:enable Gitlab/NoCodeCoverageComment + + def source + :pipeline_execution_policy_forced + end + end + end + end +end + +Gitlab::Ci::ProjectConfig::PipelineExecutionPolicyForced.prepend_mod diff --git a/lib/gitlab/ci/project_config/security_policy_default.rb b/lib/gitlab/ci/project_config/security_policy_default.rb new file mode 100644 index 0000000000000..d2b46780ce0e6 --- /dev/null +++ b/lib/gitlab/ci/project_config/security_policy_default.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class ProjectConfig + class SecurityPolicyDefault < Gitlab::Ci::ProjectConfig::Source + # rubocop:disable Gitlab/NoCodeCoverageComment -- overridden and tested in EE + # :nocov: + def content + nil + end + # :nocov: + # rubocop:enable Gitlab/NoCodeCoverageComment + + def source + :security_policies_default_source + end + end + end + end +end + +Gitlab::Ci::ProjectConfig::SecurityPolicyDefault.prepend_mod diff --git a/lib/gitlab/ci/project_config/source.rb b/lib/gitlab/ci/project_config/source.rb index b7a00c0f2dce9..1b3c565d47f4b 100644 --- a/lib/gitlab/ci/project_config/source.rb +++ b/lib/gitlab/ci/project_config/source.rb @@ -44,7 +44,8 @@ def url private - attr_reader :project, :sha, :custom_content, :pipeline_source, :pipeline_source_bridge + attr_reader :project, :sha, :custom_content, :pipeline_source, :pipeline_source_bridge, :triggered_for_branch, + :ref, :has_pipeline_execution_policies def ci_config_path @ci_config_path ||= project.ci_config_path_or_default diff --git a/spec/lib/gitlab/ci/project_config_spec.rb b/spec/lib/gitlab/ci/project_config_spec.rb index 76030f7c0b2f3..3405fb50f2273 100644 --- a/spec/lib/gitlab/ci/project_config_spec.rb +++ b/spec/lib/gitlab/ci/project_config_spec.rb @@ -3,11 +3,14 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::ProjectConfig, feature_category: :pipeline_composition do - let_it_be(:project) { create(:project, :empty_repo) } + let_it_be_with_reload(:project) { create(:project, :empty_repo) } let(:sha) { '123456' } let(:content) { nil } let(:source) { :push } let(:bridge) { nil } + let(:triggered_for_branch) { true } + let(:ref) { 'master' } + let(:has_pipeline_execution_policies) { false } subject(:config) do described_class.new( @@ -15,7 +18,10 @@ sha: sha, custom_content: content, pipeline_source: source, - pipeline_source_bridge: bridge + pipeline_source_bridge: bridge, + triggered_for_branch: triggered_for_branch, + ref: ref, + has_pipeline_execution_policies: has_pipeline_execution_policies ) end -- GitLab