From c74e85c28d1ea10df3582e4f936e1edb675ab8a9 Mon Sep 17 00:00:00 2001 From: Lorenz van Herwaarden <lvanherwaarden@gitlab.com> Date: Tue, 17 Dec 2024 04:06:56 +0000 Subject: [PATCH] Support group-level vulnerability management policy type in frontend This adds checks for the vulnerability management policy type group feature flag in the policy list, app, and type selector components. It also guards the resolver for group vulnerability management policies behind the same feature flag. --- .../components/policies/app.vue | 7 +- .../policies/filters/type_filter.vue | 3 +- .../components/policies/list_component.vue | 3 +- .../components/policies/utils.js | 11 ++- ...rability_management_policies.query.graphql | 26 ++++++ ...ulnerability_management_policy_resolver.rb | 8 +- ...erability_management_policy_type_group.yml | 2 +- .../components/policies/app_spec.js | 87 +++++++++++++++---- .../policies/filters/type_filter_spec.js | 20 +++-- .../components/policies/utils_spec.js | 2 + .../mocks/mock_apollo.js | 6 ++ ...ability_management_policy_resolver_spec.rb | 14 ++- 12 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 ee/app/assets/javascripts/security_orchestration/graphql/queries/group_vulnerability_management_policies.query.graphql rename ee/config/feature_flags/{wip => beta}/vulnerability_management_policy_type_group.yml (96%) diff --git a/ee/app/assets/javascripts/security_orchestration/components/policies/app.vue b/ee/app/assets/javascripts/security_orchestration/components/policies/app.vue index 808ee699c1b14..a445dd78f3da7 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policies/app.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policies/app.vue @@ -18,6 +18,7 @@ import groupScanResultPoliciesQuery from '../../graphql/queries/group_scan_resul import projectPipelineExecutionPoliciesQuery from '../../graphql/queries/project_pipeline_execution_policies.query.graphql'; import groupPipelineExecutionPoliciesQuery from '../../graphql/queries/group_pipeline_execution_policies.query.graphql'; import projectVulnerabilityManagementPoliciesQuery from '../../graphql/queries/project_vulnerability_management_policies.query.graphql'; +import groupVulnerabilityManagementPoliciesQuery from '../../graphql/queries/group_vulnerability_management_policies.query.graphql'; import ListHeader from './list_header.vue'; import ListComponent from './list_component.vue'; import { @@ -41,6 +42,7 @@ const NAMESPACE_QUERY_DICT = { }, vulnerabilityManagement: { [NAMESPACE_TYPES.PROJECT]: projectVulnerabilityManagementPoliciesQuery, + [NAMESPACE_TYPES.GROUP]: groupVulnerabilityManagementPoliciesQuery, }, }; @@ -215,7 +217,10 @@ export default { ); }, vulnerabilityManagementPolicyEnabled() { - return this.glFeatures.vulnerabilityManagementPolicyType; + return ( + this.glFeatures.vulnerabilityManagementPolicyType || + this.glFeatures.vulnerabilityManagementPolicyTypeGroup + ); }, }, methods: { diff --git a/ee/app/assets/javascripts/security_orchestration/components/policies/filters/type_filter.vue b/ee/app/assets/javascripts/security_orchestration/components/policies/filters/type_filter.vue index e831e21e22564..622172a528c30 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policies/filters/type_filter.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policies/filters/type_filter.vue @@ -21,7 +21,8 @@ export default { }, computed: { options() { - return this.glFeatures.vulnerabilityManagementPolicyType + return this.glFeatures.vulnerabilityManagementPolicyType || + this.glFeatures.vulnerabilityManagementPolicyTypeGroup ? { ...POLICY_TYPE_FILTER_OPTIONS, ...VULNERABILITY_MANAGEMENT_FILTER_OPTION, diff --git a/ee/app/assets/javascripts/security_orchestration/components/policies/list_component.vue b/ee/app/assets/javascripts/security_orchestration/components/policies/list_component.vue index 6dec884a2b695..4333a678e8acf 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policies/list_component.vue +++ b/ee/app/assets/javascripts/security_orchestration/components/policies/list_component.vue @@ -144,7 +144,8 @@ export default { }, computed: { policyTypeFilterOptions() { - return this.glFeatures.vulnerabilityManagementPolicyType + return this.glFeatures.vulnerabilityManagementPolicyType || + this.glFeatures.vulnerabilityManagementPolicyTypeGroup ? { ...POLICY_TYPE_FILTER_OPTIONS, ...VULNERABILITY_MANAGEMENT_FILTER_OPTION, diff --git a/ee/app/assets/javascripts/security_orchestration/components/policies/utils.js b/ee/app/assets/javascripts/security_orchestration/components/policies/utils.js index a911edbeadb41..2b958f5179c33 100644 --- a/ee/app/assets/javascripts/security_orchestration/components/policies/utils.js +++ b/ee/app/assets/javascripts/security_orchestration/components/policies/utils.js @@ -28,9 +28,14 @@ const validateFilter = (allowedValues, value, lowerCase = false) => { * @returns {boolean} */ export const validateTypeFilter = (value) => { - const options = gon.features?.vulnerabilityManagementPolicyType - ? { ...POLICY_TYPE_FILTER_OPTIONS, ...VULNERABILITY_MANAGEMENT_FILTER_OPTION } - : POLICY_TYPE_FILTER_OPTIONS; + const { vulnerabilityManagementPolicyType, vulnerabilityManagementPolicyTypeGroup } = + window.gon.features || {}; + + let options = POLICY_TYPE_FILTER_OPTIONS; + if (vulnerabilityManagementPolicyType || vulnerabilityManagementPolicyTypeGroup) { + options = { ...options, ...VULNERABILITY_MANAGEMENT_FILTER_OPTION }; + } + return validateFilter(options, value, true); }; diff --git a/ee/app/assets/javascripts/security_orchestration/graphql/queries/group_vulnerability_management_policies.query.graphql b/ee/app/assets/javascripts/security_orchestration/graphql/queries/group_vulnerability_management_policies.query.graphql new file mode 100644 index 0000000000000..53763e84c637a --- /dev/null +++ b/ee/app/assets/javascripts/security_orchestration/graphql/queries/group_vulnerability_management_policies.query.graphql @@ -0,0 +1,26 @@ +#import "../fragments/scan_policy_source.fragment.graphql" +#import "../fragments/policy_scope.fragment.graphql" + +query groupVulnerabilityManagementPolicies( + $fullPath: ID! + $relationship: SecurityPolicyRelationType = INHERITED +) { + namespace: group(fullPath: $fullPath) { + id + vulnerabilityManagementPolicies(relationship: $relationship) { + nodes { + name + yaml + editPath + enabled + policyScope { + ...PolicyScope + } + source { + ...SecurityPolicySource + } + updatedAt + } + } + } +} diff --git a/ee/app/graphql/resolvers/security/vulnerability_management_policy_resolver.rb b/ee/app/graphql/resolvers/security/vulnerability_management_policy_resolver.rb index 49ea5683bba19..584b6f963742a 100644 --- a/ee/app/graphql/resolvers/security/vulnerability_management_policy_resolver.rb +++ b/ee/app/graphql/resolvers/security/vulnerability_management_policy_resolver.rb @@ -18,11 +18,15 @@ class VulnerabilityManagementPolicyResolver < BaseResolver default_value: true def resolve(**args) - if Feature.disabled?(:vulnerability_management_policy_type, project) + if object.is_a?(Group) && Feature.disabled?(:vulnerability_management_policy_type_group, object) + raise_resource_not_available_error! '`vulnerability_management_policy_type_group` feature flag is disabled.' + end + + if object.is_a?(Project) && Feature.disabled?(:vulnerability_management_policy_type, object) raise_resource_not_available_error! '`vulnerability_management_policy_type` feature flag is disabled.' end - policies = ::Security::VulnerabilityManagementPoliciesFinder.new(context[:current_user], project, args).execute + policies = ::Security::VulnerabilityManagementPoliciesFinder.new(context[:current_user], object, args).execute construct_vulnerability_management_policies(policies) end end diff --git a/ee/config/feature_flags/wip/vulnerability_management_policy_type_group.yml b/ee/config/feature_flags/beta/vulnerability_management_policy_type_group.yml similarity index 96% rename from ee/config/feature_flags/wip/vulnerability_management_policy_type_group.yml rename to ee/config/feature_flags/beta/vulnerability_management_policy_type_group.yml index 415b16888ca44..79fa2ea0128df 100644 --- a/ee/config/feature_flags/wip/vulnerability_management_policy_type_group.yml +++ b/ee/config/feature_flags/beta/vulnerability_management_policy_type_group.yml @@ -5,5 +5,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155858 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/467370 milestone: '17.2' group: group::security insights -type: wip +type: beta default_enabled: false diff --git a/ee/spec/frontend/security_orchestration/components/policies/app_spec.js b/ee/spec/frontend/security_orchestration/components/policies/app_spec.js index 5ab5962f4fb0f..cb3ac0c8e53ac 100644 --- a/ee/spec/frontend/security_orchestration/components/policies/app_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policies/app_spec.js @@ -23,6 +23,7 @@ import groupScanResultPoliciesQuery from 'ee/security_orchestration/graphql/quer import projectPipelineExecutionPoliciesQuery from 'ee/security_orchestration/graphql/queries/project_pipeline_execution_policies.query.graphql'; import groupPipelineExecutionPoliciesQuery from 'ee/security_orchestration/graphql/queries/group_pipeline_execution_policies.query.graphql'; import projectVulnerabilityManagementPoliciesQuery from 'ee/security_orchestration/graphql/queries/project_vulnerability_management_policies.query.graphql'; +import groupVulnerabilityManagementPoliciesQuery from 'ee/security_orchestration/graphql/queries/group_vulnerability_management_policies.query.graphql'; import { mockPipelineExecutionPoliciesResponse } from '../../mocks/mock_pipeline_execution_policy_data'; import { mockVulnerabilityManagementPoliciesResponse } from '../../mocks/mock_vulnerability_management_policy_data'; import { @@ -30,9 +31,10 @@ import { groupScanExecutionPolicies, projectScanResultPolicies, groupScanResultPolicies, - groupPipelineResultPolicies, projectPipelineResultPolicies, + groupPipelineResultPolicies, projectVulnerabilityManagementPolicies, + groupVulnerabilityManagementPolicies, mockLinkedSppItemsResponse, } from '../../mocks/mock_apollo'; import { @@ -61,6 +63,9 @@ const groupPipelineExecutionPoliciesSpy = groupPipelineResultPolicies( const projectVulnerabilityManagementPoliciesSpy = projectVulnerabilityManagementPolicies( mockVulnerabilityManagementPoliciesResponse, ); +const groupVulnerabilityManagementPoliciesSpy = groupVulnerabilityManagementPolicies( + mockVulnerabilityManagementPoliciesResponse, +); const linkedSppItemsResponseSpy = mockLinkedSppItemsResponse(); const defaultRequestHandlers = { @@ -71,6 +76,7 @@ const defaultRequestHandlers = { projectPipelineExecutionPolicies: projectPipelineExecutionPoliciesSpy, groupPipelineExecutionPolicies: groupPipelineExecutionPoliciesSpy, projectVulnerabilityManagementPolicies: projectVulnerabilityManagementPoliciesSpy, + groupVulnerabilityManagementPolicies: groupVulnerabilityManagementPoliciesSpy, linkedSppItemsResponse: linkedSppItemsResponseSpy, }; @@ -105,6 +111,10 @@ describe('App', () => { projectVulnerabilityManagementPoliciesQuery, requestHandlers.projectVulnerabilityManagementPolicies, ], + [ + groupVulnerabilityManagementPoliciesQuery, + requestHandlers.groupVulnerabilityManagementPolicies, + ], ]), }); }; @@ -126,6 +136,11 @@ describe('App', () => { createWrapper({ provide: { glFeatures: { vulnerabilityManagementPolicyType: true } } }); expect(findPoliciesList().props('isLoadingPolicies')).toBe(true); }); + + it('renders the policies list correctly when vulnerabilityManagementPolicyTypeGroup is true', () => { + createWrapper({ provide: { glFeatures: { vulnerabilityManagementPolicyTypeGroup: true } } }); + expect(findPoliciesList().props('isLoadingPolicies')).toBe(true); + }); }); describe('default', () => { @@ -151,36 +166,39 @@ describe('App', () => { }); }); - describe('when vulnerabilityManagementPolicyType is false', () => { - it.each` - type | projectHandler - ${'vulnerability management'} | ${'projectVulnerabilityManagementPolicies'} - `('does not fetch project-level $type policies', ({ projectHandler }) => { - expect(requestHandlers[projectHandler]).not.toHaveBeenCalled(); - }); + it('does not fetch project-level vulnerability management policies', () => { + expect(requestHandlers.projectVulnerabilityManagementPolicies).not.toHaveBeenCalled(); }); describe('when vulnerabilityManagementPolicyType is true', () => { beforeEach(async () => { - gon.features = { vulnerabilityManagementPolicyType: true }; - createWrapper({ provide: { glFeatures: { vulnerabilityManagementPolicyType: true } }, }); await waitForPromises(); }); - it.each` - type | projectHandler - ${'vulnerability management'} | ${'projectVulnerabilityManagementPolicies'} - `('fetches project-level $type policies', ({ projectHandler }) => { - expect(requestHandlers[projectHandler]).toHaveBeenCalledWith({ + it('fetches project-level vulnerability management policies', () => { + expect(requestHandlers.projectVulnerabilityManagementPolicies).toHaveBeenCalledWith({ fullPath: namespacePath, relationship: POLICY_SOURCE_OPTIONS.ALL.value, }); }); }); + describe('when vulnerabilityManagementPolicyTypeGroup is true', () => { + beforeEach(async () => { + createWrapper({ + provide: { glFeatures: { vulnerabilityManagementPolicyTypeGroup: true } }, + }); + await waitForPromises(); + }); + + it('does not fetch group-level vulnerability management policies', () => { + expect(requestHandlers.groupVulnerabilityManagementPolicies).not.toHaveBeenCalled(); + }); + }); + it('renders the policy header correctly', () => { expect(findPoliciesHeader().props('hasInvalidPolicies')).toBe(false); }); @@ -264,6 +282,45 @@ describe('App', () => { expect(linkedSppItemsResponseSpy).toHaveBeenCalledTimes(0); }); + it('does not fetch group-level vulnerability management policies', () => { + expect(requestHandlers.groupVulnerabilityManagementPolicies).not.toHaveBeenCalled(); + }); + + describe('when vulnerabilityManagementPolicyTypeGroup is true', () => { + beforeEach(async () => { + createWrapper({ + provide: { + namespaceType: NAMESPACE_TYPES.GROUP, + glFeatures: { vulnerabilityManagementPolicyTypeGroup: true }, + }, + }); + await waitForPromises(); + }); + + it('fetches group-level vulnerability management polices', () => { + expect(requestHandlers.groupVulnerabilityManagementPolicies).toHaveBeenCalledWith({ + fullPath: namespacePath, + relationship: POLICY_SOURCE_OPTIONS.ALL.value, + }); + }); + }); + + describe('when vulnerabilityManagementPolicyType is true', () => { + beforeEach(async () => { + createWrapper({ + provide: { + namespaceType: NAMESPACE_TYPES.GROUP, + glFeatures: { vulnerabilityManagementPolicyType: true }, + }, + }); + await waitForPromises(); + }); + + it('does not fetch project-level vulnerability management policies', () => { + expect(requestHandlers.projectVulnerabilityManagementPolicies).not.toHaveBeenCalled(); + }); + }); + it.each` type | groupHandler | projectHandler ${'scan execution'} | ${'groupScanExecutionPolicies'} | ${'projectScanExecutionPolicies'} diff --git a/ee/spec/frontend/security_orchestration/components/policies/filters/type_filter_spec.js b/ee/spec/frontend/security_orchestration/components/policies/filters/type_filter_spec.js index 77182b531f9e0..09777c13ea098 100644 --- a/ee/spec/frontend/security_orchestration/components/policies/filters/type_filter_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policies/filters/type_filter_spec.js @@ -9,15 +9,19 @@ import TypeFilter from 'ee/security_orchestration/components/policies/filters/ty describe('TypeFilter component', () => { let wrapper; - const createWrapper = ({ value = '', vulnerabilityManagementPolicyType = true } = {}) => { + const createWrapper = ({ + value = '', + glFeatures = { + vulnerabilityManagementPolicyTypeGroup: true, + vulnerabilityManagementPolicyType: true, + }, + } = {}) => { wrapper = shallowMount(TypeFilter, { propsData: { value, }, provide: { - glFeatures: { - vulnerabilityManagementPolicyType, - }, + glFeatures, }, stubs: { GlCollapsibleListbox, @@ -37,7 +41,13 @@ describe('TypeFilter component', () => { }); it('does not pass vulnerability management option when feature flag is disabled', () => { - createWrapper({ vulnerabilityManagementPolicyType: false }); + // Both project-level and group-level feature flags need to be disabled + createWrapper({ + glFeatures: { + vulnerabilityManagementPolicyType: false, + vulnerabilityManagementPolicyTypeGroup: false, + }, + }); expect(findToggle().props('items')).toMatchObject(Object.values(POLICY_TYPE_FILTER_OPTIONS)); }); diff --git a/ee/spec/frontend/security_orchestration/components/policies/utils_spec.js b/ee/spec/frontend/security_orchestration/components/policies/utils_spec.js index ba0064556d548..0dec163db693c 100644 --- a/ee/spec/frontend/security_orchestration/components/policies/utils_spec.js +++ b/ee/spec/frontend/security_orchestration/components/policies/utils_spec.js @@ -63,8 +63,10 @@ describe('utils', () => { }); it('returns false for vulnerability management filter option when feature flag is disabled', () => { + // Both project-level and group-level feature flags need to be disabled window.gon.features = { vulnerabilityManagementPolicyType: false, + vulnerabilityManagementPolicyTypeGroup: false, }; expect( validateTypeFilter( diff --git a/ee/spec/frontend/security_orchestration/mocks/mock_apollo.js b/ee/spec/frontend/security_orchestration/mocks/mock_apollo.js index 04d320b7aa133..36df5239eb3b9 100644 --- a/ee/spec/frontend/security_orchestration/mocks/mock_apollo.js +++ b/ee/spec/frontend/security_orchestration/mocks/mock_apollo.js @@ -43,6 +43,12 @@ export const projectVulnerabilityManagementPolicies = (nodes) => namespaceType: 'Project', policyType: 'vulnerabilityManagementPolicies', }); +export const groupVulnerabilityManagementPolicies = (nodes) => + mockPolicyResponse({ + nodes, + namespaceType: 'Group', + policyType: 'vulnerabilityManagementPolicies', + }); export const mockLinkSecurityPolicyProjectResponses = { success: jest.fn().mockResolvedValue({ diff --git a/ee/spec/graphql/resolvers/security/vulnerability_management_policy_resolver_spec.rb b/ee/spec/graphql/resolvers/security/vulnerability_management_policy_resolver_spec.rb index c0be3e4288ece..026e150e12a28 100644 --- a/ee/spec/graphql/resolvers/security/vulnerability_management_policy_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/security/vulnerability_management_policy_resolver_spec.rb @@ -40,7 +40,7 @@ it_behaves_like 'as an orchestration policy' - context 'when feature flag `vulnerability_management_policy_type` disabled' do + context 'when feature flag `vulnerability_management_policy_type` disabled and project' do before do stub_feature_flags(vulnerability_management_policy_type: false) end @@ -51,4 +51,16 @@ expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable) end end + + context 'when feature flag `vulnerability_management_policy_type_group` disabled and group' do + before do + stub_feature_flags(vulnerability_management_policy_type_group: false) + end + + it 'returns a resource not available error' do + result = resolve(described_class, obj: Group.new, ctx: { current_user: user }) + + expect(result).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end end -- GitLab