diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb index 3f0e849d7efd1e91f94c818754938e429ba802b8..52a9e057148aaf7cc75b3de38e67fa127b4e8214 100644 --- a/app/finders/concerns/packages/finder_helper.rb +++ b/app/finders/concerns/packages/finder_helper.rb @@ -38,6 +38,18 @@ def packages_visible_to_user(user, within_group:, with_package_registry_enabled: ::Packages::Package.for_projects(projects.select(:id)).installable end + def packages_visible_to_user_including_public_registries(user, within_group:) + return ::Packages::Package.none unless within_group + + return ::Packages::Package.none unless Ability.allowed?(user, :read_package_within_public_registries, + within_group.packages_policy_subject) + + projects = projects_visible_to_reporters(user, within_group: within_group, + within_public_package_registry: !Ability.allowed?(user, :read_group, within_group)) + + ::Packages::Package.for_projects(projects.select(:id)).installable + end + def projects_visible_to_user(user, within_group:) return ::Project.none unless within_group return ::Project.none unless Ability.allowed?(user, :read_group, within_group) @@ -45,13 +57,17 @@ def projects_visible_to_user(user, within_group:) projects_visible_to_reporters(user, within_group: within_group) end - def projects_visible_to_reporters(user, within_group:) - if user.is_a?(DeployToken) - user.accessible_projects - else - within_group.all_projects - .public_or_visible_to_user(user, ::Gitlab::Access::REPORTER) + def projects_visible_to_reporters(user, within_group:, within_public_package_registry: false) + return user.accessible_projects if user.is_a?(DeployToken) + + unless within_public_package_registry + return within_group.all_projects.public_or_visible_to_user(user, ::Gitlab::Access::REPORTER) end + + ::Project + .public_or_visible_to_user(user, Gitlab::Access::REPORTER) + .or(::Project.with_public_package_registry) + .in_namespace(within_group.self_and_descendants) end def package_type diff --git a/app/finders/packages/nuget/package_finder.rb b/app/finders/packages/nuget/package_finder.rb index 684b99f8647a928c13a285fbc82d0cfd5d3e3d0e..54c83286d20c2336bb16335583780a7a9c6b08a3 100644 --- a/app/finders/packages/nuget/package_finder.rb +++ b/app/finders/packages/nuget/package_finder.rb @@ -3,6 +3,8 @@ module Packages module Nuget class PackageFinder < ::Packages::GroupOrProjectPackageFinder + extend ::Gitlab::Utils::Override + MAX_PACKAGES_COUNT = 300 FORCE_NORMALIZATION_CLIENT_VERSION = '>= 3' @@ -36,6 +38,15 @@ def find_by_version(result) ) end + override :group_packages + def group_packages + if ::Feature.disabled?(:allow_anyone_to_pull_public_nuget_packages_on_group_level, @project_or_group) + return super + end + + packages_visible_to_user_including_public_registries(@current_user, within_group: @project_or_group) + end + def client_forces_normalized_version? return true if @params[:client_version].blank? diff --git a/app/models/project.rb b/app/models/project.rb index 236da1ca1bddc297673c099cea27b11db6c9de98..477d6d12522dfc53f44db2e30e3eda76fac36e80 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -778,6 +778,13 @@ def self.integration_association_name(name) scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_package_registry_enabled, -> { with_feature_enabled(:package_registry) } + scope :with_public_package_registry, -> do + where_exists( + ::ProjectFeature + .where(::ProjectFeature.arel_table[:project_id].eq(arel_table[:id])) + .with_feature_access_level(:package_registry, ::ProjectFeature::PUBLIC) + ) + end scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) } scope :with_merge_requests_available_for_user, ->(current_user) { with_feature_available_for_user(:merge_requests, current_user) } scope :with_issues_or_mrs_available_for_user, ->(user) do diff --git a/app/policies/packages/policies/group_policy.rb b/app/policies/packages/policies/group_policy.rb index d8c20c7a90aff66c84268885c2335cb8fb40f27b..3a03aa32cf9d2d0045ebd474b3ad99b788fa81bd 100644 --- a/app/policies/packages/policies/group_policy.rb +++ b/app/policies/packages/policies/group_policy.rb @@ -7,6 +7,13 @@ class GroupPolicy < BasePolicy overrides(:read_package) + # Because we need to defer the evaluation of this condition to be after :read_group is evaluated, + # we put its score higher than the score of :read_group (122) + condition(:has_projects_with_public_package_registry, scope: :subject, score: 150) do + ::Gitlab::CurrentSettings.package_registry_allow_anyone_to_pull_option && + @subject.all_projects.with_public_package_registry.any? + end + rule { group.public_group }.policy do enable :read_package end @@ -22,6 +29,15 @@ class GroupPolicy < BasePolicy rule { group.write_package_registry_deploy_token }.policy do enable :read_package end + + rule { can?(:read_group) | has_projects_with_public_package_registry }.policy do + # We add a new permission and don't reuse :read_group here for two reasons: + # 1. This's a bit of expensive rule to compute, so we need to narrow it down to a more targeted permission + # that only allows access to the public package registry in private/internal groups + # 2. The :read_group permission is more broad and used in many places. This may grant access to other + # package-related actions that we don't want to. + enable :read_package_within_public_registries + end end end end diff --git a/config/feature_flags/gitlab_com_derisk/allow_anyone_to_pull_public_nuget_packages_on_group_level.yml b/config/feature_flags/gitlab_com_derisk/allow_anyone_to_pull_public_nuget_packages_on_group_level.yml new file mode 100644 index 0000000000000000000000000000000000000000..61840591af92fc3bc1f2e146f75d16d39d6b7a2d --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/allow_anyone_to_pull_public_nuget_packages_on_group_level.yml @@ -0,0 +1,9 @@ +--- +name: allow_anyone_to_pull_public_nuget_packages_on_group_level +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/383537 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155119 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/464556 +milestone: '17.1' +group: group::package registry +type: gitlab_com_derisk +default_enabled: false diff --git a/db/post_migrate/20240607140843_add_index_on_project_feature_project_id_when_public_package_registry.rb b/db/post_migrate/20240607140843_add_index_on_project_feature_project_id_when_public_package_registry.rb new file mode 100644 index 0000000000000000000000000000000000000000..214f15f1d10c3fc9de6b24fb98161155ba923260 --- /dev/null +++ b/db/post_migrate/20240607140843_add_index_on_project_feature_project_id_when_public_package_registry.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexOnProjectFeatureProjectIdWhenPublicPackageRegistry < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.1' + + INDEX_NAME = 'index_project_features_on_project_id_on_public_package_registry' + PROJECT_FEATURES_PUBLIC = 30 + + def up + add_concurrent_index :project_features, :project_id, + where: "package_registry_access_level = #{PROJECT_FEATURES_PUBLIC}", name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :project_features, INDEX_NAME + end +end diff --git a/db/post_migrate/20240607140927_add_index_packages_project_id_lower_name_when_nuget_installable_with_version.rb b/db/post_migrate/20240607140927_add_index_packages_project_id_lower_name_when_nuget_installable_with_version.rb new file mode 100644 index 0000000000000000000000000000000000000000..ea5a6740c423e11463d0e2942496582de0631d3b --- /dev/null +++ b/db/post_migrate/20240607140927_add_index_packages_project_id_lower_name_when_nuget_installable_with_version.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddIndexPackagesProjectIdLowerNameWhenNugetInstallableWithVersion < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.1' + + INDEX_NAME = 'idx_pkgs_project_id_lower_name_when_nuget_installable_version' + NUGET_TYPE = 'package_type = 4' + WITH_VERSION = 'version IS NOT NULL' + INSTALLABLE_STATUS = 'status IN (0, 1)' + + def up + add_concurrent_index :packages_packages, 'project_id, LOWER(name)', # rubocop:disable Migration/PreventIndexCreation -- I'm replicating an existing index with a more selective where clause + where: "#{NUGET_TYPE} AND #{WITH_VERSION} AND #{INSTALLABLE_STATUS}", + name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :packages_packages, name: INDEX_NAME + end +end diff --git a/db/post_migrate/20240607141037_remove_index_packages_packages_on_project_id_and_lower_name_to_packages.rb b/db/post_migrate/20240607141037_remove_index_packages_packages_on_project_id_and_lower_name_to_packages.rb new file mode 100644 index 0000000000000000000000000000000000000000..55931e15833154b7ceaa69278e6b3bbe5f44c12d --- /dev/null +++ b/db/post_migrate/20240607141037_remove_index_packages_packages_on_project_id_and_lower_name_to_packages.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class RemoveIndexPackagesPackagesOnProjectIdAndLowerNameToPackages < Gitlab::Database::Migration[2.2] + disable_ddl_transaction! + milestone '17.1' + + INDEX_NAME = 'index_packages_packages_on_project_id_and_lower_name' + NUGET_PACKAGE_TYPE = 4 + + def up + remove_concurrent_index_by_name :packages_packages, INDEX_NAME + end + + def down + add_concurrent_index( + :packages_packages, + 'project_id, LOWER(name)', + name: INDEX_NAME, + where: "package_type = #{NUGET_PACKAGE_TYPE}" + ) + end +end diff --git a/db/schema_migrations/20240607140843 b/db/schema_migrations/20240607140843 new file mode 100644 index 0000000000000000000000000000000000000000..b0b435afeaa56788f3e3f6e4d7f407f76d7f4892 --- /dev/null +++ b/db/schema_migrations/20240607140843 @@ -0,0 +1 @@ +1f72fabfcc43e0fdf24e9d28caa8f5643c0a659bbfe62f084ed7926f4ff8c197 \ No newline at end of file diff --git a/db/schema_migrations/20240607140927 b/db/schema_migrations/20240607140927 new file mode 100644 index 0000000000000000000000000000000000000000..e825ce57dc2015616ab1765c9a334e1f4ba9d8a8 --- /dev/null +++ b/db/schema_migrations/20240607140927 @@ -0,0 +1 @@ +7f380a2f886edb615f5a6216211132d7f8f59d65d8ab5179541ef3aba9c852e4 \ No newline at end of file diff --git a/db/schema_migrations/20240607141037 b/db/schema_migrations/20240607141037 new file mode 100644 index 0000000000000000000000000000000000000000..752cde900e3ce74018cccd84c759991c32917eb1 --- /dev/null +++ b/db/schema_migrations/20240607141037 @@ -0,0 +1 @@ +382ac12d66f4777b5743d86428b0acb1a3db7fa7384b42640d4da6c9fcbcf304 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index eb601d410cb72fc71510716a56bfee7757ec7f4b..37ac387f8d2984bca11e217cb6324390fbb7a657 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -25219,6 +25219,8 @@ CREATE INDEX idx_pkgs_nuget_symbols_on_lowercase_signature_and_file_name ON pack CREATE INDEX idx_pkgs_on_project_id_name_version_on_installable_terraform ON packages_packages USING btree (project_id, name, version, id) WHERE ((package_type = 12) AND (status = ANY (ARRAY[0, 1]))); +CREATE INDEX idx_pkgs_project_id_lower_name_when_nuget_installable_version ON packages_packages USING btree (project_id, lower((name)::text)) WHERE ((package_type = 4) AND (version IS NOT NULL) AND (status = ANY (ARRAY[0, 1]))); + CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_cloud_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_cloud_last_sync_at, project_id) WHERE (jira_dvcs_cloud_last_sync_at IS NOT NULL); CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_server_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_server_last_sync_at, project_id) WHERE (jira_dvcs_server_last_sync_at IS NOT NULL); @@ -27725,8 +27727,6 @@ CREATE INDEX index_packages_packages_on_name_trigram ON packages_packages USING CREATE INDEX index_packages_packages_on_project_id_and_created_at ON packages_packages USING btree (project_id, created_at); -CREATE INDEX index_packages_packages_on_project_id_and_lower_name ON packages_packages USING btree (project_id, lower((name)::text)) WHERE (package_type = 4); - CREATE INDEX index_packages_packages_on_project_id_and_lower_version ON packages_packages USING btree (project_id, lower((version)::text)) WHERE (package_type = 4); CREATE INDEX index_packages_packages_on_project_id_and_package_type ON packages_packages USING btree (project_id, package_type); @@ -27903,6 +27903,8 @@ CREATE UNIQUE INDEX index_project_features_on_project_id_include_container_regis COMMENT ON INDEX index_project_features_on_project_id_include_container_registry IS 'Included column (container_registry_access_level) improves performance of the ContainerRepository.for_group_and_its_subgroups scope query'; +CREATE INDEX index_project_features_on_project_id_on_public_package_registry ON project_features USING btree (project_id) WHERE (package_registry_access_level = 30); + CREATE INDEX index_project_features_on_project_id_ral_20 ON project_features USING btree (project_id) WHERE (repository_access_level = 20); CREATE INDEX index_project_group_links_on_group_id_and_project_id ON project_group_links USING btree (group_id, project_id); diff --git a/lib/api/helpers/packages/basic_auth_helpers.rb b/lib/api/helpers/packages/basic_auth_helpers.rb index 4f301d7038a1a2ae71ae52ee762324a16f29e918..1237621e8b24707ce499434ee7a66b6eb68eea0f 100644 --- a/lib/api/helpers/packages/basic_auth_helpers.rb +++ b/lib/api/helpers/packages/basic_auth_helpers.rb @@ -40,16 +40,24 @@ def authorized_project_find!(action: :read_project) project end - def find_authorized_group! - group = find_group(params[:id]) + def find_authorized_group!(action: :read_group) + strong_memoize_with(:find_authorized_group, action) do + group = find_group(params[:id]) + + subject = case action + when :read_package_within_public_registries + group&.packages_policy_subject + when :read_group + group + end + + unless group && can?(current_user, action, subject) + break unauthorized_or! { not_found! } + end - unless group && can?(current_user, :read_group, group) - return unauthorized_or! { not_found! } + group end - - group end - strong_memoize_attr :find_authorized_group! def authorize!(action, subject = :global, reason = nil) return if can?(current_user, action, subject) diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index 037cad29b990fb15d05e4e97b6e93f1e4d15ffc9..632aaa1c528e30918447ce1b364b2ac2df128032 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -7,7 +7,6 @@ module PackagesHelpers include ::Gitlab::Utils::StrongMemoize MAX_PACKAGE_FILE_SIZE = 50.megabytes.freeze - ALLOWED_REQUIRED_PERMISSIONS = %i[read_package read_group].freeze def require_packages_enabled! not_found! unless ::Gitlab.config.packages.enabled @@ -35,12 +34,16 @@ def authorize_destroy_package!(subject = user_project) def authorize_packages_access!(subject = user_project, required_permission = :read_package) require_packages_enabled! - return forbidden! unless required_permission.in?(ALLOWED_REQUIRED_PERMISSIONS) - if required_permission == :read_package + case required_permission + when :read_package authorize_read_package!(subject) - else + when :read_package_within_public_registries + authorize!(required_permission, subject.packages_policy_subject) + when :read_group authorize!(required_permission, subject) + else + forbidden! end end diff --git a/lib/api/nuget_group_packages.rb b/lib/api/nuget_group_packages.rb index 394f8911e9ed02ef5916b1d87379b624016fc049..ac652ebebec24179bcddfd6be0eea0a7b4de8483 100644 --- a/lib/api/nuget_group_packages.rb +++ b/lib/api/nuget_group_packages.rb @@ -30,7 +30,7 @@ class NugetGroupPackages < ::API::Base include ::Gitlab::Utils::StrongMemoize def project_or_group - find_authorized_group! + find_authorized_group!(action: required_permission) end def project_or_group_without_auth @@ -55,7 +55,16 @@ def snowplow_gitlab_standard_context_without_auth end def required_permission - :read_group + if allow_anyone_to_pull_public_packages? + :read_package_within_public_registries + else + :read_group + end + end + + def allow_anyone_to_pull_public_packages? + options[:path].first.in?(%w[index *package_version]) && + ::Feature.enabled?(:allow_anyone_to_pull_public_nuget_packages_on_group_level, project_or_group_without_auth) end end @@ -77,7 +86,7 @@ def required_permission namespace '/nuget' do after_validation do # This API can't be accessed anonymously - require_authenticated! + require_authenticated! unless allow_anyone_to_pull_public_packages? end include ::API::Concerns::Packages::Nuget::PrivateEndpoints diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb index 4145e1e2a54187c151317da7fba3a0000abb97da..de7ead202337b747204a4983df360c790bef334e 100644 --- a/spec/finders/concerns/packages/finder_helper_spec.rb +++ b/spec/finders/concerns/packages/finder_helper_spec.rb @@ -3,6 +3,22 @@ require 'spec_helper' RSpec.describe ::Packages::FinderHelper, feature_category: :package_registry do + let_it_be(:finder_class) do + Class.new do + include ::Packages::FinderHelper + + def method_missing(method_name, *args, **kwargs) + send(method_name, *args, **kwargs) + end + + def respond_to_missing? + true + end + end + end + + let_it_be(:finder) { finder_class.new } + describe '#packages_for_project' do let_it_be_with_reload(:project1) { create(:project) } let_it_be(:package1) { create(:package, project: project1) } @@ -10,19 +26,7 @@ let_it_be(:project2) { create(:project) } let_it_be(:package3) { create(:package, project: project2) } - let(:finder_class) do - Class.new do - include ::Packages::FinderHelper - - def execute(project1) - packages_for_project(project1) - end - end - end - - let(:finder) { finder_class.new } - - subject { finder.execute(project1) } + subject { finder.packages_for_project(project1) } it { is_expected.to eq [package1] } end @@ -38,23 +42,7 @@ def execute(project1) let_it_be(:package2) { create(:package, project: project2) } let_it_be(:package3) { create(:package, :error, project: project2) } - let(:finder_class) do - Class.new do - include ::Packages::FinderHelper - - def initialize(user) - @current_user = user - end - - def execute(group) - packages_for(@current_user, within_group: group) - end - end - end - - let(:finder) { finder_class.new(user) } - - subject { finder.execute(group) } + subject { finder.packages_for(user, within_group: group) } shared_examples 'returning both packages' do it { is_expected.to contain_exactly(package1, package2) } @@ -91,13 +79,13 @@ def execute(group) end context 'without a group' do - subject { finder.execute(nil) } + let(:group) { nil } it_behaves_like 'returning no packages' end context 'with a subgroup' do - subject { finder.execute(subgroup) } + let(:group) { subgroup } it_behaves_like 'returning package2' end @@ -123,20 +111,20 @@ def execute(group) end context 'without a group' do - subject { finder.execute(nil) } + let(:group) { nil } it_behaves_like 'returning no packages' end context 'with a subgroup' do - subject { finder.execute(subgroup) } + let(:group) { subgroup } it_behaves_like 'returning both packages' end end end - describe '#packages_visible_to_user' do + context 'for packages visible to user' do using RSpec::Parameterized::TableSyntax let_it_be_with_reload(:group) { create(:group) } @@ -147,24 +135,6 @@ def execute(group) let_it_be(:package2) { create(:package, project: project2) } let_it_be(:package3) { create(:package, :error, project: project2) } - let(:finder_class) do - Class.new do - include ::Packages::FinderHelper - - def initialize(user) - @current_user = user - end - - def execute(group) - packages_visible_to_user(@current_user, within_group: group) - end - end - end - - let(:finder) { finder_class.new(user) } - - subject { finder.execute(group) } - shared_examples 'returning both packages' do it { is_expected.to contain_exactly(package1, package2) } end @@ -173,98 +143,125 @@ def execute(group) it { is_expected.to eq [package1] } end + shared_examples 'returning package2' do + it { is_expected.to eq [package2] } + end + shared_examples 'returning no packages' do it { is_expected.to be_empty } end - context 'with a user' do - let_it_be(:user) { create(:user) } + describe '#packages_visible_to_user' do + subject { finder.packages_visible_to_user(user, within_group: group) } + + context 'with a user' do + let_it_be(:user) { create(:user) } + + where(:group_visibility, :subgroup_visibility, :project2_visibility, :user_role, :shared_example_name) do + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :maintainer | 'returning both packages' + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :developer | 'returning both packages' + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :guest | 'returning both packages' + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :anonymous | 'returning both packages' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :maintainer | 'returning both packages' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :developer | 'returning both packages' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :guest | 'returning package1' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :anonymous | 'returning package1' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both packages' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning package1' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning package1' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both packages' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning no packages' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning no packages' + end - where(:group_visibility, :subgroup_visibility, :project2_visibility, :user_role, :shared_example_name) do - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :maintainer | 'returning both packages' - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :developer | 'returning both packages' - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :guest | 'returning both packages' - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | :anonymous | 'returning both packages' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :maintainer | 'returning both packages' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :developer | 'returning both packages' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :guest | 'returning package1' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | :anonymous | 'returning package1' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both packages' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning package1' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning package1' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :maintainer | 'returning both packages' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :developer | 'returning both packages' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :guest | 'returning no packages' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | :anonymous | 'returning no packages' - end + with_them do + before do + unless user_role == :anonymous + group.send("add_#{user_role}", user) + subgroup.send("add_#{user_role}", user) + project1.send("add_#{user_role}", user) + project2.send("add_#{user_role}", user) + end - with_them do - before do - unless user_role == :anonymous - group.send("add_#{user_role}", user) - subgroup.send("add_#{user_role}", user) - project1.send("add_#{user_role}", user) - project2.send("add_#{user_role}", user) + project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false)) + subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false)) + project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) end - project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false)) - subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false)) - project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) - group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + it_behaves_like params[:shared_example_name] end - it_behaves_like params[:shared_example_name] - end + context 'when the second project has the package registry disabled' do + before do + project1.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project2.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC, + package_registry_access_level: 'disabled', packages_enabled: false) + end - context 'when the second project has the package registry disabled' do - before do - project1.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) - project2.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC, - package_registry_access_level: 'disabled', packages_enabled: false) - end + it_behaves_like 'returning both packages' - it_behaves_like 'returning both packages' + context 'with with_package_registry_enabled set to true' do + subject do + finder.packages_visible_to_user(user, within_group: group, with_package_registry_enabled: true) + end - context 'with with_package_registry_enabled set to true' do - let(:finder_class) do - Class.new do - include ::Packages::FinderHelper + it_behaves_like 'returning package1' + end + end + end - def initialize(user) - @current_user = user - end + context 'with a group deploy token' do + let_it_be(:user) { create(:deploy_token, :group, read_package_registry: true) } + let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } - def execute(group) - packages_visible_to_user(@current_user, within_group: group, with_package_registry_enabled: true) - end - end + where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do + 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | 'returning both packages' + 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | 'returning both packages' + 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | 'returning both packages' + 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both packages' + end + + with_them do + before do + project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false)) + subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false)) + project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) end - it_behaves_like 'returning package1' + it_behaves_like params[:shared_example_name] end end end - context 'with a group deploy token' do - let_it_be(:user) { create(:deploy_token, :group, read_package_registry: true) } - let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } + describe '#packages_visible_to_user_including_public_registries' do + subject { finder.packages_visible_to_user_including_public_registries(user, within_group: group) } - where(:group_visibility, :subgroup_visibility, :project2_visibility, :shared_example_name) do - 'PUBLIC' | 'PUBLIC' | 'PUBLIC' | 'returning both packages' - 'PUBLIC' | 'PUBLIC' | 'PRIVATE' | 'returning both packages' - 'PUBLIC' | 'PRIVATE' | 'PRIVATE' | 'returning both packages' - 'PRIVATE' | 'PRIVATE' | 'PRIVATE' | 'returning both packages' - end + let(:user) { nil } - with_them do - before do - project2.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project2_visibility, false)) - subgroup.update!(visibility_level: Gitlab::VisibilityLevel.const_get(subgroup_visibility, false)) - project1.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) - group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + before do + [subgroup, group, project1, project2].each do |entity| + entity.update!(visibility_level: Gitlab::VisibilityLevel.const_get(:PRIVATE, false)) end + project1.project_feature.update!(package_registry_access_level: project1_package_registry_access_level) + project2.project_feature.update!(package_registry_access_level: project2_package_registry_access_level) + end + where(:project1_package_registry_access_level, :project2_package_registry_access_level, :shared_example_name) do + ::ProjectFeature::PUBLIC | ::ProjectFeature::PUBLIC | 'returning both packages' + ::ProjectFeature::PUBLIC | ::ProjectFeature::PRIVATE | 'returning package1' + ::ProjectFeature::PUBLIC | ::ProjectFeature::DISABLED | 'returning package1' + ::ProjectFeature::PUBLIC | ::ProjectFeature::ENABLED | 'returning package1' + ::ProjectFeature::PRIVATE | ::ProjectFeature::PUBLIC | 'returning package2' + ::ProjectFeature::DISABLED | ::ProjectFeature::PUBLIC | 'returning package2' + ::ProjectFeature::ENABLED | ::ProjectFeature::PUBLIC | 'returning package2' + ::ProjectFeature::PRIVATE | ::ProjectFeature::PRIVATE | 'returning no packages' + end + + with_them do it_behaves_like params[:shared_example_name] end end @@ -279,23 +276,7 @@ def execute(group) let_it_be_with_reload(:subgroup) { create(:group, parent: group) } let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) } - let(:finder_class) do - Class.new do - include ::Packages::FinderHelper - - def initialize(user) - @current_user = user - end - - def execute(group) - projects_visible_to_user(@current_user, within_group: group) - end - end - end - - let(:finder) { finder_class.new(user) } - - subject { finder.execute(group) } + subject { finder.projects_visible_to_user(user, within_group: group) } shared_examples 'returning both projects' do it { is_expected.to contain_exactly(project1, project2) } diff --git a/spec/finders/packages/nuget/package_finder_spec.rb b/spec/finders/packages/nuget/package_finder_spec.rb index 8230d132d75d64fcb582b8386b76985860075c72..d2730a9c0483307b26c65e3f3476c2666916ec49 100644 --- a/spec/finders/packages/nuget/package_finder_spec.rb +++ b/spec/finders/packages/nuget/package_finder_spec.rb @@ -158,5 +158,26 @@ it { is_expected.to be_empty } end + + context 'with public package registry in private group' do + let(:target) { group } + + before_all do + [subgroup, group, project].each do |entity| + entity.update!(visibility_level: Gitlab::VisibilityLevel.const_get(:PRIVATE, false)) + end + project.project_feature.update!(package_registry_access_level: ::ProjectFeature::PUBLIC) + end + + it { is_expected.to match_array([package1, package2]) } + + context 'when allow_anyone_to_pull_public_nuget_packages_on_group_level FF is disabled' do + before do + stub_feature_flags(allow_anyone_to_pull_public_nuget_packages_on_group_level: false) + end + + it { is_expected.to be_empty } + end + end end end diff --git a/spec/lib/api/helpers/packages_helpers_spec.rb b/spec/lib/api/helpers/packages_helpers_spec.rb index f44581dcd853dc71ce97a435e0ae7247ca5ed442..e20bb01e0605b5b8168e429bece86b756edb89b0 100644 --- a/spec/lib/api/helpers/packages_helpers_spec.rb +++ b/spec/lib/api/helpers/packages_helpers_spec.rb @@ -42,6 +42,17 @@ expect(subject).to eq nil end end + + context 'with read_public_package_registry permission' do + subject { helper.authorize_packages_access!(group, :read_package_within_public_registries) } + + it 'authorizes packages access' do + expect(helper).to receive(:require_packages_enabled!) + expect(helper).to receive(:authorize!).with(:read_package_within_public_registries, instance_of(::Packages::Policies::Group)) + + expect(subject).to eq nil + end + end end describe 'authorize_read_package!' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 08417fd3a4df285fa2c193524f77e878ce12c15e..8af36ef57d088d9ebdb4931f5666906b49edc4d0 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -7057,6 +7057,15 @@ def has_external_wiki end end + describe '.with_public_package_registry' do + let_it_be(:project) { create(:project, package_registry_access_level: ::ProjectFeature::PUBLIC) } + let_it_be(:other_project) { create(:project, package_registry_access_level: ::ProjectFeature::ENABLED) } + + subject { described_class.with_public_package_registry } + + it { is_expected.to contain_exactly(project) } + end + describe '.not_a_fork' do let_it_be(:project) { create(:project, :public) } diff --git a/spec/policies/packages/policies/group_policy_spec.rb b/spec/policies/packages/policies/group_policy_spec.rb index d0d9a9a22f5cab3c4032f17d1c4266523ad387cc..165984dea1dcd12ea567770214e332d667c360ad 100644 --- a/spec/policies/packages/policies/group_policy_spec.rb +++ b/spec/policies/packages/policies/group_policy_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Packages::Policies::GroupPolicy do +RSpec.describe Packages::Policies::GroupPolicy, feature_category: :package_registry do include_context 'GroupPolicy context' subject { described_class.new(current_user, group.packages_policy_subject) } @@ -76,4 +76,67 @@ it { is_expected.to be_allowed(:read_package) } end end + + describe 'read public package registry' do + using RSpec::Parameterized::TableSyntax + + let_it_be_with_reload(:project) { create(:project, group: group) } + let(:current_user) { can_read_group ? reporter : external_user } + + subject { described_class.new(current_user, group.packages_policy_subject) } + + before do + group.update!(visibility_level: Gitlab::VisibilityLevel.const_get(group_visibility, false)) + project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false)) + project.project_feature.update!(package_registry_access_level: package_registry_access_level) + stub_application_setting(package_registry_allow_anyone_to_pull_option: application_setting) + end + + where(:group_visibility, :project_visibility, :package_registry_access_level, :can_read_group, + :application_setting, :result) do + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::DISABLED | true | true | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::DISABLED | true | false | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::DISABLED | false | true | false + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::DISABLED | false | false | false + + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PRIVATE | true | true | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PRIVATE | true | false | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PRIVATE | false | true | false + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PRIVATE | false | false | false + + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::ENABLED | true | true | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::ENABLED | true | false | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::ENABLED | false | true | false + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::ENABLED | false | false | false + + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PUBLIC | true | true | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PUBLIC | true | false | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PUBLIC | false | true | true + 'PRIVATE' | 'PRIVATE' | ::ProjectFeature::PUBLIC | false | false | false + + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::DISABLED | true | true | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::DISABLED | true | false | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::DISABLED | false | true | false + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::DISABLED | false | false | false + + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PRIVATE | true | true | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PRIVATE | true | false | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PRIVATE | false | true | false + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PRIVATE | false | false | false + + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::ENABLED | true | true | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::ENABLED | true | false | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::ENABLED | false | true | false + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::ENABLED | false | false | false + + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PUBLIC | true | true | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PUBLIC | true | false | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PUBLIC | false | true | true + 'INTERNAL' | 'PRIVATE' | ::ProjectFeature::PUBLIC | false | false | false + end + + with_them do + it { is_expected.to public_send(result ? :be_allowed : :be_disallowed, :read_package_within_public_registries) } + end + end end diff --git a/spec/requests/api/nuget_group_packages_spec.rb b/spec/requests/api/nuget_group_packages_spec.rb index 8b8c28a13c5dcea2920e2268c9386e32d2319f15..50f5b780754aa688311066d05af64dca0a84a235 100644 --- a/spec/requests/api/nuget_group_packages_spec.rb +++ b/spec/requests/api/nuget_group_packages_spec.rb @@ -38,29 +38,31 @@ def snowplow_context(user_role: :developer) end describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/index' do + let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/index.json" } + it_behaves_like 'handling nuget metadata requests with package name', example_names_with_status: { - anonymous_requests_example_name: 'rejects nuget packages access', - anonymous_requests_status: :unauthorized, guest_requests_example_name: 'rejects nuget packages access', - guest_requests_status: :not_found - } do - let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/index.json" } - end + guest_requests_status: :not_found, + invalid_target_not_found_status: :not_found + } + + it_behaves_like 'allows anyone to pull public nuget packages on group level' end describe 'GET /api/v4/groups/:id/-/packages/nuget/metadata/*package_name/*package_version' do + let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/#{package.version}.json" } + it_behaves_like 'handling nuget metadata requests with package name and package version', example_names_with_status: { - anonymous_requests_example_name: 'rejects nuget packages access', - anonymous_requests_status: :unauthorized, guest_requests_example_name: 'rejects nuget packages access', - guest_requests_status: :not_found - } do - let(:url) { "/groups/#{target.id}/-/packages/nuget/metadata/#{package_name}/#{package.version}.json" } - end + guest_requests_status: :not_found, + invalid_target_not_found_status: :not_found + } + + it_behaves_like 'allows anyone to pull public nuget packages on group level' end describe 'GET /api/v4/groups/:id/-/packages/nuget/query' do @@ -109,11 +111,11 @@ def update_visibility_to(visibility) subject { get api(url), headers: {} } - shared_examples 'handling mixed visibilities' do + shared_examples 'handling mixed visibilities' do |public_status: :success, non_public_status: :not_found| where(:group_visibility, :subgroup_visibility, :expected_status) do - 'PUBLIC' | 'PUBLIC' | :unauthorized - 'PUBLIC' | 'INTERNAL' | :unauthorized - 'PUBLIC' | 'PRIVATE' | :unauthorized + 'PUBLIC' | 'PUBLIC' | public_status + 'PUBLIC' | 'INTERNAL' | non_public_status + 'PUBLIC' | 'PRIVATE' | non_public_status 'INTERNAL' | 'INTERNAL' | :unauthorized 'INTERNAL' | 'PRIVATE' | :unauthorized 'PRIVATE' | 'PRIVATE' | :unauthorized @@ -143,7 +145,7 @@ def update_visibility_to(visibility) end describe 'GET /api/v4/groups/:id/-/packages/nuget/query' do - it_behaves_like 'handling mixed visibilities' do + it_behaves_like 'handling mixed visibilities', public_status: :unauthorized, non_public_status: :unauthorized do let(:url) { "/groups/#{target.id}/-/packages/nuget/query?#{query_parameters.to_query}" } end end diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index b3e0b6e27ee791ac15b8eace5bf23f0b2a31e235..0e41bee4f4f5f6b210c4406e0f6cf6c0dfc68689 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -14,6 +14,7 @@ let_it_be(:owner) { create(:user, owner_of: group) } let_it_be(:admin) { create(:admin) } let_it_be(:non_group_member) { create(:user) } + let_it_be(:external_user) { create(:user, :external) } let_it_be(:organization_owner) { create(:organization_user, :owner, organization: organization).user } diff --git a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb index 150e9a4e00436ede81466ece2632f6ba97e735c2..d5b90456dd0c536d941e66a32e3eaa8239b00170 100644 --- a/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_endpoints_shared_examples.rb @@ -44,10 +44,9 @@ RSpec.shared_examples 'handling nuget metadata requests with package name' do |example_names_with_status: {}| include_context 'with expected presenters dependency groups' - anonymous_requests_example_name = example_names_with_status.fetch(:anonymous_requests_example_name, 'process nuget metadata request at package name level') - anonymous_requests_status = example_names_with_status.fetch(:anonymous_requests_status, :success) guest_requests_example_name = example_names_with_status.fetch(:guest_requests_example_name, 'rejects nuget packages access') guest_requests_status = example_names_with_status.fetch(:guest_requests_status, :forbidden) + invalid_target_not_found_status = example_names_with_status.fetch(:invalid_target_not_found_status, :unauthorized) let_it_be(:package_name) { 'Dummy.Package' } let_it_be(:packages) { create_list(:nuget_package, 5, :with_metadatum, name: package_name, project: project) } @@ -71,7 +70,7 @@ 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name level' | :success 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status + 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name level' | :success 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name level' | :success 'PRIVATE' | :guest | true | true | guest_requests_example_name | guest_requests_status 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -103,19 +102,18 @@ end end - it_behaves_like 'rejects nuget access with unknown target id' + it_behaves_like 'rejects nuget access with unknown target id', not_found_response: invalid_target_not_found_status - it_behaves_like 'rejects nuget access with invalid target id' + it_behaves_like 'rejects nuget access with invalid target id', not_found_response: invalid_target_not_found_status end end RSpec.shared_examples 'handling nuget metadata requests with package name and package version' do |example_names_with_status: {}| include_context 'with expected presenters dependency groups' - anonymous_requests_example_name = example_names_with_status.fetch(:anonymous_requests_example_name, 'process nuget metadata request at package name and package version level') - anonymous_requests_status = example_names_with_status.fetch(:anonymous_requests_status, :success) guest_requests_example_name = example_names_with_status.fetch(:guest_requests_example_name, 'rejects nuget packages access') guest_requests_status = example_names_with_status.fetch(:guest_requests_status, :forbidden) + invalid_target_not_found_status = example_names_with_status.fetch(:invalid_target_not_found_status, :unauthorized) let_it_be(:package_name) { 'Dummy.Package' } let_it_be(:package) { create(:nuget_package, :with_metadatum, name: package_name, project: project) } @@ -139,7 +137,7 @@ 'PUBLIC' | :guest | false | true | 'process nuget metadata request at package name and package version level' | :success 'PUBLIC' | :developer | false | false | 'rejects nuget packages access' | :unauthorized 'PUBLIC' | :guest | false | false | 'rejects nuget packages access' | :unauthorized - 'PUBLIC' | :anonymous | false | true | anonymous_requests_example_name | anonymous_requests_status + 'PUBLIC' | :anonymous | false | true | 'process nuget metadata request at package name and package version level' | :success 'PRIVATE' | :developer | true | true | 'process nuget metadata request at package name and package version level' | :success 'PRIVATE' | :guest | true | true | guest_requests_example_name | guest_requests_status 'PRIVATE' | :developer | true | false | 'rejects nuget packages access' | :unauthorized @@ -172,9 +170,9 @@ end end - it_behaves_like 'rejects nuget access with unknown target id' + it_behaves_like 'rejects nuget access with unknown target id', not_found_response: invalid_target_not_found_status - it_behaves_like 'rejects nuget access with invalid target id' + it_behaves_like 'rejects nuget access with invalid target id', not_found_response: invalid_target_not_found_status end RSpec.shared_examples 'handling nuget search requests' do |example_names_with_status: {}| diff --git a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb index 372b1b7c21d536c4eb42266cc27e79e9e8a9487d..7019392a91a5a758d974799224e9df1787fabaad 100644 --- a/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/nuget_packages_shared_examples.rb @@ -472,7 +472,7 @@ RSpec.shared_examples 'rejects nuget access with unknown target id' do |not_found_response: :unauthorized| context 'with an unknown target' do - let(:target) { double(id: 1234567890) } + let(:target) { double(id: non_existing_record_id) } context 'as anonymous' do it_behaves_like 'rejects nuget packages access', :anonymous, not_found_response @@ -486,6 +486,33 @@ end end +RSpec.shared_examples 'allows anyone to pull public nuget packages on group level' do + let_it_be(:package_name) { 'dummy.package' } + let_it_be(:package) { create(:nuget_package, project: project, name: package_name) } + let_it_be(:external_user) { create(:user, external: true) } + let_it_be(:personal_access_token) { create(:personal_access_token, user: external_user) } + + subject { get api(url), headers: basic_auth_header(external_user.username, personal_access_token.token) } + + before do + [subgroup, group, project].each do |entity| + entity.update!(visibility_level: Gitlab::VisibilityLevel.const_get(:PRIVATE, false)) + end + project.project_feature.update!(package_registry_access_level: ::ProjectFeature::PUBLIC) + stub_application_setting(package_registry_allow_anyone_to_pull_option: true) + end + + it_behaves_like 'returning response status', :ok + + context 'when allow_anyone_to_pull_public_nuget_packages_on_group_level FF is disabled' do + before do + stub_feature_flags(allow_anyone_to_pull_public_nuget_packages_on_group_level: false) + end + + it_behaves_like 'returning response status', :not_found + end +end + RSpec.shared_examples 'nuget authorize upload endpoint' do using RSpec::Parameterized::TableSyntax include_context 'workhorse headers' @@ -776,7 +803,7 @@ end context 'when target does not exist' do - let(:target) { double(id: 1234567890) } + let(:target) { double(id: non_existing_record_id) } it_behaves_like 'returning response status', :not_found end