diff --git a/ee/app/validators/json_schemas/security_orchestration_policy.json b/ee/app/validators/json_schemas/security_orchestration_policy.json index 6bb8ad4877468bafb8a8bf3bb96d54c08fe3c514..ae480cf2b3af829af0d49b946de87ee707404a31 100644 --- a/ee/app/validators/json_schemas/security_orchestration_policy.json +++ b/ee/app/validators/json_schemas/security_orchestration_policy.json @@ -1781,6 +1781,14 @@ "policy_scope": { "type": "object", "properties": { + "operator": { + "description": "Specifies the logical operator used to combine multiple policy scope conditions. Use 'AND' to require all specified conditions to be met (more restrictive), or 'OR' to require at least one condition to be met (more permissive).", + "type": "string", + "enum": [ + "AND", + "OR" + ] + }, "compliance_frameworks": { "description": "Specifies for which compliance frameworks this policy should be applied to.", "type": "array", diff --git a/ee/lib/security/security_orchestration_policies/policy_scope_checker.rb b/ee/lib/security/security_orchestration_policies/policy_scope_checker.rb index 226c81d19603e36c4991e87bbffb4e53a2ac15ea..59ecab056843ab886064f666f69171d33610cbf5 100644 --- a/ee/lib/security/security_orchestration_policies/policy_scope_checker.rb +++ b/ee/lib/security/security_orchestration_policies/policy_scope_checker.rb @@ -5,6 +5,8 @@ module SecurityOrchestrationPolicies class PolicyScopeChecker include Gitlab::InternalEventsTracking + OR_OPERATOR = 'OR' + def initialize(project:) @project = project end @@ -28,9 +30,19 @@ def security_policy_applicable?(security_policy) attr_accessor :project def scope_applicable?(policy_scope) - applicable_for_compliance_framework?(policy_scope) && - applicable_for_project?(policy_scope) && + [ + applicable_for_compliance_framework?(policy_scope), + applicable_for_project?(policy_scope), applicable_for_group?(policy_scope) + ].inject(or_operator?(policy_scope) ? :| : :&) + end + + def or_operator?(policy_scope) + policy_scope[:operator] == OR_OPERATOR + end + + def default_value_for_operator(policy_scope) + or_operator?(policy_scope) ? false : true end def applicable_for_compliance_framework?(policy_scope) @@ -38,7 +50,7 @@ def applicable_for_compliance_framework?(policy_scope) track_policy_scope_check(:compliance_framework, [policy_scope_compliance_frameworks]) - return true if policy_scope_compliance_frameworks.blank? + return default_value_for_operator(policy_scope) if policy_scope_compliance_frameworks.blank? compliance_framework_ids = project.compliance_framework_ids return false if compliance_framework_ids.blank? @@ -52,6 +64,10 @@ def applicable_for_project?(policy_scope) track_policy_scope_check(:project, [policy_scope_included_projects, policy_scope_excluded_projects]) + if policy_scope_excluded_projects.blank? && policy_scope_included_projects.blank? + return default_value_for_operator(policy_scope) + end + return false if policy_scope_excluded_projects.any? { |policy_project| policy_project[:id] == project.id } return true if policy_scope_included_projects.blank? @@ -64,7 +80,9 @@ def applicable_for_group?(policy_scope) track_policy_scope_check(:group, [policy_scope_included_groups, policy_scope_excluded_groups]) - return true if policy_scope_included_groups.blank? && policy_scope_excluded_groups.blank? + if policy_scope_included_groups.blank? && policy_scope_excluded_groups.blank? + return default_value_for_operator(policy_scope) + end ancestor_group_ids = project.group&.self_and_ancestor_ids.to_a diff --git a/ee/spec/lib/security/security_orchestration_policies/policy_scope_checker_spec.rb b/ee/spec/lib/security/security_orchestration_policies/policy_scope_checker_spec.rb index 2bdaec738957ac9593c89fa29e09f3173586b946..b61e74b7dc3c91e8c1119c83b6a49a5b53e86055 100644 --- a/ee/spec/lib/security/security_orchestration_policies/policy_scope_checker_spec.rb +++ b/ee/spec/lib/security/security_orchestration_policies/policy_scope_checker_spec.rb @@ -5,6 +5,7 @@ RSpec.describe Security::SecurityOrchestrationPolicies::PolicyScopeChecker, feature_category: :security_policy_management do let_it_be_with_refind(:root_group) { create(:group) } let_it_be_with_refind(:group) { create(:group, parent: root_group) } + let_it_be_with_refind(:other_group) { create(:group) } let_it_be_with_refind(:project) { create(:project, group: group) } let_it_be(:compliance_framework) { create(:compliance_framework, namespace: root_group) } @@ -17,6 +18,12 @@ it { is_expected.to eq true } end + context 'when only operator is set in policy scope' do + let(:policy_scope) { { operator: 'AND' } } + + it { is_expected.to eq true } + end + context 'when policy is scoped for compliance framework' do let(:policy_scope) do { @@ -59,6 +66,28 @@ end it { is_expected.to eq true } + + context 'and when AND operator is provided' do + let(:policy_scope) do + { + operator: 'AND', + compliance_frameworks: [{ id: compliance_framework_2.id }] + } + end + + it { is_expected.to eq true } + end + + context 'and when OR operator is provided' do + let(:policy_scope) do + { + operator: 'OR', + compliance_frameworks: [{ id: compliance_framework_2.id }] + } + end + + it { is_expected.to eq true } + end end context 'when policy additionally excludes the project from policy' do @@ -72,6 +101,34 @@ end it { is_expected.to eq false } + + context 'and when AND operator is provided' do + let(:policy_scope) do + { + operator: 'AND', + compliance_frameworks: [{ id: compliance_framework.id }], + projects: { + excluding: [{ id: project.id }] + } + } + end + + it { is_expected.to eq false } + end + + context 'and when OR operator is provided' do + let(:policy_scope) do + { + operator: 'OR', + compliance_frameworks: [{ id: compliance_framework.id }], + projects: { + excluding: [{ id: project.id }] + } + } + end + + it { is_expected.to eq true } + end end context 'when non-existing compliance framework is set' do @@ -130,6 +187,100 @@ it { is_expected.to eq false } end + + context 'when additionally excluding group scope is matching same group as project' do + let(:policy_scope) do + { + projects: { + including: [{ id: project.id }] + }, + groups: { + excluding: [{ id: group.id }] + } + } + end + + it { is_expected.to eq false } + + context 'and when AND operator is provided' do + let(:policy_scope) do + { + operator: 'AND', + projects: { + including: [{ id: project.id }] + }, + groups: { + excluding: [{ id: group.id }] + } + } + end + + it { is_expected.to eq false } + end + + context 'and when OR operator is provided' do + let(:policy_scope) do + { + operator: 'OR', + projects: { + including: [{ id: project.id }] + }, + groups: { + excluding: [{ id: group.id }] + } + } + end + + it { is_expected.to eq true } + end + end + + context 'when additionally including group scope is not matching same group as project' do + let(:policy_scope) do + { + projects: { + including: [{ id: project.id }] + }, + groups: { + including: [{ id: other_group.id }] + } + } + end + + it { is_expected.to eq false } + + context 'and when AND operator is provided' do + let(:policy_scope) do + { + operator: 'AND', + projects: { + including: [{ id: project.id }] + }, + groups: { + including: [{ id: other_group.id }] + } + } + end + + it { is_expected.to eq false } + end + + context 'and when OR operator is provided' do + let(:policy_scope) do + { + operator: 'OR', + projects: { + including: [{ id: project.id }] + }, + groups: { + including: [{ id: other_group.id }] + } + } + end + + it { is_expected.to eq true } + end + end end end