diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue new file mode 100644 index 0000000000000000000000000000000000000000..32f05d0e298cb26359935821e2b608b8bf3afa73 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue @@ -0,0 +1,111 @@ + + + diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue index 06af69ff250e1bdea526242fc5ce3b53ef14df79..8e4c50b199be1acb40e1450fcd64244ef9d7e2ab 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue @@ -8,6 +8,7 @@ import { } from '~/packages_and_registries/settings/project/constants'; import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue'; import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { @@ -18,7 +19,10 @@ export default { ), GlAlert, PackagesCleanupPolicy, + PackagesProtectionRules: () => + import('~/packages_and_registries/settings/project/components/packages_protection_rules.vue'), }, + mixins: [glFeatureFlagsMixin()], inject: [ 'showContainerRegistrySettings', 'showPackageRegistrySettings', @@ -32,6 +36,11 @@ export default { showAlert: false, }; }, + computed: { + showProtectedPackagesSettings() { + return this.showPackageRegistrySettings && this.glFeatures.packagesProtectedPackages; + }, + }, mounted() { this.checkAlert(); }, @@ -60,6 +69,7 @@ export default { > {{ $options.i18n.UPDATE_SETTINGS_SUCCESS_MESSAGE }} + diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql new file mode 100644 index 0000000000000000000000000000000000000000..e0a072b93e4c0f9b8d53830b1062011e234e7db1 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql @@ -0,0 +1,13 @@ +query getProjectPackageProtectionRules($projectPath: ID!, $first: Int) { + project(fullPath: $projectPath) { + id + packagesProtectionRules(first: $first) { + nodes { + id + packageNamePattern + packageType + pushProtectedUpToAccessLevel + } + } + } +} diff --git a/app/controllers/projects/settings/packages_and_registries_controller.rb b/app/controllers/projects/settings/packages_and_registries_controller.rb index 76c9cead360db1c518049c832870093eb1c0f813..fd4dbdab95ff0d989cbb02e91f6ca56c53e4f192 100644 --- a/app/controllers/projects/settings/packages_and_registries_controller.rb +++ b/app/controllers/projects/settings/packages_and_registries_controller.rb @@ -12,6 +12,7 @@ class PackagesAndRegistriesController < Projects::ApplicationController urgency :low def show + push_frontend_feature_flag(:packages_protected_packages, project) end def cleanup_tags diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ee4d1e9bcd7dc2858955897ffc4a88e570b97487..27c3537f710e6a9eed63a55c8fe8b34834ca61f1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -34537,6 +34537,12 @@ msgid_plural "PackageRegistry|Package has %{updatesCount} archived updates" msgstr[0] "" msgstr[1] "" +msgid "PackageRegistry|Package name pattern" +msgstr "" + +msgid "PackageRegistry|Package type" +msgstr "" + msgid "PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}" msgstr "" @@ -34561,6 +34567,9 @@ msgstr "" msgid "PackageRegistry|Project-level" msgstr "" +msgid "PackageRegistry|Protected packages" +msgstr "" + msgid "PackageRegistry|Publish packages if their name or version matches this regex." msgstr "" @@ -34579,6 +34588,9 @@ msgstr "" msgid "PackageRegistry|Published to the %{project} Package Registry %{datetime}" msgstr "" +msgid "PackageRegistry|Push protected up to access level" +msgstr "" + msgid "PackageRegistry|PyPI" msgstr "" @@ -34684,6 +34696,9 @@ msgstr "" msgid "PackageRegistry|Unable to load package" msgstr "" +msgid "PackageRegistry|When a package is protected then only certain user roles are able to update and delete the protected package. This helps to avoid tampering with the package." +msgstr "" + msgid "PackageRegistry|When a package with same name and version is uploaded to the registry, more assets are added to the package. To save storage space, keep only the most recent assets." msgstr "" diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..26d4e4dbb4f2fc079b01568f0f95f2490bc9e651 --- /dev/null +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_protection_rules_spec.js @@ -0,0 +1,80 @@ +import { GlTable } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import PackagesProtectionRules from '~/packages_and_registries/settings/project/components/packages_protection_rules.vue'; +import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; +import packagesProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql'; + +import { packagesProtectionRuleQueryPayload, packagesProtectionRulesData } from '../mock_data'; + +Vue.use(VueApollo); + +describe('Packages protection rules project settings', () => { + let wrapper; + let fakeApollo; + + const defaultProvidedValues = { + projectPath: 'path', + }; + const findSettingsBlock = () => wrapper.findComponent(SettingsBlock); + const findTable = () => wrapper.findComponent(GlTable); + const findTableRows = () => findTable().find('tbody').findAll('tr'); + + const mountComponent = (mountFn = shallowMount, provide = defaultProvidedValues, config) => { + wrapper = mountFn(PackagesProtectionRules, { + stubs: { + SettingsBlock, + }, + provide, + ...config, + }); + }; + + const createComponent = ({ + mountFn = shallowMount, + provide = defaultProvidedValues, + resolver = jest.fn().mockResolvedValue(packagesProtectionRuleQueryPayload()), + } = {}) => { + const requestHandlers = [[packagesProtectionRuleQuery, resolver]]; + + fakeApollo = createMockApollo(requestHandlers); + + mountComponent(mountFn, provide, { + apolloProvider: fakeApollo, + }); + }; + + it('renders the setting block with table', async () => { + createComponent(); + + await waitForPromises(); + + expect(findSettingsBlock().exists()).toBe(true); + expect(findTable().exists()).toBe(true); + }); + + it('renders table with container registry protection rules', async () => { + createComponent({ mountFn: mount }); + + await waitForPromises(); + + expect(findTable().exists()).toBe(true); + + packagesProtectionRulesData.forEach((protectionRule, i) => { + expect(findTableRows().at(i).text()).toContain(protectionRule.packageNamePattern); + expect(findTableRows().at(i).text()).toContain(protectionRule.packageType); + expect(findTableRows().at(i).text()).toContain(protectionRule.pushProtectedUpToAccessLevel); + }); + }); + + it('renders table with pagination', async () => { + createComponent(); + + await waitForPromises(); + + expect(findTable().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js index dfcabd14489f1ceb6151cb650bc32ea1343e5b2d..1afc9b62ba22a29528ff8e8b17e40a87f715d36a 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/components/registry_settings_app_spec.js @@ -6,6 +6,7 @@ import * as commonUtils from '~/lib/utils/common_utils'; import component from '~/packages_and_registries/settings/project/components/registry_settings_app.vue'; import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue'; import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue'; +import PackagesProtectionRules from '~/packages_and_registries/settings/project/components/packages_protection_rules.vue'; import DependencyProxyPackagesSettings from 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue'; import { SHOW_SETUP_SUCCESS_ALERT, @@ -19,6 +20,7 @@ describe('Registry Settings app', () => { const findContainerExpirationPolicy = () => wrapper.findComponent(ContainerExpirationPolicy); const findPackagesCleanupPolicy = () => wrapper.findComponent(PackagesCleanupPolicy); + const findPackagesProtectionRules = () => wrapper.findComponent(PackagesProtectionRules); const findDependencyProxyPackagesSettings = () => wrapper.findComponent(DependencyProxyPackagesSettings); const findAlert = () => wrapper.findComponent(GlAlert); @@ -29,6 +31,7 @@ describe('Registry Settings app', () => { showPackageRegistrySettings: true, showDependencyProxySettings: false, ...(IS_EE && { showDependencyProxySettings: true }), + glFeatures: { packagesProtectedPackages: true }, }; const mountComponent = (provide = defaultProvide) => { @@ -95,6 +98,7 @@ describe('Registry Settings app', () => { expect(findContainerExpirationPolicy().exists()).toBe(showContainerRegistrySettings); expect(findPackagesCleanupPolicy().exists()).toBe(showPackageRegistrySettings); + expect(findPackagesProtectionRules().exists()).toBe(showPackageRegistrySettings); }, ); @@ -108,5 +112,20 @@ describe('Registry Settings app', () => { expect(findDependencyProxyPackagesSettings().exists()).toBe(value); }); } + + describe('when feature flag "packagesProtectedPackages" is disabled', () => { + it.each([true, false])( + 'package protection rules settings is hidden if showPackageRegistrySettings is %s', + (showPackageRegistrySettings) => { + mountComponent({ + ...defaultProvide, + showPackageRegistrySettings, + glFeatures: { packagesProtectedPackages: false }, + }); + + expect(findPackagesProtectionRules().exists()).toBe(false); + }, + ); + }); }); }); diff --git a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js index 3204ca01f99d2414399658f2c59fd2554ad590a4..5c546289b145e22ee4c9dfa504a68b655b0918ed 100644 --- a/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js +++ b/spec/frontend/packages_and_registries/settings/project/settings/mock_data.js @@ -79,3 +79,36 @@ export const packagesCleanupPolicyMutationPayload = ({ override, errors = [] } = }, }, }); + +export const packagesProtectionRulesData = [ + { + id: `gid://gitlab/Packages::Protection::Rule/14`, + packageNamePattern: `@flight/flight-maintainer-14-*`, + packageType: 'NPM', + pushProtectedUpToAccessLevel: 'MAINTAINER', + }, + { + id: `gid://gitlab/Packages::Protection::Rule/15`, + packageNamePattern: `@flight/flight-maintainer-15-*`, + packageType: 'NPM', + pushProtectedUpToAccessLevel: 'MAINTAINER', + }, + { + id: 'gid://gitlab/Packages::Protection::Rule/16', + packageNamePattern: '@flight/flight-owner-16-*', + packageType: 'NPM', + pushProtectedUpToAccessLevel: 'OWNER', + }, +]; + +export const packagesProtectionRuleQueryPayload = ({ override, errors = [] } = {}) => ({ + data: { + project: { + id: '1', + packagesProtectionRules: { + nodes: override || packagesProtectionRulesData, + }, + errors, + }, + }, +});