diff --git a/app/finders/packages/package_protection_rule_push_finder.rb b/app/finders/packages/package_protection_rule_push_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..ed2b9f8fcd34abcda761564bed90f793e4041453 --- /dev/null +++ b/app/finders/packages/package_protection_rule_push_finder.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Packages + class PackageProtectionRulePushFinder + def initialize(package, current_user) + @current_user = current_user + @package = package + + validate_params! + end + + def execute + current_user_project_authorization_access_level = current_user.max_member_access_for_project(project.id) + + if current_user_project_authorization_access_level == Gitlab::Access::NO_ACCESS + return ::Packages::Protection::Rule.none.to_a + end + + project + .package_protection_rules + .for_package_type(package.package_type) + .push_protection_applicable_for_access_level(current_user_project_authorization_access_level) + .then { |scope| ::Packages::Protection::Rule.matching_package_name(package.name, scope) } + end + + private + + attr_reader :current_user, :package + + delegate :project, to: :package, private: true + + def validate_params! + raise ArgumentError, 'package cannot be nil' unless package + raise ArgumentError, 'current_user cannot be nil' unless current_user + end + end +end diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb index 7665f30be62edb9f965be8b4f93b7b7109a9cdd5..9583951f89443068a6a59d75464d7f08011837c0 100644 --- a/app/models/packages/package.rb +++ b/app/models/packages/package.rb @@ -404,6 +404,13 @@ def publish_creation_event ) end + def push_protected_from?(user) + return true unless user + + # If there is one matching package protection rule, then we consider the package protected + Packages::PackageProtectionRulePushFinder.new(self, user).execute.present? + end + private def composer_tag_version? diff --git a/app/models/packages/protection.rb b/app/models/packages/protection.rb new file mode 100644 index 0000000000000000000000000000000000000000..ebaecf899921100ac42f02ae100ae85ddf0b4f09 --- /dev/null +++ b/app/models/packages/protection.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Packages + module Protection + def self.table_name_prefix + 'packages_protection_' + end + end +end diff --git a/app/models/packages/protection/rule.rb b/app/models/packages/protection/rule.rb new file mode 100644 index 0000000000000000000000000000000000000000..842f485b492de2eeb0043f80b582dcd31e407631 --- /dev/null +++ b/app/models/packages/protection/rule.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Packages + module Protection + class Rule < ApplicationRecord + enum package_type: Packages::Package.package_types.slice(:npm) + + belongs_to :project, inverse_of: :package_protection_rules + + validates :package_name_pattern, presence: true, uniqueness: { scope: [:project_id, :package_type] }, + length: { maximum: 255 } + validates :package_type, presence: true + validates :push_protected_up_to_access_level, presence: true, + inclusion: { in: [ + Gitlab::Access::DEVELOPER, + Gitlab::Access::MAINTAINER, + Gitlab::Access::OWNER + ] } + + scope :for_package_type, ->(package_type) { where(package_type: package_type) } + + scope :push_protection_applicable_for_access_level, ->(user_access_level) do + where(push_protected_up_to_access_level: user_access_level..) + end + + def self.matching_package_name(package_name, scope) + scope.select do |package_protection_rule| + RefMatcher.new(package_protection_rule.package_name_pattern).matches?(package_name) + end + end + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index dd197e4a43daf6b7048c85cb959f8f13937ecc0f..dd4d96a14d6c9df3927c5fde479c4a48dd549a26 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -268,6 +268,9 @@ def self.integration_association_name(name) dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :npm_metadata_caches, class_name: 'Packages::Npm::MetadataCache' has_one :packages_cleanup_policy, class_name: 'Packages::Cleanup::Policy', inverse_of: :project + has_many :package_protection_rules, + class_name: 'Packages::Protection::Rule', + inverse_of: :project has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/db/docs/packages_protection_rules.yml b/db/docs/packages_protection_rules.yml new file mode 100644 index 0000000000000000000000000000000000000000..3007c956e269e5b9f55c6a6f9d2a6d93b96e7ec3 --- /dev/null +++ b/db/docs/packages_protection_rules.yml @@ -0,0 +1,10 @@ +--- +table_name: packages_protection_rules +classes: +- Packages::Protection::Rule +feature_categories: +- package_registry +description: Represents package protection rules for package registry. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124776 +milestone: '16.4' +gitlab_schema: gitlab_main diff --git a/db/migrate/20230903170000_create_packages_protection_rules.rb b/db/migrate/20230903170000_create_packages_protection_rules.rb new file mode 100644 index 0000000000000000000000000000000000000000..24dc30dc060ee06be5c07acdac4ec5315fb9b1d3 --- /dev/null +++ b/db/migrate/20230903170000_create_packages_protection_rules.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreatePackagesProtectionRules < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def change + create_table :packages_protection_rules do |t| + t.references :project, null: false, index: false, foreign_key: { on_delete: :cascade } + t.timestamps_with_timezone null: false + t.integer :push_protected_up_to_access_level, null: false + t.integer :package_type, limit: 2, null: false + t.text :package_name_pattern, limit: 255, null: false + end + + add_index :packages_protection_rules, [:project_id, :package_type, :package_name_pattern], unique: true, + name: :i_packages_unique_project_id_package_type_package_name_pattern + end +end diff --git a/db/schema_migrations/20230903170000 b/db/schema_migrations/20230903170000 new file mode 100644 index 0000000000000000000000000000000000000000..5189c60657a9f8fb3680ec04c920cefb72e09ab5 --- /dev/null +++ b/db/schema_migrations/20230903170000 @@ -0,0 +1 @@ +07fdd8579009aa1d68106cc9919d82cfca5702373d8b69c54ea7fa552209d540 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 14b61af7aa2183e394f185098bc87ca0ac0e4a0f..2375c92cfaaeef23ad4393c90a440987e9446b08 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11942,11 +11942,11 @@ CREATE TABLE application_settings ( package_registry_allow_anyone_to_pull_option boolean DEFAULT true NOT NULL, bulk_import_max_download_file_size bigint DEFAULT 5120 NOT NULL, max_import_remote_file_size bigint DEFAULT 10240 NOT NULL, - sentry_clientside_traces_sample_rate double precision DEFAULT 0.0 NOT NULL, protected_paths_for_get_request text[] DEFAULT '{}'::text[] NOT NULL, max_decompressed_archive_size integer DEFAULT 25600 NOT NULL, - ci_max_total_yaml_size_bytes integer DEFAULT 157286400 NOT NULL, + sentry_clientside_traces_sample_rate double precision DEFAULT 0.0 NOT NULL, prometheus_alert_db_indicators_settings jsonb, + ci_max_total_yaml_size_bytes integer DEFAULT 157286400 NOT NULL, decompress_archive_file_timeout integer DEFAULT 210 NOT NULL, search_rate_limit_allowlist text[] DEFAULT '{}'::text[] NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), @@ -20231,6 +20231,26 @@ CREATE SEQUENCE packages_packages_id_seq ALTER SEQUENCE packages_packages_id_seq OWNED BY packages_packages.id; +CREATE TABLE packages_protection_rules ( + id bigint NOT NULL, + project_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + push_protected_up_to_access_level integer NOT NULL, + package_type smallint NOT NULL, + package_name_pattern text NOT NULL, + CONSTRAINT check_d2d75d206d CHECK ((char_length(package_name_pattern) <= 255)) +); + +CREATE SEQUENCE packages_protection_rules_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE packages_protection_rules_id_seq OWNED BY packages_protection_rules.id; + CREATE TABLE packages_pypi_metadata ( package_id bigint NOT NULL, required_python text DEFAULT ''::text, @@ -26118,6 +26138,8 @@ ALTER TABLE ONLY packages_package_files ALTER COLUMN id SET DEFAULT nextval('pac ALTER TABLE ONLY packages_packages ALTER COLUMN id SET DEFAULT nextval('packages_packages_id_seq'::regclass); +ALTER TABLE ONLY packages_protection_rules ALTER COLUMN id SET DEFAULT nextval('packages_protection_rules_id_seq'::regclass); + ALTER TABLE ONLY packages_rpm_repository_files ALTER COLUMN id SET DEFAULT nextval('packages_rpm_repository_files_id_seq'::regclass); ALTER TABLE ONLY packages_tags ALTER COLUMN id SET DEFAULT nextval('packages_tags_id_seq'::regclass); @@ -28458,6 +28480,9 @@ ALTER TABLE ONLY packages_package_files ALTER TABLE ONLY packages_packages ADD CONSTRAINT packages_packages_pkey PRIMARY KEY (id); +ALTER TABLE ONLY packages_protection_rules + ADD CONSTRAINT packages_protection_rules_pkey PRIMARY KEY (id); + ALTER TABLE ONLY packages_pypi_metadata ADD CONSTRAINT packages_pypi_metadata_pkey PRIMARY KEY (package_id); @@ -30421,6 +30446,8 @@ CREATE INDEX i_dast_profiles_tags_on_scanner_profiles_id ON dast_profiles_tags U CREATE INDEX i_dast_scanner_profiles_tags_on_scanner_profiles_id ON dast_scanner_profiles_tags USING btree (dast_scanner_profile_id); +CREATE UNIQUE INDEX i_packages_unique_project_id_package_type_package_name_pattern ON packages_protection_rules USING btree (project_id, package_type, package_name_pattern); + CREATE INDEX i_pkgs_deb_file_meta_on_updated_at_package_file_id_when_unknown ON packages_debian_file_metadata USING btree (updated_at, package_file_id) WHERE (file_type = 1); CREATE UNIQUE INDEX i_pm_licenses_on_spdx_identifier ON pm_licenses USING btree (spdx_identifier); @@ -38762,6 +38789,9 @@ ALTER TABLE ONLY clusters_integration_prometheus ALTER TABLE ONLY vulnerability_occurrence_identifiers ADD CONSTRAINT fk_rails_e4ef6d027c FOREIGN KEY (occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE; +ALTER TABLE ONLY packages_protection_rules + ADD CONSTRAINT fk_rails_e52adb5267 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY vulnerability_flags ADD CONSTRAINT fk_rails_e59393b48b FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE; diff --git a/spec/factories/packages/package_protection_rules.rb b/spec/factories/packages/package_protection_rules.rb new file mode 100644 index 0000000000000000000000000000000000000000..3038fb847e7d8dd1fd24654a8e61f0ba1951d923 --- /dev/null +++ b/spec/factories/packages/package_protection_rules.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :package_protection_rule, class: 'Packages::Protection::Rule' do + project + package_name_pattern { '@my_scope/my_package' } + package_type { :npm } + push_protected_up_to_access_level { Gitlab::Access::DEVELOPER } + end +end diff --git a/spec/finders/packages/package_protection_rule_push_finder_spec.rb b/spec/finders/packages/package_protection_rule_push_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..56d458374ae2da934f858070345fa5b4e57b0925 --- /dev/null +++ b/spec/finders/packages/package_protection_rule_push_finder_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Packages::PackageProtectionRulePushFinder, feature_category: :package_registry do + include_context 'ProjectPolicy context' + + let_it_be(:project) { private_project } + let_it_be(:package_protection_rule) do + create(:package_protection_rule, + project: project, + package_type: :npm, + push_protected_up_to_access_level: Gitlab::Access::OWNER + ) + end + + let(:current_user) { project.owner } + let(:package) do + build(:package, + project: project, + name: package_protection_rule.package_name_pattern, + package_type: package_protection_rule.package_type + ) + end + + let(:access_level_developer) { Gitlab::Access::DEVELOPER } + let(:access_level_maintainer) { Gitlab::Access::MAINTAINER } + let(:access_level_owner) { Gitlab::Access::OWNER } + + describe "#initialize" do + subject { described_class.new(package, current_user) } + + it { is_expected.to be_present } + it { is_expected.to be_a described_class } + + context 'without package' do + let(:package) { nil } + + it { expect { subject }.to raise_error ArgumentError } + end + + context 'without current_user' do + let(:current_user) { nil } + + it { expect { subject }.to raise_error ArgumentError } + end + end + + describe "#execute" do + let(:finder) { described_class.new(package, current_user) } + + subject { finder.execute } + + describe "with different users and protection levels" do + using RSpec::Parameterized::TableSyntax + + where(:current_user, :push_protected_up_to_access_level, :expected_package_protection_rules) do + ref(:non_member) | ref(:access_level_developer) | [] + ref(:guest) | ref(:access_level_developer) | [ref(:package_protection_rule)] + ref(:reporter) | ref(:access_level_developer) | [ref(:package_protection_rule)] + ref(:developer) | ref(:access_level_developer) | [ref(:package_protection_rule)] + ref(:maintainer) | ref(:access_level_developer) | [] + ref(:owner) | ref(:access_level_developer) | [] + + ref(:developer) | ref(:access_level_maintainer) | [ref(:package_protection_rule)] + ref(:maintainer) | ref(:access_level_maintainer) | [ref(:package_protection_rule)] + ref(:owner) | ref(:access_level_maintainer) | [] + + ref(:maintainer) | ref(:access_level_owner) | [ref(:package_protection_rule)] + ref(:owner) | ref(:access_level_owner) | [ref(:package_protection_rule)] + end + + with_them do + before do + package_protection_rule.reload.update!(push_protected_up_to_access_level: push_protected_up_to_access_level) + end + + it { is_expected.to match_array expected_package_protection_rules } + end + end + + describe "with admin mode enabled", :enable_admin_mode do + using RSpec::Parameterized::TableSyntax + + where(:current_user, :push_protected_up_to_access_level, :expected_package_protection_rules) do + ref(:admin) | ref(:access_level_developer) | [] + ref(:admin) | ref(:access_level_maintainer) | [] + ref(:admin) | ref(:access_level_owner) | [] + end + + with_them do + before do + package_protection_rule.reload.update!(push_protected_up_to_access_level: push_protected_up_to_access_level) + end + + it { is_expected.to match_array expected_package_protection_rules } + end + end + + context 'with wildcard-based matching package protection rule' do + let_it_be(:package_protection_rule_wildcard) do + create(:package_protection_rule, + project: project, + package_name_pattern: "#{package_protection_rule.package_name_pattern}*", + push_protected_up_to_access_level: Gitlab::Access::OWNER + ) + end + + it { is_expected.to be_an Array } + it { is_expected.to contain_exactly(package_protection_rule, package_protection_rule_wildcard) } + end + + context 'with non-matching package protection rule' do + before do + package.assign_attributes( + name: "#{package_protection_rule.package_name_pattern}_no_match" + ) + end + + it { is_expected.to be_an Array } + it { is_expected.to be_empty } + end + + context 'for package of non-matching package_type' do + before do + package.assign_attributes(package_type: :maven) + end + + it { is_expected.to be_an Array } + it { is_expected.to be_empty } + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index de3f28a5b90641d311be5b101f4f4b25b841d2f4..5ca5270a8515cf538b9bfed2bc2bfb23357d0b12 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -738,6 +738,7 @@ project: - project_registry - packages - package_files +- package_protection_rules - rpm_repository_files - npm_metadata_caches - packages_cleanup_policy diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 74ad2925058975f86de78e5c22b096b10fd9e3d1..0213a3af7064255c9cb147b172204b1c1450d13a 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -1531,4 +1531,36 @@ end end end + + describe '#push_protected_from?' do + let_it_be(:project) { create(:project) } + let_it_be(:package) { create(:npm_package, project: project) } + let_it_be(:user) { project.owner } + + subject { package.push_protected_from?(user) } + + context 'without PackageProtectionRule' do + it { is_expected.to be_falsy } + end + + context 'with PackageProtectionRule' do + let_it_be(:package_protection_rule) do + create( + :package_protection_rule, + project: project, + package_name_pattern: package.name, + package_type: package.package_type, + push_protected_up_to_access_level: Gitlab::Access::OWNER + ) + end + + it { is_expected.to be_truthy } + end + + context 'without user' do + let(:user) { nil } + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/models/packages/protection/rule_spec.rb b/spec/models/packages/protection/rule_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6214fb0b5c0fcb803a07b72d3c98e4ad0716f407 --- /dev/null +++ b/spec/models/packages/protection/rule_spec.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :package_registry do + it_behaves_like 'having unique enum values' + + describe 'relationships' do + it { is_expected.to belong_to(:project).inverse_of(:package_protection_rules) } + end + + describe 'enums' do + describe '#package_type' do + it { is_expected.to define_enum_for(:package_type).with_values(npm: Packages::Package.package_types[:npm]) } + end + end + + describe 'validations' do + subject { build(:package_protection_rule) } + + describe '#package_name_pattern' do + it { is_expected.to validate_presence_of(:package_name_pattern) } + it { is_expected.to validate_uniqueness_of(:package_name_pattern).scoped_to(:project_id, :package_type) } + it { is_expected.to validate_length_of(:package_name_pattern).is_at_most(255) } + end + + describe '#package_type' do + it { is_expected.to validate_presence_of(:package_type) } + end + + describe '#push_protected_up_to_access_level' do + it { is_expected.to validate_presence_of(:push_protected_up_to_access_level) } + + it { + is_expected.to validate_inclusion_of(:push_protected_up_to_access_level).in_array([Gitlab::Access::DEVELOPER, + Gitlab::Access::MAINTAINER, Gitlab::Access::OWNER]) + } + end + end + + describe '.matching_package_name' do + let_it_be(:package_protection_rule) do + create(:package_protection_rule, package_name_pattern: '@my-scope/my_package') + end + + let_it_be(:ppr_with_wildcard_start) do + create(:package_protection_rule, package_name_pattern: '*@my-scope/my_package-with-wildcard-start') + end + + let_it_be(:ppr_with_wildcard_end) do + create(:package_protection_rule, package_name_pattern: '@my-scope/my_package-with-wildcard-end*') + end + + let_it_be(:ppr_with_wildcard_inbetween) do + create(:package_protection_rule, package_name_pattern: '@my-scope/*my_package-with-wildcard-inbetween') + end + + let_it_be(:ppr_with_wildcard_multiples) do + create(:package_protection_rule, package_name_pattern: '**@my-scope/**my_package-with-wildcard-multiple**') + end + + let_it_be(:ppr_with_wildcard_escaped) do + create(:package_protection_rule, package_name_pattern: '@my-scope/my_package-with-wildcard-escaped-\*') + end + + let_it_be(:ppr_with_underscore) do + create(:package_protection_rule, package_name_pattern: '@my-scope/my_package-with_____underscore') + end + + let_it_be(:ppr_with_percent_sign) do + create(:package_protection_rule, package_name_pattern: '@my-scope/my_package-with-percent-sign-%') + end + + let_it_be(:ppr_with_percent_sign_escaped) do + create(:package_protection_rule, package_name_pattern: '@my-scope/my_package-with-percent-sign-escaped-\%') + end + + let_it_be(:ppr_with_unsupported_regex_characters) do + create(:package_protection_rule, + package_name_pattern: '@my-scope/my_package-with-unsupported-regex-characters.+') + end + + let(:package_name) { package_protection_rule.package_name_pattern } + + subject { described_class.matching_package_name(package_name, described_class.all) } + + context 'with several package protection rule scenarios' do + using RSpec::Parameterized::TableSyntax + + where(:package_name, :expected_package_protection_rules) do + '@my-scope/my_package' | [ref(:package_protection_rule)] + '@my-scope/my2package' | [] + '@my-scope/my_package-2' | [] + + # With wildcard pattern at the start + '@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)] + '@my-scope/my_package-with-wildcard-start-any' | [] + 'prefix-@my-scope/my_package-with-wildcard-start' | [ref(:ppr_with_wildcard_start)] + 'prefix-@my-scope/my_package-with-wildcard-start-any' | [] + + # With wildcard pattern at the end + '@my-scope/my_package-with-wildcard-end' | [ref(:ppr_with_wildcard_end)] + '@my-scope/my_package-with-wildcard-end:1234567890' | [ref(:ppr_with_wildcard_end)] + 'prefix-@my-scope/my_package-with-wildcard-end' | [] + 'prefix-@my-scope/my_package-with-wildcard-end:1234567890' | [] + + # With wildcard pattern inbetween + '@my-scope/my_package-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)] + '@my-scope/any-my_package-with-wildcard-inbetween' | [ref(:ppr_with_wildcard_inbetween)] + '@my-scope/any-my_package-my_package-wildcard-inbetween-any' | [] + + # With multiple wildcard pattern are used + '@my-scope/my_package-with-wildcard-multiple' | [ref(:ppr_with_wildcard_multiples)] + 'prefix-@my-scope/any-my_package-with-wildcard-multiple-any' | [ref(:ppr_with_wildcard_multiples)] + '****@my-scope/****my_package-with-wildcard-multiple****' | [ref(:ppr_with_wildcard_multiples)] + 'prefix-@other-scope/any-my_package-with-wildcard-multiple-any' | [] + + # With wildcard escaped + '@my-scope/my_package-with-wildcard-escaped-\*' | [ref(:ppr_with_wildcard_escaped)] + '@my-scope/my_package-with-wildcard-escaped-\any-character' | [ref(:ppr_with_wildcard_escaped)] + '@my-scope/my_package-with-wildcard-escaped-\%' | [ref(:ppr_with_wildcard_escaped)] + '@my-scope/my_package-with-wildcard-escaped-any-character' | [] + + # With underscore + '@my-scope/my_package-with_____underscore' | [ref(:ppr_with_underscore)] + '@my-scope/my_package-with_any_underscore' | [] + + # With percent sign + '@my-scope/my_package-with-percent-sign-%' | [ref(:ppr_with_percent_sign)] + '@my-scope/my_package-with-percent-sign-any-character' | [] + + # With percent sign escaped + '@my-scope/my_package-with-percent-sign-escaped-\%' | [ref(:ppr_with_percent_sign_escaped)] + '@my-scope/my_package-with-percent-sign-escaped-\*' | [] + '@my-scope/my_package-with-percent-sign-escaped-\any-character' | [] + '@my-scope/my_package-with-percent-sign-escaped-any-character' | [] + + '@my-scope/my_package-with-unsupported-regex-characters.+' | [ref(:ppr_with_unsupported_regex_characters)] + '@my-scope/my_package-with-unsupported-regex-characters.' | [] + '@my-scope/my_package-with-unsupported-regex-characters' | [] + '@my-scope/my_package-with-unsupported-regex-characters-any' | [] + + # Special cases + nil | [] + '' | [] + 'any_package' | [] + end + + with_them do + it { is_expected.to match_array(expected_package_protection_rules) } + end + end + + context 'with multiple matching package protection rules' do + let!(:package_protection_rule_second_match) do + create(:package_protection_rule, package_name_pattern: "#{package_name}*") + end + + it { is_expected.to contain_exactly(package_protection_rule_second_match, package_protection_rule) } + end + + context 'with scope `none`' do + subject { described_class.matching_package_name(package_name, described_class.none) } + + it { is_expected.to be_empty } + end + end + + describe '.push_protection_applicable_for_access_level' do + let_it_be(:ppr_for_developer) do + create(:package_protection_rule, package_name_pattern: 'package-name-pattern-for-developer', + push_protected_up_to_access_level: Gitlab::Access::DEVELOPER) + end + + let_it_be(:ppr_for_maintainer) do + create(:package_protection_rule, package_name_pattern: 'package-name-pattern-for-maintainer', + push_protected_up_to_access_level: Gitlab::Access::MAINTAINER) + end + + let_it_be(:ppr_for_owner) do + create(:package_protection_rule, package_name_pattern: 'package-name-pattern-for-owner', + push_protected_up_to_access_level: Gitlab::Access::OWNER) + end + + subject { described_class.push_protection_applicable_for_access_level(push_protected_up_to_access_level) } + + context 'with different scenarios' do + let_it_be(:access_level_guest) { Gitlab::Access::GUEST } + let_it_be(:access_level_reporter) { Gitlab::Access::REPORTER } + let_it_be(:access_level_developer) { Gitlab::Access::DEVELOPER } + let_it_be(:access_level_maintainer) { Gitlab::Access::MAINTAINER } + let_it_be(:access_level_owner) { Gitlab::Access::OWNER } + let_it_be(:access_level_admin) { Gitlab::Access::ADMIN } + let_it_be(:access_level_no_access) { Gitlab::Access::NO_ACCESS } + + using RSpec::Parameterized::TableSyntax + + # rubocop:disable Layout/LineLength + where(:push_protected_up_to_access_level, :expected_package_protection_rules) do + ref(:access_level_guest) | [ref(:ppr_for_developer), ref(:ppr_for_maintainer), ref(:ppr_for_owner)] + ref(:access_level_reporter) | [ref(:ppr_for_developer), ref(:ppr_for_maintainer), ref(:ppr_for_owner)] + ref(:access_level_developer) | [ref(:ppr_for_developer), ref(:ppr_for_maintainer), ref(:ppr_for_owner)] + ref(:access_level_maintainer) | [ref(:ppr_for_maintainer), ref(:ppr_for_owner)] + ref(:access_level_owner) | [ref(:ppr_for_owner)] + ref(:access_level_admin) | [] + ref(:access_level_no_access) | [ref(:ppr_for_developer), ref(:ppr_for_maintainer), ref(:ppr_for_owner)] + + # Special cases + nil | [ref(:ppr_for_developer), ref(:ppr_for_maintainer), ref(:ppr_for_owner)] + 0 | [ref(:ppr_for_developer), ref(:ppr_for_maintainer), ref(:ppr_for_owner)] + -1 | [ref(:ppr_for_developer), ref(:ppr_for_maintainer), ref(:ppr_for_owner)] + end + # rubocop:enable Layout/LineLength + + with_them do + it { is_expected.to eq expected_package_protection_rules } + end + end + end + + describe '.for_package_type' do + let_it_be(:ppr_for_npm) do + create(:package_protection_rule, package_type: :npm) + end + + it { expect(described_class.for_package_type(:npm)).to eq [ppr_for_npm] } + it { expect(described_class.for_package_type('npm')).to eq [ppr_for_npm] } + it { expect(described_class.for_package_type(Packages::Package.package_types.fetch(:npm))).to eq [ppr_for_npm] } + + it { expect(described_class.for_package_type('')).to eq [] } + it { expect(described_class.for_package_type(nil)).to eq [] } + it { expect(described_class.for_package_type(Packages::Package.package_types.fetch(:pypi))).to eq [] } + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index db5b5c914c9d061ba4c38ea1488302bd119d3bed..6193a565d76fb4ac3db45dea20ec88b2c8966895 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -152,6 +152,7 @@ it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::ProjectDistribution').dependent(:destroy) } it { is_expected.to have_many(:npm_metadata_caches).class_name('Packages::Npm::MetadataCache') } it { is_expected.to have_one(:packages_cleanup_policy).class_name('Packages::Cleanup::Policy').inverse_of(:project) } + it { is_expected.to have_many(:package_protection_rules).class_name('Packages::Protection::Rule').inverse_of(:project) } it { is_expected.to have_many(:pipeline_artifacts).dependent(:restrict_with_error) } it { is_expected.to have_many(:terraform_states).class_name('Terraform::State').inverse_of(:project) } it { is_expected.to have_many(:timelogs) } diff --git a/spec/policies/packages/policies/project_policy_spec.rb b/spec/policies/packages/policies/project_policy_spec.rb index fde10f64be82ef5fcb46c5798a09e0cfa4dc723d..1f8a653f984b7137743770ef53f1b4795502a769 100644 --- a/spec/policies/packages/policies/project_policy_spec.rb +++ b/spec/policies/packages/policies/project_policy_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Policies::ProjectPolicy do +RSpec.describe Packages::Policies::ProjectPolicy, feature_category: :package_registry do include_context 'ProjectPolicy context' let(:project) { public_project } diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml index e7dd9cea9229f264d1de826e5c3c78d97dc1768f..bfcf6bddfb7af441fe54d6c432e28de1895f51fe 100644 --- a/spec/support/finder_collection_allowlist.yml +++ b/spec/support/finder_collection_allowlist.yml @@ -51,6 +51,7 @@ - Packages::Go::VersionFinder - Packages::PackageFileFinder - Packages::PackageFinder +- Packages::PackageProtectionRulePushFinder - Packages::Pypi::PackageFinder - Projects::Integrations::Jira::ByIdsFinder - Projects::Integrations::Jira::IssuesFinder