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 808ee699c1b14152b60ce1f1767a771704c28d3e..a445dd78f3da751aa79ff7e17bb9baa996d9deef 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 e831e21e225642b1414709a8f1467691a171d0a8..622172a528c3030762fbcfc65d725ba06cea905a 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 6dec884a2b695efb10efae528da7c23cef75090a..4333a678e8acf7ed3684875f4abbe809a4bbc880 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 a911edbeadb4112c3326a185393c0096b13941d1..2b958f5179c337d8df1fff3b4a157a78b9f9c242 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 0000000000000000000000000000000000000000..53763e84c637a6a43e378275cd4b177a428a412c --- /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 49ea5683bba199e1acbcd04634d54f3ba8d27596..584b6f963742a0b96dc04ae8f9a149239fd3bdfe 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 415b16888ca443c7b9d2fe2efb557ac59cd91a01..79fa2ea0128df7cad0075830c3160a6184ebe64b 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 5ab5962f4fb0f3080794109f3e437b42853a4e1a..cb3ac0c8e53ac477a429dde550c6ac4a6548ba52 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 77182b531f9e067e09732e98b953d8a298540747..09777c13ea0989b110ca7cf695af487dc1a22cbb 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 ba0064556d548a6324eb77268d46e4038cca2d77..0dec163db693cfd0adf5d65a0bf0202e0f4ab630 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 04d320b7aa133df47f39c0e6f3f8886985db53ef..36df5239eb3b9856604e9b3e3dffbfd03b8729f9 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 c0be3e4288ecef6c56b86eb502a670e958f1581c..026e150e12a287c1de63503223204b9e7469550a 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