Skip to content
代码片段 群组 项目
未验证 提交 7a4df91e 编辑于 作者: Artur Fedorov's avatar Artur Fedorov 提交者: GitLab
浏览文件

This MR adds policy scope on project level

Now user can also add policy scope for
spp projects with multiple dependencies

Changelog: added
EE: true
上级 1a6d26be
No related branches found
No related tags found
无相关合并请求
......@@ -138,11 +138,11 @@ export default {
return this.namespaceType === NAMESPACE_TYPES.GROUP;
},
shouldShowScope() {
return (
this.glFeatures.securityPoliciesPolicyScope &&
this.securityPoliciesPolicyScopeToggleEnabled &&
this.isGroupLevel
);
const featureFlagEnabled = this.isGroupLevel
? this.glFeatures.securityPoliciesPolicyScope
: this.glFeatures.securityPoliciesPolicyScopeProject;
return featureFlagEnabled && this.securityPoliciesPolicyScopeToggleEnabled;
},
deleteModalTitle() {
return sprintf(s__('SecurityOrchestration|Delete policy: %{policy}'), {
......
<script>
import { GlAlert, GlCollapsibleListbox, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { GlAlert, GlCollapsibleListbox, GlIcon, GlSprintf, GlLoadingIcon } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { NAMESPACE_TYPES } from 'ee/security_orchestration/constants';
import PolicyPopover from 'ee/security_orchestration/components/policy_popover.vue';
import getSppLinkedProjectsNamespaces from 'ee/security_orchestration/graphql/queries/get_spp_linked_projects_namespaces.graphql';
import GroupProjectsDropdown from '../../group_projects_dropdown.vue';
import ComplianceFrameworkDropdown from './compliance_framework_dropdown.vue';
import {
......@@ -28,6 +32,13 @@ export default {
PROJECT_SCOPE_TYPE_LISTBOX_ITEMS,
EXCEPTION_TYPE_LISTBOX_ITEMS,
i18n: {
policyScopeLoadingText: s__('SecurityOrchestration|Fetching the scope information.'),
policyScopeErrorText: s__(
'SecurityOrchestration|Failed to fetch the scope information. Please refresh the page to try again.',
),
policyScopeFrameworkCopyProject: s__(
'SecurityOrchestration|Apply this policy to current project.',
),
policyScopeFrameworkCopy: s__(
`SecurityOrchestration|Apply this policy to %{projectScopeType}named %{frameworkSelector}`,
),
......@@ -45,14 +56,47 @@ export default {
},
name: 'ScopeSection',
components: {
ComplianceFrameworkDropdown,
GlAlert,
GlIcon,
GlCollapsibleListbox,
ComplianceFrameworkDropdown,
GlLoadingIcon,
GlSprintf,
GroupProjectsDropdown,
PolicyPopover,
},
inject: ['namespacePath', 'rootNamespacePath'],
apollo: {
linkedSppItems: {
query: getSppLinkedProjectsNamespaces,
variables() {
return {
fullPath: this.namespacePath,
};
},
update(data) {
const {
securityPolicyProjectLinkedProjects: { nodes: linkedProjects = [] },
securityPolicyProjectLinkedNamespaces: { nodes: linkedNamespaces = [] },
} = data?.project || {};
const items = [...linkedProjects, ...linkedNamespaces];
if (isEmpty(this.policyScope) && items.length > 1 && !this.isGroupLevel) {
this.triggerChanged({ compliance_frameworks: [] });
}
return items;
},
error() {
this.showLinkedSppItemsError = true;
},
skip() {
return this.shouldSkipDependenciesCheck;
},
},
},
mixins: [glFeatureFlagsMixin()],
inject: ['namespacePath', 'rootNamespacePath', 'namespaceType'],
props: {
policyScope: {
type: Object,
......@@ -83,9 +127,26 @@ export default {
projectsPayloadKey,
showAlert: false,
errorDescription: '',
linkedSppItems: [],
showLinkedSppItemsError: false,
};
},
computed: {
isGroupLevel() {
return this.namespaceType === NAMESPACE_TYPES.GROUP;
},
shouldSkipDependenciesCheck() {
return this.isGroupLevel || !this.glFeatures.securityPoliciesPolicyScopeProject;
},
groupProjectsFullPath() {
return this.isGroupLevel ? this.namespacePath : this.rootNamespacePath;
},
hasMultipleProjectsLinked() {
return this.linkedSppItems.length > 1;
},
showScopeSelector() {
return this.isGroupLevel || this.hasMultipleProjectsLinked;
},
projectIds() {
/**
* Protection from manual yam input as objects
......@@ -132,6 +193,9 @@ export default {
? this.$options.i18n.policyScopeFrameworkCopy
: this.$options.i18n.policyScopeProjectCopy;
},
showLoader() {
return this.$apollo.queries.linkedSppItems?.loading && !this.isGroupLevel;
},
},
methods: {
resetPolicyScope() {
......@@ -180,59 +244,85 @@ export default {
{{ errorDescription }}
</gl-alert>
<div class="gl-display-flex gl-gap-3 gl-align-items-center gl-flex-wrap gl-mt-2 gl-mb-6">
<gl-sprintf :message="policyScopeCopy">
<template #projectScopeType>
<gl-collapsible-listbox
data-testid="project-scope-type"
:items="$options.PROJECT_SCOPE_TYPE_LISTBOX_ITEMS"
:selected="selectedProjectScopeType"
:toggle-text="selectedProjectScopeText"
@select="selectProjectScopeType"
/>
</template>
<template #frameworkSelector>
<div class="gl-display-inline-flex gl-align-items-center gl-flex-wrap gl-gap-3">
<compliance-framework-dropdown
:selected-framework-ids="complianceFrameworksIds"
:full-path="rootNamespacePath"
@framework-query-error="
setShowAlert($options.i18n.complianceFrameworkErrorDescription)
"
@select="setSelectedFrameworkIds"
<div v-if="showLoader" class="gl-display-flex gl-gap-3 gl-align-items-baseline gl-mb-4">
<gl-loading-icon inline />
<span data-testid="loading-text">{{ $options.i18n.policyScopeLoadingText }}</span>
</div>
<div v-else class="gl-display-flex gl-gap-3 gl-align-items-center gl-flex-wrap gl-mt-2 gl-mb-6">
<template v-if="showLinkedSppItemsError">
<div
data-testid="policy-scope-project-error"
class="gl-display-flex gl-align-items-center gl-gap-3"
>
<gl-icon class="gl-text-red-500" name="status_warning" />
<p data-testid="policy-scope-project-error-text" class="gl-text-red-500 gl-m-0">
{{ $options.i18n.policyScopeErrorText }}
</p>
</div>
</template>
<template v-else-if="showScopeSelector">
<gl-sprintf :message="policyScopeCopy">
<template #projectScopeType>
<gl-collapsible-listbox
data-testid="project-scope-type"
:items="$options.PROJECT_SCOPE_TYPE_LISTBOX_ITEMS"
:selected="selectedProjectScopeType"
:toggle-text="selectedProjectScopeText"
@select="selectProjectScopeType"
/>
</template>
<policy-popover
:content="$options.i18n.complianceFrameworkPopoverContent"
:href="$options.COMPLIANCE_FRAMEWORK_PATH"
:title="$options.i18n.complianceFrameworkPopoverTitle"
target="compliance-framework-icon"
<template #frameworkSelector>
<div class="gl-display-inline-flex gl-align-items-center gl-flex-wrap gl-gap-3">
<compliance-framework-dropdown
:selected-framework-ids="complianceFrameworksIds"
:full-path="rootNamespacePath"
@framework-query-error="
setShowAlert($options.i18n.complianceFrameworkErrorDescription)
"
@select="setSelectedFrameworkIds"
/>
<policy-popover
:content="$options.i18n.complianceFrameworkPopoverContent"
:href="$options.COMPLIANCE_FRAMEWORK_PATH"
:title="$options.i18n.complianceFrameworkPopoverTitle"
target="compliance-framework-icon"
/>
</div>
</template>
<template #exceptionType>
<gl-collapsible-listbox
v-if="showExceptionTypeDropdown"
data-testid="exception-type"
:items="$options.EXCEPTION_TYPE_LISTBOX_ITEMS"
:toggle-text="selectedExceptionTypeText"
:selected="selectedExceptionType"
@select="selectExceptionType"
/>
</div>
</template>
<template #exceptionType>
<gl-collapsible-listbox
v-if="showExceptionTypeDropdown"
data-testid="exception-type"
:items="$options.EXCEPTION_TYPE_LISTBOX_ITEMS"
:toggle-text="selectedExceptionTypeText"
:selected="selectedExceptionType"
@select="selectExceptionType"
/>
</template>
<template #projectSelector>
<group-projects-dropdown
v-if="showGroupProjectsDropdown"
:group-full-path="namespacePath"
:selected="projectIds"
@projects-query-error="setShowAlert($options.i18n.groupProjectErrorDescription)"
@select="setSelectedProjectIds"
/>
</template>
</gl-sprintf>
</template>
<template #projectSelector>
<group-projects-dropdown
v-if="showGroupProjectsDropdown"
:group-full-path="groupProjectsFullPath"
:selected="projectIds"
state
@projects-query-error="setShowAlert($options.i18n.groupProjectErrorDescription)"
@select="setSelectedProjectIds"
/>
</template>
</gl-sprintf>
</template>
<template v-else>
<p data-testid="policy-scope-project-text">
{{ $options.i18n.policyScopeFrameworkCopyProject }}
</p>
</template>
</div>
</div>
</template>
......@@ -300,21 +300,34 @@ describe('EditorLayout component', () => {
describe('policy scope', () => {
it.each`
flagEnabled | securityPoliciesPolicyScopeToggleEnabled | type | expectedResult
${true} | ${true} | ${NAMESPACE_TYPES.GROUP} | ${true}
${true} | ${true} | ${NAMESPACE_TYPES.PROJECT} | ${false}
${false} | ${false} | ${NAMESPACE_TYPES.GROUP} | ${false}
${false} | ${false} | ${NAMESPACE_TYPES.PROJECT} | ${false}
${true} | ${false} | ${NAMESPACE_TYPES.GROUP} | ${false}
${true} | ${false} | ${NAMESPACE_TYPES.PROJECT} | ${false}
flagEnabledGroup | flagEnabledProject | securityPoliciesPolicyScopeToggleEnabled | type | expectedResult
${true} | ${false} | ${true} | ${NAMESPACE_TYPES.GROUP} | ${true}
${true} | ${false} | ${true} | ${NAMESPACE_TYPES.PROJECT} | ${false}
${false} | ${false} | ${false} | ${NAMESPACE_TYPES.GROUP} | ${false}
${false} | ${false} | ${false} | ${NAMESPACE_TYPES.PROJECT} | ${false}
${true} | ${false} | ${false} | ${NAMESPACE_TYPES.GROUP} | ${false}
${true} | ${false} | ${false} | ${NAMESPACE_TYPES.PROJECT} | ${false}
${false} | ${false} | ${true} | ${NAMESPACE_TYPES.GROUP} | ${false}
${false} | ${true} | ${true} | ${NAMESPACE_TYPES.PROJECT} | ${true}
${false} | ${false} | ${false} | ${NAMESPACE_TYPES.PROJECT} | ${false}
${true} | ${true} | ${false} | ${NAMESPACE_TYPES.GROUP} | ${false}
`(
'renders policy scope conditionally for $namespaceType level based on feature flag',
({ flagEnabled, securityPoliciesPolicyScopeToggleEnabled, type, expectedResult }) => {
({
flagEnabledGroup,
flagEnabledProject,
securityPoliciesPolicyScopeToggleEnabled,
type,
expectedResult,
}) => {
factory({
provide: {
securityPoliciesPolicyScopeToggleEnabled,
namespaceType: type,
glFeatures: { securityPoliciesPolicyScope: flagEnabled },
glFeatures: {
securityPoliciesPolicyScope: flagEnabledGroup,
securityPoliciesPolicyScopeProject: flagEnabledProject,
},
},
});
......
import { GlAlert, GlSprintf } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlAlert, GlSprintf, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { NAMESPACE_TYPES } from 'ee/security_orchestration/constants';
import waitForPromises from 'helpers/wait_for_promises';
import ScopeSection from 'ee/security_orchestration/components/policy_editor/scope/scope_section.vue';
import ComplianceFrameworkDropdown from 'ee/security_orchestration/components/policy_editor/scope/compliance_framework_dropdown.vue';
import GroupProjectsDropdown from 'ee/security_orchestration/components/group_projects_dropdown.vue';
import getSppLinkedProjectsNamespaces from 'ee/security_orchestration/graphql/queries/get_spp_linked_projects_namespaces.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
PROJECTS_WITH_FRAMEWORK,
ALL_PROJECTS_IN_GROUP,
......@@ -14,16 +20,42 @@ import {
describe('PolicyScope', () => {
let wrapper;
let requestHandler;
const createHandler = ({ projects = [], namespaces = [] } = {}) =>
jest.fn().mockResolvedValue({
data: {
project: {
id: '1',
securityPolicyProjectLinkedProjects: {
nodes: projects,
},
securityPolicyProjectLinkedNamespaces: {
nodes: namespaces,
},
},
},
});
const createComponent = ({ propsData } = {}) => {
const createMockApolloProvider = (handler) => {
Vue.use(VueApollo);
requestHandler = handler;
return createMockApollo([[getSppLinkedProjectsNamespaces, requestHandler]]);
};
const createComponent = ({ propsData, provide = {}, handler = createHandler() } = {}) => {
wrapper = shallowMountExtended(ScopeSection, {
apolloProvider: createMockApolloProvider(handler),
propsData: {
policyScope: {},
...propsData,
},
provide: {
namespaceType: NAMESPACE_TYPES.GROUP,
namespacePath: 'gitlab-org',
rootNamespacePath: 'gitlab-org',
rootNamespacePath: 'gitlab-org-root',
...provide,
},
stubs: {
GlSprintf,
......@@ -36,6 +68,12 @@ describe('PolicyScope', () => {
const findGroupProjectsDropdown = () => wrapper.findComponent(GroupProjectsDropdown);
const findProjectScopeTypeDropdown = () => wrapper.findByTestId('project-scope-type');
const findExceptionTypeDropdown = () => wrapper.findByTestId('exception-type');
const findPolicyScopeProjectText = () => wrapper.findByTestId('policy-scope-project-text');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findLoadingText = () => wrapper.findByTestId('loading-text');
const findErrorMessage = () => wrapper.findByTestId('policy-scope-project-error');
const findErrorMessageText = () => wrapper.findByTestId('policy-scope-project-error-text');
const findIcon = () => wrapper.findComponent(GlIcon);
beforeEach(() => {
createComponent();
......@@ -190,6 +228,7 @@ describe('PolicyScope', () => {
expect(findExceptionTypeDropdown().props('selected')).toBe(EXCEPT_PROJECTS);
expect(findExceptionTypeDropdown().exists()).toBe(true);
expect(findGroupProjectsDropdown().exists()).toBe(true);
expect(findGroupProjectsDropdown().props('state')).toBe(true);
expect(findGroupProjectsDropdown().props('selected')).toEqual([
convertToGraphQLId(TYPENAME_PROJECT, 'id1'),
convertToGraphQLId(TYPENAME_PROJECT, 'id2'),
......@@ -237,4 +276,190 @@ describe('PolicyScope', () => {
expect(findGlAlert().exists()).toBe(true);
});
});
describe('project level', () => {
it('should check linked items on project level', () => {
createComponent({
provide: {
namespaceType: NAMESPACE_TYPES.PROJECT,
glFeatures: {
securityPoliciesPolicyScopeProject: true,
},
},
});
expect(requestHandler).toHaveBeenCalledWith({ fullPath: 'gitlab-org' });
});
it('should not check linked items on group level', async () => {
createComponent();
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
expect(findComplianceFrameworkDropdown().exists()).toBe(true);
expect(requestHandler).toHaveBeenCalledTimes(0);
expect(findPolicyScopeProjectText().exists()).toBe(false);
});
it('show text message for project without linked items', async () => {
createComponent({
provide: {
namespaceType: NAMESPACE_TYPES.PROJECT,
},
});
await waitForPromises();
expect(findPolicyScopeProjectText().text()).toBe('Apply this policy to current project.');
});
it('show compliance framework selector for projects with links', async () => {
createComponent({
provide: {
namespaceType: NAMESPACE_TYPES.PROJECT,
glFeatures: {
securityPoliciesPolicyScopeProject: true,
},
},
handler: createHandler({
projects: [
{ id: '1', name: 'name1' },
{ id: '2', name: 'name2 ' },
],
namespaces: [
{ id: '1', name: 'name1' },
{ id: '2', name: 'name2 ' },
],
}),
});
await waitForPromises();
expect(findPolicyScopeProjectText().exists()).toBe(false);
expect(findComplianceFrameworkDropdown().exists()).toBe(true);
});
it('shows loading state', () => {
createComponent({
provide: {
namespaceType: NAMESPACE_TYPES.PROJECT,
glFeatures: {
securityPoliciesPolicyScopeProject: true,
},
},
});
expect(findLoadingIcon().exists()).toBe(true);
expect(findLoadingText().text()).toBe('Fetching the scope information.');
});
it('shows error message when spp query fails', async () => {
createComponent({
provide: {
namespaceType: NAMESPACE_TYPES.PROJECT,
glFeatures: {
securityPoliciesPolicyScopeProject: true,
},
},
handler: jest.fn().mockRejectedValue({}),
});
await waitForPromises();
expect(findErrorMessage().exists()).toBe(true);
expect(findErrorMessageText().text()).toBe(
'Failed to fetch the scope information. Please refresh the page to try again.',
);
expect(findIcon().props('name')).toBe('status_warning');
});
it('emits default policy scope on project level for SPP with multiple dependencies', async () => {
createComponent({
provide: {
namespaceType: NAMESPACE_TYPES.PROJECT,
glFeatures: {
securityPoliciesPolicyScopeProject: true,
},
},
handler: createHandler({
projects: [
{ id: '1', name: 'name1' },
{ id: '2', name: 'name2 ' },
],
namespaces: [
{ id: '1', name: 'name1' },
{ id: '2', name: 'name2 ' },
],
}),
});
await waitForPromises();
expect(wrapper.emitted('changed')).toEqual([[{ compliance_frameworks: [] }]]);
});
it('does not emit default policy scope on group level', async () => {
createComponent({
provide: {
namespaceType: NAMESPACE_TYPES.GROUP,
},
});
await waitForPromises();
expect(wrapper.emitted('changed')).toBeUndefined();
});
it('does not check dependencies on project level when ff is disabled', async () => {
createComponent({
provide: {
namespaceType: NAMESPACE_TYPES.PROJECT,
glFeatures: {
securityPoliciesPolicyScopeProject: false,
},
},
});
await waitForPromises();
expect(requestHandler).toHaveBeenCalledTimes(0);
expect(findLoadingIcon().exists()).toBe(false);
});
});
describe('namespace', () => {
it.each`
namespaceType | expectedResult
${NAMESPACE_TYPES.GROUP} | ${'gitlab-org'}
${NAMESPACE_TYPES.PROJECT} | ${'gitlab-org-root'}
`(
'queries different namespaces on group and project level',
async ({ namespaceType, expectedResult }) => {
createComponent({
provide: {
namespaceType,
glFeatures: {
securityPoliciesPolicyScopeProject: true,
},
},
handler: createHandler({
projects: [
{ id: '1', name: 'name1' },
{ id: '2', name: 'name2 ' },
],
namespaces: [
{ id: '1', name: 'name1' },
{ id: '2', name: 'name2 ' },
],
}),
});
await waitForPromises();
await findProjectScopeTypeDropdown().vm.$emit('select', SPECIFIC_PROJECTS);
expect(findGroupProjectsDropdown().props('groupFullPath')).toBe(expectedResult);
},
);
});
});
......@@ -44665,6 +44665,9 @@ msgstr ""
msgid "SecurityOrchestration|Apply this policy to %{projectScopeType}named %{frameworkSelector}"
msgstr ""
 
msgid "SecurityOrchestration|Apply this policy to current project."
msgstr ""
msgid "SecurityOrchestration|Are you sure you want to delete this policy? This action cannot be undone."
msgstr ""
 
......@@ -44764,6 +44767,9 @@ msgstr ""
msgid "SecurityOrchestration|Exceptions"
msgstr ""
 
msgid "SecurityOrchestration|Failed to fetch the scope information. Please refresh the page to try again."
msgstr ""
msgid "SecurityOrchestration|Failed to load cluster agents."
msgstr ""
 
......@@ -44776,6 +44782,9 @@ msgstr ""
msgid "SecurityOrchestration|Failed to load images."
msgstr ""
 
msgid "SecurityOrchestration|Fetching the scope information."
msgstr ""
msgid "SecurityOrchestration|Fill in branch name with project name in the format of %{boldStart}branch-name@project-path,%{boldEnd} separate with `,`"
msgstr ""
 
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册