From 09117ff518d1d8f48bfcd52c46a9ad95ab27d26b Mon Sep 17 00:00:00 2001 From: Gerardo Date: Wed, 31 May 2023 14:54:14 +0200 Subject: [PATCH] Introducing basics for protecting packages This MR introduces the basics for protecting packages, e.g. database migration, data model, etc. The scope of the basic functionality was discussed in the epic: https://gitlab.com/groups/gitlab-org/-/epics/5574 Changelog: added --- app/models/packages/protection.rb | 9 +++++ app/models/packages/protection/rule.rb | 21 ++++++++++ app/models/project.rb | 3 ++ db/docs/packages_protection_rules.yml | 10 +++++ ...170000_create_packages_protection_rules.rb | 18 +++++++++ db/schema_migrations/20230903170000 | 1 + db/structure.sql | 34 +++++++++++++++- .../packages/package_protection_rules.rb | 10 +++++ spec/lib/gitlab/import_export/all_models.yml | 1 + spec/models/packages/protection/rule_spec.rb | 40 +++++++++++++++++++ spec/models/project_spec.rb | 1 + .../packages/policies/project_policy_spec.rb | 2 +- 12 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 app/models/packages/protection.rb create mode 100644 app/models/packages/protection/rule.rb create mode 100644 db/docs/packages_protection_rules.yml create mode 100644 db/migrate/20230903170000_create_packages_protection_rules.rb create mode 100644 db/schema_migrations/20230903170000 create mode 100644 spec/factories/packages/package_protection_rules.rb create mode 100644 spec/models/packages/protection/rule_spec.rb diff --git a/app/models/packages/protection.rb b/app/models/packages/protection.rb new file mode 100644 index 00000000000000..ebaecf89992110 --- /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 00000000000000..bb65be92b90f09 --- /dev/null +++ b/app/models/packages/protection/rule.rb @@ -0,0 +1,21 @@ +# 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 + ] } + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index 259911088011fc..ad34948786b5bf 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 00000000000000..3007c956e269e5 --- /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 00000000000000..d5e8a4af558404 --- /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 + + t.index [:project_id, :package_type, :package_name_pattern], unique: true, + name: :i_packages_unique_project_id_package_type_package_name_pattern + end + end +end diff --git a/db/schema_migrations/20230903170000 b/db/schema_migrations/20230903170000 new file mode 100644 index 00000000000000..5189c60657a9f8 --- /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 6148a375c48f6a..010b351989e938 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11954,11 +11954,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)), @@ -20271,6 +20271,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, @@ -26164,6 +26184,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); @@ -28507,6 +28529,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); @@ -30470,6 +30495,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); @@ -38823,6 +38850,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 00000000000000..3038fb847e7d8d --- /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/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index de3f28a5b90641..5ca5270a8515cf 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/protection/rule_spec.rb b/spec/models/packages/protection/rule_spec.rb new file mode 100644 index 00000000000000..b368687e6d8051 --- /dev/null +++ b/spec/models/packages/protection/rule_spec.rb @@ -0,0 +1,40 @@ +# 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 +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index db5b5c914c9d06..6193a565d76fb4 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 fde10f64be82ef..1f8a653f984b71 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 } -- GitLab